1 Commits

Author SHA1 Message Date
54056c654d adjusted migration
Some checks failed
API CI / api-tests (pull_request) Failing after 21s
CI / test (pull_request) Failing after 22s
Initial bayer app

Show Pack Classification

Adjusted docker compose to bayer specifics

Adjusted Dockerfile for Bayer

Adding secret flags to group, add secret pools to packages

Adjusted View for Package creation

Prep configs, added Package Create Modal

wip

More on PES

wip

wip
2026-04-21 22:53:30 +02:00
32 changed files with 151 additions and 604 deletions

View File

@ -30,13 +30,12 @@ RUN mkdir -p -m 0700 /root/.ssh \
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts && ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
# We'll need access to private repos, let docker make use of host ssh agent and use it like: # We'll need access to private repos, let docker make use of host ssh agent and use it like:
# docker build --ssh default -t envipath/envipy-bayer:1.0 . # docker build --ssh default -t envipath/envipy:1.0 .
RUN --mount=type=ssh \ RUN --mount=type=ssh \
uv sync --locked --extra ms-login --extra pepper-plugin uv sync --locked --extra ms-login --extra pepper-plugin
# Now copy source and do a final sync to install the project itself # Now copy source and do a final sync to install the project itself
# Ensure .dockerignore is reasonable # Ensure .dockerignore is reasonable
COPY bb4g bb4g
COPY biotransformer biotransformer COPY biotransformer biotransformer
COPY bayer bayer COPY bayer bayer
COPY bridge bridge COPY bridge bridge

View File

