forked from enviPath/enviPy
[Feature] PEPPER in enviPath (#332)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#332
This commit is contained in:
@ -8,7 +8,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
exclude: ^static/images/
|
||||
exclude: ^static/images/|fixtures/
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.3
|
||||
|
||||
0
bridge/__init__.py
Normal file
0
bridge/__init__.py
Normal file
233
bridge/contracts.py
Normal file
233
bridge/contracts.py
Normal file
@ -0,0 +1,233 @@
|
||||
import enum
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .dto import BuildResult, EnviPyDTO, EvaluationResult, RunResult
|
||||
|
||||
|
||||
class PropertyType(enum.Enum):
|
||||
"""
|
||||
Enumeration representing different types of properties.
|
||||
|
||||
PropertyType is an Enum class that defines categories or types of properties
|
||||
based on their weight or nature. It can typically be used when classifying
|
||||
objects or entities by their weight classification, such as lightweight or heavy.
|
||||
"""
|
||||
|
||||
LIGHTWEIGHT = "lightweight"
|
||||
HEAVY = "heavy"
|
||||
|
||||
|
||||
class Plugin(ABC):
|
||||
"""
|
||||
Defines an abstract base class Plugin to serve as a blueprint for plugins.
|
||||
|
||||
This class establishes the structure that all plugin implementations must
|
||||
follow. It enforces the presence of required methods to ensure consistent
|
||||
functionality across all derived classes.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def identifier(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""
|
||||
Represents an abstract method that provides a contract for implementing a method
|
||||
to return a name as a string. Must be implemented in subclasses.
|
||||
Name must be unique across all plugins.
|
||||
|
||||
Methods
|
||||
-------
|
||||
name() -> str
|
||||
Abstract method to be defined in subclasses, which returns a string
|
||||
representing a name.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def display(self) -> str:
|
||||
"""
|
||||
An abstract method that must be implemented by subclasses to display
|
||||
specific information or behavior. The method ensures that all subclasses
|
||||
provide their own implementation of the display functionality.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Raises this error when the method is not implemented
|
||||
in a subclass.
|
||||
|
||||
Returns:
|
||||
str: A string used in dropdown menus or other user interfaces to display
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Property(Plugin):
|
||||
@abstractmethod
|
||||
def requires_rule_packages(self) -> bool:
|
||||
"""
|
||||
Defines an abstract method to determine whether rule packages are required.
|
||||
|
||||
This method should be implemented by subclasses to specify if they depend
|
||||
on rule packages for their functioning.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the subclass has not implemented this method.
|
||||
|
||||
@return: A boolean indicating if rule packages are required.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def requires_data_packages(self) -> bool:
|
||||
"""
|
||||
Defines an abstract method to determine whether data packages are required.
|
||||
|
||||
This method should be implemented by subclasses to specify if they depend
|
||||
on data packages for their functioning.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the subclass has not implemented this method.
|
||||
|
||||
Returns:
|
||||
bool: True if the service requires data packages, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_type(self) -> PropertyType:
|
||||
"""
|
||||
An abstract method that provides the type of property. This method must
|
||||
be implemented by subclasses to specify the appropriate property type.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the method is not implemented by a subclass.
|
||||
|
||||
Returns:
|
||||
PropertyType: The type of the property associated with the implementation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_heavy(self):
|
||||
"""
|
||||
Determines if the current property type is heavy.
|
||||
|
||||
This method evaluates whether the property type returned from the `get_type()`
|
||||
method is classified as `HEAVY`. It utilizes the `PropertyType.HEAVY` constant
|
||||
for this comparison.
|
||||
|
||||
Raises:
|
||||
AttributeError: If the `get_type()` method is not defined or does not return
|
||||
a valid value.
|
||||
|
||||
Returns:
|
||||
bool: True if the property type is `HEAVY`, otherwise False.
|
||||
"""
|
||||
return self.get_type() == PropertyType.HEAVY
|
||||
|
||||
@abstractmethod
|
||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||
"""
|
||||
Abstract method to prepare and construct a specific build process based on the provided
|
||||
environment data transfer object (EnviPyDTO). This method should be implemented by
|
||||
subclasses to handle the particular requirements of the environment.
|
||||
|
||||
Parameters:
|
||||
eP : EnviPyDTO
|
||||
The data transfer object containing environment details for the build process.
|
||||
|
||||
*args :
|
||||
Additional positional arguments required for the build.
|
||||
|
||||
**kwargs :
|
||||
Additional keyword arguments to offer flexibility and customization for
|
||||
the build process.
|
||||
|
||||
Returns:
|
||||
BuildResult | None
|
||||
Returns a BuildResult instance if the build operation succeeds, else returns None.
|
||||
|
||||
Raises:
|
||||
NotImplementedError
|
||||
If the method is not implemented in a subclass.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||
"""
|
||||
Represents an abstract base class for executing a generic process with
|
||||
provided parameters and returning a standardized result.
|
||||
|
||||
Attributes:
|
||||
None.
|
||||
|
||||
Methods:
|
||||
run(eP, *args, **kwargs):
|
||||
Executes a task with specified input parameters and optional
|
||||
arguments, returning the outcome in the form of a RunResult object.
|
||||
This is an abstract method and must be implemented in subclasses.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the subclass does not implement the abstract
|
||||
method.
|
||||
|
||||
Parameters:
|
||||
eP (EnviPyDTO): The primary object containing information or data required
|
||||
for processing. Mandatory.
|
||||
*args: Variable length argument list for additional positional arguments.
|
||||
**kwargs: Arbitrary keyword arguments for additional options or settings.
|
||||
|
||||
Returns:
|
||||
RunResult: A result object encapsulating the status, output, or details
|
||||
of the process execution.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||
"""
|
||||
Abstract method for evaluating data based on the given input and additional arguments.
|
||||
|
||||
This method is intended to be implemented by subclasses and provides
|
||||
a mechanism to perform an evaluation procedure based on input encapsulated
|
||||
in an EnviPyDTO object.
|
||||
|
||||
Parameters:
|
||||
eP : EnviPyDTO
|
||||
The data transfer object containing necessary input for evaluation.
|
||||
*args : tuple
|
||||
Additional positional arguments for the evaluation process.
|
||||
**kwargs : dict
|
||||
Additional keyword arguments for the evaluation process.
|
||||
|
||||
Returns:
|
||||
EvaluationResult
|
||||
The result of the evaluation performed by the method.
|
||||
|
||||
Raises:
|
||||
NotImplementedError
|
||||
If the method is not implemented in the subclass.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||
"""
|
||||
An abstract method designed to build and evaluate a model or system using the provided
|
||||
environmental parameters and additional optional arguments.
|
||||
|
||||
Args:
|
||||
eP (EnviPyDTO): The environmental parameters required for building and evaluating.
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
EvaluationResult: The result of the evaluation process.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the method is not implemented by a subclass.
|
||||
"""
|
||||
pass
|
||||
140
bridge/dto.py
Normal file
140
bridge/dto.py
Normal file
@ -0,0 +1,140 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Protocol
|
||||
|
||||
from envipy_additional_information import EnviPyModel, register
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from utilities.chem import FormatConverter, ProductSet
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Context:
|
||||
uuid: str
|
||||
url: str
|
||||
work_dir: str
|
||||
|
||||
|
||||
class CompoundProto(Protocol):
|
||||
url: str | None
|
||||
name: str | None
|
||||
smiles: str
|
||||
|
||||
|
||||
class RuleProto(Protocol):
|
||||
url: str
|
||||
name: str
|
||||
|
||||
def apply(self, smiles, *args, **kwargs): ...
|
||||
|
||||
|
||||
class ReactionProto(Protocol):
|
||||
url: str
|
||||
name: str
|
||||
rules: List[RuleProto]
|
||||
|
||||
|
||||
class EnviPyDTO(Protocol):
|
||||
def get_context(self) -> Context: ...
|
||||
|
||||
def get_compounds(self) -> List[CompoundProto]: ...
|
||||
|
||||
def get_reactions(self) -> List[ReactionProto]: ...
|
||||
|
||||
def get_rules(self) -> List[RuleProto]: ...
|
||||
|
||||
@staticmethod
|
||||
def standardize(smiles, remove_stereo=False, canonicalize_tautomers=False): ...
|
||||
|
||||
@staticmethod
|
||||
def apply(
|
||||
smiles: str,
|
||||
smirks: str,
|
||||
preprocess_smiles: bool = True,
|
||||
bracketize: bool = True,
|
||||
standardize: bool = True,
|
||||
kekulize: bool = True,
|
||||
remove_stereo: bool = True,
|
||||
reactant_filter_smarts: str | None = None,
|
||||
product_filter_smarts: str | None = None,
|
||||
) -> List["ProductSet"]: ...
|
||||
|
||||
|
||||
class PredictedProperty(EnviPyModel):
|
||||
pass
|
||||
|
||||
|
||||
@register("buildresult")
|
||||
class BuildResult(EnviPyModel):
|
||||
data: dict[str, Any] | List[dict[str, Any]] | None
|
||||
|
||||
|
||||
@register("runresult")
|
||||
class RunResult(EnviPyModel):
|
||||
producer: HttpUrl
|
||||
description: Optional[str] = None
|
||||
result: PredictedProperty | List[PredictedProperty]
|
||||
|
||||
|
||||
@register("evaluationresult")
|
||||
class EvaluationResult(EnviPyModel):
|
||||
data: dict[str, Any] | List[dict[str, Any]] | None
|
||||
|
||||
|
||||
class BaseDTO(EnviPyDTO):
|
||||
def __init__(
|
||||
self,
|
||||
uuid: str,
|
||||
url: str,
|
||||
work_dir: str,
|
||||
compounds: List[CompoundProto],
|
||||
reactions: List[ReactionProto],
|
||||
rules: List[RuleProto],
|
||||
):
|
||||
self.uuid = uuid
|
||||
self.url = url
|
||||
self.work_dir = work_dir
|
||||
self.compounds = compounds
|
||||
self.reactions = reactions
|
||||
self.rules = rules
|
||||
|
||||
def get_context(self) -> Context:
|
||||
return Context(uuid=self.uuid, url=self.url, work_dir=self.work_dir)
|
||||
|
||||
def get_compounds(self) -> List[CompoundProto]:
|
||||
return self.compounds
|
||||
|
||||
def get_reactions(self) -> List[ReactionProto]:
|
||||
return self.reactions
|
||||
|
||||
def get_rules(self) -> List[RuleProto]:
|
||||
return self.rules
|
||||
|
||||
@staticmethod
|
||||
def standardize(smiles, remove_stereo=False, canonicalize_tautomers=False):
|
||||
return FormatConverter.standardize(
|
||||
smiles, remove_stereo=remove_stereo, canonicalize_tautomers=canonicalize_tautomers
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def apply(
|
||||
smiles: str,
|
||||
smirks: str,
|
||||
preprocess_smiles: bool = True,
|
||||
bracketize: bool = True,
|
||||
standardize: bool = True,
|
||||
kekulize: bool = True,
|
||||
remove_stereo: bool = True,
|
||||
reactant_filter_smarts: str | None = None,
|
||||
product_filter_smarts: str | None = None,
|
||||
) -> List["ProductSet"]:
|
||||
return FormatConverter.apply(
|
||||
smiles,
|
||||
smirks,
|
||||
preprocess_smiles,
|
||||
bracketize,
|
||||
standardize,
|
||||
kekulize,
|
||||
remove_stereo,
|
||||
reactant_filter_smarts,
|
||||
product_filter_smarts,
|
||||
)
|
||||
@ -14,7 +14,6 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from envipy_plugins import Classifier, Property, Descriptor
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
|
||||
@ -128,6 +127,13 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
if os.environ.get("USE_TEMPLATE_DB", False) == "True":
|
||||
DATABASES["default"]["TEST"] = {
|
||||
"NAME": f"test_{os.environ['TEMPLATE_DB']}",
|
||||
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
@ -317,16 +323,13 @@ DEFAULT_MODEL_THRESHOLD = 0.25
|
||||
|
||||
# Loading Plugins
|
||||
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
|
||||
if PLUGINS_ENABLED:
|
||||
from utilities.plugin import discover_plugins
|
||||
BASE_PLUGINS = [
|
||||
"pepper.PEPPER",
|
||||
]
|
||||
|
||||
CLASSIFIER_PLUGINS = discover_plugins(_cls=Classifier)
|
||||
PROPERTY_PLUGINS = discover_plugins(_cls=Property)
|
||||
DESCRIPTOR_PLUGINS = discover_plugins(_cls=Descriptor)
|
||||
else:
|
||||
CLASSIFIER_PLUGINS = {}
|
||||
PROPERTY_PLUGINS = {}
|
||||
DESCRIPTOR_PLUGINS = {}
|
||||
CLASSIFIER_PLUGINS = {}
|
||||
PROPERTY_PLUGINS = {}
|
||||
DESCRIPTOR_PLUGINS = {}
|
||||
|
||||
SENTRY_ENABLED = os.environ.get("SENTRY_ENABLED", "False") == "True"
|
||||
if SENTRY_ENABLED:
|
||||
|
||||
@ -49,7 +49,6 @@ class AdditionalInformationAPITests(TestCase):
|
||||
description="Test scenario for additional information tests",
|
||||
scenario_type="biodegradation",
|
||||
scenario_date="2024-01-01",
|
||||
additional_information={}, # Initialize with empty dict
|
||||
)
|
||||
cls.other_scenario = Scenario.objects.create(
|
||||
package=cls.other_package,
|
||||
@ -57,7 +56,6 @@ class AdditionalInformationAPITests(TestCase):
|
||||
description="Scenario in package without access",
|
||||
scenario_type="biodegradation",
|
||||
scenario_date="2024-01-01",
|
||||
additional_information={},
|
||||
)
|
||||
|
||||
def test_list_all_schemas(self):
|
||||
|
||||
@ -9,6 +9,7 @@ from envipy_additional_information import registry
|
||||
from envipy_additional_information.groups import GroupEnum
|
||||
from epapi.utils.schema_transformers import build_rjsf_output
|
||||
from epapi.utils.validation_errors import handle_validation_error
|
||||
from epdb.models import AdditionalInformation
|
||||
from ..dal import get_scenario_for_read, get_scenario_for_write
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -44,12 +45,14 @@ def list_scenario_info(request, scenario_uuid: UUID):
|
||||
scenario = get_scenario_for_read(request.user, scenario_uuid)
|
||||
|
||||
result = []
|
||||
for ai in scenario.get_additional_information():
|
||||
|
||||
for ai in AdditionalInformation.objects.filter(scenario=scenario):
|
||||
result.append(
|
||||
{
|
||||
"type": ai.__class__.__name__,
|
||||
"type": ai.get().__class__.__name__,
|
||||
"uuid": getattr(ai, "uuid", None),
|
||||
"data": ai.model_dump(mode="json"),
|
||||
"data": ai.data,
|
||||
"attach_object": ai.content_object.simple_json() if ai.content_object else None,
|
||||
}
|
||||
)
|
||||
return result
|
||||
@ -85,20 +88,17 @@ def update_scenario_info(
|
||||
scenario = get_scenario_for_write(request.user, scenario_uuid)
|
||||
ai_uuid_str = str(ai_uuid)
|
||||
|
||||
# Find item to determine type for validation
|
||||
found_type = None
|
||||
for type_name, items in scenario.additional_information.items():
|
||||
if any(item.get("uuid") == ai_uuid_str for item in items):
|
||||
found_type = type_name
|
||||
break
|
||||
ai = AdditionalInformation.objects.filter(uuid=ai_uuid_str, scenario=scenario)
|
||||
|
||||
if found_type is None:
|
||||
raise HttpError(404, f"Additional information not found: {ai_uuid}")
|
||||
if not ai.exists():
|
||||
raise HttpError(404, f"Additional information with UUID {ai_uuid} not found")
|
||||
|
||||
ai = ai.first()
|
||||
|
||||
# Get the model class for validation
|
||||
cls = registry.get_model(found_type.lower())
|
||||
cls = registry.get_model(ai.type.lower())
|
||||
if not cls:
|
||||
raise HttpError(500, f"Unknown model type in data: {found_type}")
|
||||
raise HttpError(500, f"Unknown model type in data: {ai.type}")
|
||||
|
||||
# Validate the payload against the model
|
||||
try:
|
||||
|
||||
@ -13,9 +13,9 @@ from epdb.logic import PackageManager
|
||||
from epdb.views import _anonymous_or_real
|
||||
from ..pagination import EnhancedPageNumberPagination
|
||||
from ..schemas import (
|
||||
ReviewStatusFilter,
|
||||
ScenarioOutSchema,
|
||||
ScenarioCreateSchema,
|
||||
ScenarioReviewStatusAndRelatedFilter,
|
||||
)
|
||||
from ..dal import get_user_entities_for_read, get_package_entities_for_read
|
||||
from envipy_additional_information import registry
|
||||
@ -29,7 +29,7 @@ router = Router()
|
||||
@paginate(
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
filter_schema=ScenarioReviewStatusAndRelatedFilter,
|
||||
filter_schema=ReviewStatusFilter,
|
||||
)
|
||||
def list_all_scenarios(request):
|
||||
user = request.user
|
||||
@ -44,7 +44,7 @@ def list_all_scenarios(request):
|
||||
@paginate(
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
filter_schema=ScenarioReviewStatusAndRelatedFilter,
|
||||
filter_schema=ReviewStatusFilter,
|
||||
)
|
||||
def list_package_scenarios(request, package_uuid: UUID):
|
||||
user = request.user
|
||||
|
||||
@ -22,12 +22,6 @@ class StructureReviewStatusFilter(FilterSchema):
|
||||
review_status: Annotated[Optional[bool], FilterLookup("compound__package__reviewed")] = None
|
||||
|
||||
|
||||
class ScenarioReviewStatusAndRelatedFilter(ReviewStatusFilter):
|
||||
"""Filter schema for review_status and parent query parameter."""
|
||||
|
||||
exclude_related: Annotated[Optional[bool], FilterLookup("parent__isnull")] = None
|
||||
|
||||
|
||||
# Base schema for all package-scoped entities
|
||||
class PackageEntityOutSchema(Schema):
|
||||
"""Base schema for entities belonging to a package."""
|
||||
|
||||
@ -2,6 +2,7 @@ from django.conf import settings as s
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
Edge,
|
||||
@ -16,6 +17,7 @@ from .models import (
|
||||
Node,
|
||||
ParallelRule,
|
||||
Pathway,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Scenario,
|
||||
Setting,
|
||||
@ -27,8 +29,20 @@ from .models import (
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
|
||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = ["username", "email", "is_active", "is_staff", "is_superuser"]
|
||||
list_display = [
|
||||
"username",
|
||||
"email",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
]
|
||||
|
||||
|
||||
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||
@ -65,6 +79,10 @@ class EnviFormerAdmin(EPAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class PropertyPluginModelAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
list_display = ["cc_string", "link", "image_link"]
|
||||
|
||||
@ -117,6 +135,7 @@ class ExternalIdentifierAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(AdditionalInformation, AdditionalInformationAdmin)
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
@ -125,6 +144,7 @@ admin.site.register(JobLog, JobLogAdmin)
|
||||
admin.site.register(Package, PackageAdmin)
|
||||
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
|
||||
admin.site.register(EnviFormer, EnviFormerAdmin)
|
||||
admin.site.register(PropertyPluginModel, PropertyPluginModelAdmin)
|
||||
admin.site.register(License, LicenseAdmin)
|
||||
admin.site.register(Compound, CompoundAdmin)
|
||||
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
||||
|
||||
@ -15,3 +15,9 @@ class EPDBConfig(AppConfig):
|
||||
|
||||
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
||||
logger.info(f"Using Package model: {model_name}")
|
||||
|
||||
if settings.PLUGINS_ENABLED:
|
||||
from bridge.contracts import Property
|
||||
from utilities.plugin import discover_plugins
|
||||
|
||||
settings.PROPERTY_PLUGINS.update(**discover_plugins(_cls=Property))
|
||||
|
||||
@ -22,6 +22,7 @@ from epdb.models import (
|
||||
Node,
|
||||
Pathway,
|
||||
Permission,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Rule,
|
||||
Setting,
|
||||
@ -1109,10 +1110,11 @@ class SettingManager(object):
|
||||
description: str = None,
|
||||
max_nodes: int = None,
|
||||
max_depth: int = None,
|
||||
rule_packages: List[Package] = None,
|
||||
rule_packages: List[Package] | None = None,
|
||||
model: EPModel = None,
|
||||
model_threshold: float = None,
|
||||
expansion_scheme: ExpansionSchemeChoice = ExpansionSchemeChoice.BFS,
|
||||
property_models: List["PropertyPluginModel"] | None = None,
|
||||
):
|
||||
new_s = Setting()
|
||||
|
||||
@ -1133,6 +1135,11 @@ class SettingManager(object):
|
||||
new_s.rule_packages.add(r)
|
||||
new_s.save()
|
||||
|
||||
if property_models is not None:
|
||||
for pm in property_models:
|
||||
new_s.property_models.add(pm)
|
||||
new_s.save()
|
||||
|
||||
usp = UserSettingPermission()
|
||||
usp.user = user
|
||||
usp.setting = new_s
|
||||
|
||||
@ -41,9 +41,7 @@ class Command(BaseCommand):
|
||||
"SequentialRule",
|
||||
"Scenario",
|
||||
"Setting",
|
||||
"MLRelativeReasoning",
|
||||
"RuleBasedRelativeReasoning",
|
||||
"EnviFormer",
|
||||
"EPModel",
|
||||
"ApplicabilityDomain",
|
||||
"EnzymeLink",
|
||||
]
|
||||
|
||||
76
epdb/management/commands/recreate_db.py
Normal file
76
epdb/management/commands/recreate_db.py
Normal file
@ -0,0 +1,76 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
type=str,
|
||||
help="Name of the database to recreate. Default is 'appdb'",
|
||||
default="appdb",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--dump",
|
||||
type=str,
|
||||
help="Path to the dump file",
|
||||
default="./fixtures/db.dump",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-ou",
|
||||
"--oldurl",
|
||||
type=str,
|
||||
help="Old URL, e.g. https://envipath.org/",
|
||||
default="https://envipath.org/",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-nu",
|
||||
"--newurl",
|
||||
type=str,
|
||||
help="New URL, e.g. http://localhost:8000/",
|
||||
default="http://localhost:8000/",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dump_file = options["dump"]
|
||||
|
||||
if not os.path.exists(dump_file):
|
||||
raise ValueError(f"Dump file {dump_file} does not exist")
|
||||
|
||||
print(f"Dropping database {options['name']} y/n: ", end="")
|
||||
|
||||
if input() in "yY":
|
||||
result = subprocess.run(
|
||||
["dropdb", "appdb"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
else:
|
||||
raise ValueError("Aborted")
|
||||
|
||||
print(f"Creating database {options['name']}")
|
||||
|
||||
result = subprocess.run(
|
||||
["createdb", "appdb"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
print(f"Restoring database {options['name']} from {dump_file}")
|
||||
|
||||
result = subprocess.run(
|
||||
["pg_restore", "-d", "appdb", dump_file, "--no-owner"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
call_command("localize_urls", "--old", options["oldurl"], "--new", options["newurl"])
|
||||
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
@ -0,0 +1,179 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-12 09:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0015_user_is_reviewer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="enviformer",
|
||||
name="model_status",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="mlrelativereasoning",
|
||||
name="model_status",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="rulebasedrelativereasoning",
|
||||
name="model_status",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="epmodel",
|
||||
name="model_status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("INITIAL", "Initial"),
|
||||
("INITIALIZING", "Model is initializing."),
|
||||
("BUILDING", "Model is building."),
|
||||
(
|
||||
"BUILT_NOT_EVALUATED",
|
||||
"Model is built and can be used for predictions, Model is not evaluated yet.",
|
||||
),
|
||||
("EVALUATING", "Model is evaluating"),
|
||||
("FINISHED", "Model has finished building and evaluation."),
|
||||
("ERROR", "Model has failed."),
|
||||
],
|
||||
default="INITIAL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="enviformer",
|
||||
name="eval_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="enviformer",
|
||||
name="rule_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="mlrelativereasoning",
|
||||
name="eval_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="mlrelativereasoning",
|
||||
name="rule_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rulebasedrelativereasoning",
|
||||
name="eval_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rulebasedrelativereasoning",
|
||||
name="rule_packages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PropertyPluginModel",
|
||||
fields=[
|
||||
(
|
||||
"epmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="epdb.epmodel",
|
||||
),
|
||||
),
|
||||
("threshold", models.FloatField(default=0.5)),
|
||||
("eval_results", models.JSONField(blank=True, default=dict, null=True)),
|
||||
("multigen_eval", models.BooleanField(default=False)),
|
||||
("plugin_identifier", models.CharField(max_length=255)),
|
||||
(
|
||||
"app_domain",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="epdb.applicabilitydomain",
|
||||
),
|
||||
),
|
||||
(
|
||||
"data_packages",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
),
|
||||
),
|
||||
(
|
||||
"eval_packages",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
),
|
||||
),
|
||||
(
|
||||
"rule_packages",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("epdb.epmodel",),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="setting",
|
||||
name="property_models",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="settings",
|
||||
to="epdb.propertypluginmodel",
|
||||
verbose_name="Setting Property Models",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="PluginModel",
|
||||
),
|
||||
]
|
||||
93
epdb/migrations/0017_additionalinformation.py
Normal file
93
epdb/migrations/0017_additionalinformation.py
Normal file
@ -0,0 +1,93 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-20 12:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("epdb", "0016_remove_enviformer_model_status_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AdditionalInformation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
||||
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
||||
("type", models.TextField(verbose_name="Additional Information Type")),
|
||||
("data", models.JSONField(blank=True, default=dict, null=True)),
|
||||
("object_id", models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
(
|
||||
"package",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Package",
|
||||
),
|
||||
),
|
||||
(
|
||||
"scenario",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="scenario_additional_information",
|
||||
to="epdb.scenario",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"indexes": [
|
||||
models.Index(fields=["type"], name="epdb_additi_type_394349_idx"),
|
||||
models.Index(
|
||||
fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["scenario", "content_type", "object_id"],
|
||||
name="epdb_additi_scenari_ef2bf5_idx",
|
||||
),
|
||||
],
|
||||
"constraints": [
|
||||
models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
models.Q(("content_type__isnull", True), ("object_id__isnull", True)),
|
||||
models.Q(("content_type__isnull", False), ("object_id__isnull", False)),
|
||||
_connector="OR",
|
||||
),
|
||||
name="ck_addinfo_gfk_pair",
|
||||
),
|
||||
models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
("scenario__isnull", False),
|
||||
("content_type__isnull", False),
|
||||
_connector="OR",
|
||||
),
|
||||
name="ck_addinfo_not_both_null",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
@ -0,0 +1,132 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-20 12:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def get_additional_information(scenario):
|
||||
from envipy_additional_information import registry
|
||||
from envipy_additional_information.parsers import TypeOfAerationParser
|
||||
|
||||
for k, vals in scenario.additional_information.items():
|
||||
if k == "enzyme":
|
||||
continue
|
||||
|
||||
if k == "SpikeConentration":
|
||||
k = "SpikeConcentration"
|
||||
|
||||
if k == "AerationType":
|
||||
k = "TypeOfAeration"
|
||||
|
||||
for v in vals:
|
||||
# Per default additional fields are ignored
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[k](**v)
|
||||
except Exception:
|
||||
if k == "TypeOfAeration":
|
||||
toa = TypeOfAerationParser()
|
||||
inst = toa.from_string(v["type"])
|
||||
|
||||
# Add uuid to uniquely identify objects for manipulation
|
||||
if "uuid" in v:
|
||||
inst.__dict__["uuid"] = v["uuid"]
|
||||
|
||||
yield inst
|
||||
|
||||
|
||||
def forward_func(apps, schema_editor):
|
||||
Scenario = apps.get_model("epdb", "Scenario")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||
|
||||
bulk = []
|
||||
related = []
|
||||
ctype = {o.model: o for o in ContentType.objects.all()}
|
||||
parents = Scenario.objects.prefetch_related(
|
||||
"compound_set",
|
||||
"compoundstructure_set",
|
||||
"reaction_set",
|
||||
"rule_set",
|
||||
"pathway_set",
|
||||
"node_set",
|
||||
"edge_set",
|
||||
).filter(parent__isnull=True)
|
||||
|
||||
for i, scenario in enumerate(parents):
|
||||
print(f"{i + 1}/{len(parents)}", end="\r")
|
||||
if scenario.parent is not None:
|
||||
related.append(scenario.parent)
|
||||
continue
|
||||
|
||||
for ai in get_additional_information(scenario):
|
||||
bulk.append(
|
||||
AdditionalInformation(
|
||||
package=scenario.package,
|
||||
scenario=scenario,
|
||||
type=ai.__class__.__name__,
|
||||
data=ai.model_dump(mode="json"),
|
||||
)
|
||||
)
|
||||
|
||||
print("\n", len(bulk))
|
||||
|
||||
related = Scenario.objects.prefetch_related(
|
||||
"compound_set",
|
||||
"compoundstructure_set",
|
||||
"reaction_set",
|
||||
"rule_set",
|
||||
"pathway_set",
|
||||
"node_set",
|
||||
"edge_set",
|
||||
).filter(parent__isnull=False)
|
||||
|
||||
for i, scenario in enumerate(related):
|
||||
print(f"{i + 1}/{len(related)}", end="\r")
|
||||
parent = scenario.parent
|
||||
# Check to which objects this scenario is attached to
|
||||
for ai in get_additional_information(scenario):
|
||||
rel_objs = [
|
||||
"compound",
|
||||
"compoundstructure",
|
||||
"reaction",
|
||||
"rule",
|
||||
"pathway",
|
||||
"node",
|
||||
"edge",
|
||||
]
|
||||
for rel_obj in rel_objs:
|
||||
for o in getattr(scenario, f"{rel_obj}_set").all():
|
||||
bulk.append(
|
||||
AdditionalInformation(
|
||||
package=scenario.package,
|
||||
scenario=parent,
|
||||
type=ai.__class__.__name__,
|
||||
data=ai.model_dump(mode="json"),
|
||||
content_type=ctype[rel_obj],
|
||||
object_id=o.pk,
|
||||
)
|
||||
)
|
||||
|
||||
print("Start creating additional information objects...")
|
||||
AdditionalInformation.objects.bulk_create(bulk)
|
||||
print("Done!")
|
||||
print(len(bulk))
|
||||
|
||||
Scenario.objects.filter(parent__isnull=False).delete()
|
||||
# Call ai save to fix urls
|
||||
ais = AdditionalInformation.objects.all()
|
||||
total = ais.count()
|
||||
|
||||
for i, ai in enumerate(ais):
|
||||
print(f"{i + 1}/{total}", end="\r")
|
||||
ai.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0017_additionalinformation"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0018_auto_20260220_1203"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scenario",
|
||||
name="additional_information",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scenario",
|
||||
name="parent",
|
||||
),
|
||||
]
|
||||
605
epdb/models.py
605
epdb/models.py
@ -29,6 +29,8 @@ from polymorphic.models import PolymorphicModel
|
||||
from sklearn.metrics import jaccard_score, precision_score, recall_score
|
||||
from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
from bridge.contracts import Property
|
||||
from bridge.dto import RunResult, PredictedProperty
|
||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||
from utilities.ml import (
|
||||
ApplicabilityDomainPCA,
|
||||
@ -667,6 +669,23 @@ class ScenarioMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class AdditionalInformationMixin(models.Model):
|
||||
"""
|
||||
Optional mixin: lets you do compound.additional_information.all()
|
||||
without an explicit M2M table.
|
||||
"""
|
||||
|
||||
additional_information = GenericRelation(
|
||||
"epdb.AdditionalInformation",
|
||||
content_type_field="content_type",
|
||||
object_id_field="object_id",
|
||||
related_query_name="target",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
cc_string = models.TextField(blank=False, null=False, verbose_name="CC string")
|
||||
link = models.URLField(blank=False, null=False, verbose_name="link")
|
||||
@ -745,7 +764,9 @@ class Package(EnviPathModel):
|
||||
swappable = "EPDB_PACKAGE_MODEL"
|
||||
|
||||
|
||||
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
||||
class Compound(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1073,7 +1094,9 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
||||
unique_together = [("uuid", "package")]
|
||||
|
||||
|
||||
class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
||||
class CompoundStructure(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
||||
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
||||
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
||||
@ -1167,10 +1190,11 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
hls: Dict[Scenario, List[HalfLife]] = defaultdict(list)
|
||||
|
||||
for n in self.related_nodes:
|
||||
for scen in n.scenarios.all().order_by("name"):
|
||||
for ai in scen.get_additional_information():
|
||||
if isinstance(ai, HalfLife):
|
||||
hls[scen].append(ai)
|
||||
for ai in n.additional_information.filter(scenario__isnull=False).order_by(
|
||||
"scenario__name"
|
||||
):
|
||||
if isinstance(ai.get(), HalfLife):
|
||||
hls[ai.scenario].append(ai.get())
|
||||
|
||||
return dict(hls)
|
||||
|
||||
@ -1195,7 +1219,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
||||
return ".".join(self.ec_number.split(".")[:3]) + ".-"
|
||||
|
||||
|
||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1424,8 +1448,6 @@ class SimpleRDKitRule(SimpleRule):
|
||||
return "{}/simple-rdkit-rule/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
class ParallelRule(Rule):
|
||||
simple_rules = models.ManyToManyField("epdb.SimpleRule", verbose_name="Simple rules")
|
||||
|
||||
@ -1561,7 +1583,9 @@ class SequentialRuleOrdering(models.Model):
|
||||
order_index = models.IntegerField(null=False, blank=False)
|
||||
|
||||
|
||||
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
|
||||
class Reaction(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1757,7 +1781,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
||||
return res
|
||||
|
||||
|
||||
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2140,7 +2164,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||
|
||||
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2175,6 +2199,11 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
def d3_json(self):
|
||||
app_domain_data = self.get_app_domain_assessment_data()
|
||||
|
||||
predicted_properties = defaultdict(list)
|
||||
for ai in self.additional_information.all():
|
||||
if isinstance(ai.get(), PredictedProperty):
|
||||
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
||||
|
||||
return {
|
||||
"depth": self.depth,
|
||||
"stereo_removed": self.stereo_removed,
|
||||
@ -2193,6 +2222,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
else None,
|
||||
"uncovered_functional_groups": False,
|
||||
},
|
||||
"predicted_properties": predicted_properties,
|
||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||
"timeseries": self.get_timeseries_data(),
|
||||
}
|
||||
@ -2210,6 +2240,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
stereo_removed = True
|
||||
|
||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
||||
|
||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
||||
@ -2233,10 +2264,10 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||
|
||||
def get_timeseries_data(self):
|
||||
for scenario in self.scenarios.all():
|
||||
for ai in scenario.get_additional_information():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
for ai in self.additional_information.all():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
def get_app_domain_assessment_data(self):
|
||||
@ -2267,7 +2298,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return res
|
||||
|
||||
|
||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2409,38 +2440,11 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
)
|
||||
|
||||
|
||||
class EPModel(PolymorphicModel, EnviPathModel):
|
||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
|
||||
def _url(self):
|
||||
return "{}/model/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
class PackageBasedModel(EPModel):
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
)
|
||||
threshold = models.FloatField(null=False, blank=False, default=0.5)
|
||||
eval_results = JSONField(null=True, blank=True, default=dict)
|
||||
app_domain = models.ForeignKey(
|
||||
"epdb.ApplicabilityDomain", on_delete=models.SET_NULL, null=True, blank=True, default=None
|
||||
)
|
||||
multigen_eval = models.BooleanField(null=False, blank=False, default=False)
|
||||
|
||||
INITIAL = "INITIAL"
|
||||
INITIALIZING = "INITIALIZING"
|
||||
BUILDING = "BUILDING"
|
||||
@ -2467,6 +2471,35 @@ class PackageBasedModel(EPModel):
|
||||
def ready_for_prediction(self) -> bool:
|
||||
return self.model_status in [self.BUILT_NOT_EVALUATED, self.EVALUATING, self.FINISHED]
|
||||
|
||||
def _url(self):
|
||||
return "{}/model/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
class PackageBasedModel(EPModel):
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
blank=True,
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
blank=True,
|
||||
)
|
||||
threshold = models.FloatField(null=False, blank=False, default=0.5)
|
||||
eval_results = JSONField(null=True, blank=True, default=dict)
|
||||
app_domain = models.ForeignKey(
|
||||
"epdb.ApplicabilityDomain", on_delete=models.SET_NULL, null=True, blank=True, default=None
|
||||
)
|
||||
multigen_eval = models.BooleanField(null=False, blank=False, default=False)
|
||||
|
||||
@property
|
||||
def pr_curve(self):
|
||||
if self.model_status != self.FINISHED:
|
||||
@ -3011,7 +3044,7 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
|
||||
mod = joblib.load(os.path.join(s.MODEL_DIR, f"{self.uuid}_mod.pkl"))
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
start = datetime.now()
|
||||
ds = self.load_dataset()
|
||||
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
|
||||
@ -3111,7 +3144,7 @@ class MLRelativeReasoning(PackageBasedModel):
|
||||
mod.base_clf.n_jobs = -1
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
start = datetime.now()
|
||||
ds = self.load_dataset()
|
||||
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
|
||||
@ -3419,16 +3452,16 @@ class EnviFormer(PackageBasedModel):
|
||||
mod = load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt)
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
return self.predict_batch([smiles])[0]
|
||||
|
||||
def predict_batch(self, smiles_list):
|
||||
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
||||
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
||||
canon_smiles = [
|
||||
".".join(
|
||||
[FormatConverter.standardize(s, remove_stereo=True) for s in smiles.split(".")]
|
||||
)
|
||||
for smiles in smiles_list
|
||||
for smi in smiles
|
||||
]
|
||||
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
||||
start = datetime.now()
|
||||
@ -3777,8 +3810,216 @@ class EnviFormer(PackageBasedModel):
|
||||
return []
|
||||
|
||||
|
||||
class PluginModel(EPModel):
|
||||
pass
|
||||
class PropertyPluginModel(PackageBasedModel):
|
||||
plugin_identifier = models.CharField(max_length=255)
|
||||
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
blank=True,
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
blank=True,
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
package: "Package",
|
||||
plugin_identifier: str,
|
||||
rule_packages: List["Package"] | None,
|
||||
data_packages: List["Package"] | None,
|
||||
name: "str" = None,
|
||||
description: str = None,
|
||||
):
|
||||
mod = PropertyPluginModel()
|
||||
mod.package = package
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"PropertyPluginModel {PropertyPluginModel.objects.filter(package=package).count() + 1}"
|
||||
|
||||
mod.name = name
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if plugin_identifier is None:
|
||||
raise ValueError("Plugin identifier must be set")
|
||||
|
||||
impl = s.PROPERTY_PLUGINS.get(plugin_identifier, None)
|
||||
|
||||
if impl is None:
|
||||
raise ValueError(f"Unknown plugin identifier: {plugin_identifier}")
|
||||
|
||||
inst = impl()
|
||||
|
||||
mod.plugin_identifier = plugin_identifier
|
||||
|
||||
if inst.requires_rule_packages() and (rule_packages is None or len(rule_packages) == 0):
|
||||
raise ValueError("Plugin requires rules but none were provided")
|
||||
elif not inst.requires_rule_packages() and (
|
||||
rule_packages is not None and len(rule_packages) > 0
|
||||
):
|
||||
raise ValueError("Plugin does not require rules but some were provided")
|
||||
|
||||
if rule_packages is None:
|
||||
rule_packages = []
|
||||
|
||||
if inst.requires_data_packages() and (data_packages is None or len(data_packages) == 0):
|
||||
raise ValueError("Plugin requires data but none were provided")
|
||||
elif not inst.requires_data_packages() and (
|
||||
data_packages is not None and len(data_packages) > 0
|
||||
):
|
||||
raise ValueError("Plugin does not require data but some were provided")
|
||||
|
||||
if data_packages is None:
|
||||
data_packages = []
|
||||
|
||||
mod.save()
|
||||
|
||||
for p in rule_packages:
|
||||
mod.rule_packages.add(p)
|
||||
|
||||
for p in data_packages:
|
||||
mod.data_packages.add(p)
|
||||
|
||||
mod.save()
|
||||
return mod
|
||||
|
||||
def instance(self) -> "Property":
|
||||
"""
|
||||
Returns an instance of the plugin implementation.
|
||||
|
||||
This method retrieves the implementation of the plugin identified by
|
||||
`self.plugin_identifier` from the `PROPERTY_PLUGINS` mapping, then
|
||||
instantiates and returns it.
|
||||
|
||||
Returns:
|
||||
object: An instance of the plugin implementation.
|
||||
"""
|
||||
impl = s.PROPERTY_PLUGINS[self.plugin_identifier]
|
||||
instance = impl()
|
||||
return instance
|
||||
|
||||
def build_dataset(self):
|
||||
"""
|
||||
Required by general model contract but actual implementation resides in plugin.
|
||||
"""
|
||||
return
|
||||
|
||||
def build_model(self):
|
||||
from bridge.dto import BaseDTO
|
||||
|
||||
self.model_status = self.BUILDING
|
||||
self.save()
|
||||
|
||||
compounds = CompoundStructure.objects.filter(compound__package__in=self.data_packages.all())
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
_ = instance.build(eP)
|
||||
|
||||
self.model_status = self.BUILT_NOT_EVALUATED
|
||||
self.save()
|
||||
|
||||
def predict(self, smiles, *args, **kwargs) -> RunResult:
|
||||
return self.predict_batch([smiles], *args, **kwargs)
|
||||
|
||||
def predict_batch(self, smiles: List[str], *args, **kwargs) -> RunResult:
|
||||
from bridge.dto import BaseDTO, CompoundProto
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TempCompound(CompoundProto):
|
||||
url = None
|
||||
name = None
|
||||
smiles: str
|
||||
|
||||
batch = [TempCompound(smiles=smi) for smi in smiles]
|
||||
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, batch, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
return instance.run(eP, *args, **kwargs)
|
||||
|
||||
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
||||
from bridge.dto import BaseDTO
|
||||
|
||||
if self.model_status != self.BUILT_NOT_EVALUATED:
|
||||
raise ValueError("Model must be built before evaluation")
|
||||
|
||||
self.model_status = self.EVALUATING
|
||||
self.save()
|
||||
|
||||
if eval_packages is not None:
|
||||
for p in eval_packages:
|
||||
self.eval_packages.add(p)
|
||||
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
if self.eval_packages.count() > 0:
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
compounds = CompoundStructure.objects.filter(
|
||||
compound__package__in=self.data_packages.all()
|
||||
)
|
||||
else:
|
||||
reactions = Reaction.objects.filter(package__in=self.eval_packages.all())
|
||||
compounds = CompoundStructure.objects.filter(
|
||||
compound__package__in=self.eval_packages.all()
|
||||
)
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
try:
|
||||
if self.eval_packages.count() > 0:
|
||||
res = instance.evaluate(eP, **kwargs)
|
||||
self.eval_results = res.data
|
||||
else:
|
||||
res = instance.build_and_evaluate(eP)
|
||||
self.eval_results = self.compute_averages(res.data)
|
||||
|
||||
self.model_status = self.FINISHED
|
||||
self.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during evaluation: {type(e).__name__}, {e}")
|
||||
self.model_status = self.ERROR
|
||||
self.save()
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def compute_averages(data):
|
||||
sum_dict = {}
|
||||
for result in data:
|
||||
for key, value in result.items():
|
||||
sum_dict.setdefault(key, []).append(value)
|
||||
sum_dict = {k: sum(v) / len(data) for k, v in sum_dict.items()}
|
||||
return sum_dict
|
||||
|
||||
|
||||
class Scenario(EnviPathModel):
|
||||
@ -3790,11 +4031,6 @@ class Scenario(EnviPathModel):
|
||||
max_length=256, null=False, blank=False, default="Not specified"
|
||||
)
|
||||
|
||||
# for Referring Scenarios this property will be filled
|
||||
parent = models.ForeignKey("self", on_delete=models.CASCADE, default=None, null=True)
|
||||
|
||||
additional_information = models.JSONField(verbose_name="Additional Information")
|
||||
|
||||
def _url(self):
|
||||
return "{}/scenario/{}".format(self.package.url, self.uuid)
|
||||
|
||||
@ -3810,11 +4046,14 @@ class Scenario(EnviPathModel):
|
||||
):
|
||||
new_s = Scenario()
|
||||
new_s.package = package
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
|
||||
|
||||
new_s.name = name
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
@ -3826,19 +4065,14 @@ class Scenario(EnviPathModel):
|
||||
if scenario_type is not None and scenario_type.strip() != "":
|
||||
new_s.scenario_type = scenario_type
|
||||
|
||||
add_inf = defaultdict(list)
|
||||
|
||||
for info in additional_information:
|
||||
cls_name = info.__class__.__name__
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
|
||||
ai_data["uuid"] = f"{uuid4()}"
|
||||
add_inf[cls_name].append(ai_data)
|
||||
|
||||
new_s.additional_information = add_inf
|
||||
# TODO Remove
|
||||
new_s.additional_information = {}
|
||||
|
||||
new_s.save()
|
||||
|
||||
for ai in additional_information:
|
||||
AdditionalInformation.create(package, ai, scenario=new_s)
|
||||
|
||||
return new_s
|
||||
|
||||
@transaction.atomic
|
||||
@ -3852,19 +4086,9 @@ class Scenario(EnviPathModel):
|
||||
Returns:
|
||||
str: UUID of the created item
|
||||
"""
|
||||
cls_name = data.__class__.__name__
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
generated_uuid = str(uuid4())
|
||||
ai_data["uuid"] = generated_uuid
|
||||
ai = AdditionalInformation.create(self.package, ai=data, scenario=self)
|
||||
|
||||
if cls_name not in self.additional_information:
|
||||
self.additional_information[cls_name] = []
|
||||
|
||||
self.additional_information[cls_name].append(ai_data)
|
||||
self.save()
|
||||
|
||||
return generated_uuid
|
||||
return str(ai.uuid)
|
||||
|
||||
@transaction.atomic
|
||||
def update_additional_information(self, ai_uuid: str, data: "EnviPyModel") -> None:
|
||||
@ -3878,110 +4102,158 @@ class Scenario(EnviPathModel):
|
||||
Raises:
|
||||
ValueError: If item with given UUID not found or type mismatch
|
||||
"""
|
||||
found_type = None
|
||||
found_idx = -1
|
||||
ai = AdditionalInformation.objects.filter(uuid=ai_uuid, scenario=self)
|
||||
|
||||
# Find the item by UUID
|
||||
for type_name, items in self.additional_information.items():
|
||||
for idx, item_data in enumerate(items):
|
||||
if item_data.get("uuid") == ai_uuid:
|
||||
found_type = type_name
|
||||
found_idx = idx
|
||||
break
|
||||
if found_type:
|
||||
break
|
||||
if ai.exists() and ai.count() == 1:
|
||||
ai = ai.first()
|
||||
# Verify the model type matches (prevent type changes)
|
||||
new_type = data.__class__.__name__
|
||||
if new_type != ai.type:
|
||||
raise ValueError(
|
||||
f"Cannot change type from {ai.type} to {new_type}. "
|
||||
f"Delete and create a new item instead."
|
||||
)
|
||||
|
||||
if found_type is None:
|
||||
ai.data = data.__class__(
|
||||
**json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
).model_dump(mode="json")
|
||||
ai.save()
|
||||
else:
|
||||
raise ValueError(f"Additional information with UUID {ai_uuid} not found")
|
||||
|
||||
# Verify the model type matches (prevent type changes)
|
||||
new_type = data.__class__.__name__
|
||||
if new_type != found_type:
|
||||
raise ValueError(
|
||||
f"Cannot change type from {found_type} to {new_type}. "
|
||||
f"Delete and create a new item instead."
|
||||
)
|
||||
|
||||
# Update the item data, preserving UUID
|
||||
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
ai_data["uuid"] = ai_uuid
|
||||
|
||||
self.additional_information[found_type][found_idx] = ai_data
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def remove_additional_information(self, ai_uuid):
|
||||
found_type = None
|
||||
found_idx = -1
|
||||
ai = AdditionalInformation.objects.filter(uuid=ai_uuid, scenario=self)
|
||||
|
||||
for k, vals in self.additional_information.items():
|
||||
for i, v in enumerate(vals):
|
||||
if v["uuid"] == ai_uuid:
|
||||
found_type = k
|
||||
found_idx = i
|
||||
break
|
||||
|
||||
if found_type is not None and found_idx >= 0:
|
||||
if len(self.additional_information[found_type]) == 1:
|
||||
del self.additional_information[found_type]
|
||||
else:
|
||||
self.additional_information[found_type].pop(found_idx)
|
||||
self.save()
|
||||
if ai.exists() and ai.count() == 1:
|
||||
ai.delete()
|
||||
else:
|
||||
raise ValueError(f"Could not find additional information with uuid {ai_uuid}")
|
||||
|
||||
@transaction.atomic
|
||||
def set_additional_information(self, data: Dict[str, "EnviPyModel"]):
|
||||
new_ais = defaultdict(list)
|
||||
for k, vals in data.items():
|
||||
for v in vals:
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
|
||||
if hasattr(v, "uuid"):
|
||||
ai_data["uuid"] = str(v.uuid)
|
||||
else:
|
||||
ai_data["uuid"] = str(uuid4())
|
||||
raise NotImplementedError("Not implemented yet")
|
||||
|
||||
new_ais[k].append(ai_data)
|
||||
def get_additional_information(self, direct_only=True):
|
||||
ais = AdditionalInformation.objects.filter(scenario=self)
|
||||
|
||||
self.additional_information = new_ais
|
||||
self.save()
|
||||
|
||||
def get_additional_information(self):
|
||||
from envipy_additional_information import registry
|
||||
|
||||
for k, vals in self.additional_information.items():
|
||||
if k == "enzyme":
|
||||
continue
|
||||
|
||||
for v in vals:
|
||||
# Per default additional fields are ignored
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[k](**v)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load additional information {k}: {e}")
|
||||
if s.SENTRY_ENABLED:
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
capture_exception(e)
|
||||
|
||||
# Add uuid to uniquely identify objects for manipulation
|
||||
if "uuid" in v:
|
||||
inst.__dict__["uuid"] = v["uuid"]
|
||||
|
||||
yield inst
|
||||
if direct_only:
|
||||
return ais.filter(content_object__isnull=True)
|
||||
else:
|
||||
return ais
|
||||
|
||||
def related_pathways(self):
|
||||
scens = [self]
|
||||
if self.parent is not None:
|
||||
scens.append(self.parent)
|
||||
|
||||
return Pathway.objects.filter(
|
||||
scenarios__in=scens, package__reviewed=True, package=self.package
|
||||
scenarios=self, package__reviewed=True, package=self.package
|
||||
).distinct()
|
||||
|
||||
|
||||
class AdditionalInformation(models.Model):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
uuid = models.UUIDField(unique=True, default=uuid4, editable=False)
|
||||
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
|
||||
kv = JSONField(null=True, blank=True, default=dict)
|
||||
# class name of pydantic model
|
||||
type = models.TextField(blank=False, null=False, verbose_name="Additional Information Type")
|
||||
# serialized pydantic model
|
||||
data = models.JSONField(null=True, blank=True, default=dict)
|
||||
|
||||
# The link to scenario is optional - e.g. when setting predicted properties to objects
|
||||
scenario = models.ForeignKey(
|
||||
"epdb.Scenario",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scenario_additional_information",
|
||||
)
|
||||
|
||||
# Generic target (Compound/Reaction/Pathway/...)
|
||||
content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveBigIntegerField(null=True, blank=True)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
package: "Package",
|
||||
ai: "EnviPyModel",
|
||||
scenario=None,
|
||||
content_object=None,
|
||||
skip_cleaning=False,
|
||||
):
|
||||
add_inf = AdditionalInformation()
|
||||
add_inf.package = package
|
||||
add_inf.type = ai.__class__.__name__
|
||||
|
||||
# dump, sanitize, validate before saving
|
||||
_ai = ai.__class__(**json.loads(nh3.clean(ai.model_dump_json()).strip()))
|
||||
|
||||
add_inf.data = _ai.model_dump(mode="json")
|
||||
|
||||
if scenario is not None:
|
||||
add_inf.scenario = scenario
|
||||
|
||||
if content_object is not None:
|
||||
add_inf.content_object = content_object
|
||||
|
||||
add_inf.save()
|
||||
|
||||
return add_inf
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.url:
|
||||
self.url = self._url()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _url(self):
|
||||
if self.content_object is not None:
|
||||
return f"{self.content_object.url}/additional-information/{self.uuid}"
|
||||
|
||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
from envipy_additional_information import registry
|
||||
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[self.type](**self.data)
|
||||
except Exception as e:
|
||||
print(f"Error loading {self.type}: {e}")
|
||||
raise e
|
||||
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
|
||||
return inst
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.type} ({self.uuid})"
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["type"]),
|
||||
models.Index(fields=["scenario", "type"]),
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
models.Index(fields=["scenario", "content_type", "object_id"]),
|
||||
]
|
||||
constraints = [
|
||||
# Generic FK must be complete or empty
|
||||
models.CheckConstraint(
|
||||
name="ck_addinfo_gfk_pair",
|
||||
check=(
|
||||
(Q(content_type__isnull=True) & Q(object_id__isnull=True))
|
||||
| (Q(content_type__isnull=False) & Q(object_id__isnull=False))
|
||||
),
|
||||
),
|
||||
# Disallow "floating" info
|
||||
models.CheckConstraint(
|
||||
name="ck_addinfo_not_both_null",
|
||||
check=Q(scenario__isnull=False) | Q(content_type__isnull=False),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class UserSettingPermission(Permission):
|
||||
uuid = models.UUIDField(
|
||||
null=False, blank=False, verbose_name="UUID of this object", primary_key=True, default=uuid4
|
||||
@ -4028,6 +4300,13 @@ class Setting(EnviPathModel):
|
||||
null=True, blank=True, verbose_name="Setting Model Threshold", default=0.25
|
||||
)
|
||||
|
||||
property_models = models.ManyToManyField(
|
||||
"PropertyPluginModel",
|
||||
verbose_name="Setting Property Models",
|
||||
related_name="settings",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
expansion_scheme = models.CharField(
|
||||
max_length=20,
|
||||
choices=ExpansionSchemeChoice.choices,
|
||||
|
||||
@ -11,7 +11,17 @@ from django.core.mail import EmailMultiAlternatives
|
||||
from django.utils import timezone
|
||||
|
||||
from epdb.logic import SPathway
|
||||
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
|
||||
from epdb.models import (
|
||||
AdditionalInformation,
|
||||
Edge,
|
||||
EPModel,
|
||||
JobLog,
|
||||
Node,
|
||||
Pathway,
|
||||
Rule,
|
||||
Setting,
|
||||
User,
|
||||
)
|
||||
from utilities.chem import FormatConverter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -66,9 +76,9 @@ def mul(a, b):
|
||||
|
||||
|
||||
@shared_task(queue="predict")
|
||||
def predict_simple(model_pk: int, smiles: str):
|
||||
def predict_simple(model_pk: int, smiles: str, *args, **kwargs):
|
||||
mod = get_ml_model(model_pk)
|
||||
res = mod.predict(smiles)
|
||||
res = mod.predict(smiles, *args, **kwargs)
|
||||
return res
|
||||
|
||||
|
||||
@ -229,9 +239,28 @@ def predict(
|
||||
if JobLog.objects.filter(task_id=self.request.id).exists():
|
||||
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=pw.url)
|
||||
|
||||
# dispatch property job
|
||||
compute_properties.delay(pw_pk, pred_setting_pk)
|
||||
|
||||
return pw.url
|
||||
|
||||
|
||||
@shared_task(bind=True, queue="background")
|
||||
def compute_properties(self, pathway_pk: int, setting_pk: int):
|
||||
pw = Pathway.objects.get(id=pathway_pk)
|
||||
setting = Setting.objects.get(id=setting_pk)
|
||||
|
||||
nodes = [n for n in pw.nodes]
|
||||
smiles = [n.default_node_label.smiles for n in nodes]
|
||||
|
||||
for prop_mod in setting.property_models.all():
|
||||
if prop_mod.instance().is_heavy():
|
||||
rr = prop_mod.predict_batch(smiles)
|
||||
for idx, pred in enumerate(rr.result):
|
||||
n = nodes[idx]
|
||||
_ = AdditionalInformation.create(pw.package, ai=pred, content_object=n)
|
||||
|
||||
|
||||
@shared_task(bind=True, queue="background")
|
||||
def identify_missing_rules(
|
||||
self,
|
||||
|
||||
158
epdb/views.py
158
epdb/views.py
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Iterable
|
||||
|
||||
import nh3
|
||||
from django.conf import settings as s
|
||||
@ -28,6 +28,7 @@ from .logic import (
|
||||
UserManager,
|
||||
)
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
APIToken,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
@ -46,6 +47,7 @@ from .models import (
|
||||
Node,
|
||||
Pathway,
|
||||
Permission,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Rule,
|
||||
RuleBasedRelativeReasoning,
|
||||
@ -401,7 +403,7 @@ def breadcrumbs(
|
||||
def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
scens = []
|
||||
for scenario_url in scenario_urls:
|
||||
# As empty lists will be removed in POST request well send ['']
|
||||
# As empty lists will be removed in POST request we'll send ['']
|
||||
if scenario_url == "":
|
||||
continue
|
||||
|
||||
@ -413,6 +415,7 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
|
||||
|
||||
def set_aliases(current_user, attach_object, aliases: List[str]):
|
||||
# As empty lists will be removed in POST request we'll send ['']
|
||||
if aliases == [""]:
|
||||
aliases = []
|
||||
|
||||
@ -421,7 +424,7 @@ def set_aliases(current_user, attach_object, aliases: List[str]):
|
||||
|
||||
|
||||
def copy_object(current_user, target_package: "Package", source_object_url: str):
|
||||
# Ensures that source is readable
|
||||
# Ensures that source object is readable
|
||||
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
|
||||
|
||||
if source_package == target_package:
|
||||
@ -429,7 +432,7 @@ def copy_object(current_user, target_package: "Package", source_object_url: str)
|
||||
|
||||
parser = EPDBURLParser(source_object_url)
|
||||
|
||||
# if the url won't contain a package or is a plain package
|
||||
# if the url don't contain a package or is a plain package
|
||||
if not parser.contains_package_url():
|
||||
raise ValueError(f"Object {source_object_url} can't be copied!")
|
||||
|
||||
@ -714,12 +717,36 @@ def models(request):
|
||||
|
||||
# Keep model_types for potential modal/action use
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": "ml-relative-reasoning",
|
||||
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
|
||||
"EnviFormer": "enviformer",
|
||||
"ML Relative Reasoning": {
|
||||
"type": "ml-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"Rule Based Relative Reasoning": {
|
||||
"type": "rule-based-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v.display()] = k
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": v().requires_rule_packages,
|
||||
"requires_data_packages": v().requires_data_packages,
|
||||
}
|
||||
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "model"
|
||||
@ -830,16 +857,36 @@ def package_models(request, package_uuid):
|
||||
)
|
||||
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": "mlrr",
|
||||
"Rule Based Relative Reasoning": "rbrr",
|
||||
"ML Relative Reasoning": {
|
||||
"type": "ml-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"Rule Based Relative Reasoning": {
|
||||
"type": "rule-based-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.FLAGS.get("ENVIFORMER", False):
|
||||
context["model_types"]["EnviFormer"] = "enviformer"
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v.display()] = k
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": v().requires_rule_packages,
|
||||
"requires_data_packages": v().requires_data_packages,
|
||||
}
|
||||
|
||||
return render(request, "collections/models_paginated.html", context)
|
||||
|
||||
@ -900,8 +947,24 @@ def package_models(request, package_uuid):
|
||||
]
|
||||
|
||||
mod = RuleBasedRelativeReasoning.create(**params)
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS.values():
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
|
||||
pass
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
|
||||
params["plugin_identifier"] = model_type
|
||||
impl = s.PROPERTY_PLUGINS[model_type]
|
||||
inst = impl()
|
||||
|
||||
if inst.requires_rule_packages():
|
||||
params["rule_packages"] = [
|
||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||
]
|
||||
else:
|
||||
params["rule_packages"] = []
|
||||
|
||||
if not inst.requires_data_packages():
|
||||
del params["data_packages"]
|
||||
|
||||
mod = PropertyPluginModel.create(**params)
|
||||
else:
|
||||
return error(
|
||||
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
|
||||
@ -925,14 +988,18 @@ def package_model(request, package_uuid, model_uuid):
|
||||
if request.method == "GET":
|
||||
classify = request.GET.get("classify", False)
|
||||
ad_assessment = request.GET.get("app-domain-assessment", False)
|
||||
# TODO this needs to be generic
|
||||
half_life = request.GET.get("half_life", False)
|
||||
|
||||
if classify or ad_assessment:
|
||||
if any([classify, ad_assessment, half_life]):
|
||||
smiles = request.GET.get("smiles", "").strip()
|
||||
|
||||
# Check if smiles is non empty and valid
|
||||
if smiles == "":
|
||||
return JsonResponse({"error": "Received empty SMILES"}, status=400)
|
||||
|
||||
stereo = FormatConverter.has_stereo(smiles)
|
||||
|
||||
try:
|
||||
stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
except ValueError:
|
||||
@ -966,6 +1033,19 @@ def package_model(request, package_uuid, model_uuid):
|
||||
|
||||
return JsonResponse(res, safe=False)
|
||||
|
||||
elif half_life:
|
||||
from epdb.tasks import dispatch_eager, predict_simple
|
||||
|
||||
_, run_res = dispatch_eager(
|
||||
current_user, predict_simple, current_model.pk, stand_smiles, include_svg=True
|
||||
)
|
||||
|
||||
# Here we expect a single result
|
||||
if isinstance(run_res.result, Iterable):
|
||||
return JsonResponse(run_res.result[0].model_dump(mode="json"), safe=False)
|
||||
|
||||
return JsonResponse(run_res.result.model_dump(mode="json"), safe=False)
|
||||
|
||||
else:
|
||||
app_domain_assessment = current_model.app_domain.assess(stand_smiles)
|
||||
return JsonResponse(app_domain_assessment, safe=False)
|
||||
@ -980,7 +1060,11 @@ def package_model(request, package_uuid, model_uuid):
|
||||
context["model"] = current_model
|
||||
context["current_object"] = current_model
|
||||
|
||||
return render(request, "objects/model.html", context)
|
||||
if isinstance(current_model, PropertyPluginModel):
|
||||
context["plugin_identifier"] = current_model.plugin_identifier
|
||||
return render(request, "objects/model/property_model.html", context)
|
||||
else:
|
||||
return render(request, "objects/model/classification_model.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
if hidden := request.POST.get("hidden", None):
|
||||
@ -1940,6 +2024,7 @@ def package_pathways(request, package_uuid):
|
||||
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
|
||||
else:
|
||||
prediction_setting = current_user.prediction_settings()
|
||||
|
||||
pw = Pathway.create(
|
||||
current_package,
|
||||
stand_smiles,
|
||||
@ -2504,8 +2589,10 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
context["breadcrumbs"] = breadcrumbs(current_package, "scenario", current_scenario)
|
||||
|
||||
context["scenario"] = current_scenario
|
||||
# Get scenarios that have current_scenario as a parent
|
||||
context["children"] = current_scenario.scenario_set.order_by("name")
|
||||
|
||||
context["associated_additional_information"] = AdditionalInformation.objects.filter(
|
||||
scenario=current_scenario
|
||||
)
|
||||
|
||||
# Note: Modals now fetch schemas and data from API endpoints
|
||||
# Keeping these for backwards compatibility if needed elsewhere
|
||||
@ -2612,11 +2699,22 @@ def user(request, user_uuid):
|
||||
|
||||
context["user"] = requested_user
|
||||
|
||||
model_qs = EPModel.objects.none()
|
||||
for p in PackageManager.get_all_readable_packages(requested_user, include_reviewed=True):
|
||||
model_qs |= p.models
|
||||
accessible_packages = PackageManager.get_all_readable_packages(
|
||||
requested_user, include_reviewed=True
|
||||
)
|
||||
|
||||
context["models"] = model_qs
|
||||
property_models = PropertyPluginModel.objects.filter(
|
||||
package__in=accessible_packages
|
||||
).order_by("name")
|
||||
|
||||
tp_prediction_models = (
|
||||
EPModel.objects.filter(package__in=accessible_packages)
|
||||
.exclude(id__in=[pm.id for pm in property_models])
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
context["models"] = tp_prediction_models
|
||||
context["property_models"] = property_models
|
||||
|
||||
context["tokens"] = APIToken.objects.filter(user=requested_user)
|
||||
|
||||
@ -2853,6 +2951,18 @@ def settings(request):
|
||||
else:
|
||||
raise BadRequest("Neither Model-Based nor Rule-Based as Method selected!")
|
||||
|
||||
property_model_urls = request.POST.getlist("prediction-setting-property-models")
|
||||
|
||||
if property_model_urls:
|
||||
mods = []
|
||||
for pm_url in property_model_urls:
|
||||
model = PropertyPluginModel.objects.get(url=pm_url)
|
||||
|
||||
if PackageManager.readable(current_user, model.package):
|
||||
mods.append(model)
|
||||
|
||||
params["property_models"] = mods
|
||||
|
||||
created_setting = SettingManager.create_setting(
|
||||
current_user,
|
||||
name=name,
|
||||
|
||||
BIN
fixtures/db.dump
Normal file
BIN
fixtures/db.dump
Normal file
Binary file not shown.
361
pepper/__init__.py
Normal file
361
pepper/__init__.py
Normal file
@ -0,0 +1,361 @@
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import polars as pl
|
||||
|
||||
from pydantic import computed_field
|
||||
from sklearn.metrics import (
|
||||
mean_absolute_error,
|
||||
mean_squared_error,
|
||||
r2_score,
|
||||
root_mean_squared_error,
|
||||
)
|
||||
from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
# Once stable these will be exposed by enviPy-plugins lib
|
||||
from envipy_additional_information import register # noqa: I001
|
||||
from bridge.contracts import Property, PropertyType # noqa: I001
|
||||
from bridge.dto import (
|
||||
BuildResult,
|
||||
EnviPyDTO,
|
||||
EvaluationResult,
|
||||
PredictedProperty,
|
||||
RunResult,
|
||||
) # noqa: I001
|
||||
|
||||
from .impl.pepper import Pepper # noqa: I001
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register("pepperprediction")
|
||||
class PepperPrediction(PredictedProperty):
|
||||
mean: float | None
|
||||
std: float | None
|
||||
log_mean: float | None
|
||||
log_std: float | None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def svg(self, xscale="linear", quantiles=(0.01, 0.99), n_points=2000) -> Optional[str]:
|
||||
import io
|
||||
|
||||
import matplotlib.patches as mpatches
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from scipy import stats
|
||||
|
||||
"""
|
||||
Plot the lognormal distribution of chemical half-lives where parameters are
|
||||
given on a base-10 log scale: log10(half-life) ~ Normal(mu_log10, sigma_log10^2).
|
||||
|
||||
Shades:
|
||||
- x < a in green (Non-persistent)
|
||||
- a <= x <= b in yellow (Persistent)
|
||||
- x > b in red (Very persistent)
|
||||
|
||||
Legend shows the shaded color and the probability mass in each region.
|
||||
"""
|
||||
|
||||
sigma_log10 = self.log_std
|
||||
mu_log10 = self.log_mean
|
||||
|
||||
if sigma_log10 <= 0:
|
||||
raise ValueError("sigma_log10 must be > 0")
|
||||
# Persistent and Very Persistent thresholds in days from REACH (https://doi.org/10.26434/chemrxiv-2025-xmslf)
|
||||
p = 120
|
||||
vp = 180
|
||||
|
||||
# Convert base-10 log parameters to natural-log parameters for SciPy's lognorm
|
||||
ln10 = np.log(10.0)
|
||||
mu_ln = mu_log10 * ln10
|
||||
sigma_ln = sigma_log10 * ln10
|
||||
|
||||
# SciPy parameterization: lognorm(s=sigma_ln, scale=exp(mu_ln))
|
||||
dist = stats.lognorm(s=sigma_ln, scale=np.exp(mu_ln))
|
||||
|
||||
# Exact probabilities
|
||||
p_green = dist.cdf(p) # P(X < a)
|
||||
p_yellow = dist.cdf(vp) - p_green # P(a <= X <= b)
|
||||
p_red = 1.0 - dist.cdf(vp) # P(X > b)
|
||||
|
||||
# Plotting range
|
||||
q_low, q_high = dist.ppf(quantiles)
|
||||
x_min = max(1e-12, min(q_low, p) * 0.9)
|
||||
x_max = max(q_high, vp) * 1.1
|
||||
|
||||
# Build x-grid (linear days axis)
|
||||
if xscale == "log":
|
||||
x = np.logspace(np.log10(x_min), np.log10(x_max), n_points)
|
||||
else:
|
||||
x = np.linspace(x_min, x_max, n_points)
|
||||
y = dist.pdf(x)
|
||||
|
||||
# Masks for shading
|
||||
mask_green = x < p
|
||||
mask_yellow = (x >= p) & (x <= vp)
|
||||
mask_red = x > vp
|
||||
|
||||
# Plot
|
||||
fig, ax = plt.subplots(figsize=(9, 5.5))
|
||||
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
||||
|
||||
if np.any(mask_green):
|
||||
ax.fill_between(x[mask_green], y[mask_green], 0, color="tab:green", alpha=0.3)
|
||||
if np.any(mask_yellow):
|
||||
ax.fill_between(x[mask_yellow], y[mask_yellow], 0, color="gold", alpha=0.35)
|
||||
if np.any(mask_red):
|
||||
ax.fill_between(x[mask_red], y[mask_red], 0, color="tab:red", alpha=0.3)
|
||||
|
||||
# Threshold lines
|
||||
ax.axvline(p, color="gray", ls="--", lw=1)
|
||||
ax.axvline(vp, color="gray", ls="--", lw=1)
|
||||
|
||||
# Labels & title
|
||||
ax.set_title(
|
||||
f"Half-life Distribution (Lognormal)\nlog10 parameters: μ={mu_log10:g}, σ={sigma_log10:g}"
|
||||
)
|
||||
ax.set_xlabel("Half-life (days)")
|
||||
ax.set_ylabel("Probability density")
|
||||
ax.grid(True, alpha=0.25)
|
||||
|
||||
if xscale == "log":
|
||||
ax.set_xscale("log") # not used in this example, but supported
|
||||
|
||||
# Legend with probabilities
|
||||
patches = [
|
||||
mpatches.Patch(
|
||||
color="tab:green",
|
||||
alpha=0.3,
|
||||
label=f"Non-persistent (<{p:g} d): {p_green:.2%}",
|
||||
),
|
||||
mpatches.Patch(
|
||||
color="gold",
|
||||
alpha=0.35,
|
||||
label=f"Persistent ({p:g}–{vp:g} d): {p_yellow:.2%}",
|
||||
),
|
||||
mpatches.Patch(
|
||||
color="tab:red",
|
||||
alpha=0.3,
|
||||
label=f"Very persistent (>{vp:g} d): {p_red:.2%}",
|
||||
),
|
||||
]
|
||||
ax.legend(handles=patches, frameon=True)
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
# --- Export to SVG string ---
|
||||
buf = io.StringIO()
|
||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||
svg = buf.getvalue()
|
||||
plt.close(fig)
|
||||
buf.close()
|
||||
|
||||
return svg
|
||||
|
||||
|
||||
class PEPPER(Property):
|
||||
def identifier(self) -> str:
|
||||
return "pepper"
|
||||
|
||||
def display(self) -> str:
|
||||
return "PEPPER"
|
||||
|
||||
def name(self) -> str:
|
||||
return "Predict Environmental Pollutant PERsistence"
|
||||
|
||||
def requires_rule_packages(self) -> bool:
|
||||
return False
|
||||
|
||||
def requires_data_packages(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_type(self) -> PropertyType:
|
||||
return PropertyType.HEAVY
|
||||
|
||||
def generate_dataset(self, eP: EnviPyDTO) -> pl.DataFrame:
|
||||
"""
|
||||
Generates a dataset in the form of a Polars DataFrame containing compound information, including
|
||||
SMILES strings and logarithmic values of degradation half-lives (dt50).
|
||||
|
||||
The dataset is built by iterating over a list of compounds, standardizing SMILES strings, and
|
||||
calculating the logarithmic mean of the half-life intervals for different environmental scenarios
|
||||
associated with each compound.
|
||||
|
||||
The resulting DataFrame will only include unique rows based on SMILES and logarithmic half-life
|
||||
values.
|
||||
|
||||
Parameters:
|
||||
eP (EnviPyDTO): An object that provides access to compound data and utility functions for
|
||||
standardization and retrieval of half-life information.
|
||||
|
||||
Returns:
|
||||
pl.DataFrame: The resulting dataset with unique rows containing compound structure identifiers,
|
||||
standardized SMILES strings, and logarithmic half-life values.
|
||||
|
||||
Raises:
|
||||
Exception: Exceptions are caught and logged during data processing, specifically when retrieving
|
||||
half-life information.
|
||||
|
||||
Note:
|
||||
- The logarithmic mean is calculated from the start and end intervals of the dt50 (half-life).
|
||||
- Compounds not associated with any half-life data are skipped, and errors encountered during processing
|
||||
are logged without halting the execution.
|
||||
"""
|
||||
columns = ["structure_id", "smiles", "dt50_log"]
|
||||
rows = []
|
||||
|
||||
for c in eP.get_compounds():
|
||||
hls = c.half_lifes()
|
||||
|
||||
if len(hls):
|
||||
stand_smiles = eP.standardize(c.smiles, remove_stereo=True)
|
||||
for scenario, half_lives in hls.items():
|
||||
for h in half_lives:
|
||||
# In the original Pepper code they take the mean of the start and end interval.
|
||||
half_mean = (h.dt50.start + h.dt50.end) / 2
|
||||
rows.append([str(c.url), stand_smiles, math.log10(half_mean)])
|
||||
|
||||
df = pl.DataFrame(data=rows, schema=columns, orient="row", infer_schema_length=None)
|
||||
|
||||
df = df.unique(subset=["smiles", "dt50_log"], keep="any", maintain_order=False)
|
||||
|
||||
return df
|
||||
|
||||
def save_dataset(self, df: pl.DataFrame, path: str):
|
||||
with open(path, "wb") as fh:
|
||||
pickle.dump(df, fh)
|
||||
|
||||
def load_dataset(self, path: str) -> pl.DataFrame:
|
||||
with open(path, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
|
||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||
logger.info(f"Start building PEPPER {eP.get_context().uuid}")
|
||||
df = self.generate_dataset(eP)
|
||||
|
||||
if df.shape[0] == 0:
|
||||
raise ValueError("No data found for building model")
|
||||
|
||||
p = Pepper()
|
||||
|
||||
p, train_ds = p.train_model(df)
|
||||
|
||||
ds_store_path = os.path.join(
|
||||
eP.get_context().work_dir, f"pepper_ds_{eP.get_context().uuid}.pkl"
|
||||
)
|
||||
self.save_dataset(train_ds, ds_store_path)
|
||||
|
||||
model_store_path = os.path.join(
|
||||
eP.get_context().work_dir, f"pepper_{eP.get_context().uuid}.pkl"
|
||||
)
|
||||
p.save_model(model_store_path)
|
||||
logger.info(f"Finished building PEPPER {eP.get_context().uuid}")
|
||||
|
||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||
load_path = os.path.join(eP.get_context().work_dir, f"pepper_{eP.get_context().uuid}.pkl")
|
||||
|
||||
p = Pepper.load_model(load_path)
|
||||
|
||||
X_new = [c.smiles for c in eP.get_compounds()]
|
||||
|
||||
predictions = p.predict_batch(X_new)
|
||||
|
||||
results = []
|
||||
|
||||
for p in zip(*predictions):
|
||||
if p[0] is None or p[1] is None:
|
||||
result = {"log_mean": None, "mean": None, "log_std": None, "std": None, "svg": None}
|
||||
else:
|
||||
result = {
|
||||
"log_mean": p[0],
|
||||
"mean": 10 ** p[0],
|
||||
"log_std": p[1],
|
||||
"std": 10 ** p[1],
|
||||
}
|
||||
|
||||
results.append(PepperPrediction(**result))
|
||||
|
||||
rr = RunResult(
|
||||
producer=eP.get_context().url,
|
||||
description=f"Generated at {datetime.now()}",
|
||||
result=results,
|
||||
)
|
||||
|
||||
return rr
|
||||
|
||||
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult | None:
|
||||
logger.info(f"Start evaluating PEPPER {eP.get_context().uuid}")
|
||||
load_path = os.path.join(eP.get_context().work_dir, f"pepper_{eP.get_context().uuid}.pkl")
|
||||
|
||||
p = Pepper.load_model(load_path)
|
||||
|
||||
df = self.generate_dataset(eP)
|
||||
ds = p.preprocess_data(df)
|
||||
|
||||
y_pred = p.predict_batch(ds["smiles"])
|
||||
|
||||
# We only need the mean
|
||||
if isinstance(y_pred, tuple):
|
||||
y_pred = y_pred[0]
|
||||
|
||||
res = self.eval_stats(ds["dt50_bayesian_mean"], y_pred)
|
||||
|
||||
logger.info(f"Finished evaluating PEPPER {eP.get_context().uuid}")
|
||||
return EvaluationResult(data=res)
|
||||
|
||||
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult | None:
|
||||
logger.info(f"Start evaluating PEPPER {eP.get_context().uuid}")
|
||||
ds_load_path = os.path.join(
|
||||
eP.get_context().work_dir, f"pepper_ds_{eP.get_context().uuid}.pkl"
|
||||
)
|
||||
ds = self.load_dataset(ds_load_path)
|
||||
|
||||
n_splits = kwargs.get("n_splits", 20)
|
||||
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42)
|
||||
|
||||
fold_metrics: List[dict[str, Any]] = []
|
||||
for split_id, (train_index, test_index) in enumerate(shuff.split(ds)):
|
||||
logger.info(f"Evaluation fold {split_id}/{n_splits} PEPPER {eP.get_context().uuid}")
|
||||
train = ds[train_index]
|
||||
test = ds[test_index]
|
||||
model = Pepper()
|
||||
model.train_model(train, preprocess=False)
|
||||
|
||||
features = test[model.descriptors.get_descriptor_names()].rows()
|
||||
y_pred = model.predict_batch(features, is_smiles=False)
|
||||
|
||||
# We only need the mean for eval statistics but mean, std can be returned
|
||||
if isinstance(y_pred, tuple) or isinstance(y_pred, list):
|
||||
y_pred = y_pred[0]
|
||||
|
||||
# Remove None if they occur
|
||||
y_true_filtered, y_pred_filtered = [], []
|
||||
for t, p in zip(test["dt50_bayesian_mean"], y_pred):
|
||||
if p is None:
|
||||
continue
|
||||
y_true_filtered.append(t)
|
||||
y_pred_filtered.append(p)
|
||||
|
||||
if len(y_true_filtered) == 0:
|
||||
print("Skipping empty fold")
|
||||
continue
|
||||
|
||||
fold_metrics.append(self.eval_stats(y_true_filtered, y_pred_filtered))
|
||||
|
||||
logger.info(f"Finished evaluating PEPPER {eP.get_context().uuid}")
|
||||
return EvaluationResult(data=fold_metrics)
|
||||
|
||||
@staticmethod
|
||||
def eval_stats(y_true, y_pred) -> dict[str, float]:
|
||||
scores_dic = {
|
||||
"r2": r2_score(y_true, y_pred),
|
||||
"mse": mean_squared_error(y_true, y_pred),
|
||||
"rmse": root_mean_squared_error(y_true, y_pred),
|
||||
"mae": mean_absolute_error(y_true, y_pred),
|
||||
}
|
||||
return scores_dic
|
||||
0
pepper/impl/__init__.py
Normal file
0
pepper/impl/__init__.py
Normal file
196
pepper/impl/bayesian.py
Normal file
196
pepper/impl/bayesian.py
Normal file
@ -0,0 +1,196 @@
|
||||
import emcee
|
||||
import numpy as np
|
||||
from scipy.stats import lognorm, norm
|
||||
|
||||
|
||||
class Bayesian:
|
||||
def __init__(self, y, comment_list=None):
|
||||
if comment_list is None:
|
||||
comment_list = []
|
||||
self.y = y
|
||||
self.comment_list = comment_list
|
||||
# LOQ default settings
|
||||
self.LOQ_lower = -1 # (2.4 hours)
|
||||
self.LOQ_upper = 3 # 1000 days
|
||||
# prior default settings
|
||||
self.prior_mu_mean = 1.5
|
||||
self.prior_mu_std = 2
|
||||
self.prior_sigma_mean = 0.4
|
||||
self.prior_sigma_std = 0.4
|
||||
self.lower_limit_sigma = 0.2
|
||||
# EMCEE defaults
|
||||
self.nwalkers = 10
|
||||
self.iterations = 2000
|
||||
self.burn_in = 100
|
||||
ndim = 2 # number of dimensions (mean, std)
|
||||
# backend = emcee.backends.HDFBackend("backend.h5")
|
||||
# backend.reset(self.nwalkers, ndim)
|
||||
self.sampler = emcee.EnsembleSampler(self.nwalkers, ndim, self.logPosterior)
|
||||
self.posterior_mu = None
|
||||
self.posterior_sigma = None
|
||||
|
||||
def get_censored_values_only(self):
|
||||
censored_values = []
|
||||
for i, comment in enumerate(self.comment_list):
|
||||
if comment in ["<", ">"]:
|
||||
censored_values.append(self.y[i])
|
||||
elif self.y[i] > self.LOQ_upper or self.y[i] < self.LOQ_lower:
|
||||
censored_values.append(self.y[i])
|
||||
return censored_values
|
||||
|
||||
# Class functions
|
||||
def determine_LOQ(self):
|
||||
"""
|
||||
Determines if the LOQ is upper or lower, and the value (if not default)
|
||||
:return: upper_LOQ , lower_LOQ
|
||||
"""
|
||||
|
||||
censored_values = self.get_censored_values_only()
|
||||
|
||||
# Find upper LOQ
|
||||
upper_LOQ = np.nan
|
||||
# bigger than global LOQ
|
||||
if max(self.y) >= self.LOQ_upper:
|
||||
upper_LOQ = self.LOQ_upper
|
||||
# case if exactly 365 days
|
||||
elif max(self.y) == 2.562: # 365 days
|
||||
upper_LOQ = 2.562
|
||||
self.LOQ_upper = upper_LOQ
|
||||
# case if "bigger than" indication in comments
|
||||
elif ">" in self.comment_list:
|
||||
i = 0
|
||||
while i < len(self.y):
|
||||
if self.y[i] == min(censored_values) and self.comment_list[i] == ">":
|
||||
self.LOQ_upper = self.y[i]
|
||||
break
|
||||
i += 1
|
||||
|
||||
# Find lower LOQ
|
||||
lower_LOQ = np.nan
|
||||
# smaller than global LOQ
|
||||
if min(self.y) <= self.LOQ_lower:
|
||||
lower_LOQ = self.LOQ_lower
|
||||
# case if exactly 1 day
|
||||
elif min(self.y) == 0: # 1 day
|
||||
lower_LOQ = 0
|
||||
self.LOQ_lower = 0
|
||||
# case if "smaller than" indication in comments
|
||||
elif "<" in self.comment_list:
|
||||
i = 0
|
||||
while i < len(self.y):
|
||||
if self.y[i] == max(censored_values) and self.comment_list[i] == "<":
|
||||
self.LOQ_lower = self.y[i]
|
||||
break
|
||||
i += 1
|
||||
return upper_LOQ, lower_LOQ
|
||||
|
||||
def logLikelihood(self, theta, sigma):
|
||||
"""
|
||||
Likelihood function (the probability of a dataset (mean, std) given the model parameters)
|
||||
Convert not censored observations into type ’numeric’
|
||||
:param theta: mean half-life value to be evaluated
|
||||
:param sigma: std half-life value to be evaluated
|
||||
:return: log_likelihood
|
||||
"""
|
||||
upper_LOQ, lower_LOQ = self.determine_LOQ()
|
||||
|
||||
n_censored_upper = 0
|
||||
n_censored_lower = 0
|
||||
y_not_cen = []
|
||||
|
||||
if np.isnan(upper_LOQ) and np.isnan(lower_LOQ):
|
||||
y_not_cen = self.y
|
||||
else:
|
||||
for i in self.y:
|
||||
if np.isnan(upper_LOQ) and i >= upper_LOQ: # censor above threshold
|
||||
n_censored_upper += 1
|
||||
if np.isnan(lower_LOQ) and i <= lower_LOQ: # censor below threshold
|
||||
n_censored_lower += 1
|
||||
else: # do not censor
|
||||
y_not_cen.append(i)
|
||||
|
||||
LL_left_cen = 0
|
||||
LL_right_cen = 0
|
||||
LL_not_cen = 0
|
||||
|
||||
# likelihood for not censored observations
|
||||
if n_censored_lower > 0: # loglikelihood for left censored observations
|
||||
LL_left_cen = n_censored_lower * norm.logcdf(
|
||||
lower_LOQ, loc=theta, scale=sigma
|
||||
) # cumulative distribution function CDF
|
||||
|
||||
if n_censored_upper > 0: # loglikelihood for right censored observations
|
||||
LL_right_cen = n_censored_upper * norm.logsf(
|
||||
upper_LOQ, loc=theta, scale=sigma
|
||||
) # survival function (1-CDF)
|
||||
|
||||
if len(y_not_cen) > 0: # loglikelihood for uncensored values
|
||||
LL_not_cen = sum(
|
||||
norm.logpdf(y_not_cen, loc=theta, scale=sigma)
|
||||
) # probability density function PDF
|
||||
|
||||
return LL_left_cen + LL_not_cen + LL_right_cen
|
||||
|
||||
def get_prior_probability_sigma(self, sigma):
|
||||
# convert mean and sd to logspace parameters, to see this formula check
|
||||
# https://en.wikipedia.org/wiki/Log-normal_distribution under Method of moments section
|
||||
temp = 1 + (self.prior_sigma_std / self.prior_sigma_mean) ** 2
|
||||
meanlog = self.prior_sigma_mean / np.sqrt(temp)
|
||||
sdlog = np.sqrt(np.log(temp))
|
||||
# calculate of logpdf of sigma
|
||||
norm_pdf_sigma = lognorm.logpdf(sigma, s=sdlog, loc=self.lower_limit_sigma, scale=meanlog)
|
||||
return norm_pdf_sigma
|
||||
|
||||
def get_prior_probability_theta(self, theta):
|
||||
norm_pdf_theta = norm.logpdf(theta, loc=self.prior_mu_mean, scale=self.prior_mu_std)
|
||||
return norm_pdf_theta
|
||||
|
||||
def logPrior(self, par):
|
||||
"""
|
||||
Obtain prior loglikelihood of [theta, sigma]
|
||||
:param par: par = [theta,sigma]
|
||||
:return: loglikelihood
|
||||
"""
|
||||
# calculate the mean and standard deviation in the log-space
|
||||
norm_pdf_mean = self.get_prior_probability_theta(par[0])
|
||||
norm_pdf_std = self.get_prior_probability_sigma(par[1])
|
||||
log_norm_pdf = [norm_pdf_mean, norm_pdf_std]
|
||||
return sum(log_norm_pdf)
|
||||
|
||||
def logPosterior(self, par):
|
||||
"""
|
||||
Obtain posterior loglikelihood
|
||||
:param par: [theta, sigma]
|
||||
:return: posterior loglikelihood
|
||||
"""
|
||||
logpri = self.logPrior(par)
|
||||
if not np.isfinite(logpri):
|
||||
return -np.inf
|
||||
loglikelihood = self.logLikelihood(par[0], par[1])
|
||||
return logpri + loglikelihood
|
||||
|
||||
def get_posterior_distribution(self):
|
||||
"""
|
||||
Sample posterior distribution and get median of mean and std samples
|
||||
:return: posterior half-life mean and std
|
||||
"""
|
||||
if self.posterior_mu:
|
||||
return self.posterior_mu, self.posterior_sigma
|
||||
|
||||
# Sampler parameters
|
||||
ndim = 2 # number of dimensions (mean,std)
|
||||
p0 = abs(np.random.randn(self.nwalkers, ndim)) # only positive starting numbers (for std)
|
||||
|
||||
# Sample distribution
|
||||
self.sampler.run_mcmc(p0, self.iterations)
|
||||
# get chain and log_prob in one-dimensional array (merged chains with burn-in)
|
||||
samples = self.sampler.get_chain(flat=True, discard=100)
|
||||
# get median mean and std
|
||||
self.posterior_mu = np.median(samples[:, 0])
|
||||
self.posterior_sigma = np.median(samples[:, 1])
|
||||
return self.posterior_mu, self.posterior_sigma
|
||||
|
||||
|
||||
# Utility functions
|
||||
def get_normal_distribution(x, mu, sig):
|
||||
return np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
|
||||
@ -0,0 +1,11 @@
|
||||
GPR:
|
||||
name: Gaussian Process Regressor
|
||||
regressor: GaussianProcessRegressor
|
||||
regressor_params:
|
||||
normalize_y: True
|
||||
n_restarts_optimizer: 0
|
||||
kernel: "ConstantKernel(1.0, (1e-3, 1e3)) * Matern(length_scale=2.5, length_scale_bounds=(1e-3, 1e3), nu=0.5)"
|
||||
feature_reduction_method: None
|
||||
feature_reduction_parameters:
|
||||
pca:
|
||||
n_components: 34
|
||||
60
pepper/impl/descriptors.py
Normal file
60
pepper/impl/descriptors.py
Normal file
@ -0,0 +1,60 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from mordred import Calculator, descriptors
|
||||
from padelpy import from_smiles
|
||||
from rdkit import Chem
|
||||
|
||||
|
||||
class Descriptor(ABC):
|
||||
@abstractmethod
|
||||
def get_molecule_descriptors(self, molecule: str) -> List[float | int] | None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_descriptor_names(self) -> List[str]:
|
||||
pass
|
||||
|
||||
|
||||
class Mordred(Descriptor):
|
||||
calc = Calculator(descriptors, ignore_3D=True)
|
||||
|
||||
def get_molecule_descriptors(self, molecule: str) -> List[float | int] | None:
|
||||
mol = Chem.MolFromSmiles(molecule)
|
||||
res = list(self.calc(mol))
|
||||
return res
|
||||
|
||||
def get_descriptor_names(self) -> List[str]:
|
||||
return [f"Mordred_{i}" for i in range(len(self.calc.descriptors))]
|
||||
|
||||
|
||||
class PaDEL(Descriptor):
|
||||
calc = Calculator(descriptors)
|
||||
|
||||
def get_molecule_descriptors(self, molecule: str) -> List[float | int] | None:
|
||||
try:
|
||||
padel_descriptors = from_smiles(molecule, threads=1)
|
||||
except RuntimeError:
|
||||
return []
|
||||
|
||||
formatted = []
|
||||
for k, v in padel_descriptors.items():
|
||||
try:
|
||||
formatted.append(float(v))
|
||||
except ValueError:
|
||||
formatted.append(0.0)
|
||||
|
||||
return formatted
|
||||
|
||||
def get_descriptor_names(self) -> List[str]:
|
||||
return [f"PaDEL_{i}" for i in range(1875)]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mol = "CC1=CC(O)=CC=C1[N+](=O)[O-]"
|
||||
|
||||
m = Mordred()
|
||||
print(list(m.get_molecule_descriptors(mol)))
|
||||
|
||||
p = PaDEL()
|
||||
print(list(p.get_molecule_descriptors(mol)))
|
||||
329
pepper/impl/pepper.py
Normal file
329
pepper/impl/pepper.py
Normal file
@ -0,0 +1,329 @@
|
||||
import importlib.resources
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import yaml
|
||||
from joblib import Parallel, delayed
|
||||
from scipy.cluster import hierarchy
|
||||
from scipy.spatial.distance import squareform
|
||||
from scipy.stats import spearmanr
|
||||
from sklearn.feature_selection import VarianceThreshold
|
||||
from sklearn.gaussian_process import GaussianProcessRegressor
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import FunctionTransformer, MinMaxScaler
|
||||
|
||||
from .bayesian import Bayesian
|
||||
from .descriptors import Mordred
|
||||
|
||||
|
||||
class Pepper:
|
||||
def __init__(self, config_path=None, random_state=42):
|
||||
self.random_state = random_state
|
||||
if config_path is None:
|
||||
config_path = importlib.resources.files("pepper.impl.config").joinpath(
|
||||
"regressor_settings_singlevalue_soil_paper_GPR_optimized.yml"
|
||||
)
|
||||
with open(config_path, "r") as file:
|
||||
regressor_settings = yaml.safe_load(file)
|
||||
if len(regressor_settings) > 1:
|
||||
logging.warning(
|
||||
f"More than one regressor config found in {config_path}, using the first one"
|
||||
)
|
||||
self.regressor_settings = regressor_settings[list(regressor_settings.keys())[0]]
|
||||
if "kernel" in self.regressor_settings["regressor_params"]:
|
||||
from sklearn.gaussian_process.kernels import ConstantKernel, Matern # noqa: F401
|
||||
|
||||
# We could hard-code the kernels they have, maybe better than using eval
|
||||
self.regressor_settings["regressor_params"]["kernel"] = eval(
|
||||
self.regressor_settings["regressor_params"]["kernel"]
|
||||
)
|
||||
# We assume the YAML has the key regressor containing a regressor name
|
||||
self.regressor = self.get_regressor_by_name(self.regressor_settings["regressor"])
|
||||
if "regressor_params" in self.regressor_settings: # Set params if any are given
|
||||
self.regressor.set_params(**self.regressor_settings["regressor_params"])
|
||||
|
||||
# TODO we could make this configurable
|
||||
self.descriptors = Mordred()
|
||||
self.descriptor_subset = None
|
||||
|
||||
self.min_max_scaler = MinMaxScaler().set_output(transform="polars")
|
||||
self.feature_preselector = Pipeline(
|
||||
[
|
||||
(
|
||||
"variance_threshold",
|
||||
VarianceThreshold(threshold=0.02).set_output(transform="polars"),
|
||||
),
|
||||
# Feature selection based on variance threshold
|
||||
(
|
||||
"custom_feature_selection",
|
||||
FunctionTransformer(
|
||||
func=self.remove_highly_correlated_features,
|
||||
validate=False,
|
||||
kw_args={"corr_method": "spearman", "cluster_threshold": 0.01},
|
||||
).set_output(transform="polars"),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_regressor_by_name(self, regressor_string):
|
||||
"""
|
||||
Load regressor function from a regressor name
|
||||
:param regressor_string: name of regressor as defined in config file (function name with parentheses)
|
||||
:return: Regressor object
|
||||
"""
|
||||
# if regressor_string == 'RandomForestRegressor':
|
||||
# return RandomForestRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'GradientBoostingRegressor':
|
||||
# return GradientBoostingRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'AdaBoostRegressor':
|
||||
# return AdaBoostRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'MLPRegressor':
|
||||
# return MLPRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'SVR':
|
||||
# return SVR()
|
||||
# elif regressor_string == 'KNeighborsRegressor':
|
||||
# return KNeighborsRegressor()
|
||||
if regressor_string == "GaussianProcessRegressor":
|
||||
return GaussianProcessRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'DecisionTreeRegressor':
|
||||
# return DecisionTreeRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'Ridge':
|
||||
# return Ridge(random_state=self.random_state)
|
||||
# elif regressor_string == 'SGDRegressor':
|
||||
# return SGDRegressor(random_state=self.random_state)
|
||||
# elif regressor_string == 'KernelRidge':
|
||||
# return KernelRidge()
|
||||
# elif regressor_string == 'LinearRegression':
|
||||
# return LinearRegression()
|
||||
# elif regressor_string == 'LSVR':
|
||||
# return SVR(kernel='linear') # Linear Support Vector Regressor
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"No regressor type defined for regressor_string = {regressor_string}"
|
||||
)
|
||||
|
||||
def train_model(self, train_data, preprocess=True):
|
||||
"""
|
||||
Fit self.regressor and preprocessors. train_data is a pl.DataFrame
|
||||
"""
|
||||
if preprocess:
|
||||
# Compute the mean and std of half-lives per structure
|
||||
train_data = self.preprocess_data(train_data)
|
||||
|
||||
# train_data structure:
|
||||
# columns = [
|
||||
# "structure_id",
|
||||
# "smiles",
|
||||
# "dt50_log",
|
||||
# "dt50_bayesian_mean",
|
||||
# "dt50_bayesian_std",
|
||||
# ] + self.descriptors.get_descriptor_names()
|
||||
|
||||
# only select descriptor features for feature preselector
|
||||
df = train_data[self.descriptors.get_descriptor_names()]
|
||||
|
||||
# Remove columns having at least None, nan, inf, "" value
|
||||
df = Pepper.keep_clean_columns(df)
|
||||
|
||||
# Scale and Remove highly correlated features as well as features having a low variance
|
||||
x_train_normal = self.min_max_scaler.fit_transform(df)
|
||||
x_train_normal = self.feature_preselector.fit_transform(x_train_normal)
|
||||
|
||||
# Store subset, as this is the input used for prediction
|
||||
self.descriptor_subset = x_train_normal.columns
|
||||
|
||||
y_train = train_data["dt50_bayesian_mean"].to_numpy()
|
||||
y_train_std = train_data["dt50_bayesian_std"].to_numpy()
|
||||
|
||||
self.regressor.set_params(alpha=y_train_std)
|
||||
self.regressor.fit(x_train_normal, y_train)
|
||||
|
||||
return self, train_data
|
||||
|
||||
@staticmethod
|
||||
def keep_clean_columns(df: pl.DataFrame) -> pl.DataFrame:
|
||||
"""
|
||||
Filters out columns from the DataFrame that contain null values, NaN, or infinite values.
|
||||
|
||||
This static method takes a DataFrame as input and evaluates each of its columns to determine
|
||||
if the column contains invalid values. Columns that have null values, NaN, or infinite values
|
||||
are excluded from the resulting DataFrame. The method is especially useful for cleaning up a
|
||||
dataset by keeping only the valid columns.
|
||||
|
||||
Parameters:
|
||||
df (polars.DataFrame): The input DataFrame to be cleaned.
|
||||
|
||||
Returns:
|
||||
polars.DataFrame: A DataFrame containing only columns without null, NaN, or infinite values.
|
||||
"""
|
||||
valid_cols = []
|
||||
|
||||
for col in df.columns:
|
||||
s = df[col]
|
||||
|
||||
# Check nulls
|
||||
has_null = s.null_count() > 0
|
||||
|
||||
# Check NaN and inf only for numeric columns
|
||||
if s.dtype.is_numeric():
|
||||
has_nan = s.is_nan().any()
|
||||
has_inf = s.is_infinite().any()
|
||||
else:
|
||||
has_nan = False
|
||||
has_inf = False
|
||||
|
||||
if not (has_null or has_nan or has_inf):
|
||||
valid_cols.append(col)
|
||||
|
||||
return df.select(valid_cols)
|
||||
|
||||
def preprocess_data(self, dataset):
|
||||
groups = [group for group in dataset.group_by("structure_id")]
|
||||
|
||||
# Unless explicitly set compute everything serial
|
||||
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
||||
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
||||
delayed(compute_bayes_per_group)(group[1])
|
||||
for group in dataset.group_by("structure_id")
|
||||
)
|
||||
else:
|
||||
results = []
|
||||
for g in groups:
|
||||
results.append(compute_bayes_per_group(g[1]))
|
||||
|
||||
bayes_stats = pl.concat(results, how="vertical")
|
||||
dataset = dataset.join(bayes_stats, on="structure_id", how="left")
|
||||
|
||||
# Remove duplicates after calculating mean, std
|
||||
dataset = dataset.unique(subset="structure_id")
|
||||
|
||||
# Calculate and normalise features, make a "desc" column with the features
|
||||
dataset = dataset.with_columns(
|
||||
pl.col("smiles")
|
||||
.map_elements(
|
||||
self.descriptors.get_molecule_descriptors, return_dtype=pl.List(pl.Float64)
|
||||
)
|
||||
.alias("desc")
|
||||
)
|
||||
|
||||
# If a SMILES fails to get desc it is removed
|
||||
dataset = dataset.filter(pl.col("desc").is_not_null() & (pl.col("desc").list.len() > 0))
|
||||
|
||||
# Flatten the features into the dataset
|
||||
dataset = dataset.with_columns(
|
||||
pl.col("desc").list.to_struct(fields=self.descriptors.get_descriptor_names())
|
||||
).unnest("desc")
|
||||
|
||||
return dataset
|
||||
|
||||
def predict_batch(self, batch: List[str], is_smiles: bool = True) -> List[List[float | None]]:
|
||||
if is_smiles:
|
||||
rows = [self.descriptors.get_molecule_descriptors(smiles) for smiles in batch]
|
||||
else:
|
||||
rows = batch
|
||||
|
||||
# Create Dataframe with all descriptors
|
||||
initial_desc_rows_df = pl.DataFrame(
|
||||
data=rows, schema=self.descriptors.get_descriptor_names(), orient="row"
|
||||
)
|
||||
|
||||
# Before checking for invalid values per row, select only required columns
|
||||
initial_desc_rows_df = initial_desc_rows_df.select(
|
||||
list(self.min_max_scaler.feature_names_in_)
|
||||
)
|
||||
|
||||
to_pad = []
|
||||
adjusted_rows = []
|
||||
for i, row in enumerate(initial_desc_rows_df.rows()):
|
||||
# neither infs nor nans are found -> rows seems to be valid input
|
||||
if row and not any(math.isinf(x) for x in row) and not any(math.isnan(x) for x in row):
|
||||
adjusted_rows.append(row)
|
||||
else:
|
||||
to_pad.append(i)
|
||||
|
||||
if adjusted_rows:
|
||||
desc_rows_df = pl.DataFrame(
|
||||
data=adjusted_rows, schema=list(self.min_max_scaler.feature_names_in_), orient="row"
|
||||
)
|
||||
x_normal = self.min_max_scaler.transform(desc_rows_df)
|
||||
x_normal = x_normal[self.descriptor_subset]
|
||||
|
||||
res = self.regressor.predict(x_normal, return_std=True)
|
||||
|
||||
# Convert to lists
|
||||
res = [list(res[0]), list(res[1])]
|
||||
|
||||
# If we had rows containing bad input (inf, nan) insert Nones at the correct position
|
||||
if to_pad:
|
||||
for i in to_pad:
|
||||
res[0].insert(i, None)
|
||||
res[1].insert(i, None)
|
||||
|
||||
return res
|
||||
|
||||
else:
|
||||
return [[None] * len(batch), [None] * len(batch)]
|
||||
|
||||
@staticmethod
|
||||
def remove_highly_correlated_features(
|
||||
X_train,
|
||||
corr_method: str = "spearman",
|
||||
cluster_threshold: float = 0.01,
|
||||
ignore=False,
|
||||
):
|
||||
if ignore:
|
||||
return X_train
|
||||
# pass
|
||||
else:
|
||||
# Using spearmanr from scipy to achieve pandas.corr in polars
|
||||
corr = spearmanr(X_train, axis=0).statistic
|
||||
|
||||
# Ensure the correlation matrix is symmetric
|
||||
corr = (corr + corr.T) / 2
|
||||
np.fill_diagonal(corr, 1)
|
||||
corr = np.nan_to_num(corr)
|
||||
|
||||
# code from https://scikit-learn.org/stable/auto_examples/inspection/
|
||||
# plot_permutation_importance_multicollinear.html
|
||||
# We convert the correlation matrix to a distance matrix before performing
|
||||
# hierarchical clustering using Ward's linkage.
|
||||
distance_matrix = 1 - np.abs(corr)
|
||||
dist_linkage = hierarchy.ward(squareform(distance_matrix))
|
||||
|
||||
cluster_ids = hierarchy.fcluster(dist_linkage, cluster_threshold, criterion="distance")
|
||||
cluster_id_to_feature_ids = defaultdict(list)
|
||||
|
||||
for idx, cluster_id in enumerate(cluster_ids):
|
||||
cluster_id_to_feature_ids[cluster_id].append(idx)
|
||||
|
||||
my_selected_features = [v[0] for v in cluster_id_to_feature_ids.values()]
|
||||
X_train_sel = X_train[:, my_selected_features]
|
||||
|
||||
return X_train_sel
|
||||
|
||||
def save_model(self, path):
|
||||
with open(path, "wb") as save_file:
|
||||
pickle.dump(self, save_file, protocol=5)
|
||||
|
||||
@staticmethod
|
||||
def load_model(path) -> "Pepper":
|
||||
with open(path, "rb") as load_file:
|
||||
return pickle.load(load_file)
|
||||
|
||||
|
||||
def compute_bayes_per_group(group):
|
||||
"""Get mean and std using bayesian"""
|
||||
mean, std = Bayesian(group["dt50_log"]).get_posterior_distribution()
|
||||
return pl.DataFrame(
|
||||
{
|
||||
"structure_id": [group["structure_id"][0]],
|
||||
"dt50_bayesian_mean": [mean],
|
||||
"dt50_bayesian_std": [std],
|
||||
}
|
||||
)
|
||||
@ -36,7 +36,7 @@ dependencies = [
|
||||
[tool.uv.sources]
|
||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
|
||||
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.4.2" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", branch = "develop" }
|
||||
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
|
||||
|
||||
[project.optional-dependencies]
|
||||
@ -51,7 +51,13 @@ dev = [
|
||||
"pytest-django>=4.11.1",
|
||||
"pytest-cov>=7.0.0",
|
||||
]
|
||||
|
||||
pepper-plugin = [
|
||||
"matplotlib>=3.10.8",
|
||||
"pyyaml>=6.0.3",
|
||||
"emcee>=3.1.6",
|
||||
"mordredcommunity==2.0.7",
|
||||
"padelpy" # Remove once we're certain we'll go with mordred
|
||||
]
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
|
||||
@ -161,8 +161,18 @@ document.addEventListener("alpine:init", () => {
|
||||
set value(v) {
|
||||
this.data[this.fieldName] = v;
|
||||
},
|
||||
get multiple() {
|
||||
return !!(this.fieldSchema.items && this.fieldSchema.items.enum);
|
||||
|
||||
},
|
||||
get options() {
|
||||
return this.fieldSchema.enum || [];
|
||||
if (this.fieldSchema.enum) {
|
||||
return this.fieldSchema.enum;
|
||||
} else if (this.fieldSchema.items && this.fieldSchema.items.enum) {
|
||||
return this.fieldSchema.items.enum;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@ -453,6 +453,29 @@ function draw(pathway, elem) {
|
||||
}
|
||||
}
|
||||
|
||||
if (predictedPropertyViewEnabled) {
|
||||
|
||||
var tempContent = "";
|
||||
|
||||
if (Object.keys(n.predicted_properties).length > 0) {
|
||||
|
||||
if ("PepperPrediction" in n.predicted_properties) {
|
||||
// TODO needs to be generic once we store it as AddInf
|
||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||
if (s["mean"] != null) {
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tempContent === "") {
|
||||
tempContent = "<b>No predicted properties for this Node</b><br>";
|
||||
}
|
||||
|
||||
popupContent += tempContent
|
||||
}
|
||||
|
||||
popupContent += "<img src='" + n.image + "'><br>"
|
||||
if (n.scenarios.length > 0) {
|
||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||
@ -473,7 +496,6 @@ function draw(pathway, elem) {
|
||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
||||
|
||||
if (e.reaction.rules) {
|
||||
console.log(e.reaction.rules);
|
||||
for (var rule of e.reaction.rules) {
|
||||
popupContent += "Rule <a href='" + rule.url + "'>" + rule.name + "</a><br>";
|
||||
}
|
||||
|
||||
@ -43,14 +43,12 @@
|
||||
class="select select-bordered w-full"
|
||||
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||
x-model="value"
|
||||
:multiple="multiple"
|
||||
>
|
||||
<option value="" :selected="!value">Select...</option>
|
||||
|
||||
<template x-for="opt in options" :key="opt">
|
||||
<option
|
||||
:value="opt"
|
||||
:selected="value === opt"
|
||||
x-text="opt"
|
||||
></option>
|
||||
<option :value="opt" x-text="opt"></option>
|
||||
</template>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
isSubmitting: false,
|
||||
modelType: '',
|
||||
buildAppDomain: false,
|
||||
requiresRulePackages: false,
|
||||
requiresDataPackages: false,
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
@ -24,6 +26,21 @@
|
||||
return this.modelType === 'enviformer';
|
||||
},
|
||||
|
||||
get showRulePackages() {
|
||||
console.log(this.requiresRulePackages);
|
||||
return this.requiresRulePackages;
|
||||
},
|
||||
|
||||
get showDataPackages() {
|
||||
return this.requiresDataPackages;
|
||||
},
|
||||
|
||||
updateRequirements(event) {
|
||||
const option = event.target.selectedOptions[0];
|
||||
this.requiresRulePackages = option.dataset.requires_rule_packages === 'True';
|
||||
this.requiresDataPackages = option.dataset.requires_data_packages === 'True';
|
||||
},
|
||||
|
||||
submit(formId) {
|
||||
const form = document.getElementById(formId);
|
||||
if (form && form.checkValidity()) {
|
||||
@ -111,17 +128,24 @@
|
||||
name="model-type"
|
||||
class="select select-bordered w-full"
|
||||
x-model="modelType"
|
||||
x-on:change="updateRequirements($event)"
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected>Select Model Type</option>
|
||||
{% for k, v in model_types.items %}
|
||||
<option value="{{ v }}">{{ k }}</option>
|
||||
<option
|
||||
value="{{ v.type }}"
|
||||
data-requires_rule_packages="{{ v.requires_rule_packages }}"
|
||||
data-requires_data_packages="{{ v.requires_data_packages }}"
|
||||
>
|
||||
{{ k }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Rule Packages (MLRR, RBRR) -->
|
||||
<div class="form-control mb-3" x-show="showMlrr || showRbrr" x-cloak>
|
||||
<div class="form-control mb-3" x-show="showRulePackages" x-cloak>
|
||||
<label class="label" for="model-rule-packages">
|
||||
<span class="label-text">Rule Packages</span>
|
||||
</label>
|
||||
@ -152,11 +176,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Data Packages (MLRR, RBRR, Enviformer) -->
|
||||
<div
|
||||
class="form-control mb-3"
|
||||
x-show="showMlrr || showRbrr || showEnviformer"
|
||||
x-cloak
|
||||
>
|
||||
<div class="form-control mb-3" x-show="showDataPackages" x-cloak>
|
||||
<label class="label" for="model-data-packages">
|
||||
<span class="label-text">Data Packages</span>
|
||||
</label>
|
||||
|
||||
@ -233,6 +233,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if property_models %}
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="prediction-setting-property-models">
|
||||
<span class="label-text">Select Property Models</span>
|
||||
</label>
|
||||
<select
|
||||
id="prediction-setting-property-models"
|
||||
name="prediction-setting-property-models"
|
||||
class="select select-bordered w-full"
|
||||
multiple
|
||||
>
|
||||
<option value="" disabled selected>Select the model</option>
|
||||
{% for pm in property_models %}
|
||||
<option value="{{ pm.url }}">{{ pm.name|safe }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
|
||||
144
templates/objects/model/_model_base.html
Normal file
144
templates/objects/model/_model_base.html
Normal file
@ -0,0 +1,144 @@
|
||||
{% extends "framework_modern.html" %}
|
||||
{% load static %}
|
||||
{% load envipytags %}
|
||||
{% block content %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_model_modal.html" %}
|
||||
{% include "modals/objects/evaluate_model_modal.html" %}
|
||||
{% include "modals/objects/retrain_model_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block libraries %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="space-y-2 p-4">
|
||||
<!-- Header Section -->
|
||||
<div class="card bg-base-100">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-2xl">{{ model.name }}</h2>
|
||||
<div id="actionsButton" class="dropdown dropdown-end hidden">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-wrench"
|
||||
>
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
/>
|
||||
</svg>
|
||||
Actions
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||
>
|
||||
{% block actions %}
|
||||
{% include "actions/objects/model.html" %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2">{{ model.description|safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Status -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Model Status</div>
|
||||
<div class="collapse-content">{{ model.status }}</div>
|
||||
</div>
|
||||
|
||||
{% block packages %}
|
||||
{% if model.rule_packages.all|length > 0 %}
|
||||
<!-- Rule Packages -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Rule Packages</div>
|
||||
<div class="collapse-content">
|
||||
<ul class="menu bg-base-100 rounded-box w-full">
|
||||
{% for p in model.rule_packages.all %}
|
||||
<li>
|
||||
<a href="{{ p.url }}" class="hover:bg-base-200"
|
||||
>{{ p.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if model.data_packages.all|length > 0 %}
|
||||
<!-- Reaction Packages -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Reaction Packages
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<ul class="menu bg-base-100 rounded-box w-full">
|
||||
{% for p in model.data_packages.all %}
|
||||
<li>
|
||||
<a href="{{ p.url }}" class="hover:bg-base-200"
|
||||
>{{ p.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if model.eval_packages.all|length > 0 %}
|
||||
<!-- Eval Packages -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Eval Packages</div>
|
||||
<div class="collapse-content">
|
||||
<ul class="menu bg-base-100 rounded-box w-full">
|
||||
{% for p in model.eval_packages.all %}
|
||||
<li>
|
||||
<a href="{{ p.url }}" class="hover:bg-base-200"
|
||||
>{{ p.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block usemodel %}
|
||||
{% endblock %}
|
||||
|
||||
{% block evaluation %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
function makeLoadingGif(selector, gifPath) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.innerHTML = '<img src="' + gifPath + '" alt="Loading...">';
|
||||
}
|
||||
}
|
||||
|
||||
function clear(divid) {
|
||||
const element = document.getElementById(divid);
|
||||
if (element) {
|
||||
element.classList.remove("alert", "alert-error");
|
||||
element.innerHTML = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
||||
430
templates/objects/model/classification_model.html
Normal file
430
templates/objects/model/classification_model.html
Normal file
@ -0,0 +1,430 @@
|
||||
{% extends "objects/model/_model_base.html" %}
|
||||
{% load static %}
|
||||
{% load envipytags %}
|
||||
|
||||
{% block libraries %}
|
||||
<!-- Include required libs -->
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.js"></script>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
{% endblock %}
|
||||
|
||||
{% block usemodel %}
|
||||
{% if model.ready_for_prediction %}
|
||||
<!-- Predict Panel -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium" id="predictTitle">
|
||||
Predict
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="form-control">
|
||||
<div class="join w-full">
|
||||
<input
|
||||
id="smiles-to-predict"
|
||||
type="text"
|
||||
class="input input-bordered join-item grow"
|
||||
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
type="button"
|
||||
id="predict-button"
|
||||
>
|
||||
Predict!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictLoading" class="mt-2 flex hidden justify-center">
|
||||
<div class="h-8 w-8">
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictResultTable" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if model.ready_for_prediction and model.app_domain %}
|
||||
<!-- App Domain -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Applicability Domain Assessment
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="form-control">
|
||||
<div class="join w-full">
|
||||
<input
|
||||
id="smiles-to-assess"
|
||||
type="text"
|
||||
class="input input-bordered join-item grow"
|
||||
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
type="button"
|
||||
id="assess-button"
|
||||
>
|
||||
Assess!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="appDomainLoading" class="mt-2 flex hidden justify-center">
|
||||
<div class="h-8 w-8">
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="appDomainAssessmentResultTable" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function handlePredictionResponse(data) {
|
||||
let stereo = data["stereo"];
|
||||
data = data["pred"];
|
||||
let res = "";
|
||||
if (stereo) {
|
||||
res +=
|
||||
"<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span><br>";
|
||||
}
|
||||
res += "<table class='table table-zebra'>";
|
||||
res += "<thead>";
|
||||
res += "<th scope='col'>#</th>";
|
||||
|
||||
const columns = ["products", "image", "probability", "btrule"];
|
||||
|
||||
for (const col of columns) {
|
||||
res += "<th scope='col'>" + col + "</th>";
|
||||
}
|
||||
res += "</thead>";
|
||||
res += "<tbody>";
|
||||
let cnt = 1;
|
||||
for (const transformation in data) {
|
||||
res += "<tr>";
|
||||
res += "<th scope='row'>" + cnt + "</th>";
|
||||
res +=
|
||||
"<th scope='row'>" +
|
||||
data[transformation]["products"][0].join(", ") +
|
||||
"</th>";
|
||||
res +=
|
||||
"<th scope='row'>" +
|
||||
"<img width='400' src='{% url 'depict' %}?smiles=" +
|
||||
encodeURIComponent(data[transformation]["products"][0].join(".")) +
|
||||
"'></th>";
|
||||
res +=
|
||||
"<th scope='row'>" +
|
||||
data[transformation]["probability"].toFixed(3) +
|
||||
"</th>";
|
||||
if (data[transformation]["btrule"] != null) {
|
||||
res +=
|
||||
"<th scope='row'>" +
|
||||
"<a href='" +
|
||||
data[transformation]["btrule"]["url"] +
|
||||
"' class='link link-primary'>" +
|
||||
data[transformation]["btrule"]["name"] +
|
||||
"</a>" +
|
||||
"</th>";
|
||||
} else {
|
||||
res += "<th scope='row'>N/A</th>";
|
||||
}
|
||||
res += "</tr>";
|
||||
cnt += 1;
|
||||
}
|
||||
|
||||
res += "</tbody>";
|
||||
res += "</table>";
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.innerHTML = res;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Show actions button if there are actions
|
||||
const actionsButton = document.getElementById("actionsButton");
|
||||
const actionsList = actionsButton?.querySelector("ul");
|
||||
if (actionsList && actionsList.children.length > 0) {
|
||||
actionsButton?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Predict button handler
|
||||
const predictButton = document.getElementById("predict-button");
|
||||
if (predictButton) {
|
||||
predictButton.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
clear("predictResultTable");
|
||||
|
||||
const smilesInput = document.getElementById("smiles-to-predict");
|
||||
const smiles = smilesInput ? smilesInput.value.trim() : "";
|
||||
|
||||
if (smiles === "") {
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
"Please enter a SMILES string to predict!";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
smiles: smiles,
|
||||
classify: "ILikeCats!",
|
||||
});
|
||||
|
||||
fetch("?" + params.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-CSRFToken":
|
||||
document.querySelector("[name=csrf-token]").content,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((err) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
handlePredictionResponse(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
error.error || "Error while processing response :/";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Assess button handler
|
||||
const assessButton = document.getElementById("assess-button");
|
||||
if (assessButton) {
|
||||
assessButton.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
clear("appDomainAssessmentResultTable");
|
||||
|
||||
const smilesInput = document.getElementById("smiles-to-assess");
|
||||
const smiles = smilesInput ? smilesInput.value.trim() : "";
|
||||
|
||||
if (smiles === "") {
|
||||
const resultTable = document.getElementById(
|
||||
"appDomainAssessmentResultTable",
|
||||
);
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
"Please enter a SMILES string to predict!";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
smiles: smiles,
|
||||
"app-domain-assessment": "ILikeCats!",
|
||||
});
|
||||
|
||||
fetch("?" + params.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-CSRFToken":
|
||||
document.querySelector("[name=csrf-token]").content,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((err) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
if (typeof handleAssessmentResponse === "function") {
|
||||
handleAssessmentResponse("{% url 'depict' %}", data);
|
||||
}
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
const resultTable = document.getElementById(
|
||||
"appDomainAssessmentResultTable",
|
||||
);
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
error.error || "Error while processing response :/";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block evaluation %}
|
||||
{# prettier-ignore-start #}
|
||||
{% if model.model_status == 'FINISHED' %}
|
||||
<!-- Single Gen Curve Panel -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked/>
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Precision Recall Curve
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<div id="sg-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if model.multigen_eval %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked/>
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Multi Gen Precision Recall Curve
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<div id="mg-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<script>
|
||||
function makeChart(selector, data) {
|
||||
const x = ['Recall'];
|
||||
const y = ['Precision'];
|
||||
const thres = ['threshold'];
|
||||
|
||||
function compare(a, b) {
|
||||
if (a.threshold < b.threshold)
|
||||
return -1;
|
||||
else if (a.threshold > b.threshold)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getIndexForValue(data, val, val_name) {
|
||||
for (const idx in data) {
|
||||
if (data[idx][val_name] == val) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
console.warn('PR curve data is empty');
|
||||
return;
|
||||
}
|
||||
const dataLength = data.length;
|
||||
data.sort(compare);
|
||||
|
||||
for (const idx in data) {
|
||||
const d = data[idx];
|
||||
x.push(d.recall);
|
||||
y.push(d.precision);
|
||||
thres.push(d.threshold);
|
||||
}
|
||||
const chart = c3.generate({
|
||||
bindto: selector,
|
||||
data: {
|
||||
onclick: function (d, e) {
|
||||
const idx = d.index;
|
||||
const thresh = data[dataLength - idx - 1].threshold;
|
||||
},
|
||||
x: 'Recall',
|
||||
y: 'Precision',
|
||||
columns: [
|
||||
x,
|
||||
y,
|
||||
]
|
||||
},
|
||||
size: {
|
||||
height: 400,
|
||||
width: 480
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
max: 1,
|
||||
min: 0,
|
||||
label: 'Recall',
|
||||
padding: 0,
|
||||
tick: {
|
||||
fit: true,
|
||||
values: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
||||
}
|
||||
},
|
||||
y: {
|
||||
max: 1,
|
||||
min: 0,
|
||||
label: 'Precision',
|
||||
padding: 0,
|
||||
tick: {
|
||||
fit: true,
|
||||
values: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
||||
}
|
||||
}
|
||||
},
|
||||
point: {
|
||||
r: 4
|
||||
},
|
||||
tooltip: {
|
||||
format: {
|
||||
title: function (recall) {
|
||||
const idx = getIndexForValue(data, recall, "recall");
|
||||
if (idx != -1) {
|
||||
return "Threshold: " + data[idx].threshold;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
value: function (precision, ratio, id) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
{% if model.model_status == 'FINISHED' %}
|
||||
// Precision Recall Curve
|
||||
makeChart('#sg-chart', {{ model.pr_curve|safe }});
|
||||
{% if model.multigen_eval %}
|
||||
// Multi Gen Precision Recall Curve
|
||||
makeChart('#mg-chart', {{ model.mg_pr_curve|safe }});
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
});
|
||||
</script>
|
||||
{# prettier-ignore-end #}
|
||||
{% endblock %}
|
||||
168
templates/objects/model/property_model.html
Normal file
168
templates/objects/model/property_model.html
Normal file
@ -0,0 +1,168 @@
|
||||
{% extends "objects/model/_model_base.html" %}
|
||||
{% load static %}
|
||||
{% load envipytags %}
|
||||
|
||||
{% block libraries %}
|
||||
{% endblock %}
|
||||
|
||||
{% block usemodel %}
|
||||
|
||||
{% if model.ready_for_prediction %}
|
||||
<!-- Predict Panel -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium" id="predictTitle">
|
||||
Predict
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="form-control">
|
||||
<div class="join w-full">
|
||||
<input
|
||||
id="smiles-to-predict"
|
||||
type="text"
|
||||
class="input input-bordered join-item grow"
|
||||
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
type="button"
|
||||
id="predict-button"
|
||||
>
|
||||
Predict!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictLoading" class="mt-2 flex hidden justify-center">
|
||||
<div class="h-8 w-8">
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictResultTable" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Show actions button if there are actions
|
||||
const actionsButton = document.getElementById("actionsButton");
|
||||
const actionsList = actionsButton?.querySelector("ul");
|
||||
if (actionsList && actionsList.children.length > 0) {
|
||||
actionsButton?.classList.remove("hidden");
|
||||
}
|
||||
// Predict button handler
|
||||
const predictButton = document.getElementById("predict-button");
|
||||
if (predictButton) {
|
||||
predictButton.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
clear("predictResultTable");
|
||||
const smilesInput = document.getElementById("smiles-to-predict");
|
||||
const smiles = smilesInput ? smilesInput.value.trim() : "";
|
||||
|
||||
if (smiles === "") {
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
"Please enter a SMILES string to predict!";
|
||||
}
|
||||
return;
|
||||
}
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||
const params = new URLSearchParams({
|
||||
smiles: smiles,
|
||||
half_life: "ILikeCats!",
|
||||
});
|
||||
fetch("?" + params.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-CSRFToken":
|
||||
document.querySelector("[name=csrf-token]").content,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((err) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add("hidden");
|
||||
}
|
||||
if (data.svg === null) {
|
||||
document.getElementById("predictResultTable").innerHTML =
|
||||
"<span class='alert alert-error alert-soft'>Processing failed...</span><br>";
|
||||
return;
|
||||
}
|
||||
handlePredictionResponse(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
resultTable.innerHTML =
|
||||
error.error || "Error while processing response :/";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handlePredictionResponse(data) {
|
||||
let stereo = data["stereo"];
|
||||
data = data["svg"];
|
||||
let res = "";
|
||||
if (stereo) {
|
||||
res +=
|
||||
"<span class='alert alert-warning'>Removed stereochemistry for prediction</span><br>";
|
||||
}
|
||||
res += "<div class='flex justify-center'>" + data + "<\div>";
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.innerHTML = res;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block evaluation %}
|
||||
{% if model.model_status == 'FINISHED' %}
|
||||
<!-- Model Statistics Panel -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Model Statistics</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
id="model-stats"
|
||||
class="overflow-x-auto rounded-box shadow-md bg-base-100"
|
||||
>
|
||||
<table class="table table-fixed w-full">
|
||||
<thead class="text-base">
|
||||
<tr>
|
||||
<th class="w-1/5">Metric</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for metric, value in model.eval_results.items %}
|
||||
<tr>
|
||||
<td>{{ metric|upper }}</td>
|
||||
<td>{{ value|floatformat:4 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -160,7 +160,7 @@
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||
>
|
||||
{% if pathway.setting.model.app_domain %}
|
||||
<li>
|
||||
@ -206,6 +206,37 @@
|
||||
OECD 301F View
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="pred-prop-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
id="pred-prop-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="slash"
|
||||
viewBox="0 0 100 30"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="0" y1="30" x2="100" y2="0" />
|
||||
</svg>
|
||||
Show Predicted Properties
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -441,6 +472,8 @@
|
||||
var appDomainViewEnabled = false;
|
||||
// Global switch for timeseries view
|
||||
var timeseriesViewEnabled = false;
|
||||
// Predicted Property View
|
||||
var predictedPropertyViewEnabled = false;
|
||||
|
||||
function goFullscreen(id) {
|
||||
var element = document.getElementById(id);
|
||||
@ -563,6 +596,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Predicted Propertes toggle
|
||||
const predPropBtn = document.getElementById("pred-prop-toggle-button");
|
||||
if (predPropBtn) {
|
||||
predPropBtn.addEventListener("click", function () {
|
||||
predictedPropertyViewEnabled = !predictedPropertyViewEnabled;
|
||||
const icon = document.getElementById("pred-prop-icon");
|
||||
|
||||
if (predictedPropertyViewEnabled) {
|
||||
icon.innerHTML +=
|
||||
'<svg class="slash" viewBox="0 0 100 30" preserveAspectRatio="none" aria-hidden="true"><line x1="0" y1="30" x2="100" y2="0"/></svg>';
|
||||
} else {
|
||||
icon.innerHTML =
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show actions button if there are actions
|
||||
const actionsButton = document.getElementById("actionsButton");
|
||||
const actionsList = actionsButton?.querySelector("ul");
|
||||
|
||||
@ -123,7 +123,64 @@
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template x-for="item in items" :key="item.uuid">
|
||||
<template
|
||||
x-for="item in items.filter(i => i.attach_object === null)"
|
||||
:key="item.uuid"
|
||||
>
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div
|
||||
class="flex-1"
|
||||
x-data="schemaRenderer({
|
||||
rjsf: schemas[item.type.toLowerCase()],
|
||||
data: item.data,
|
||||
mode: 'view'
|
||||
})"
|
||||
x-init="init()"
|
||||
>
|
||||
{% include "components/schema_form.html" %}
|
||||
</div>
|
||||
{% if meta.can_edit %}
|
||||
<button
|
||||
class="btn btn-sm btn-ghost ml-2"
|
||||
@click="deleteItem(item.uuid)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-trash"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
x-if="items.filter(i => i.attach_object !== null).length !== 0"
|
||||
>
|
||||
<h4 class="card-title mb-4 text-lg">
|
||||
Additional Information that are attached to objects referring
|
||||
to this Scenario
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template
|
||||
x-for="item in items.filter(i => i.attach_object !== null)"
|
||||
:key="item.uuid"
|
||||
>
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
@ -171,82 +228,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if scenario.parent %}
|
||||
<div class="card bg-base-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title mb-4 text-lg">
|
||||
Parent Scenario Additional Information
|
||||
</h3>
|
||||
<div
|
||||
x-data="{
|
||||
items: [],
|
||||
schemas: {},
|
||||
loading: true,
|
||||
error: null,
|
||||
async init() {
|
||||
try {
|
||||
// Use the unified API client for loading data
|
||||
const { items, schemas } = await window.AdditionalInformationApi.loadSchemasAndItems('{{ scenario.parent.uuid }}');
|
||||
this.items = items;
|
||||
this.schemas = schemas;
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
console.error('Error loading additional information:', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
}"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center justify-center p-4">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error state -->
|
||||
<template x-if="error">
|
||||
<div class="alert alert-error mb-4">
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Items list -->
|
||||
<template x-if="!loading && !error">
|
||||
<div class="space-y-4">
|
||||
<template x-if="items.length === 0">
|
||||
<p class="text-base-content/60">
|
||||
No additional information available.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template x-for="item in items" :key="item.uuid">
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div
|
||||
class="flex-1"
|
||||
x-data="schemaRenderer({
|
||||
rjsf: schemas[item.type.toLowerCase()],
|
||||
data: item.data,
|
||||
mode: 'view'
|
||||
})"
|
||||
x-init="init()"
|
||||
>
|
||||
{% include "components/schema_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pathways -->
|
||||
{% if scenario.related_pathways %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
@ -265,43 +246,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Related Scenarios -->
|
||||
{% if children.exists %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">Related Scenarios</div>
|
||||
<div class="collapse-content">
|
||||
<ul class="menu bg-base-100 rounded-box">
|
||||
{% for s in children %}
|
||||
<li>
|
||||
<a href="{{ s.url }}" class="hover:bg-base-200"
|
||||
>{{ s.name }} <i>({{ s.package.name }})</i></a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Parent Scenarios -->
|
||||
{% if scenario.parent %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">Parent Scenario</div>
|
||||
<div class="collapse-content">
|
||||
<ul class="menu bg-base-100 rounded-box">
|
||||
<li>
|
||||
<a href="{{ scenario.parent.url }}" class="hover:bg-base-200"
|
||||
>{{ scenario.parent.name }}
|
||||
<i>({{ scenario.parent.package.name }})</i></a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@ -87,6 +87,39 @@
|
||||
<td>Expansion Scheme</td>
|
||||
<td>{{ setting_to_render.expansion_scheme }}</td>
|
||||
</tr>
|
||||
|
||||
{% if setting_to_render.property_models.all %}
|
||||
<tr>
|
||||
{% for prop_model in setting_to_render.property_models.all %}
|
||||
<td>Property Models</td>
|
||||
<td>
|
||||
<div class="space-y-2">
|
||||
<table class="table-xs table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property Type</th>
|
||||
<th>Model</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ prop_model.instance.display }}</td>
|
||||
<td>
|
||||
<a
|
||||
href="{{ prop_model.url }}"
|
||||
class="link link-primary"
|
||||
>
|
||||
{{ prop_model.name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,6 @@ from epdb.models import (
|
||||
Node,
|
||||
ParallelRule,
|
||||
Pathway,
|
||||
PluginModel,
|
||||
Reaction,
|
||||
Rule,
|
||||
RuleBasedRelativeReasoning,
|
||||
@ -427,8 +426,6 @@ class PackageExporter:
|
||||
model_dict["model_type"] = "MLRelativeReasoning"
|
||||
elif isinstance(model, EnviFormer):
|
||||
model_dict["model_type"] = "EnviFormer"
|
||||
elif isinstance(model, PluginModel):
|
||||
model_dict["model_type"] = "PluginModel"
|
||||
else:
|
||||
model_dict["model_type"] = "EPModel"
|
||||
|
||||
@ -952,8 +949,6 @@ class PackageImporter:
|
||||
model = MLRelativeReasoning.objects.create(**common_fields)
|
||||
elif model_type == "EnviFormer":
|
||||
model = EnviFormer.objects.create(**common_fields)
|
||||
elif model_type == "PluginModel":
|
||||
model = PluginModel.objects.create(**common_fields)
|
||||
else:
|
||||
model = EPModel.objects.create(**common_fields)
|
||||
|
||||
|
||||
@ -45,13 +45,14 @@ def discover_plugins(_cls: Type = None) -> Dict[str, Type]:
|
||||
|
||||
plugins = {}
|
||||
|
||||
# Load plugins dropped to plugin dir
|
||||
for entry_point in importlib.metadata.entry_points(group="enviPy_plugins"):
|
||||
try:
|
||||
plugin_class = entry_point.load()
|
||||
if _cls:
|
||||
if issubclass(plugin_class, _cls):
|
||||
instance = plugin_class()
|
||||
plugins[instance.name()] = instance
|
||||
plugins[instance.identifier()] = instance
|
||||
else:
|
||||
if (
|
||||
issubclass(plugin_class, Classifier)
|
||||
@ -59,9 +60,17 @@ def discover_plugins(_cls: Type = None) -> Dict[str, Type]:
|
||||
or issubclass(plugin_class, Property)
|
||||
):
|
||||
instance = plugin_class()
|
||||
plugins[instance.name()] = instance
|
||||
plugins[instance.identifier()] = plugin_class
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading plugin {entry_point.name}: {e}")
|
||||
|
||||
# load direct modules
|
||||
for plugin_module in s.BASE_PLUGINS:
|
||||
module_path, class_name = plugin_module.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
plugin_class = getattr(module, class_name)
|
||||
instance = plugin_class()
|
||||
plugins[instance.identifier()] = plugin_class
|
||||
|
||||
return plugins
|
||||
|
||||
306
uv.lock
generated
306
uv.lock
generated
@ -377,6 +377,72 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/56/6d6872f79d14c0cb02f1646cbb4592eef935857c0951a105874b7b62a0c3/contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", size = 13277, upload-time = "2021-06-27T06:54:20.972Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.12.0"
|
||||
@ -507,6 +573,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
@ -638,6 +713,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/69/2ce28341493e35b7b182e2c0474288a350edf1cb27220517fb8abd4143c9/django_stubs_ext-5.2.6-py3-none-any.whl", hash = "sha256:c3736dcea49666140c92a1471dda83c31e8d5e71dc364ab3cedc7698dbf01cee", size = 9980, upload-time = "2025-10-05T15:30:49.957Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emcee"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/53/1045ee878cb24281387079f8ee4f0ade1622c6aae1ed1fd91a53e4fa5b19/emcee-3.1.6.tar.gz", hash = "sha256:11af4daf6ab8f9ca69681e3c29054665db7bbd87fd4eb8e437d2c3a1248c637d", size = 2871117, upload-time = "2024-04-19T10:03:19.555Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ef/2196b9bf88ffa1bde45853c72df021fbd07a8fa91a0f59a22d14a050dc04/emcee-3.1.6-py2.py3-none-any.whl", hash = "sha256:f2d63752023bdccf744461450e512a5b417ae7d28f18e12acd76a33de87580cb", size = 47351, upload-time = "2024-04-19T10:03:17.522Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enviformer"
|
||||
version = "0.1.0"
|
||||
@ -698,6 +785,13 @@ dev = [
|
||||
ms-login = [
|
||||
{ name = "msal" },
|
||||
]
|
||||
pepper-plugin = [
|
||||
{ name = "emcee" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "mordredcommunity" },
|
||||
{ name = "padelpy" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
@ -711,16 +805,20 @@ requires-dist = [
|
||||
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" },
|
||||
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
||||
{ name = "django-stubs", marker = "extra == 'dev'", specifier = ">=5.2.4" },
|
||||
{ name = "emcee", marker = "extra == 'pepper-plugin'", specifier = ">=3.1.6" },
|
||||
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.4" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.4.2" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop" },
|
||||
{ name = "envipy-ambit", git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" },
|
||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||
{ name = "jsonref", specifier = ">=1.1.0" },
|
||||
{ name = "matplotlib", marker = "extra == 'pepper-plugin'", specifier = ">=3.10.8" },
|
||||
{ name = "mordredcommunity", marker = "extra == 'pepper-plugin'", specifier = "==2.0.7" },
|
||||
{ name = "msal", marker = "extra == 'ms-login'", specifier = ">=1.33.0" },
|
||||
{ name = "networkx", specifier = ">=3.4.2" },
|
||||
{ name = "nh3", specifier = "==0.3.2" },
|
||||
{ name = "padelpy", marker = "extra == 'pepper-plugin'" },
|
||||
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.37.0" },
|
||||
{ name = "polars", specifier = "==1.35.1" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
|
||||
@ -729,6 +827,7 @@ requires-dist = [
|
||||
{ name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.11.1" },
|
||||
{ name = "pytest-playwright", marker = "extra == 'dev'", specifier = ">=0.7.1" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||
{ name = "pyyaml", marker = "extra == 'pepper-plugin'", specifier = ">=6.0.3" },
|
||||
{ name = "rdkit", specifier = ">=2025.3.2" },
|
||||
{ name = "redis", specifier = ">=6.1.0" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
@ -737,12 +836,12 @@ requires-dist = [
|
||||
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.32.0" },
|
||||
{ name = "setuptools", specifier = ">=80.8.0" },
|
||||
]
|
||||
provides-extras = ["ms-login", "dev"]
|
||||
provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
||||
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.4.2"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.4.2#03f2c27c48503fbfc393aaa502cfe52298c55f35" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#40459366648a03b01432998b32fdabd5556a1bae" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
@ -782,6 +881,47 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.61.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
@ -1039,6 +1179,78 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520, upload-time = "2024-03-06T19:58:29.765Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kombu"
|
||||
version = "5.5.4"
|
||||
@ -1152,6 +1364,76 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "contourpy" },
|
||||
{ name = "cycler" },
|
||||
{ name = "fonttools" },
|
||||
{ name = "kiwisolver" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyparsing" },
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mordredcommunity"
|
||||
version = "2.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "networkx" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "rdkit" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/cb/953ff6385cd299dab12cbe7bbcd16dbcf778d14499a229b8e882c8b629d3/mordredcommunity-2.0.7.tar.gz", hash = "sha256:6719be351c5fd80461739a4e79acb4480f0c9fb1eb2f7a3ab576c9092e1d74a8", size = 130842, upload-time = "2026-01-22T15:05:10.826Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/28/a5f6bf29558e8eaaac089aa3fcecfee2f8a44b7f4783cd86c5722f3c4530/mordredcommunity-2.0.7-py3-none-any.whl", hash = "sha256:36093d078df9c35419b26ca422a1c7f9ff3693b92ea5e43ea351de245f60020a", size = 176002, upload-time = "2026-01-22T15:05:09.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
@ -1573,6 +1855,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "padelpy"
|
||||
version = "0.1.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/19/6af9c17ce403cb372dd7ed75a5e6f00d0a7695ffaccd60584c33e9defebe/padelpy-0.1.16.tar.gz", hash = "sha256:bbe11fd93b3f7914f57b5dafbf83c5070161246fce4c626c1f394d73efabf394", size = 20873861, upload-time = "2023-11-10T22:49:06.075Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/31/75879902fbdd5079a2177845a70c5d5de2915317c1187a83af3a857e70a8/padelpy-0.1.16-py3-none-any.whl", hash = "sha256:fb2814d48c498981c8ba10613e752e6ba856ccbd532aedcdc555154e87abf5b1", size = 20889833, upload-time = "2023-11-10T22:49:02.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastel"
|
||||
version = "0.2.1"
|
||||
@ -1998,6 +2289,15 @@ crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
|
||||
Reference in New Issue
Block a user