18 Commits

Author SHA1 Message Date
14cfc1e4d7 [Feature] Integrate DOI Links, Handle Cycles in Pathway Viz (#407)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#407
2026-06-03 06:00:38 +12:00
868bbf5c05 [Fix] False as multi_step default in legacy API (#406)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#406
2026-05-29 08:12:52 +12:00
be5ee1d1d7 [Fix] Propagate multi_step in Edge.create to Reaction.create to ensure deduplication is working (#405)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#405
2026-05-29 07:39:32 +12:00
20fd949dfd [Fix] Implement legacy PFASConfidence object construction (#404)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#404
2026-05-28 23:57:34 +12:00
c9b643fe6e [Feature] Make JobLog Page Paginated (#403)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#403
2026-05-28 23:01:27 +12:00
1a9f1cf9af [Fix] Legacy API Node Depth Parsing, description validation in Reaction.create (#402)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#402
2026-05-28 09:53:39 +12:00
c7c7e17e43 [Feature] Implement legacy endpoints to enable copy via python client (#400)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#400
2026-05-28 01:41:28 +12:00
674e10c7fa [Fix] Legacy API Package Endpoint, Pepper Unit in Pathway View (#399)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#399
2026-05-27 21:09:17 +12:00
8079b80d57 [Fix] Fix Typo in multi_step assignment (#397)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#397
2026-05-21 09:56:21 +12:00
76e63fda2c [Fix] Fix BART Upload (#396)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#396
2026-05-21 03:16:02 +12:00
1e43c298d2 [Fix] Simplify Depth adjustment (#386)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#386
2026-05-12 21:04:56 +12:00
b39fc7eaf8 [Fix] Update Node depth when adding new Edges to a Pathway (#384)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#384
2026-05-12 09:40:35 +12:00
a2fc9f72cb [Feature] Make use of HalfLifeModel Enum (#383)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#383
2026-05-12 09:23:56 +12:00
734b02767e [Fix] Update plotting imports and thread handling in Pepper class (#382)
- plt.subplot does not work reliably with async/ threads.
- Bug in thread run that would fail with env set (string to number)

Reviewed-on: enviPath/enviPy#382
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2026-05-12 06:43:26 +12:00
9d70db2ca2 [Fix] Wrong indentation in welcome mail (#373)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#373
2026-04-22 08:47:05 +12:00
fec26d0233 [Feature] Admin Actions for Activation and Affiliation Request (#372)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#372
2026-04-22 08:36:31 +12:00
689f7998eb [Dep] Updated enviFormer, additional information lib, aiohttp, fsspec (#371)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#371
2026-04-22 06:38:31 +12:00
8498e59fa1 [Feature] Changes required for non public tenants (#370)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#370
2026-04-22 06:08:39 +12:00
31 changed files with 1096 additions and 290 deletions

View File

@ -36,11 +36,13 @@ RUN --mount=type=ssh \
# Now copy source and do a final sync to install the project itself
# Ensure .dockerignore is reasonable
COPY biotransformer biotransformer
COPY bridge bridge
COPY envipath envipath
COPY epapi epapi
COPY epauth epauth
COPY epdb epdb
COPY epiuclid epiuclid
COPY fixtures fixtures
COPY migration migration
COPY pepper pepper

View File

@ -40,6 +40,9 @@ if "migration" in s.INSTALLED_APPS:
if s.MS_ENTRA_ENABLED:
urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
if s.TENANT != "public":
urlpatterns.append(path(f"{PATH_PREFIX}", include(f"{s.TENANT}.urls")))
# Custom error handlers
handler400 = "epdb.views.handler400"
handler403 = "epdb.views.handler403"

View File

@ -0,0 +1,26 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from epdb.models import JobLog
from ..pagination import EnhancedPageNumberPagination
from ..schemas import JobLogOutSchema
router = Router()
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
)
def list_all_joblogs(request):
"""
List all JobLogs from reviewed packages.
"""
current_user = request.user
if current_user.is_superuser:
return JobLog.objects.all().order_by("-created")
else:
return JobLog.objects.filter(user=current_user).order_by("-created")

View File

@ -15,6 +15,7 @@ from .endpoints import (
additional_information,
settings,
groups,
joblogs,
)
# Main router with authentication
@ -37,6 +38,7 @@ router.add_router("", structure.router)
router.add_router("", additional_information.router)
router.add_router("", settings.router)
router.add_router("", groups.router)
router.add_router("", joblogs.router)
if s.IUCLID_EXPORT_ENABLED:
from epiuclid.api import router as iuclid_router

View File

@ -1,7 +1,10 @@
from ninja import FilterSchema, FilterLookup, Schema
from datetime import datetime
from typing import Annotated, Optional, List, Dict, Any
from uuid import UUID
from django.urls import reverse
from ninja import Field, FilterSchema, FilterLookup, Schema
# Filter schema for query parameters
class ReviewStatusFilter(FilterSchema):
@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
url: str = ""
name: str
description: str
class SimpleUserOutSchema(Schema):
uuid: UUID
url: str
name: str = Field(alias="username")
class JobLogOutSchema(Schema):
user: SimpleUserOutSchema
id: UUID = Field(alias="task_id")
url: str
name: str = Field(alias="job_name")
created: datetime = Field(alias="created")
status: str = Field(alias="status")
done: Optional[datetime] = Field(None, alias="done_at")
@staticmethod
def resolve_url(obj):
return reverse("job detail", kwargs={"job_uuid": obj.task_id})

View File

@ -1,12 +1,32 @@
import msal
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.contrib.auth import login
from django.shortcuts import redirect
from django.contrib.auth import get_user_model
from epdb.logic import UserManager
def get_msal_app_with_cache(request):
"""
Create MSAL app with session-based token cache.
"""
cache = msal.SerializableTokenCache()
# Load cache from session if it exists
if request.session.get("msal_token_cache"):
cache.deserialize(request.session["msal_token_cache"])
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY,
token_cache=cache,
)
return msal_app, cache
def entra_login(request):
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
@ -23,11 +43,7 @@ def entra_login(request):
def entra_callback(request):
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY,
)
msal_app, cache = get_msal_app_with_cache(request)
flow = request.session.pop("msal_auth_flow", None)
if not flow:
@ -36,11 +52,18 @@ def entra_callback(request):
# Acquire token using the flow and callback request
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
# Save the token cache to session
if cache.has_state_changed:
request.session["msal_token_cache"] = cache.serialize()
claims = result["id_token_claims"]
user_name = claims["name"]
user_email = claims["emailaddress"]
user_oid = claims["oid"]
user_name = claims.get("name")
user_email = claims.get("emailaddress", claims.get("email"))
user_oid = claims.get("oid")
if not all([user_name, user_email, user_oid]):
raise ValueError("Missing required claims in ID token")
# Get implementing class
User = get_user_model()
@ -57,4 +80,51 @@ def entra_callback(request):
login(request, u)
return redirect("/") # Handle errors
return redirect(s.SERVER_URL) # Handle errors
def get_access_token_from_request(request, scopes=None):
"""
Get an access token from the request using MSAL token cache.
"""
if scopes is None:
scopes = s.MS_ENTRA_SCOPES
# Get user from request (must be authenticated)
if not request.user.is_authenticated:
return None
# Create MSAL app with persistent cache
msal_app, cache = get_msal_app_with_cache(request)
# Try to get accounts from cache
accounts = msal_app.get_accounts()
if not accounts:
return None
# Find the account that matches the current user
user_account = None
for account in accounts:
if account.get("local_account_id") == str(request.user.uuid):
user_account = account
break
# If no matching account found, use the first available account
if not user_account and accounts:
user_account = accounts[0]
if not user_account:
return None
# Try to acquire token silently from cache
result = msal_app.acquire_token_silent(scopes=scopes, account=user_account)
# Save cache changes back to session
if cache.has_state_changed:
request.session["msal_token_cache"] = cache.serialize()
if result and "access_token" in result:
return result
return None

View File

@ -1,5 +1,8 @@
import logging
from django.conf import settings as s
from django.contrib import admin
from django.contrib import messages
from .models import (
AdditionalInformation,
@ -29,6 +32,8 @@ from .models import (
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
class AdditionalInformationAdmin(admin.ModelAdmin):
pass
@ -45,6 +50,113 @@ class UserAdmin(admin.ModelAdmin):
"date_joined",
]
actions = ["send_welcome_mail", "send_affiliation_mail"]
@admin.action(description="Send welcome mail")
def send_welcome_mail(self, request, queryset):
from django.core.mail import EmailMultiAlternatives
tpl = """Hello {username},
Your account has been successfully activated.
To log in, please visit
https://envipath.org/password_reset/
and request a new password.
If you have any questions or feedback, feel free to visit our community forum at
https://community.envipath.org/.
You do not need to register again for the forum - you can log in using your enviPath account by clicking "Log In" and then "Log in with enviPath."
Best regards,
The enviPath Team"""
users = []
for user in queryset:
if user.is_active:
logger.info(f"{user.username} already active - not sending mail again")
continue
try:
msg = EmailMultiAlternatives(
"Your enviPath Account Is Now Active",
tpl.format(username=user.username),
"admin@envipath.org",
[user.email],
bcc=["admin@envipath.org"],
)
msg.send(fail_silently=False)
user.is_active = True
user.password = "ASDF"
user.save()
users.append(user)
logger.info(f"{user.username} -> {user.email} mail sent")
except Exception as e:
logger.info(f"Error sending mail to {user.username}: {e}")
self.message_user(
request, f"Sent welcome mail to {[u.email for u in users]}", messages.SUCCESS
)
@admin.action(description="Send affiliation mail")
def send_affiliation_mail(self, request, queryset):
from django.core.mail import EmailMultiAlternatives
tpl = """Dear {username},
Thank you for your interest in enviPath!
Please note that the public enviPath system is intended for non-commercial use only.
We see that you registered using the email address {email}.
If possible, we kindly ask you to register using an official email address that reflects your affiliation (e.g., a university, NGO, or research organization).
If you would like us to update your account, simply reply to this email and let us know which address we should use.
We will then change it in our system, and you will receive a password reset email at the new address.
If you are registering with a company email address and are interested in commercial use, you are very welcome to book a meeting with us so we can discuss how we can best support you.
To book a meeting, please visit https://envipath.com/book
If changing to an affiliation email address is not possible, please contact us at registration@envipath.org
Best regards,
enviPath team"""
users = []
for user in queryset:
if user.is_active or user.contacted:
logger.info(
f"{user.username} already active or already contacted - not sending mail again"
)
continue
try:
msg = EmailMultiAlternatives(
"Regarding your enviPath registration",
tpl.format(username=user.username, email=user.email),
"admin@envipath.org",
[user.email],
bcc=["admin@envipath.org"],
)
msg.send(fail_silently=False)
user.contacted = True
user.save()
users.append(user)
logger.info(f"{user.username} -> {user.email} affiliation mail sent")
except Exception as e:
logger.info(f"Error sending mail to {user.username}: {e}")
self.message_user(
request, f"Sent affiliation mail to {[u.email for u in users]}", messages.SUCCESS
)
class UserPackagePermissionAdmin(admin.ModelAdmin):
pass

View File

@ -386,23 +386,50 @@ class PackageSchema(Schema):
@staticmethod
def resolve_readers(obj: Package):
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.READ[0]
).values_list("user", flat=True)
).distinct()
readers = []
return [{u.id: u.get_name()} for u in users]
user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
users = User.objects.filter(id__in=user_ids).distinct()
for u in users:
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
"group", flat=True
)
groups = Group.objects.filter(id__in=group_ids).distinct()
for g in groups:
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
return readers
@staticmethod
def resolve_writers(obj: Package):
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.WRITE[0]
).values_list("user", flat=True)
).distinct()
writers = []
return [{u.id: u.get_name()} for u in users]
user_ids = UserPackagePermission.objects.filter(
package=obj,
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
).values_list("user", flat=True)
users = User.objects.filter(id__in=user_ids).distinct()
for u in users:
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
group_ids = GroupPackagePermission.objects.filter(
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
).values_list("group", flat=True)
groups = Group.objects.filter(id__in=group_ids).distinct()
for g in groups:
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
return writers
@staticmethod
def resolve_review_comment(obj):
@ -799,6 +826,27 @@ def delete_compound(request, package_uuid, compound_uuid):
}
class CreateCompoundStructure(Schema):
smiles: str
name: str | None = None
description: str | None = None
inchi: str | None = None
molfile: str | None = None
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
def create_package_compound_structure(
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
):
try:
p = get_package_for_write(request.user, package_uuid)
c = Compound.objects.get(package=p, uuid=compound_uuid)
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
return redirect(cs.url)
except ValueError as e:
return 400, {"message": str(e)}
@router.delete(
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
)
@ -1325,6 +1373,7 @@ class ScenarioSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
collectionID: Optional[str] = None
date: str = Field(None, alias="scenario_date")
description: str = Field(None, alias="description")
id: str = Field(None, alias="url")
identifier: str = "scenario"
@ -1468,6 +1517,7 @@ def create_package_additional_information(request, package_uuid):
scen = request.POST.get("scenario")
scenario = Scenario.objects.get(package=p, url=scen)
if request.POST.get("adInfoTypes[]"):
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
attach_obj = url_parser.get_object()
@ -1491,6 +1541,33 @@ def create_package_additional_information(request, package_uuid):
content_object=attach_obj,
)
elif request.POST.get("ais"):
import json
parsed_ais = json.loads(request.POST.get("ais"))
for ai_type, ais in parsed_ais.items():
for ai in ais:
attach_obj = None
if ai.get("related"):
url_parser = EPDBURLParser(ai.get("related").get("url"))
attach_obj = url_parser.get_object()
if not hasattr(attach_obj, "additional_information"):
raise ValueError("Can't attach additional information to this object!")
if not attach_obj.url.startswith(p.url):
raise ValueError(
"Additional Information can only be set to objects stored in the same package!"
)
AdditionalInformation.create(
p,
AdditionalInformation.from_dict(ai_type, ai),
scenario=scenario,
content_object=attach_obj,
)
# TODO implement additional information endpoint ?
return redirect(f"{scenario.url}")
except ValueError:
@ -1540,6 +1617,7 @@ class PathwayNode(Schema):
name: str = Field(None, alias="name")
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
smiles: str = Field(None, alias="default_node_label.smiles")
pseudo: bool = Field(False, alias="pseudo")
@staticmethod
def resolve_atom_count(obj: Node):
@ -1693,6 +1771,29 @@ def create_package_pathway(
return 403, {"message": str(e)}
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
def update_pathway(request, package_uuid, pathway_uuid):
try:
p = get_package_for_write(request.user, package_uuid)
if request.POST.get("scenario"):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
pw.scenarios.add(scen)
pw.save()
return redirect(f"{pw.url}")
else:
return 400, {"message": "No scenario specified!"}
except ValueError:
return 403, {
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
def delete_pathway(request, package_uuid, pathway_uuid):
try:
@ -1813,7 +1914,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
node_depth = int(n.nodeDepth)
node_depth = int(float(n.nodeDepth))
else:
node_depth = -1
@ -1958,6 +2059,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
for pr in e.products.split(","):
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
multi_step = False
if e.multistep and e.multistep.strip() == "true":
multi_step = True
new_e = Edge.create(
pathway=pw,
start_nodes=educts,
@ -1965,11 +2070,15 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
rule=None,
name=None,
description=e.edgeReason,
multi_step=multi_step,
)
# Update depths as sideeffect of above operation
pw.update_depths()
return redirect(new_e.url)
except ValueError:
return 403, {"message": "Adding node failed!"}
return 403, {"message": "Adding Edge failed!"}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")

View File

@ -264,8 +264,12 @@ class GroupManager(object):
return bool(re.findall(GroupManager.group_pattern, url))
@staticmethod
def create_group(current_user, name, description):
def create_group(current_user, name, description, *args, **kwargs):
g = Group()
if "uuid" in kwargs:
g.uuid = kwargs["uuid"]
# Clean for potential XSS
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@ -341,52 +345,17 @@ class PackageManager(object):
@staticmethod
def readable(user, package):
if (
UserPackagePermission.objects.filter(package=package, user=user).exists()
or GroupPackagePermission.objects.filter(
package=package, group__in=GroupManager.get_groups(user)
return (
PackageManager.has_package_permission(user, package, "read") | package.reviewed is True
)
or package.reviewed is True
or user.is_superuser
):
return True
return False
@staticmethod
def writable(user, package):
if (
UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.WRITE[0]
).exists()
or GroupPackagePermission.objects.filter(
package=package,
group__in=GroupManager.get_groups(user),
permission=Permission.WRITE[0],
).exists()
or UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.ALL[0]
).exists()
or user.is_superuser
):
return True
return False
return PackageManager.has_package_permission(user, package, "write")
@staticmethod
def administrable(user, package):
if (
UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.ALL[0]
).exists()
or GroupPackagePermission.objects.filter(
package=package,
group__in=GroupManager.get_groups(user),
permission=Permission.ALL[0],
).exists()
or user.is_superuser
):
return True
return False
return PackageManager.has_package_permission(user, package, "all")
@staticmethod
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
@ -470,7 +439,9 @@ class PackageManager(object):
# remove package if user is owner and package is reviewed e.g. admin
qs = qs.filter(reviewed=False)
return qs.distinct()
qs = qs.distinct()
return qs
@staticmethod
def get_all_writeable_packages(user):
@ -514,7 +485,9 @@ class PackageManager(object):
qs = qs.filter(reviewed=False)
return qs.distinct()
qs = qs.distinct()
return qs
@staticmethod
def get_packages():
@ -716,6 +689,10 @@ class PackageManager(object):
struc.description = structure["description"]
struc.aliases = structure.get("aliases", [])
struc.smiles = structure["smiles"]
if structure.get("molfile"):
struc.molfile = structure["molfile"]
struc.save()
for scen in structure["scenarios"]:
@ -1018,52 +995,9 @@ class PackageManager(object):
print("Fixing Node depths...")
total_pws = Pathway.objects.filter(package=pack).count()
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
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)
pw.update_depths()
print(f"{p + 1}/{total_pws} fixed.", end="\r")
return pack

View File

@ -0,0 +1,49 @@
# Generated by Django 6.0.3 on 2026-04-21 11:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0022_alter_classifierpluginmodel_data_packages_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="compoundstructure",
options={},
),
migrations.AlterModelOptions(
name="epmodel",
options={},
),
migrations.AlterModelOptions(
name="parallelrule",
options={},
),
migrations.AlterModelOptions(
name="rule",
options={},
),
migrations.AlterModelOptions(
name="sequentialrule",
options={},
),
migrations.AlterModelOptions(
name="simpleambitrule",
options={},
),
migrations.AlterModelOptions(
name="simplerdkitrule",
options={},
),
migrations.AlterModelOptions(
name="simplerule",
options={},
),
migrations.AddField(
model_name="compoundstructure",
name="molfile",
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 6.0.3 on 2026-04-21 19:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0023_alter_compoundstructure_options_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="contacted",
field=models.BooleanField(blank=True, null=True),
),
]

View File

@ -0,0 +1,56 @@
# 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

@ -0,0 +1,48 @@
# Generated by Django 6.0.3 on 2026-06-02 17:18
from django.db import migrations
from envipy_additional_information import DOI
def forward_func(apps, schema_editor):
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
refs = AdditionalInformation.objects.filter(type="Reference")
remaining = []
for ref in refs:
r = ref.data["reference"]
try:
# PubMed IDs are plain ints, try parsing
_ = int(r)
# Nothing to do
except ValueError:
DOMAINS = [
"http://dx.doi.org/",
"https://dx.doi.org/",
"http://doi.org/",
"https://doi.org/",
]
for d in DOMAINS:
r = r.replace(d, "")
if r.startswith("10."):
ref.type = DOI.__name__
ref.data = {"doi": r}
ref.save()
else:
remaining.append(ref)
if len(remaining) > 0:
raise ValueError(f"Could not parse {len(remaining)} references")
class Migration(migrations.Migration):
dependencies = [
("epdb", "0025_auto_20260511_2025"),
]
operations = [
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
]

View File

@ -75,6 +75,7 @@ class User(AbstractUser):
blank=False,
)
is_reviewer = models.BooleanField(default=False)
contacted = models.BooleanField(null=True, blank=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
@ -1112,6 +1113,7 @@ class CompoundStructure(
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
molfile = models.TextField(blank=True, null=True, verbose_name="Molfile")
external_identifiers = GenericRelation("ExternalIdentifier")
@ -1208,6 +1210,9 @@ class CompoundStructure(
return dict(hls)
def d3_json(self):
return {}
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
@ -1690,7 +1695,7 @@ class Reaction(
if name is not None and name.strip() != "":
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and name.strip() != "":
if description is not None and description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.multi_step = multi_step
@ -1768,7 +1773,7 @@ class Reaction(
return new_reaction
def smirks(self):
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
return f"{'.'.join([cs.smiles for cs in self.educts.all().order_by('-pk')])}>>{'.'.join([cs.smiles for cs in self.products.all().order_by('-pk')])}"
@property
def as_svg(self):
@ -1881,6 +1886,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
if n not in queue:
queue.append(n)
for i in queue:
processed.add(i)
while len(queue):
current = queue.pop()
processed.add(current)
@ -2173,6 +2181,50 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
):
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()
processed = set()
for n in self.root_nodes:
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:
processed.add(n)
for e in self.edges:
if n in e.start_nodes.all():
for p in e.end_nodes.all():
if p not in processed:
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 and depth != 0:
n.depth = depth
n.save()
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
pathway = models.ForeignKey(
@ -2214,7 +2266,11 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
if isinstance(ai.get(), PropertyPrediction):
predicted_properties[ai.get().__class__.__name__].append(ai.data)
return {
# If we have Subclasses of a CompoundStructure we can overwrite keys (e.g. images)
# by overwriting keys
structure_data = self.default_node_label.d3_json()
res = {
"depth": self.depth,
"stereo_removed": self.stereo_removed,
"url": self.url,
@ -2223,6 +2279,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
"image_svg": IndigoUtils.mol_to_svg(
self.default_node_label.smiles, width=40, height=40
),
"image_type": "svg",
"name": self.get_name(),
"smiles": self.default_node_label.smiles,
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
@ -2235,8 +2292,11 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
"predicted_properties": predicted_properties,
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
"timeseries": self.get_timeseries_data(),
**structure_data,
}
return res
@staticmethod
@transaction.atomic
def create(
@ -2392,6 +2452,8 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
rule: Optional[Rule] = None,
name: Optional[str] = None,
description: Optional[str] = None,
*args,
**kwargs,
):
e = Edge()
e.pathway = pathway
@ -2421,7 +2483,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
educts=[n.default_node_label for n in e.start_nodes.all()],
products=[n.default_node_label for n in e.end_nodes.all()],
rules=rule,
multi_step=False,
multi_step=kwargs.get("multi_step", False),
)
e.edge_label = r
@ -4429,18 +4491,22 @@ class AdditionalInformation(models.Model):
return f"{self.scenario.url}/additional-information/{self.uuid}"
def get(self) -> "EnviPyModel":
@staticmethod
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
from envipy_additional_information import registry
MAPPING = {c.__name__: c for c in registry.list_models().values()}
try:
inst = MAPPING[self.type](**self.data)
inst = MAPPING[ai_type](**ai_data)
except Exception as e:
print(f"Error loading {self.type}: {e}")
print(f"Error loading {ai_type}: {e}")
raise e
inst.__dict__["uuid"] = str(self.uuid)
return inst
def get(self) -> "EnviPyModel":
inst = AdditionalInformation.from_dict(self.type, self.data)
inst.__dict__["uuid"] = str(self.uuid)
return inst
def __str__(self) -> str:

View File

@ -2506,6 +2506,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
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)
else:
@ -3077,12 +3080,21 @@ def jobs(request):
{"Home": s.SERVER_URL},
{"Jobs": s.SERVER_URL + "/jobs"},
]
if current_user.is_superuser:
context["jobs"] = JobLog.objects.all().order_by("-created")
else:
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
# if current_user.is_superuser:
# context["jobs"] = JobLog.objects.all().order_by("-created")
# else:
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
return render(request, "collections/joblog.html", context)
# Context for paginated template
context["entity_type"] = "joblog"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "joblog"
context["list_mode"] = "combined"
return render(request, "collections/joblog_paginated.html", context)
# return render(request, "collections/joblog.html", context)
elif request.method == "POST":
job_name = request.POST.get("job-name")

View File

@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
import matplotlib.patches as mpatches
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.figure import Figure
from scipy import stats
"""
@ -101,7 +101,8 @@ class PepperPrediction(PropertyPrediction):
mask_red = x > vp
# Plot
fig, ax = plt.subplots(figsize=(9, 5.5))
fig = Figure(figsize=(9, 5.5))
ax = fig.subplots()
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
if np.any(mask_green):
@ -146,13 +147,12 @@ class PepperPrediction(PropertyPrediction):
]
ax.legend(handles=patches, frameon=True)
plt.tight_layout()
fig.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

View File

@ -187,8 +187,9 @@ class Pepper:
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"])(
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
if n_threads > 1:
results = Parallel(n_jobs=n_threads)(
delayed(compute_bayes_per_group)(group[1])
for group in dataset.group_by("structure_id")
)

View File

@ -293,6 +293,34 @@ document.addEventListener("alpine:init", () => {
}),
);
// PubMed link widget
Alpine.data(
"doiWidget",
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
...baseWidget(
fieldName,
data,
schema,
uiSchema,
mode,
debugErrors,
context,
),
get value() {
return this.data[this.fieldName] || "";
},
set value(v) {
this.data[this.fieldName] = v;
},
get doiUrl() {
return this.value
? `https://dx.doi.org/${this.value}`
: null;
},
}),
);
// Compound link widget
Alpine.data(
"compoundWidget",

View File

@ -463,7 +463,7 @@ function draw(pathway, elem) {
// 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>"
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
}
}
}
@ -581,11 +581,11 @@ function draw(pathway, elem) {
for (idx in parents) {
p = nodes[parents[idx]]
// console.log(p.depth)
if (p.depth >= n.depth) {
// keep the .5 steps for pseudo nodes
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
}
// if (p.depth >= n.depth) {
// // keep the .5 steps for pseudo nodes
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
// }
}
});
@ -637,6 +637,7 @@ function draw(pathway, elem) {
node.filter(d => !d.pseudo).each(function (d, i) {
const g = d3.select(this);
if (d.image_type === "svg") {
// Parse the SVG string
const parser = new DOMParser();
const svgDoc = parser.parseFromString(d.image_svg, "image/svg+xml");
@ -646,19 +647,19 @@ function draw(pathway, elem) {
const prefix = `node-${i}-`;
// Rename all IDs and fix <use> references
svgElem.querySelectorAll('[id]').forEach(el => {
svgElem.querySelectorAll("[id]").forEach(el => {
const oldId = el.id;
const newId = prefix + oldId;
el.id = newId;
const XLINK_NS = "http://www.w3.org/1999/xlink";
// Update <use> elements that reference this old ID
const uses = Array.from(svgElem.querySelectorAll('use')).filter(
u => u.getAttributeNS(XLINK_NS, 'href') === `#${oldId}`
const uses = Array.from(svgElem.querySelectorAll("use")).filter(
u => u.getAttributeNS(XLINK_NS, "href") === `#${oldId}`
);
uses.forEach(u => {
u.setAttributeNS(XLINK_NS, 'href', `#${newId}`);
u.setAttributeNS(XLINK_NS, "href", `#${newId}`);
});
});
@ -675,6 +676,17 @@ function draw(pathway, elem) {
.attr("height", svgHeight * scale)
.attr("x", -svgWidth * scale / 2)
.attr("y", -svgHeight * scale / 2);
} else {
// We have a image type different than svg
// include it via img url
g.append("svg:image")
.attr("xlink:href", d.image)
.attr("width", 40)
.attr("height", 40)
.attr("x", -20)
.attr("y", -20);
}
});
// add element to nodes array

View File

@ -1,3 +1,5 @@
{% load envipytags %}
{% if meta.can_edit %}
<li>
<a
@ -15,6 +17,11 @@
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a
>
</li>
{% epdb_slot_templates "epdb.actions.objects.pathway.add" as action_button_templates %}
{% for tpl in action_button_templates %}
{% include tpl %}
{% endfor %}
<li role="separator" class="divider"></li>
{% endif %}
<li>

View File

@ -0,0 +1,102 @@
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
{% load envipytags %}
{# Loading state #}
<div
x-show="isLoading"
class="mx-auto flex h-32 w-32 items-center justify-center"
>
{% include "components/loading-spinner.html" %}
</div>
{# Error state #}
<div
x-show="!isLoading && error"
class="alert alert-error/50 text-sm"
x-text="error"
></div>
{# Content #}
<template x-if="!isLoading && !error">
<div>
{# Empty state #}
<div
x-show="totalItems === 0"
class="text-base-content/70 py-8 text-center"
>
<p>No {{ empty_text|default:"items" }} found.</p>
</div>
{# Items list #}
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
<table class="table-zebra table">
<thead>
<tr>
<th>User</th>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Queued At</th>
<th>Done At</th>
</tr>
</thead>
<tbody>
<template x-for="obj in paginatedItems" :key="obj.url">
<tr>
<td>
<a :href="obj.user.url"><span x-text="obj.user.name"></span></a>
</td>
<td>
<a :href="obj.url"><span x-text="obj.id"></span></a>
</td>
<td><span x-text="obj.name"></span></td>
<td><span x-text="obj.status"></span></td>
<td><span x-text="obj.created"></span></td>
<td><span x-text="obj.done"></span></td>
</tr>
</template>
</tbody>
</table>
</ul>
{# Pagination controls #}
<div
x-show="totalPages > 1"
class="mt-4 flex items-center justify-between px-2"
>
<span class="text-base-content/70 text-sm">
Showing <span x-text="showingStart"></span>-<span
x-text="showingEnd"
></span>
of <span x-text="totalItems"></span>
</span>
<div class="join">
<button
class="join-item btn btn-sm"
:disabled="currentPage === 1"
@click="prevPage()"
>
«
</button>
<template x-for="item in pageNumbers" :key="item.key">
<button
class="join-item btn btn-sm"
:class="{ 'btn-active': item.page === currentPage }"
:disabled="item.isEllipsis"
@click="!item.isEllipsis && goToPage(item.page)"
x-text="item.page"
></button>
</template>
<button
class="join-item btn btn-sm"
:disabled="currentPage === totalPages"
@click="nextPage()"
>
»
</button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,13 @@
{% extends "collections/paginated_base.html" %}
{% block page_title %}Jobs{% endblock %}
{% block action_button %}
{% endblock action_button %}
{% block action_modals %}
{% endblock action_modals %}
{% block description %}
<p>List of Jobs submitted.</p>
{% endblock description %}

View File

@ -37,7 +37,11 @@
perPage: {{ per_page|default:50 }}
})"
>
{% if entity_type == 'joblog' %}
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
{% else %}
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
{% endif %}
</div>
{% else %}
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}

View File

@ -123,6 +123,17 @@
</div>
</template>
<!-- DOI link widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'doi-link'"
>
<div
x-data="doiWidget(fieldName, data, schema, uiSchema, mode, debugErrors, context)"
>
{% include "components/widgets/doi_link_widget.html" %}
</div>
</template>
<!-- Compound link widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"

View File

@ -0,0 +1,69 @@
{# DOI link widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': $store.validationErrors.hasError(fieldName, context),
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode: display as link -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value && doiUrl">
<a
:href="doiUrl"
class="link link-primary"
target="_blank"
x-text="value"
></a>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<input
type="text"
class="input input-bordered w-full"
:class="{ 'input-error': $store.validationErrors.hasError(fieldName, context) }"
placeholder="DOI e.g. 10.1016/j.jhazmat.2016.08.036"
x-model="value"
/>
</template>
<!-- Errors -->
<template x-if="$store.validationErrors.hasError(fieldName, context)">
<div class="label">
<template
x-for="errMsg in $store.validationErrors.getErrors(fieldName, context)"
:key="errMsg"
>
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "framework_modern.html" %}
{% load envipytags %}
{% block content %}
@ -82,6 +83,12 @@
<div class="collapse-content">{{ compound.description }}</div>
</div>
<!-- Extension Slot for Viz -->
{% epdb_slot_templates "epdb.objects.compound.viz" as viz_templates %}
{% for tpl in viz_templates %}
{% include tpl %}
{% endfor %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />

View File

@ -1,4 +1,5 @@
{% extends "framework_modern.html" %}
{% load envipytags %}
{% block content %}
@ -50,6 +51,12 @@
</div>
</div>
<!-- Extension Slot for Viz -->
{% epdb_slot_templates "epdb.objects.compound_structure.viz" as viz_templates %}
{% for tpl in viz_templates %}
{% include tpl %}
{% endfor %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />

View File

@ -1,5 +1,7 @@
{% extends "framework_modern.html" %}
{% load static %}
{% load envipytags %}
{% block content %}
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
@ -76,6 +78,10 @@
{% block action_modals %}
{% include "modals/objects/add_pathway_node_modal.html" %}
{% include "modals/objects/add_pathway_edge_modal.html" %}
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
{% for tpl in add_templates %}
{% include tpl %}
{% endfor %}
{% include "modals/objects/download_pathway_csv_modal.html" %}
{% include "modals/objects/download_pathway_image_modal.html" %}
{% include "modals/objects/identify_missing_rules_modal.html" %}

View File

@ -88,6 +88,10 @@ class FormatConverter(object):
def from_smiles(smiles):
return Chem.MolFromSmiles(smiles)
@staticmethod
def from_molfile(molfile: str):
return Chem.MolFromMolBlock(molfile)
@staticmethod
def to_smiles(mol, canonical=False):
return Chem.MolToSmiles(mol, canonical=canonical)
@ -171,12 +175,17 @@ class FormatConverter(object):
try:
Chem.Kekulize(mol)
except Exception:
mc = Chem.Mol(mol.ToBinary())
mol = Chem.Mol(mol.ToBinary())
if not mc.GetNumConformers():
Chem.rdDepictor.Compute2DCoords(mc)
if not mol.GetNumConformers():
Chem.rdDepictor.Compute2DCoords(mol)
pass
drawer = rdMolDraw2D.MolDraw2DCairo(*mol_size)
opts = drawer.drawOptions()
opts.clearBackground = False
drawer.DrawMolecule(mol)
drawer.FinishDrawing()
return drawer.GetDrawingText()
@staticmethod
def normalize(smiles):

View File

@ -64,7 +64,7 @@
import logging
from envipy_additional_information import HalfLife, HalfLifeWS
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel
from envipy_additional_information.information import Interval
from envipy_additional_information.parsers import (
AcidityParser,
@ -125,6 +125,7 @@ from envipy_additional_information.parsers import (
LiquidMatrixSourceParser,
OxygenUptakeRateParser,
InitiatingOrganismParser,
PFASConfidenceParser,
)
logger = logging.getLogger(__name__)
@ -141,7 +142,7 @@ def get_parameter(request, paramname):
res = request.POST.get(paramname)
if res is not None and res.strip() != "":
return res
return ValueError("Not all parameters are set!")
raise ValueError("Not all parameters are set!")
def get_parameter_or_empty_string(request, paramname):
@ -473,17 +474,12 @@ def build_additional_information_from_request(request, type_):
comment = get_parameter_or_empty_string(request, "comment")
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")
fit = get_parameter_or_empty_string(request, "fit")
if first_order != "":
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")
if model:
model = HalfLifeModel(model.upper())
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
@ -508,6 +504,10 @@ def build_additional_information_from_request(request, type_):
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
source_ws = get_parameter_or_empty_string(request, "source_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")
dt50_total = IntervalParser.from_string(hl_ws_total)
@ -674,7 +674,8 @@ def build_additional_information_from_request(request, type_):
elif type_ == "studywst":
# study_wst_cond = get_parameter(request, "studywstcond")
raise ValueError("studywstcond is not yet implemented")
elif type_ == "pfasconfidence":
return PFASConfidenceParser.from_string(get_parameter(request, "level"))
else:
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")

180
uv.lock generated
View File

@ -17,7 +17,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.13.3"
version = "3.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@ -28,76 +28,76 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
{ url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
{ url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
{ url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
{ url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
{ url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
{ url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
{ url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
{ url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
{ url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
{ url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
{ url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
{ url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
{ url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
{ url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
{ url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
{ url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
{ url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
{ url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
{ url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
{ url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
{ url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
{ url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
{ url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
{ url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
{ url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
{ url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
{ url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
{ url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
{ url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
{ url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
{ url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
{ url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
{ url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
{ url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
{ url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
{ url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
{ url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
{ url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
{ url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
{ url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
{ url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
{ url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
{ url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
{ url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
{ url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
{ url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
{ url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
{ url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
]
[[package]]
@ -894,7 +894,7 @@ 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?branch=develop#04f6a01b8c5cd1342464e004e0cfaec9abc13ac5" }
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
dependencies = [
{ name = "pydantic" },
]
@ -1066,11 +1066,11 @@ wheels = [
[[package]]
name = "fsspec"
version = "2026.2.0"
version = "2026.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" },
]
[package.optional-dependencies]
@ -2763,9 +2763,9 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
]
wheels = [
{ 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" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
{ 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-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-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" },
]
[[package]]
@ -2785,19 +2785,19 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
]
wheels = [
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
{ 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-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_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-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_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-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-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_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-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_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-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_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-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" },
]
[[package]]