@ -13,14 +13,6 @@ register_template(
"modals.collections.compound", "modals.collections.compound",
"modals/collections/new_pes_modal.html", "modals/collections/new_pes_modal.html",
) )
register_template(
"epdb.actions.objects.pathway.add",
"actions/objects/pathway_add_pes.html",
)
register_template(
"epdb.modals.objects.pathway.add",
"modals/objects/add_pathway_pes_node_modal.html"
)
# PES Viz # PES Viz
register_template( register_template(

View File

@ -1,8 +0,0 @@
<li>
<a
class="button"
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Add PES</a
>
</li>

View File

@ -103,15 +103,20 @@ def create_pes_node(request, package_uuid, pathway_uuid):
def fetch_pes(request, pes_url) -> dict: def fetch_pes(request, pes_url) -> dict:
proxies = {
"http": "http://10.185.190.100:8080",
"https": "http://10.185.190.100:8080",
}
from epauth.views import get_access_token_from_request from epauth.views import get_access_token_from_request
token = get_access_token_from_request(request) token = get_access_token_from_request(request)
if token: if token or True:
for k, v in s.PES_API_MAPPING.items(): for k, v in s.PES_API_MAPPING.items():
if pes_url.startswith(k): if pes_url.startswith(k):
pes_id = pes_url.split('/')[-1] pes_id = pes_url.split('/')[-1]
if pes_id == 'dummy': if pes_id == 'dummy' or True:
import json import json
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json")) res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
res_data["pes_url"] = pes_url res_data["pes_url"] = pes_url
@ -120,7 +125,7 @@ def fetch_pes(request, pes_url) -> dict:
headers = {"Authorization": f"Bearer {token['access_token']}"} headers = {"Authorization": f"Bearer {token['access_token']}"}
params = {"pes_reg_entity_corporate_id": pes_id} params = {"pes_reg_entity_corporate_id": pes_id}
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None) res = requests.get(v, headers=headers, params=params, proxies=proxies)
try: try:
res.raise_for_status() res.raise_for_status()

View File

@ -1,183 +0,0 @@
import json
import math
from datetime import datetime
from typing import List
import enum
import requests
from django.conf import settings as s
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
from envipy_additional_information import register
from bridge.contracts import Classifier # noqa: I001
from bridge.dto import (
BuildResult,
EnviPyDTO,
EvaluationResult,
RunResult,
TransformationProductPrediction,
) # noqa: I001
class SamplingAlgorithm(enum.Enum):
EXACT = "exact"
@register("bb4gconfig")
class BB4GConfig(EnviPyModel):
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
cutoff: int = -5
class UI:
title = "BB4G Configuration"
sampling_algorithm = UIConfig(
widget=WidgetType.SELECT,
label="BB4G Sampling Algorithm",
order=1,
placeholder="If unset defaults to 'exact'"
)
cutoff = UIConfig(
widget=WidgetType.NUMBER,
label="BB4G Cutoff",
order=2,
placeholder="If unset defaults to -5"
)
# Once stable these will be exposed by enviPy-plugins lib
class BB4G(Classifier):
Config = BB4GConfig
def __init__(self, config: BB4GConfig | None = None):
super().__init__(config)
self.url = f"{s.BB4G_URL}"
self.token = self.acquire_token()
self.header = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
def acquire_token(self):
BB4G_TENANT_ID = s.BB4G_TENANT_ID
BB4G_CLIENT_ID = s.BB4G_CLIENT_ID
BB4G_CLIENT_SECRET = s.BB4G_CLIENT_SECRET
BB4G_SCOPE = s.BB4G_SCOPE
BB4G_TOKEN_URL = f"https://login.microsoftonline.com/{BB4G_TENANT_ID}/oauth2/v2.0/token"
payload = {
"client_id": BB4G_CLIENT_ID,
"client_secret": BB4G_CLIENT_SECRET,
"scope": BB4G_SCOPE,
"grant_type": "client_credentials"
}
# No Proxy required, URL is whitelisted
res = requests.post(BB4G_TOKEN_URL, data=payload)
res.raise_for_status()
return res.json()["access_token"]
def start(self):
header = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
started = False
retries = 0
while not started and retries < 5:
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
if res.status_code == 200:
started = True
elif res.status_code in [500, 502]:
retries += 1
import time
time.sleep(5)
else:
raise ValueError(f"Unexpected status code: {res.status_code}")
@classmethod
def requires_rule_packages(cls) -> bool:
return False
@classmethod
def requires_data_packages(cls) -> bool:
return False
@classmethod
def identifier(cls) -> str:
return "bb4g"
@classmethod
def name(cls) -> str:
return "BB4G Template Free Model"
@classmethod
def display(cls) -> str:
return "BB4G Template Free Model"
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
return
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
# Ensure Service is running
self.start()
smiles = [c.smiles for c in eP.get_compounds()]
preds = self._post(smiles)
results = []
for substrate in preds.keys():
results.append(
TransformationProductPrediction(
substrate=substrate,
products=preds[substrate],
)
)
return RunResult(
producer=eP.get_context().url,
description=f"Generated at {datetime.now()}",
result=results,
)
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
pass
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
pass
def _post(self, smiles: List[str]) -> dict[str, dict[str, float]]:
header = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
result = {}
for smi in smiles:
data = {
"smiles": smi,
"sampling_alg": self.config.sampling_algorithm.value,
"cutoff": self.config.cutoff,
}
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
resp.raise_for_status()
for substrate, predictions in resp.json().items():
preds = {}
for pred in predictions:
prod = pred["prediction"]
prob = math.exp(pred["log_likelihood"])
preds[prod] = prob
result[substrate] = preds
return result

View File

@ -254,15 +254,7 @@ class Classifier(Plugin):
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None: def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
if cls.Config is None: if cls.Config is None:
return None return None
return cls.Config(**(data or {}))
# remove empty strings a.k.a unset params to not overwrite defaults
cpy = {}
if data is not None:
for k, v in data.items():
if v != "":
cpy[k] = v
return cls.Config(**cpy)
@classmethod @classmethod
def create(cls, data: dict | None = None): def create(cls, data: dict | None = None):

View File

@ -143,12 +143,6 @@ if os.environ.get("USE_TEMPLATE_DB", False) == "True":
"TEMPLATE": os.environ["TEMPLATE_DB"], "TEMPLATE": os.environ["TEMPLATE_DB"],
} }
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
@ -480,14 +474,3 @@ if DATA_POOL_MAPPING:
else: else:
DATA_POOL_MAPPING = {} DATA_POOL_MAPPING = {}
PROXIES = {}
if os.environ.get("HTTP_PROXY"):
PROXIES["http"] = os.environ.get("HTTP_PROXY")
PROXIES["https"] = os.environ.get("HTTPS_PROXY")
# BB4g
BB4G_URL = os.environ.get("BB4G_URL")
BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")

View File

@ -5,5 +5,4 @@ from . import views
urlpatterns = [ urlpatterns = [
path("entra/login/", views.entra_login, name="entra_login"), path("entra/login/", views.entra_login, name="entra_login"),
path("auth/redirect/", views.entra_callback, name="entra_callback"), path("auth/redirect/", views.entra_callback, name="entra_callback"),
path("auth/token/", views.get_token, name="get_token"),
] ]

View File

@ -2,11 +2,9 @@ import msal
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth import login from django.contrib.auth import login
from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from epdb.logic import UserManager, GroupManager from epdb.logic import UserManager
from epdb.models import Group
def get_msal_app_with_cache(request): def get_msal_app_with_cache(request):
@ -83,12 +81,10 @@ def entra_callback(request):
login(request, u) login(request, u)
# EDIT START # EDIT START
# Ensure groups exists in eP # Ensure groups exists in eP
for id, name in s.ENTRA_SECRET_GROUPS.items(): for id, name in s.ENTRA_SECRET_GROUPS.items():
if not Group.objects.filter(uuid=id).exists(): if not Group.objects.filter(uuid=id).exists():
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id)
uuid=id)
else: else:
g = Group.objects.get(uuid=id) g = Group.objects.get(uuid=id)
# Ensure its secret # Ensure its secret
@ -97,8 +93,7 @@ def entra_callback(request):
for id, name in s.ENTRA_GROUPS.items(): for id, name in s.ENTRA_GROUPS.items():
if not Group.objects.filter(uuid=id).exists(): if not Group.objects.filter(uuid=id).exists():
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id)
uuid=id)
else: else:
g = Group.objects.get(uuid=id) g = Group.objects.get(uuid=id)
@ -116,11 +111,6 @@ def get_access_token_from_request(request, scopes=None):
""" """
Get an access token from the request using MSAL token cache. Get an access token from the request using MSAL token cache.
""" """
# Check if auth via Access Token
if request.headers.get("Authorization"):
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
if scopes is None: if scopes is None:
scopes = s.MS_ENTRA_SCOPES scopes = s.MS_ENTRA_SCOPES
@ -162,9 +152,3 @@ def get_access_token_from_request(request, scopes=None):
return result return result
return None return None
def get_token(request):
token = get_access_token_from_request(request)
msg = f"{token}"
return HttpResponse(msg, content_type='text/plain')

View File

@ -1,17 +1,19 @@
import hashlib
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import jwt import jwt
import nh3
import requests import requests
import nh3
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from jwt import InvalidIssuerError
from ninja import Field, Form, Query, Router, Schema from ninja import Field, Form, Query, Router, Schema
from ninja.errors import HttpError
from ninja.security import HttpBearer from ninja.security import HttpBearer
from ninja.security import SessionAuth
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
from utilities.misc import PackageExporter from utilities.misc import PackageExporter
@ -49,26 +51,6 @@ from .models import (
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
def get_cached_jwks(tenant_id: str, force=False) -> Dict:
"""Get JWKS using Django cache"""
cache_key = f"jwks_{tenant_id}"
jwks = cache.get(cache_key)
if jwks is None or force:
# Cache miss, fetch new keys
jwks_uri = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
response = requests.get(jwks_uri)
response.raise_for_status()
jwks = response.json()
# Cache for 1 hour (3600 seconds)
cache.set(cache_key, jwks, 3600)
return jwks
def get_package_for_write(user, package_uuid): def get_package_for_write(user, package_uuid):
p = PackageManager.get_package_by_id(user, package_uuid) p = PackageManager.get_package_by_id(user, package_uuid)
if not PackageManager.writable(user, p): if not PackageManager.writable(user, p):
@ -86,7 +68,9 @@ def validate_token(token: str) -> dict:
TENANT_ID = s.MS_ENTRA_TENANT_ID TENANT_ID = s.MS_ENTRA_TENANT_ID
CLIENT_ID = s.MS_ENTRA_CLIENT_ID CLIENT_ID = s.MS_ENTRA_CLIENT_ID
jwks = get_cached_jwks(TENANT_ID) # Fetch Microsoft's public keys
jwks_uri = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"
jwks = requests.get(jwks_uri).json()
header = jwt.get_unverified_header(token) header = jwt.get_unverified_header(token)
@ -94,21 +78,13 @@ def validate_token(token: str) -> dict:
next(k for k in jwks["keys"] if k["kid"] == header["kid"]) next(k for k in jwks["keys"] if k["kid"] == header["kid"])
) )
# Handle V1 and V2 tokens claims = jwt.decode(
try: token,
claims = jwt.decode( public_key,
token, algorithms=["RS256"],
public_key, audience=[CLIENT_ID, f"api://{CLIENT_ID}"],
algorithms=["RS256"], issuer=f"https://sts.windows.net/{TENANT_ID}/",
audience=[CLIENT_ID, f"api://{CLIENT_ID}"], )
issuer=[
f"https://sts.windows.net/{TENANT_ID}/",
f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
]
)
except Exception as e:
raise ValueError(f"Token verification failed! - {e}")
return claims return claims
@ -820,7 +796,6 @@ class CreateCompound(Schema):
compoundName: str | None = None compoundName: str | None = None
compoundDescription: str | None = None compoundDescription: str | None = None
inchi: str | None = None inchi: str | None = None
pesLink: str | None = None
@router.post("/package/{uuid:package_uuid}/compound") @router.post("/package/{uuid:package_uuid}/compound")
@ -832,28 +807,9 @@ def create_package_compound(
try: try:
p = get_package_for_write(request.user, package_uuid) p = get_package_for_write(request.user, package_uuid)
# inchi is not used atm # inchi is not used atm
c = Compound.create(
if c.pesLink is not None: p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
from bayer.views import fetch_pes )
from bayer.models import PESCompound
try:
pes_data = fetch_pes(request, c.pesLink)
except ValueError as e:
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
classification = pes_data.get("classificationLevel", "")
if "secret" == classification.lower():
data_pools = pes_data.get("dataPools")
if data_pools:
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
else:
c = Compound.create(
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
)
return redirect(c.url) return redirect(c.url)
except ValueError as e: except ValueError as e:
return 400, {"message": str(e)} return 400, {"message": str(e)}
@ -1874,7 +1830,6 @@ class CreateNode(Schema):
nodeName: str | None = None nodeName: str | None = None
nodeReason: str | None = None nodeReason: str | None = None
nodeDepth: str | None = None nodeDepth: str | None = None
pesLink: str | None = None
@router.post( @router.post(
@ -1886,43 +1841,14 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
p = get_package_for_write(request.user, package_uuid) p = get_package_for_write(request.user, package_uuid)
pw = Pathway.objects.get(package=p, uuid=pathway_uuid) pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
if n.pesLink: if n.nodeDepth is not None and n.nodeDepth.strip() != "":
from bayer.views import fetch_pes node_depth = int(n.nodeDepth)
from bayer.models import PESCompound
try:
pes_data = fetch_pes(request, c.pesLink)
except ValueError as e:
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
classification = pes_data.get("classificationLevel", "")
if "secret" == classification.lower():
data_pools = pes_data.get("dataPools")
if data_pools:
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
node = Node()
node.stereo_removed = False
node.pathway = pw
node.depth = 0
node.default_node_label = c.default_structure
node.save()
node.node_labels.add(c.default_structure)
node.save()
else: else:
if n.nodeDepth is not None and n.nodeDepth.strip() != "": node_depth = -1
node_depth = int(n.nodeDepth)
else:
node_depth = -1
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason) n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
return redirect(node.url) return redirect(n.url)
except ValueError: except ValueError:
return 403, {"message": "Adding node failed!"} return 403, {"message": "Adding node failed!"}
@ -2074,9 +2000,6 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
description=e.edgeReason, description=e.edgeReason,
) )
# Update depths as sideeffect of above operation
pw.update_depths()
return redirect(new_e.url) return redirect(new_e.url)
except ValueError: except ValueError:
return 403, {"message": "Adding Edge failed!"} return 403, {"message": "Adding Edge failed!"}

View File

@ -1065,9 +1065,52 @@ class PackageManager(object):
print("Fixing Node depths...") print("Fixing Node depths...")
total_pws = Pathway.objects.filter(package=pack).count() total_pws = Pathway.objects.filter(package=pack).count()
for p, pw in enumerate(Pathway.objects.filter(package=pack)): for p, pw in enumerate(Pathway.objects.filter(package=pack)):
pw.update_depths() in_count = defaultdict(lambda: 0)
out_count = defaultdict(lambda: 0)
for e in pw.edges:
# TODO check if this will remain
for react in e.start_nodes.all():
out_count[str(react.uuid)] += 1
for prod in e.end_nodes.all():
in_count[str(prod.uuid)] += 1
root_nodes = []
for n in pw.nodes:
num_parents = in_count[str(n.uuid)]
if num_parents == 0:
# must be a root node or unconnected node
if n.depth != 0:
n.depth = 0
n.save()
# Only root node may have children
if out_count[str(n.uuid)] > 0:
root_nodes.append(n)
levels = [root_nodes]
seen = set()
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
for i, level_nodes in enumerate(levels):
new_level = []
for n in level_nodes:
for e in n.out_edges.all():
for prod in e.end_nodes.all():
if str(prod.uuid) not in seen:
old_depth = prod.depth
if old_depth != i + 1:
prod.depth = i + 1
prod.save()
new_level.append(prod)
seen.add(str(n.uuid))
if new_level:
levels.append(new_level)
print(f"{p + 1}/{total_pws} fixed.", end="\r") print(f"{p + 1}/{total_pws} fixed.", end="\r")
return pack return pack

View File

@ -1,56 +0,0 @@
# Generated by Django 6.0.3 on 2026-05-11 20:25
from django.db import migrations
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
MAPPING = {
"": HalfLifeModel.OTHER,
"HS-SFO": HalfLifeModel.HS_SFO,
"FOMC": HalfLifeModel.FOMC,
"FOTC": HalfLifeModel.DFOP,
"FMOC": HalfLifeModel.FOMC,
"DFOP": HalfLifeModel.DFOP,
"SFO + SFO": HalfLifeModel.SFO_SFO,
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
"first order kinetics": HalfLifeModel.SFO,
"SFO²": HalfLifeModel.SFO,
"HS": HalfLifeModel.HS,
"top down": HalfLifeModel.OTHER,
"SFO": HalfLifeModel.SFO,
"First Order": HalfLifeModel.SFO,
"SFO/SFO": HalfLifeModel.SFO_SFO,
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
"true": HalfLifeModel.SFO,
"SFO-SFO": HalfLifeModel.SFO_SFO,
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
}
def forward_func(apps, schema_editor):
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
hls = AdditionalInformation.objects.filter(type="HalfLife")
for hl in hls:
data = hl.data
data["model"] = MAPPING[data["model"]].value
hl.data = HalfLife(**data).model_dump(mode="json")
hl.save()
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
for hl in hlws:
data = hl.data
data["model"] = MAPPING[data["model"]].value
hl.data = HalfLifeWS(**data).model_dump(mode="json")
hl.save()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0024_user_contacted"),
]
operations = [
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
]

View File

@ -2186,56 +2186,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
): ):
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description) return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
def update_depths(self):
# Collect number of in and out links per node
in_count = defaultdict(lambda: 0)
out_count = defaultdict(lambda: 0)
for e in self.edges:
for react in e.start_nodes.all():
out_count[str(react.uuid)] += 1
for prod in e.end_nodes.all():
in_count[str(prod.uuid)] += 1
depth_map = {}
depth_map[0] = list()
for n in self.nodes:
num_parents = in_count[str(n.uuid)]
if num_parents == 0:
# must be a root node or unconnected node
if n.depth != 0:
n.depth = 0
n.save()
# Only root node may have children
if out_count[str(n.uuid)] > 0:
depth_map[0].append(n)
# At most depth len(nodes) is possible
for i in range(self.nodes.count()):
level_nodes = depth_map.get(i, [])
if len(level_nodes) == 0:
break
unique_next_level = set()
for n in level_nodes:
for e in self.edges:
if n in e.start_nodes.all():
for p in e.end_nodes.all():
unique_next_level.add(p)
if len(unique_next_level) > 0:
depth_map[i + 1] = list(unique_next_level)
for depth, nodes in depth_map.items():
for n in nodes:
if n.depth != depth:
n.depth = depth
n.save()
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin): class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
pathway = models.ForeignKey( pathway = models.ForeignKey(

View File

@ -937,14 +937,12 @@ def package_models(request, package_uuid):
"requires_rule_packages": True, "requires_rule_packages": True,
"requires_data_packages": True, "requires_data_packages": True,
}, },
} "EnviFormer": {
if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = {
"type": "enviformer", "type": "enviformer",
"requires_rule_packages": False, "requires_rule_packages": False,
"requires_data_packages": True, "requires_data_packages": True,
}, },
}
if s.FLAGS.get("PLUGINS", False): if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items(): for k, v in s.CLASSIFIER_PLUGINS.items():
@ -2539,9 +2537,6 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
substrate_nodes, product_nodes, name=edge_name, description=edge_description substrate_nodes, product_nodes, name=edge_name, description=edge_description
) )
# Update depths as sideeffect of above operation
current_pathway.update_depths()
return redirect(current_pathway.url) return redirect(current_pathway.url)
else: else:

View File

@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
import matplotlib.patches as mpatches import matplotlib.patches as mpatches
import numpy as np import numpy as np
from matplotlib.figure import Figure from matplotlib import pyplot as plt
from scipy import stats from scipy import stats
""" """
@ -101,8 +101,7 @@ class PepperPrediction(PropertyPrediction):
mask_red = x > vp mask_red = x > vp
# Plot # Plot
fig = Figure(figsize=(9, 5.5)) fig, ax = plt.subplots(figsize=(9, 5.5))
ax = fig.subplots()
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF") ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
if np.any(mask_green): if np.any(mask_green):
@ -147,12 +146,13 @@ class PepperPrediction(PropertyPrediction):
] ]
ax.legend(handles=patches, frameon=True) ax.legend(handles=patches, frameon=True)
fig.tight_layout() plt.tight_layout()
# --- Export to SVG string --- # --- Export to SVG string ---
buf = io.StringIO() buf = io.StringIO()
fig.savefig(buf, format="svg", bbox_inches="tight") fig.savefig(buf, format="svg", bbox_inches="tight")
svg = buf.getvalue() svg = buf.getvalue()
plt.close(fig)
buf.close() buf.close()
return svg return svg

View File

@ -187,9 +187,8 @@ class Pepper:
groups = [group for group in dataset.group_by("structure_id")] groups = [group for group in dataset.group_by("structure_id")]
# Unless explicitly set compute everything serial # Unless explicitly set compute everything serial
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1)) if os.environ.get("N_PEPPER_THREADS", 1) > 1:
if n_threads > 1: results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
results = Parallel(n_jobs=n_threads)(
delayed(compute_bayes_per_group)(group[1]) delayed(compute_bayes_per_group)(group[1])
for group in dataset.group_by("structure_id") for group in dataset.group_by("structure_id")
) )

View File

@ -605,36 +605,7 @@ function draw(pathway, elem) {
// Check if target is pseudo and draw marker only if not pseudo // Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)') .attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
.on("click", function(event, d) {
const wasHighlighted = d3.select(this).classed("highlighted");
d3.selectAll("line").classed("highlighted", false);
if (!wasHighlighted) {
const toHighlight = [];
toHighlight.push(d.el);
if (d.source.pseudo || d.target.pseudo) {
if (d.target.pseudo) {
d3.selectAll("line").each(e => {
if (e !== undefined && e.source.id === d.target.id) {
toHighlight.push(e.el);
}
});
} else {
d3.selectAll("line").each(e => {
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
toHighlight.push(e.el);
}
});
}
}
for (const e of toHighlight) {
d3.select(e).classed("highlighted", true);
}
}
})
// add element to links array // add element to links array
link.each(function (d) { link.each(function (d) {
@ -653,13 +624,7 @@ function draw(pathway, elem) {
.on("drag", dragged) .on("drag", dragged)
.on("end", dragended)) .on("end", dragended))
.on("click", function (event, d) { .on("click", function (event, d) {
const wasHighlighted = d3.select(this).select("circle").classed("highlighted"); d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
d3.selectAll('circle.highlighted').classed('highlighted', false);
if (!wasHighlighted) {
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
}
}) })
// Kreise für die Knoten hinzufügen // Kreise für die Knoten hinzufügen

View File

@ -9,6 +9,14 @@
<i class="glyphicon glyphicon-plus"></i> Add Compound</a <i class="glyphicon glyphicon-plus"></i> Add Compound</a
> >
</li> </li>
<li>
<a
class="button"
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Add PES</a
>
</li>
<li> <li>
<a <a
class="button" class="button"
@ -22,7 +30,7 @@
{% for tpl in action_button_templates %} {% for tpl in action_button_templates %}
{% include tpl %} {% include tpl %}
{% endfor %} {% endfor %}
<li role="separator" class="divider h-px"></li> <li role="separator" class="divider"></li>
{% endif %} {% endif %}
<li> <li>
<a <a
@ -74,7 +82,7 @@
Rules</a Rules</a
> >
</li> </li>
<li role="separator" class="divider h-px"></li> <li role="separator" class="divider"></li>
<li> <li>
<a <a
class="button" class="button"
@ -99,16 +107,11 @@
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li role="separator" class="divider h-px"></li> <li role="separator" class="divider"></li>
<li> <li>
<a <a
class="button" class="button"
onclick=" onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
const modal = document.getElementById('delete_pathway_node_modal');
modal.showModal();
window.dispatchEvent(new Event('modal-opened'));
return false;
"
> >
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a <i class="glyphicon glyphicon-trash"></i> Delete Compound</a
> >
@ -116,12 +119,7 @@
<li> <li>
<a <a
class="button" class="button"
onclick=" onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
const modal = document.getElementById('delete_pathway_edge_modal');
modal.showModal();
window.dispatchEvent(new Event('modal-opened'));
return false;
"
> >
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
> >

View File

@ -5,7 +5,7 @@
{% block action_button %} {% block action_button %}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -3,16 +3,14 @@
{% block page_title %}Models{% endblock %} {% block page_title %}Models{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
{% if meta.enabled_features.MODEL_BUILDING %} <button
<button type="button"
type="button" class="btn btn-primary btn-sm"
class="btn btn-primary btn-sm" onclick="document.getElementById('new_model_modal').showModal(); return false;"
onclick="document.getElementById('new_model_modal').showModal(); return false;" >
> New Model
New Model </button>
</button>
{% endif %}
{% endif %} {% endif %}
{% endblock action_button %} {% endblock action_button %}

View File

@ -3,6 +3,7 @@
{% block page_title %}Packages{% endblock %} {% block page_title %}Packages{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit %}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
@ -70,6 +71,7 @@
</ul> </ul>
</div> </div>
</div> </div>
{% endif %}
{% endblock action_button %} {% endblock action_button %}
{% block action_modals %} {% block action_modals %}

View File

@ -3,7 +3,7 @@
{% block page_title %}Pathways{% endblock %} {% block page_title %}Pathways{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a <a
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -3,7 +3,7 @@
{% block page_title %}Reactions{% endblock %} {% block page_title %}Reactions{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -3,7 +3,7 @@
{% block page_title %}Rules{% endblock %} {% block page_title %}Rules{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -11,7 +11,7 @@
{% endblock action_modals %} {% endblock action_modals %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -3,7 +3,7 @@
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %} {% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
{% block action_button %} {% block action_button %}
{% if meta.can_edit or not meta.url_contains_package %} {% if meta.can_edit %}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"

View File

@ -203,11 +203,11 @@
id="model-based-prediction-setting-threshold" id="model-based-prediction-setting-threshold"
name="model-based-prediction-setting-threshold" name="model-based-prediction-setting-threshold"
class="input input-bordered w-full" class="input input-bordered w-full"
value="0.25" placeholder="0.25"
type="number" type="number"
min="0" min="0"
max="1" max="1"
step="any" step="0.05"
/> />
</div> </div>

View File

@ -4,25 +4,6 @@
id="delete_pathway_edge_modal" id="delete_pathway_edge_modal"
class="modal" class="modal"
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })" x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
@modal-opened.window="
const links = d3.selectAll('line.highlighted');
console.log(links);
if (!links.empty()) {
const el = links.node();
const selectElement = document.getElementById('delete_pathway_edge_edges');
console.log(el);
console.log(el.__data__);
for (let option of selectElement.options) {
if (option.value === el.__data__.url) {
option.selected = true;
break;
}
}
selectElement.dispatchEvent(new Event('change'));
}
"
@close="reset()" @close="reset()"
> >
<div class="modal-box"> <div class="modal-box">

View File

@ -4,22 +4,6 @@
id="delete_pathway_node_modal" id="delete_pathway_node_modal"
class="modal" class="modal"
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })" x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
@modal-opened.window="
const el = d3.select('circle.highlighted').node();
if (el !== null) {
const selectElement = document.getElementById('delete_pathway_node_nodes');
for (let option of selectElement.options) {
if (option.value === el.__data__.url) {
option.selected = true;
break;
}
}
selectElement.dispatchEvent(new Event('change'));
}
"
@close="reset()" @close="reset()"
> >
<div class="modal-box"> <div class="modal-box">

View File

@ -77,6 +77,7 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/add_pathway_node_modal.html" %} {% include "modals/objects/add_pathway_node_modal.html" %}
{% include "modals/objects/add_pathway_pes_node_modal.html" %}
{% include "modals/objects/add_pathway_edge_modal.html" %} {% include "modals/objects/add_pathway_edge_modal.html" %}
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %} {% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
{% for tpl in add_templates %} {% for tpl in add_templates %}
@ -106,12 +107,12 @@
</div> </div>
<!-- Graphical Representation --> <!-- Graphical Representation -->
<div class="collapse-arrow bg-base-200 collapse overflow-y-auto"> <div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
Graphical Representation Graphical Representation
</div> </div>
<div class="collapse-content "> <div class="collapse-content">
<div class="bg-base-100 mb-2 rounded-lg p-2"> <div class="bg-base-100 mb-2 rounded-lg p-2">
<div class="navbar bg-base-100 rounded-lg"> <div class="navbar bg-base-100 rounded-lg">
<div class="flex-1"> <div class="flex-1">
@ -140,7 +141,7 @@
</div> </div>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-96 p-2" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
> >
{% include "actions/objects/pathway.html" %} {% include "actions/objects/pathway.html" %}
</ul> </ul>

View File

@ -64,7 +64,7 @@
import logging import logging
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel from envipy_additional_information import HalfLife, HalfLifeWS
from envipy_additional_information.information import Interval from envipy_additional_information.information import Interval
from envipy_additional_information.parsers import ( from envipy_additional_information.parsers import (
AcidityParser, AcidityParser,
@ -473,12 +473,17 @@ def build_additional_information_from_request(request, type_):
comment = get_parameter_or_empty_string(request, "comment") comment = get_parameter_or_empty_string(request, "comment")
source = get_parameter_or_empty_string(request, "source") source = get_parameter_or_empty_string(request, "source")
# first_order = get_parameter_or_empty_string(request, "firstOrder") first_order = get_parameter_or_empty_string(request, "firstOrder")
model = get_parameter_or_empty_string(request, "model") model = get_parameter_or_empty_string(request, "model")
fit = get_parameter_or_empty_string(request, "fit") fit = get_parameter_or_empty_string(request, "fit")
if model: if first_order != "":
model = HalfLifeModel(model.upper()) if model != "":
raise ValueError("not both, model and firstOrder can be set!")
if first_order == "true":
model = "SFO"
else:
logger.info("firstOrder is set to false which is not meaningful")
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source) return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
@ -503,10 +508,6 @@ def build_additional_information_from_request(request, type_):
comment_ws = get_parameter_or_empty_string(request, "comment_ws") comment_ws = get_parameter_or_empty_string(request, "comment_ws")
source_ws = get_parameter_or_empty_string(request, "source_ws") source_ws = get_parameter_or_empty_string(request, "source_ws")
model_ws = get_parameter_or_empty_string(request, "model_ws") model_ws = get_parameter_or_empty_string(request, "model_ws")
if model_ws:
model_ws = HalfLifeModel(model_ws.upper())
fit_ws = get_parameter_or_empty_string(request, "fit_ws") fit_ws = get_parameter_or_empty_string(request, "fit_ws")
dt50_total = IntervalParser.from_string(hl_ws_total) dt50_total = IntervalParser.from_string(hl_ws_total)

34
uv.lock generated
View File

@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
[[package]] [[package]]
name = "envipy-additional-information" name = "envipy-additional-information"
version = "0.4.2" version = "0.4.2"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#676dae1c5678539beac637b87e49b9dadfdfd85a" } source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#0a608c85c73a6ef5c38afea87d2b57fb43f01a70" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
] ]
@ -2763,9 +2763,9 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
] ]
wheels = [ wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54", upload-time = "2025-10-01T23:35:50Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58", upload-time = "2025-10-01T23:35:52Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
] ]
[[package]] [[package]]
@ -2785,19 +2785,19 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
wheels = [ wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5", upload-time = "2025-10-01T23:33:41Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d", upload-time = "2025-10-01T23:33:45Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e", upload-time = "2025-10-01T23:33:48Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d", upload-time = "2025-10-01T23:33:52Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434", upload-time = "2025-10-01T23:34:10Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d", upload-time = "2025-10-01T23:34:15Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25", upload-time = "2025-10-01T23:34:19Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de", upload-time = "2025-10-01T23:34:23Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856", upload-time = "2025-10-01T23:34:28Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88", upload-time = "2025-10-01T23:34:45Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041", upload-time = "2025-10-01T23:34:50Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab", upload-time = "2025-10-01T23:34:53Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
] ]
[[package]] [[package]]