forked from enviPath/enviPy
Compare commits
12 Commits
develop-ba
...
2e24666744
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e24666744 | |||
| d9c8c9746d | |||
| d668315064 | |||
| 05a17ecdcf | |||
| 2bdedc39ee | |||
| c9564a3dca | |||
| 512b51137f | |||
| 5750257cfb | |||
| 0bad0eb087 | |||
| c01ad663b7 | |||
| f1936eea8f | |||
| 624acccb3e |
16
Dockerfile
16
Dockerfile
@ -6,23 +6,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
curl \
|
curl \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
git \
|
git \
|
||||||
ca-certificates \
|
nodejs \
|
||||||
|
npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Node 22 + pnpm
|
# Install pnpm
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
RUN npm install -g pnpm
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& corepack enable \
|
|
||||||
&& corepack prepare pnpm@latest --activate \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
ENV PATH="/root/.local/bin:${PATH}"
|
ENV PATH="/root/.local/bin:${PATH}"
|
||||||
@ -35,13 +30,12 @@ RUN mkdir -p -m 0700 /root/.ssh \
|
|||||||
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
|
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
|
||||||
|
|
||||||
# We'll need access to private repos, let docker make use of host ssh agent and use it like:
|
# We'll need access to private repos, let docker make use of host ssh agent and use it like:
|
||||||
# docker build --ssh default -t envipath/envipy-bayer:1.0 .
|
# docker build --ssh default -t envipath/envipy:1.0 .
|
||||||
RUN --mount=type=ssh \
|
RUN --mount=type=ssh \
|
||||||
uv sync --locked --extra ms-login --extra pepper-plugin
|
uv sync --locked --extra ms-login --extra pepper-plugin
|
||||||
|
|
||||||
# Now copy source and do a final sync to install the project itself
|
# Now copy source and do a final sync to install the project itself
|
||||||
# Ensure .dockerignore is reasonable
|
# Ensure .dockerignore is reasonable
|
||||||
COPY bb4g bb4g
|
|
||||||
COPY biotransformer biotransformer
|
COPY biotransformer biotransformer
|
||||||
COPY bayer bayer
|
COPY bayer bayer
|
||||||
COPY bridge bridge
|
COPY bridge bridge
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
import enum
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from envipy_additional_information import GroupEnum as G
|
|
||||||
from envipy_additional_information import SubcategoryEnum as S
|
|
||||||
from envipy_additional_information import (
|
|
||||||
register,
|
|
||||||
register_parser_command,
|
|
||||||
EnviPyModel,
|
|
||||||
# EnviPyModelParser,
|
|
||||||
Interval,
|
|
||||||
UIConfig,
|
|
||||||
IntervalConfig,
|
|
||||||
WidgetType,
|
|
||||||
registry,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="compoundlabel", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class CompoundLabel(EnviPyModel):
|
|
||||||
label: str
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Compound Label"
|
|
||||||
label = UIConfig(widget=WidgetType.TEXT, label="Label", order=1)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("compoundlabel")
|
|
||||||
class CompoundLabelParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> CompoundLabel:
|
|
||||||
return CompoundLabel(label=data)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="studywaterstoragecapacity", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class StudyWaterStorageCapacity(EnviPyModel):
|
|
||||||
capacity: str
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Study Water Storage Capacity"
|
|
||||||
capacity = UIConfig(widget=WidgetType.TEXT, label="Study Water Storage Capacity", order=1)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("studywst")
|
|
||||||
class StudyWaterStorageCapacityParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> StudyWaterStorageCapacity:
|
|
||||||
return StudyWaterStorageCapacity(capacity=data)
|
|
||||||
|
|
||||||
|
|
||||||
class ObservationType(enum.Enum):
|
|
||||||
OBSERVED = "observed"
|
|
||||||
APPLIED = "applied"
|
|
||||||
NA = 'NA'
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="observation", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class Observation(EnviPyModel):
|
|
||||||
type: ObservationType
|
|
||||||
min_value: Optional[float] = None
|
|
||||||
max_value: Optional[float] = None
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Observation"
|
|
||||||
type = UIConfig(widget=WidgetType.SELECT, label="Observed or Applied", order=1)
|
|
||||||
min_value = UIConfig(widget=WidgetType.NUMBER, label="Min Value", order=2)
|
|
||||||
max_value = UIConfig(widget=WidgetType.NUMBER, label="Max Value", order=3)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("observation")
|
|
||||||
class ObservationParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> Observation:
|
|
||||||
parts = data.split(";")
|
|
||||||
|
|
||||||
observation_type = ObservationType(parts[0])
|
|
||||||
min_value = None
|
|
||||||
if parts[1]:
|
|
||||||
try:
|
|
||||||
min_value = float(parts[1])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
max_value = None
|
|
||||||
if parts[2]:
|
|
||||||
try:
|
|
||||||
max_value = float(parts[2])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return Observation(type=observation_type, min_value=min_value, max_value=max_value)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="kinetics", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class Kinetics(EnviPyModel):
|
|
||||||
dt50: Interval[float]
|
|
||||||
normalized_dt50: bool
|
|
||||||
chi2err: Optional[float] = None
|
|
||||||
t_test: Optional[float] = None
|
|
||||||
swarc: Optional[float] = None
|
|
||||||
visual_fit: Optional[int] = None
|
|
||||||
comment: str
|
|
||||||
source: str
|
|
||||||
kinetic_model: str
|
|
||||||
k1: Optional[float] = None
|
|
||||||
k2: Optional[float] = None
|
|
||||||
g: Optional[float] = None
|
|
||||||
tb: Optional[float] = None
|
|
||||||
alpha: Optional[float] = None
|
|
||||||
beta: Optional[float] = None
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Kinetics"
|
|
||||||
|
|
||||||
# Field config
|
|
||||||
dt50 = IntervalConfig(label="DT50 Range", order=1, unit="d")
|
|
||||||
normalized_dt50 = UIConfig(widget=WidgetType.CHECKBOX, label="Normalized DT50", order=2)
|
|
||||||
chi2err = UIConfig(widget=WidgetType.NUMBER, label="Chi2err", order=3)
|
|
||||||
t_test = UIConfig(widget=WidgetType.NUMBER, label="T-Test", order=4)
|
|
||||||
swarc = UIConfig(widget=WidgetType.NUMBER, label="SWARC", order=5)
|
|
||||||
visual_fit = UIConfig(widget=WidgetType.NUMBER, label="Visual Fit", order=6)
|
|
||||||
comment = UIConfig(widget=WidgetType.TEXTAREA, label="Comments", order=7)
|
|
||||||
source = UIConfig(widget=WidgetType.TEXT, label="Source", order=8)
|
|
||||||
kinetic_model = UIConfig(widget=WidgetType.SELECT, label="Kinetic Model", order=9)
|
|
||||||
k1 = UIConfig(widget=WidgetType.NUMBER, label="K1", order=10)
|
|
||||||
k2 = UIConfig(widget=WidgetType.NUMBER, label="K2", order=11)
|
|
||||||
g = UIConfig(widget=WidgetType.NUMBER, label="G", order=12)
|
|
||||||
tb = UIConfig(widget=WidgetType.NUMBER, label="TB", order=13)
|
|
||||||
alpha = UIConfig(widget=WidgetType.NUMBER, label="Alpha", order=14)
|
|
||||||
beta = UIConfig(widget=WidgetType.NUMBER, label="Beta", order=15)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("kineticevaluation")
|
|
||||||
class KinecticsParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> Kinetics:
|
|
||||||
parts = data.split(";")
|
|
||||||
dt50 = registry.get_parser("interval").from_string(parts[0])
|
|
||||||
normalized_dt50 = parts[1] == "true"
|
|
||||||
chi2err = float(parts[2]) if parts[2] else None
|
|
||||||
t_test = float(parts[3]) if parts[3] else None
|
|
||||||
swarc = float(parts[4]) if parts[4] else None
|
|
||||||
visual_fit = int(parts[5]) if parts[5] else None
|
|
||||||
comment = parts[6]
|
|
||||||
source = parts[7]
|
|
||||||
kinetic_model = parts[8]
|
|
||||||
k1 = float(parts[9]) if parts[9] else None
|
|
||||||
k2 = float(parts[10]) if parts[10] else None
|
|
||||||
g = float(parts[11]) if parts[11] else None
|
|
||||||
tb = float(parts[12]) if parts[12] else None
|
|
||||||
alpha = float(parts[13]) if parts[13] else None
|
|
||||||
beta = float(parts[14]) if parts[14] else None
|
|
||||||
|
|
||||||
return Kinetics(
|
|
||||||
dt50=dt50,
|
|
||||||
normalized_dt50=normalized_dt50,
|
|
||||||
chi2err=chi2err,
|
|
||||||
t_test=t_test,
|
|
||||||
swarc=swarc,
|
|
||||||
visual_fit=visual_fit,
|
|
||||||
comment=comment,
|
|
||||||
source=source,
|
|
||||||
kinetic_model=kinetic_model,
|
|
||||||
k1=k1,
|
|
||||||
k2=k2,
|
|
||||||
g=g,
|
|
||||||
tb=tb,
|
|
||||||
alpha=alpha,
|
|
||||||
beta=beta,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print(KinecticsParser.from_string("187.0 - 187.0;false;;;;;;;AFO;;;;;;"))
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from bayer import additional_information # noqa: F401
|
|
||||||
from epdb.template_registry import register_template
|
from epdb.template_registry import register_template
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -14,14 +13,6 @@ register_template(
|
|||||||
"modals.collections.compound",
|
"modals.collections.compound",
|
||||||
"modals/collections/new_pes_modal.html",
|
"modals/collections/new_pes_modal.html",
|
||||||
)
|
)
|
||||||
register_template(
|
|
||||||
"epdb.actions.objects.pathway.add",
|
|
||||||
"actions/objects/pathway_add_pes.html",
|
|
||||||
)
|
|
||||||
register_template(
|
|
||||||
"epdb.modals.objects.pathway.add",
|
|
||||||
"modals/objects/add_pathway_pes_node_modal.html"
|
|
||||||
)
|
|
||||||
|
|
||||||
# PES Viz
|
# PES Viz
|
||||||
register_template(
|
register_template(
|
||||||
@ -37,4 +28,4 @@ register_template(
|
|||||||
register_template(
|
register_template(
|
||||||
"epdb.objects.node.viz",
|
"epdb.objects.node.viz",
|
||||||
"objects/node_viz.html",
|
"objects/node_viz.html",
|
||||||
)
|
)
|
||||||
@ -232,6 +232,5 @@ class PESStructure(CompoundStructure):
|
|||||||
"is_pes": True,
|
"is_pes": True,
|
||||||
"pes_link": self.pes_link,
|
"pes_link": self.pes_link,
|
||||||
# Will overwrite image from Node
|
# Will overwrite image from Node
|
||||||
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
"image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}"
|
||||||
"image_type": "png",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
<li>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add PES</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
@ -37,10 +37,6 @@ def create_pes(request, package_uuid):
|
|||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
if current_package.classification_level != Package.Classification.SECRET:
|
|
||||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
@ -79,10 +75,6 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
if current_package.classification_level != Package.Classification.SECRET:
|
|
||||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
@ -91,10 +83,6 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
|
|
||||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure)
|
|
||||||
if node_qs.exists():
|
|
||||||
return redirect(current_pathway.url)
|
|
||||||
|
|
||||||
n = Node()
|
n = Node()
|
||||||
n.stereo_removed = False
|
n.stereo_removed = False
|
||||||
n.pathway = current_pathway
|
n.pathway = current_pathway
|
||||||
@ -115,15 +103,20 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
|
|
||||||
|
|
||||||
def fetch_pes(request, pes_url) -> dict:
|
def fetch_pes(request, pes_url) -> dict:
|
||||||
|
proxies = {
|
||||||
|
"http": "http://10.185.190.100:8080",
|
||||||
|
"https": "http://10.185.190.100:8080",
|
||||||
|
}
|
||||||
|
|
||||||
from epauth.views import get_access_token_from_request
|
from epauth.views import get_access_token_from_request
|
||||||
token = get_access_token_from_request(request)
|
token = get_access_token_from_request(request)
|
||||||
|
|
||||||
if token:
|
if token or True:
|
||||||
for k, v in s.PES_API_MAPPING.items():
|
for k, v in s.PES_API_MAPPING.items():
|
||||||
if pes_url.startswith(k):
|
if pes_url.startswith(k):
|
||||||
pes_id = pes_url.split('/')[-1]
|
pes_id = pes_url.split('/')[-1]
|
||||||
|
|
||||||
if pes_id == 'dummy':
|
if pes_id == 'dummy' or True:
|
||||||
import json
|
import json
|
||||||
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
||||||
res_data["pes_url"] = pes_url
|
res_data["pes_url"] = pes_url
|
||||||
@ -132,7 +125,7 @@ def fetch_pes(request, pes_url) -> dict:
|
|||||||
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
||||||
params = {"pes_reg_entity_corporate_id": pes_id}
|
params = {"pes_reg_entity_corporate_id": pes_id}
|
||||||
|
|
||||||
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
res = requests.get(v, headers=headers, params=params, proxies=proxies)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|||||||
199
bb4g/__init__.py
199
bb4g/__init__.py
@ -1,199 +0,0 @@
|
|||||||
import enum
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.conf import settings as s
|
|
||||||
from envipy_additional_information import register, EnviPyModel, UIConfig, WidgetType
|
|
||||||
|
|
||||||
from bridge.contracts import Classifier # noqa: I001
|
|
||||||
from bridge.dto import (
|
|
||||||
BuildResult,
|
|
||||||
EnviPyDTO,
|
|
||||||
EvaluationResult,
|
|
||||||
RunResult,
|
|
||||||
TransformationProductPrediction,
|
|
||||||
) # noqa: I001
|
|
||||||
|
|
||||||
logger = logging.getLogger("epdb")
|
|
||||||
|
|
||||||
|
|
||||||
class SamplingAlgorithm(enum.Enum):
|
|
||||||
EXACT = "exact"
|
|
||||||
|
|
||||||
|
|
||||||
@register("bb4gconfig")
|
|
||||||
class BB4GConfig(EnviPyModel):
|
|
||||||
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
|
||||||
cutoff: int = -5
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "BB4G Configuration"
|
|
||||||
sampling_algorithm = UIConfig(
|
|
||||||
widget=WidgetType.SELECT,
|
|
||||||
label="BB4G Sampling Algorithm",
|
|
||||||
order=1,
|
|
||||||
placeholder="If unset defaults to 'exact'"
|
|
||||||
)
|
|
||||||
cutoff = UIConfig(
|
|
||||||
widget=WidgetType.NUMBER,
|
|
||||||
label="BB4G Cutoff",
|
|
||||||
order=2,
|
|
||||||
placeholder="If unset defaults to -5"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Once stable these will be exposed by enviPy-plugins lib
|
|
||||||
class BB4G(Classifier):
|
|
||||||
Config = BB4GConfig
|
|
||||||
|
|
||||||
def __init__(self, config: BB4GConfig | None = None):
|
|
||||||
super().__init__(config)
|
|
||||||
self.url = f"{s.BB4G_URL}"
|
|
||||||
|
|
||||||
self.token = self.acquire_token()
|
|
||||||
self.header = {
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
def acquire_token(self):
|
|
||||||
BB4G_TENANT_ID = s.BB4G_TENANT_ID
|
|
||||||
BB4G_CLIENT_ID = s.BB4G_CLIENT_ID
|
|
||||||
BB4G_CLIENT_SECRET = s.BB4G_CLIENT_SECRET
|
|
||||||
BB4G_SCOPE = s.BB4G_SCOPE
|
|
||||||
|
|
||||||
BB4G_TOKEN_URL = f"https://login.microsoftonline.com/{BB4G_TENANT_ID}/oauth2/v2.0/token"
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"client_id": BB4G_CLIENT_ID,
|
|
||||||
"client_secret": BB4G_CLIENT_SECRET,
|
|
||||||
"scope": BB4G_SCOPE,
|
|
||||||
"grant_type": "client_credentials"
|
|
||||||
}
|
|
||||||
|
|
||||||
# No Proxy required, URL is whitelisted
|
|
||||||
res = requests.post(BB4G_TOKEN_URL, data=payload)
|
|
||||||
|
|
||||||
res.raise_for_status()
|
|
||||||
|
|
||||||
return res.json()["access_token"]
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
header = {
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
started = False
|
|
||||||
retries = 0
|
|
||||||
while not started and retries < 5:
|
|
||||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
|
||||||
logger.info(f"Starting BB4G: {res.status_code}")
|
|
||||||
if res.status_code == 200:
|
|
||||||
started = True
|
|
||||||
elif res.status_code in [500, 502]:
|
|
||||||
retries += 1
|
|
||||||
import time
|
|
||||||
time.sleep(5)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unexpected status code: {res.status_code}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def requires_rule_packages(cls) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def requires_data_packages(cls) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def identifier(cls) -> str:
|
|
||||||
return "bb4g"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def name(cls) -> str:
|
|
||||||
return "BB4G Template Free Model"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def display(cls) -> str:
|
|
||||||
return "BB4G Template Free Model"
|
|
||||||
|
|
||||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
|
||||||
return
|
|
||||||
|
|
||||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
|
||||||
|
|
||||||
# Ensure Service is running
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
smiles = [c.smiles for c in eP.get_compounds()]
|
|
||||||
preds = self._post(smiles)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for substrate in preds.keys():
|
|
||||||
results.append(
|
|
||||||
TransformationProductPrediction(
|
|
||||||
substrate=substrate,
|
|
||||||
products=preds[substrate],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return RunResult(
|
|
||||||
producer=eP.get_context().url,
|
|
||||||
description=f"Generated at {datetime.now()}",
|
|
||||||
result=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _post(self, smiles: List[str]) -> dict[str, dict[str, float]]:
|
|
||||||
header = {
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
for smi in smiles:
|
|
||||||
data = {
|
|
||||||
"smiles": smi,
|
|
||||||
"sampling_alg": self.config.sampling_algorithm.value,
|
|
||||||
"cutoff": self.config.cutoff,
|
|
||||||
}
|
|
||||||
|
|
||||||
retries = 0
|
|
||||||
while retries < 5:
|
|
||||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
|
|
||||||
proxies=s.PROXIES or None)
|
|
||||||
|
|
||||||
if resp.status_code == 418:
|
|
||||||
retries += 1
|
|
||||||
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
|
|
||||||
import time
|
|
||||||
time.sleep(60)
|
|
||||||
continue
|
|
||||||
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
for substrate, predictions in resp.json().items():
|
|
||||||
preds = {}
|
|
||||||
|
|
||||||
for pred in predictions:
|
|
||||||
prod = pred["prediction"]
|
|
||||||
prob = math.exp(pred["log_likelihood"])
|
|
||||||
preds[prod] = prob
|
|
||||||
|
|
||||||
result[substrate] = preds
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return result
|
|
||||||
@ -254,15 +254,7 @@ class Classifier(Plugin):
|
|||||||
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
||||||
if cls.Config is None:
|
if cls.Config is None:
|
||||||
return None
|
return None
|
||||||
|
return cls.Config(**(data or {}))
|
||||||
# remove empty strings a.k.a unset params to not overwrite defaults
|
|
||||||
cpy = {}
|
|
||||||
if data is not None:
|
|
||||||
for k, v in data.items():
|
|
||||||
if v != "":
|
|
||||||
cpy[k] = v
|
|
||||||
|
|
||||||
return cls.Config(**cpy)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, data: dict | None = None):
|
def create(cls, data: dict | None = None):
|
||||||
|
|||||||
@ -25,7 +25,7 @@ services:
|
|||||||
- ep_bayer_redis_data:/data
|
- ep_bayer_redis_data:/data
|
||||||
|
|
||||||
biotransformer3:
|
biotransformer3:
|
||||||
image: git.envipath.com/envipath/biotransformer3:1.0
|
image: envipath/biotransformer3:1.0
|
||||||
container_name: epbiotransformer3
|
container_name: epbiotransformer3
|
||||||
|
|
||||||
# web:
|
# web:
|
||||||
@ -40,7 +40,7 @@ services:
|
|||||||
# - ep_bayer_data:/opt/enviPy/
|
# - ep_bayer_data:/opt/enviPy/
|
||||||
|
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: git.envipath.com/envipath/envipy-bayer:1.0
|
image: envipath/envipy-bayer:1.0
|
||||||
container_name: epcelery
|
container_name: epcelery
|
||||||
env_file:
|
env_file:
|
||||||
- .env.dev
|
- .env.dev
|
||||||
|
|||||||
@ -143,12 +143,6 @@ if os.environ.get("USE_TEMPLATE_DB", False) == "True":
|
|||||||
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
||||||
"LOCATION": "unique-snowflake",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
@ -275,12 +269,6 @@ LOGGING = {
|
|||||||
"filename": os.path.join(LOG_DIR, "debug.log"),
|
"filename": os.path.join(LOG_DIR, "debug.log"),
|
||||||
"formatter": "simple",
|
"formatter": "simple",
|
||||||
},
|
},
|
||||||
"auth_file": {
|
|
||||||
"level": "INFO", # Or higher
|
|
||||||
"class": "logging.FileHandler",
|
|
||||||
"filename": os.path.join(LOG_DIR, "auth.log"),
|
|
||||||
"formatter": "simple",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
# For everything under epdb/ loaded via getlogger(__name__)
|
# For everything under epdb/ loaded via getlogger(__name__)
|
||||||
@ -301,11 +289,6 @@ LOGGING = {
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||||
},
|
},
|
||||||
"auth": {
|
|
||||||
"handlers": ["auth_file"],
|
|
||||||
"propagate": True,
|
|
||||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,16 +474,3 @@ if DATA_POOL_MAPPING:
|
|||||||
else:
|
else:
|
||||||
DATA_POOL_MAPPING = {}
|
DATA_POOL_MAPPING = {}
|
||||||
|
|
||||||
PROXIES = {}
|
|
||||||
if os.environ.get("HTTP_PROXY"):
|
|
||||||
PROXIES["http"] = os.environ.get("HTTP_PROXY")
|
|
||||||
PROXIES["https"] = os.environ.get("HTTPS_PROXY")
|
|
||||||
|
|
||||||
# BB4g
|
|
||||||
BB4G_URL = os.environ.get("BB4G_URL")
|
|
||||||
BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
|
|
||||||
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
|
|
||||||
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
|
||||||
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")
|
|
||||||
|
|
||||||
os.environ["NO_PROXY"] = "localhost,127.0.0.1,epbiotransformer3"
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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")
|
|
||||||
@ -15,7 +15,6 @@ from .endpoints import (
|
|||||||
additional_information,
|
additional_information,
|
||||||
settings,
|
settings,
|
||||||
groups,
|
groups,
|
||||||
joblogs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main router with authentication
|
# Main router with authentication
|
||||||
@ -38,7 +37,6 @@ router.add_router("", structure.router)
|
|||||||
router.add_router("", additional_information.router)
|
router.add_router("", additional_information.router)
|
||||||
router.add_router("", settings.router)
|
router.add_router("", settings.router)
|
||||||
router.add_router("", groups.router)
|
router.add_router("", groups.router)
|
||||||
router.add_router("", joblogs.router)
|
|
||||||
|
|
||||||
if s.IUCLID_EXPORT_ENABLED:
|
if s.IUCLID_EXPORT_ENABLED:
|
||||||
from epiuclid.api import router as iuclid_router
|
from epiuclid.api import router as iuclid_router
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
from datetime import datetime
|
from ninja import FilterSchema, FilterLookup, Schema
|
||||||
from typing import Annotated, Optional, List, Dict, Any
|
from typing import Annotated, Optional, List, Dict, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from django.urls import reverse
|
|
||||||
from ninja import Field, FilterSchema, FilterLookup, Schema
|
|
||||||
|
|
||||||
|
|
||||||
# Filter schema for query parameters
|
# Filter schema for query parameters
|
||||||
class ReviewStatusFilter(FilterSchema):
|
class ReviewStatusFilter(FilterSchema):
|
||||||
@ -136,23 +133,3 @@ class GroupOutSchema(Schema):
|
|||||||
url: str = ""
|
url: str = ""
|
||||||
name: str
|
name: str
|
||||||
description: 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})
|
|
||||||
|
|||||||
@ -5,5 +5,4 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("entra/login/", views.entra_login, name="entra_login"),
|
path("entra/login/", views.entra_login, name="entra_login"),
|
||||||
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
||||||
path("auth/token/", views.get_token, name="get_token"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,11 +2,9 @@ import msal
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from epdb.logic import UserManager, GroupManager
|
from epdb.logic import UserManager
|
||||||
from epdb.models import Group
|
|
||||||
|
|
||||||
|
|
||||||
def get_msal_app_with_cache(request):
|
def get_msal_app_with_cache(request):
|
||||||
@ -83,12 +81,10 @@ def entra_callback(request):
|
|||||||
login(request, u)
|
login(request, u)
|
||||||
|
|
||||||
# EDIT START
|
# EDIT START
|
||||||
|
|
||||||
# Ensure groups exists in eP
|
# Ensure groups exists in eP
|
||||||
for id, name in s.ENTRA_SECRET_GROUPS.items():
|
for id, name in s.ENTRA_SECRET_GROUPS.items():
|
||||||
if not Group.objects.filter(uuid=id).exists():
|
if not Group.objects.filter(uuid=id).exists():
|
||||||
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
|
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id)
|
||||||
uuid=id)
|
|
||||||
else:
|
else:
|
||||||
g = Group.objects.get(uuid=id)
|
g = Group.objects.get(uuid=id)
|
||||||
# Ensure its secret
|
# Ensure its secret
|
||||||
@ -97,8 +93,7 @@ def entra_callback(request):
|
|||||||
|
|
||||||
for id, name in s.ENTRA_GROUPS.items():
|
for id, name in s.ENTRA_GROUPS.items():
|
||||||
if not Group.objects.filter(uuid=id).exists():
|
if not Group.objects.filter(uuid=id).exists():
|
||||||
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
|
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", uuid=id)
|
||||||
uuid=id)
|
|
||||||
else:
|
else:
|
||||||
g = Group.objects.get(uuid=id)
|
g = Group.objects.get(uuid=id)
|
||||||
|
|
||||||
@ -116,11 +111,6 @@ def get_access_token_from_request(request, scopes=None):
|
|||||||
"""
|
"""
|
||||||
Get an access token from the request using MSAL token cache.
|
Get an access token from the request using MSAL token cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if auth via Access Token
|
|
||||||
if request.headers.get("Authorization"):
|
|
||||||
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
|
|
||||||
|
|
||||||
if scopes is None:
|
if scopes is None:
|
||||||
scopes = s.MS_ENTRA_SCOPES
|
scopes = s.MS_ENTRA_SCOPES
|
||||||
|
|
||||||
@ -162,9 +152,3 @@ def get_access_token_from_request(request, scopes=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_token(request):
|
|
||||||
token = get_access_token_from_request(request)
|
|
||||||
msg = f"{token}"
|
|
||||||
return HttpResponse(msg, content_type='text/plain')
|
|
||||||
|
|||||||
112
epdb/admin.py
112
epdb/admin.py
@ -1,8 +1,5 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib import messages
|
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
@ -32,8 +29,6 @@ from .models import (
|
|||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
@ -50,113 +45,6 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
"date_joined",
|
"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):
|
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
|
import hashlib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import nh3
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
import nh3
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.cache import cache
|
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from jwt import InvalidIssuerError
|
|
||||||
from ninja import Field, Form, Query, Router, Schema
|
from ninja import Field, Form, Query, Router, Schema
|
||||||
|
from ninja.errors import HttpError
|
||||||
from ninja.security import HttpBearer
|
from ninja.security import HttpBearer
|
||||||
|
from ninja.security import SessionAuth
|
||||||
|
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
from utilities.misc import PackageExporter
|
from utilities.misc import PackageExporter
|
||||||
@ -49,26 +51,6 @@ from .models import (
|
|||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
def get_cached_jwks(tenant_id: str, force=False) -> Dict:
|
|
||||||
"""Get JWKS using Django cache"""
|
|
||||||
cache_key = f"jwks_{tenant_id}"
|
|
||||||
|
|
||||||
jwks = cache.get(cache_key)
|
|
||||||
|
|
||||||
if jwks is None or force:
|
|
||||||
# Cache miss, fetch new keys
|
|
||||||
jwks_uri = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
|
||||||
response = requests.get(jwks_uri)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
jwks = response.json()
|
|
||||||
|
|
||||||
# Cache for 1 hour (3600 seconds)
|
|
||||||
cache.set(cache_key, jwks, 3600)
|
|
||||||
|
|
||||||
return jwks
|
|
||||||
|
|
||||||
|
|
||||||
def get_package_for_write(user, package_uuid):
|
def get_package_for_write(user, package_uuid):
|
||||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||||
if not PackageManager.writable(user, p):
|
if not PackageManager.writable(user, p):
|
||||||
@ -86,7 +68,9 @@ def validate_token(token: str) -> dict:
|
|||||||
TENANT_ID = s.MS_ENTRA_TENANT_ID
|
TENANT_ID = s.MS_ENTRA_TENANT_ID
|
||||||
CLIENT_ID = s.MS_ENTRA_CLIENT_ID
|
CLIENT_ID = s.MS_ENTRA_CLIENT_ID
|
||||||
|
|
||||||
jwks = get_cached_jwks(TENANT_ID)
|
# Fetch Microsoft's public keys
|
||||||
|
jwks_uri = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"
|
||||||
|
jwks = requests.get(jwks_uri).json()
|
||||||
|
|
||||||
header = jwt.get_unverified_header(token)
|
header = jwt.get_unverified_header(token)
|
||||||
|
|
||||||
@ -94,21 +78,13 @@ def validate_token(token: str) -> dict:
|
|||||||
next(k for k in jwks["keys"] if k["kid"] == header["kid"])
|
next(k for k in jwks["keys"] if k["kid"] == header["kid"])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle V1 and V2 tokens
|
claims = jwt.decode(
|
||||||
try:
|
token,
|
||||||
claims = jwt.decode(
|
public_key,
|
||||||
token,
|
algorithms=["RS256"],
|
||||||
public_key,
|
audience=[CLIENT_ID, f"api://{CLIENT_ID}"],
|
||||||
algorithms=["RS256"],
|
issuer=f"https://sts.windows.net/{TENANT_ID}/",
|
||||||
audience=[CLIENT_ID, f"api://{CLIENT_ID}"],
|
)
|
||||||
issuer=[
|
|
||||||
f"https://sts.windows.net/{TENANT_ID}/",
|
|
||||||
f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"Token verification failed! - {e}")
|
|
||||||
|
|
||||||
return claims
|
return claims
|
||||||
|
|
||||||
|
|
||||||
@ -439,50 +415,23 @@ class PackageSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_readers(obj: Package):
|
def resolve_readers(obj: Package):
|
||||||
readers = []
|
users = User.objects.filter(
|
||||||
|
id__in=UserPackagePermission.objects.filter(
|
||||||
|
package=obj, permission=UserPackagePermission.READ[0]
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
|
return [{u.id: u.get_name()} for u in users]
|
||||||
|
|
||||||
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
|
@staticmethod
|
||||||
def resolve_writers(obj: Package):
|
def resolve_writers(obj: Package):
|
||||||
writers = []
|
users = User.objects.filter(
|
||||||
|
id__in=UserPackagePermission.objects.filter(
|
||||||
|
package=obj, permission=UserPackagePermission.WRITE[0]
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
user_ids = UserPackagePermission.objects.filter(
|
return [{u.id: u.get_name()} for u in users]
|
||||||
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
|
@staticmethod
|
||||||
def resolve_review_comment(obj):
|
def resolve_review_comment(obj):
|
||||||
@ -847,7 +796,6 @@ class CreateCompound(Schema):
|
|||||||
compoundName: str | None = None
|
compoundName: str | None = None
|
||||||
compoundDescription: str | None = None
|
compoundDescription: str | None = None
|
||||||
inchi: str | None = None
|
inchi: str | None = None
|
||||||
pesLink: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/compound")
|
@router.post("/package/{uuid:package_uuid}/compound")
|
||||||
@ -859,32 +807,9 @@ def create_package_compound(
|
|||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
# inchi is not used atm
|
# inchi is not used atm
|
||||||
|
c = Compound.create(
|
||||||
if c.pesLink is not None:
|
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
||||||
from bayer.views import fetch_pes
|
)
|
||||||
from bayer.models import PESCompound
|
|
||||||
|
|
||||||
try:
|
|
||||||
pes_data = fetch_pes(request, c.pesLink)
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
|
||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
|
||||||
if "secret" == classification.lower():
|
|
||||||
|
|
||||||
if p.classification_level != Package.Classification.SECRET:
|
|
||||||
return 400, {"Cannot create PESs for non-secret packages."}
|
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
|
||||||
if data_pools:
|
|
||||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
|
||||||
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
|
||||||
|
|
||||||
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
|
||||||
else:
|
|
||||||
c = Compound.create(
|
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
|
||||||
)
|
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": str(e)}
|
return 400, {"message": str(e)}
|
||||||
@ -903,27 +828,6 @@ 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(
|
@router.delete(
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||||
)
|
)
|
||||||
@ -1450,7 +1354,6 @@ class ScenarioSchema(Schema):
|
|||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||||
collectionID: Optional[str] = None
|
collectionID: Optional[str] = None
|
||||||
date: str = Field(None, alias="scenario_date")
|
|
||||||
description: str = Field(None, alias="description")
|
description: str = Field(None, alias="description")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
identifier: str = "scenario"
|
identifier: str = "scenario"
|
||||||
@ -1594,56 +1497,28 @@ def create_package_additional_information(request, package_uuid):
|
|||||||
scen = request.POST.get("scenario")
|
scen = request.POST.get("scenario")
|
||||||
scenario = Scenario.objects.get(package=p, url=scen)
|
scenario = Scenario.objects.get(package=p, url=scen)
|
||||||
|
|
||||||
if request.POST.get("adInfoTypes[]"):
|
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
attach_obj = url_parser.get_object()
|
||||||
attach_obj = url_parser.get_object()
|
|
||||||
|
|
||||||
if not hasattr(attach_obj, "additional_information"):
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
raise ValueError("Can't attach additional information to this object!")
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
if not attach_obj.url.startswith(p.url):
|
if not attach_obj.url.startswith(p.url):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Additional Information can only be set to objects stored in the same package!"
|
"Additional Information can only be set to objects stored in the same package!"
|
||||||
)
|
)
|
||||||
|
|
||||||
types = request.POST.get("adInfoTypes[]", "").split(",")
|
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||||
|
|
||||||
for t in types:
|
for t in types:
|
||||||
ai = build_additional_information_from_request(request, t)
|
ai = build_additional_information_from_request(request, t)
|
||||||
|
|
||||||
AdditionalInformation.create(
|
AdditionalInformation.create(
|
||||||
p,
|
p,
|
||||||
ai,
|
ai,
|
||||||
scenario=scenario,
|
scenario=scenario,
|
||||||
content_object=attach_obj,
|
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 ?
|
# TODO implement additional information endpoint ?
|
||||||
return redirect(f"{scenario.url}")
|
return redirect(f"{scenario.url}")
|
||||||
@ -1694,7 +1569,6 @@ class PathwayNode(Schema):
|
|||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||||
pseudo: bool = Field(False, alias="pseudo")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_atom_count(obj: Node):
|
def resolve_atom_count(obj: Node):
|
||||||
@ -1848,29 +1722,6 @@ def create_package_pathway(
|
|||||||
return 403, {"message": str(e)}
|
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}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||||
try:
|
try:
|
||||||
@ -1979,67 +1830,25 @@ class CreateNode(Schema):
|
|||||||
nodeName: str | None = None
|
nodeName: str | None = None
|
||||||
nodeReason: str | None = None
|
nodeReason: str | None = None
|
||||||
nodeDepth: str | None = None
|
nodeDepth: str | None = None
|
||||||
pesLink: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||||
response={200: str | Any, 400: Error, 403: Error},
|
response={200: str | Any, 403: Error},
|
||||||
)
|
)
|
||||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
|
||||||
# TODO Code Dup from bayer.views
|
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||||
|
node_depth = int(n.nodeDepth)
|
||||||
if n.pesLink:
|
|
||||||
from bayer.views import fetch_pes
|
|
||||||
from bayer.models import PESCompound
|
|
||||||
|
|
||||||
try:
|
|
||||||
pes_data = fetch_pes(request, n.pesLink)
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
|
||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
|
||||||
if "secret" == classification.lower():
|
|
||||||
|
|
||||||
if p.classification_level != Package.Classification.SECRET:
|
|
||||||
return 400, "Cannot create PESs for non-secret packages."
|
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
|
||||||
if data_pools:
|
|
||||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
|
||||||
return 400, {
|
|
||||||
"messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"
|
|
||||||
}
|
|
||||||
|
|
||||||
c = PESCompound.create(p, pes_data, n.nodeName, n.nodeReason)
|
|
||||||
|
|
||||||
node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure)
|
|
||||||
if node_qs.exists():
|
|
||||||
return redirect(pw.url)
|
|
||||||
|
|
||||||
node = Node()
|
|
||||||
node.stereo_removed = False
|
|
||||||
node.pathway = pw
|
|
||||||
node.depth = 0
|
|
||||||
|
|
||||||
node.default_node_label = c.default_structure
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
node.node_labels.add(c.default_structure)
|
|
||||||
node.save()
|
|
||||||
else:
|
else:
|
||||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
node_depth = -1
|
||||||
node_depth = int(n.nodeDepth)
|
|
||||||
else:
|
|
||||||
node_depth = -1
|
|
||||||
|
|
||||||
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
||||||
|
|
||||||
return redirect(node.url)
|
return redirect(n.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding node failed!"}
|
||||||
|
|
||||||
@ -2182,10 +1991,6 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
for pr in e.products.split(","):
|
for pr in e.products.split(","):
|
||||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
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(
|
new_e = Edge.create(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
start_nodes=educts,
|
start_nodes=educts,
|
||||||
@ -2193,12 +1998,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
rule=None,
|
rule=None,
|
||||||
name=None,
|
name=None,
|
||||||
description=e.edgeReason,
|
description=e.edgeReason,
|
||||||
multi_step=multi_step,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update depths as sideeffect of above operation
|
|
||||||
pw.update_depths()
|
|
||||||
|
|
||||||
return redirect(new_e.url)
|
return redirect(new_e.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding Edge failed!"}
|
return 403, {"message": "Adding Edge failed!"}
|
||||||
|
|||||||
@ -1065,9 +1065,52 @@ class PackageManager(object):
|
|||||||
|
|
||||||
print("Fixing Node depths...")
|
print("Fixing Node depths...")
|
||||||
total_pws = Pathway.objects.filter(package=pack).count()
|
total_pws = Pathway.objects.filter(package=pack).count()
|
||||||
|
|
||||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||||
pw.update_depths()
|
in_count = defaultdict(lambda: 0)
|
||||||
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
for e in pw.edges:
|
||||||
|
# TODO check if this will remain
|
||||||
|
for react in e.start_nodes.all():
|
||||||
|
out_count[str(react.uuid)] += 1
|
||||||
|
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
in_count[str(prod.uuid)] += 1
|
||||||
|
|
||||||
|
root_nodes = []
|
||||||
|
for n in pw.nodes:
|
||||||
|
num_parents = in_count[str(n.uuid)]
|
||||||
|
if num_parents == 0:
|
||||||
|
# must be a root node or unconnected node
|
||||||
|
if n.depth != 0:
|
||||||
|
n.depth = 0
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
# Only root node may have children
|
||||||
|
if out_count[str(n.uuid)] > 0:
|
||||||
|
root_nodes.append(n)
|
||||||
|
|
||||||
|
levels = [root_nodes]
|
||||||
|
seen = set()
|
||||||
|
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
|
||||||
|
for i, level_nodes in enumerate(levels):
|
||||||
|
new_level = []
|
||||||
|
for n in level_nodes:
|
||||||
|
for e in n.out_edges.all():
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
if str(prod.uuid) not in seen:
|
||||||
|
old_depth = prod.depth
|
||||||
|
if old_depth != i + 1:
|
||||||
|
prod.depth = i + 1
|
||||||
|
prod.save()
|
||||||
|
|
||||||
|
new_level.append(prod)
|
||||||
|
|
||||||
|
seen.add(str(n.uuid))
|
||||||
|
|
||||||
|
if new_level:
|
||||||
|
levels.append(new_level)
|
||||||
|
|
||||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||||
|
|
||||||
return pack
|
return pack
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
|
||||||
|
|
||||||
MAPPING = {
|
|
||||||
"": HalfLifeModel.OTHER,
|
|
||||||
"HS-SFO": HalfLifeModel.HS_SFO,
|
|
||||||
"FOMC": HalfLifeModel.FOMC,
|
|
||||||
"FOTC": HalfLifeModel.DFOP,
|
|
||||||
"FMOC": HalfLifeModel.FOMC,
|
|
||||||
"DFOP": HalfLifeModel.DFOP,
|
|
||||||
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
|
||||||
"first order kinetics": HalfLifeModel.SFO,
|
|
||||||
"SFO²": HalfLifeModel.SFO,
|
|
||||||
"HS": HalfLifeModel.HS,
|
|
||||||
"top down": HalfLifeModel.OTHER,
|
|
||||||
"SFO": HalfLifeModel.SFO,
|
|
||||||
"First Order": HalfLifeModel.SFO,
|
|
||||||
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
|
||||||
"true": HalfLifeModel.SFO,
|
|
||||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def forward_func(apps, schema_editor):
|
|
||||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
|
||||||
|
|
||||||
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
|
||||||
|
|
||||||
for hl in hls:
|
|
||||||
data = hl.data
|
|
||||||
data["model"] = MAPPING[data["model"]].value
|
|
||||||
hl.data = HalfLife(**data).model_dump(mode="json")
|
|
||||||
hl.save()
|
|
||||||
|
|
||||||
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
|
||||||
|
|
||||||
for hl in hlws:
|
|
||||||
data = hl.data
|
|
||||||
data["model"] = MAPPING[data["model"]].value
|
|
||||||
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
|
||||||
hl.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0024_user_contacted"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
# 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),
|
|
||||||
]
|
|
||||||
@ -75,7 +75,6 @@ class User(AbstractUser):
|
|||||||
blank=False,
|
blank=False,
|
||||||
)
|
)
|
||||||
is_reviewer = models.BooleanField(default=False)
|
is_reviewer = models.BooleanField(default=False)
|
||||||
contacted = models.BooleanField(null=True, blank=True)
|
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
USERNAME_FIELD = "email"
|
||||||
REQUIRED_FIELDS = ["username"]
|
REQUIRED_FIELDS = ["username"]
|
||||||
@ -1703,7 +1702,7 @@ class Reaction(
|
|||||||
if name is not None and name.strip() != "":
|
if name is not None and name.strip() != "":
|
||||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if description is not None and description.strip() != "":
|
if description is not None and name.strip() != "":
|
||||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
r.multi_step = multi_step
|
r.multi_step = multi_step
|
||||||
@ -1781,7 +1780,7 @@ class Reaction(
|
|||||||
return new_reaction
|
return new_reaction
|
||||||
|
|
||||||
def smirks(self):
|
def smirks(self):
|
||||||
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')])}"
|
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
@ -1894,9 +1893,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
if n not in queue:
|
if n not in queue:
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
|
|
||||||
for i in queue:
|
|
||||||
processed.add(i)
|
|
||||||
|
|
||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
@ -2174,7 +2170,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
depth: Optional[int] = -1,
|
depth: Optional[int] = 0,
|
||||||
):
|
):
|
||||||
return Node.create(self, smiles, depth, name=name, description=description)
|
return Node.create(self, smiles, depth, name=name, description=description)
|
||||||
|
|
||||||
@ -2189,66 +2185,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
):
|
):
|
||||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||||
|
|
||||||
def update_depths(self):
|
|
||||||
# Collect number of in and out links per node
|
|
||||||
in_count = defaultdict(lambda: 0)
|
|
||||||
out_count = defaultdict(lambda: 0)
|
|
||||||
|
|
||||||
for e in self.edges:
|
|
||||||
for react in e.start_nodes.all():
|
|
||||||
out_count[str(react.uuid)] += 1
|
|
||||||
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
in_count[str(prod.uuid)] += 1
|
|
||||||
|
|
||||||
depth_map = {}
|
|
||||||
depth_map[0] = list()
|
|
||||||
processed = set()
|
|
||||||
|
|
||||||
data_driven_root_nodes = self.node_set.all().annotate(
|
|
||||||
prod_cnt=Count('edge_products'),
|
|
||||||
educt_cnt=Count('edge_educts')
|
|
||||||
).filter(prod_cnt=0, educt_cnt__gt=0).distinct()
|
|
||||||
|
|
||||||
# Eval QuerySet
|
|
||||||
root_nodes_by_depth = list(self.root_nodes)
|
|
||||||
|
|
||||||
data_driven_root_nodes.update(depth=0)
|
|
||||||
root_nodes = [n for n in data_driven_root_nodes]
|
|
||||||
|
|
||||||
for n in root_nodes_by_depth:
|
|
||||||
if n not in root_nodes:
|
|
||||||
if len(n.edge_products.all()) == 0:
|
|
||||||
root_nodes.append(n)
|
|
||||||
|
|
||||||
for n in 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):
|
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2397,7 +2333,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
edge_label = models.ForeignKey(
|
edge_label = models.ForeignKey(
|
||||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.CASCADE
|
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
start_nodes = models.ManyToManyField(
|
start_nodes = models.ManyToManyField(
|
||||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||||
@ -2476,8 +2412,6 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
rule: Optional[Rule] = None,
|
rule: Optional[Rule] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
):
|
):
|
||||||
e = Edge()
|
e = Edge()
|
||||||
e.pathway = pathway
|
e.pathway = pathway
|
||||||
@ -2507,7 +2441,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||||
rules=rule,
|
rules=rule,
|
||||||
multi_step=kwargs.get("multi_step", False),
|
multi_step=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
e.edge_label = r
|
e.edge_label = r
|
||||||
@ -4515,22 +4449,18 @@ class AdditionalInformation(models.Model):
|
|||||||
|
|
||||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||||
|
|
||||||
@staticmethod
|
def get(self) -> "EnviPyModel":
|
||||||
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
try:
|
try:
|
||||||
inst = MAPPING[ai_type](**ai_data)
|
inst = MAPPING[self.type](**self.data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading {ai_type}: {e}")
|
print(f"Error loading {self.type}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return inst
|
|
||||||
|
|
||||||
def get(self) -> "EnviPyModel":
|
|
||||||
inst = AdditionalInformation.from_dict(self.type, self.data)
|
|
||||||
inst.__dict__["uuid"] = str(self.uuid)
|
inst.__dict__["uuid"] = str(self.uuid)
|
||||||
|
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@ -937,14 +937,12 @@ def package_models(request, package_uuid):
|
|||||||
"requires_rule_packages": True,
|
"requires_rule_packages": True,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
}
|
"EnviFormer": {
|
||||||
|
|
||||||
if s.ENVIFORMER_PRESENT:
|
|
||||||
context["model_types"]["EnviFormer"] = {
|
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -2539,9 +2537,6 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
|||||||
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update depths as sideeffect of above operation
|
|
||||||
current_pathway.update_depths()
|
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -3113,21 +3108,12 @@ def jobs(request):
|
|||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||||
]
|
]
|
||||||
# if current_user.is_superuser:
|
if current_user.is_superuser:
|
||||||
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||||
# else:
|
else:
|
||||||
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
|
|
||||||
# Context for paginated template
|
return render(request, "collections/joblog.html", context)
|
||||||
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":
|
elif request.method == "POST":
|
||||||
job_name = request.POST.get("job-name")
|
job_name = request.POST.get("job-name")
|
||||||
|
|||||||
@ -21,11 +21,5 @@
|
|||||||
"django",
|
"django",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"daisyui"
|
"daisyui"
|
||||||
],
|
]
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"@parcel/watcher",
|
|
||||||
"@tailwindcss/oxide"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
|
|
||||||
import matplotlib.patches as mpatches
|
import matplotlib.patches as mpatches
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib.figure import Figure
|
from matplotlib import pyplot as plt
|
||||||
from scipy import stats
|
from scipy import stats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -101,8 +101,7 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
mask_red = x > vp
|
mask_red = x > vp
|
||||||
|
|
||||||
# Plot
|
# Plot
|
||||||
fig = Figure(figsize=(9, 5.5))
|
fig, ax = plt.subplots(figsize=(9, 5.5))
|
||||||
ax = fig.subplots()
|
|
||||||
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
||||||
|
|
||||||
if np.any(mask_green):
|
if np.any(mask_green):
|
||||||
@ -147,12 +146,13 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
]
|
]
|
||||||
ax.legend(handles=patches, frameon=True)
|
ax.legend(handles=patches, frameon=True)
|
||||||
|
|
||||||
fig.tight_layout()
|
plt.tight_layout()
|
||||||
|
|
||||||
# --- Export to SVG string ---
|
# --- Export to SVG string ---
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||||
svg = buf.getvalue()
|
svg = buf.getvalue()
|
||||||
|
plt.close(fig)
|
||||||
buf.close()
|
buf.close()
|
||||||
|
|
||||||
return svg
|
return svg
|
||||||
|
|||||||
@ -187,9 +187,8 @@ class Pepper:
|
|||||||
groups = [group for group in dataset.group_by("structure_id")]
|
groups = [group for group in dataset.group_by("structure_id")]
|
||||||
|
|
||||||
# Unless explicitly set compute everything serial
|
# Unless explicitly set compute everything serial
|
||||||
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
|
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
||||||
if n_threads > 1:
|
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
||||||
results = Parallel(n_jobs=n_threads)(
|
|
||||||
delayed(compute_bayes_per_group)(group[1])
|
delayed(compute_bayes_per_group)(group[1])
|
||||||
for group in dataset.group_by("structure_id")
|
for group in dataset.group_by("structure_id")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
allowBuilds:
|
|
||||||
'@parcel/watcher': true
|
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- '@parcel/watcher'
|
- '@parcel/watcher'
|
||||||
- '@tailwindcss/oxide'
|
- '@tailwindcss/oxide'
|
||||||
|
|||||||
@ -59,9 +59,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
get isEditMode() {
|
get isEditMode() {
|
||||||
return this.mode === "edit";
|
return this.mode === "edit";
|
||||||
},
|
},
|
||||||
get isRequired() {
|
|
||||||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text widget
|
// Text widget
|
||||||
@ -296,34 +293,6 @@ 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
|
// Compound link widget
|
||||||
Alpine.data(
|
Alpine.data(
|
||||||
"compoundWidget",
|
"compoundWidget",
|
||||||
|
|||||||
@ -22,7 +22,7 @@ function predictFromNode(url) {
|
|||||||
// data = {{ pathway.d3_json | safe }};
|
// data = {{ pathway.d3_json | safe }};
|
||||||
// elem = 'vizdiv'
|
// elem = 'vizdiv'
|
||||||
function draw(pathway, elem) {
|
function draw(pathway, elem) {
|
||||||
const initialzoom = 2.5
|
|
||||||
const nodeRadius = 20;
|
const nodeRadius = 20;
|
||||||
const linkDistance = 100;
|
const linkDistance = 100;
|
||||||
const chargeStrength = -200;
|
const chargeStrength = -200;
|
||||||
@ -63,7 +63,7 @@ function draw(pathway, elem) {
|
|||||||
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
|
node.fy = node.depth * levelSpacing + 50;
|
||||||
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -101,24 +101,10 @@ function draw(pathway, elem) {
|
|||||||
// Update pseudo node positions first
|
// Update pseudo node positions first
|
||||||
updatePseudoNodePositions();
|
updatePseudoNodePositions();
|
||||||
|
|
||||||
link.attr("d", d => {
|
link.attr("x1", d => d.source.x)
|
||||||
// Check if it's a self-loop (source equals target)
|
.attr("y1", d => d.source.y)
|
||||||
if (d.source.id === d.target.id) {
|
.attr("x2", d => d.target.x)
|
||||||
// Create a bezier curve for self-loops
|
.attr("y2", d => d.target.y);
|
||||||
const x = d.source.x;
|
|
||||||
const y = d.source.y;
|
|
||||||
const loopRadius = nodeRadius * 2; // Adjust size of the loop
|
|
||||||
|
|
||||||
// Create a circular path to the left of the node
|
|
||||||
return `M ${x},${y - nodeRadius}
|
|
||||||
C ${x - loopRadius},${y - nodeRadius - loopRadius}
|
|
||||||
${x - loopRadius},${y + nodeRadius + loopRadius}
|
|
||||||
${x},${y + nodeRadius}`;
|
|
||||||
} else {
|
|
||||||
// Regular straight line for normal edges
|
|
||||||
return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
}
|
}
|
||||||
@ -477,7 +463,7 @@ function draw(pathway, elem) {
|
|||||||
// TODO needs to be generic once we store it as AddInf
|
// TODO needs to be generic once we store it as AddInf
|
||||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||||
if (s["mean"] != null) {
|
if (s["mean"] != null) {
|
||||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
|
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -572,12 +558,11 @@ function draw(pathway, elem) {
|
|||||||
.scaleExtent([0.5, 5])
|
.scaleExtent([0.5, 5])
|
||||||
.on("zoom", (event) => {
|
.on("zoom", (event) => {
|
||||||
zoomable.attr("transform", event.transform);
|
zoomable.attr("transform", event.transform);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
// Apply zoom to the SVG element - this enables wheel zoom
|
// Apply zoom to the SVG element - this enables wheel zoom
|
||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
svg.call(zoom.scaleBy, initialzoom);
|
|
||||||
// Also apply zoom to container to catch events that might not reach SVG
|
// Also apply zoom to container to catch events that might not reach SVG
|
||||||
// This ensures drag-to-pan works even when clicking on empty space
|
// This ensures drag-to-pan works even when clicking on empty space
|
||||||
container.call(zoom);
|
container.call(zoom);
|
||||||
@ -596,11 +581,11 @@ function draw(pathway, elem) {
|
|||||||
for (idx in parents) {
|
for (idx in parents) {
|
||||||
p = nodes[parents[idx]]
|
p = nodes[parents[idx]]
|
||||||
// console.log(p.depth)
|
// console.log(p.depth)
|
||||||
// if (p.depth >= n.depth) {
|
if (p.depth >= n.depth) {
|
||||||
// // keep the .5 steps for pseudo nodes
|
// keep the .5 steps for pseudo nodes
|
||||||
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||||
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -614,47 +599,13 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Kanten zeichnen
|
// Kanten zeichnen
|
||||||
const link = zoomable.append("g")
|
const link = zoomable.append("g")
|
||||||
.selectAll("path")
|
.selectAll("line")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter().append("path")
|
.enter().append("line")
|
||||||
// Check if target is pseudo and draw marker only if not pseudo
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
.attr("marker-end", d => {
|
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
|
||||||
if (d.target.pseudo) return '';
|
|
||||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
|
||||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
|
||||||
})
|
|
||||||
.attr("fill", "none")
|
|
||||||
.on("click", function(event, d) {
|
|
||||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
|
||||||
|
|
||||||
d3.selectAll("path").classed("highlighted", false);
|
|
||||||
|
|
||||||
if (!wasHighlighted) {
|
|
||||||
const toHighlight = [];
|
|
||||||
toHighlight.push(d.el);
|
|
||||||
|
|
||||||
if (d.source.pseudo || d.target.pseudo) {
|
|
||||||
if (d.target.pseudo) {
|
|
||||||
d3.selectAll("path").each(e => {
|
|
||||||
if (e !== undefined && e.source.id === d.target.id) {
|
|
||||||
toHighlight.push(e.el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
d3.selectAll("path").each(e => {
|
|
||||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
|
||||||
toHighlight.push(e.el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e of toHighlight) {
|
|
||||||
d3.select(e).classed("highlighted", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
@ -673,13 +624,7 @@ function draw(pathway, elem) {
|
|||||||
.on("drag", dragged)
|
.on("drag", dragged)
|
||||||
.on("end", dragended))
|
.on("end", dragended))
|
||||||
.on("click", function (event, d) {
|
.on("click", function (event, d) {
|
||||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||||
|
|
||||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
|
||||||
|
|
||||||
if (!wasHighlighted) {
|
|
||||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Kreise für die Knoten hinzufügen
|
// Kreise für die Knoten hinzufügen
|
||||||
|
|||||||
@ -9,6 +9,14 @@
|
|||||||
<i class="glyphicon glyphicon-plus"></i> Add Compound</a
|
<i class="glyphicon glyphicon-plus"></i> Add Compound</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
|
||||||
|
>
|
||||||
|
<i class="glyphicon glyphicon-plus"></i> Add PES</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
@ -22,7 +30,7 @@
|
|||||||
{% for tpl in action_button_templates %}
|
{% for tpl in action_button_templates %}
|
||||||
{% include tpl %}
|
{% include tpl %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li role="separator" class="divider h-px"></li>
|
<li role="separator" class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@ -74,7 +82,7 @@
|
|||||||
Rules</a
|
Rules</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider h-px"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
@ -99,16 +107,11 @@
|
|||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider h-px"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
onclick="
|
onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
|
||||||
const modal = document.getElementById('delete_pathway_node_modal');
|
|
||||||
modal.showModal();
|
|
||||||
window.dispatchEvent(new Event('modal-opened'));
|
|
||||||
return false;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
||||||
>
|
>
|
||||||
@ -116,12 +119,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
onclick="
|
onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
|
||||||
const modal = document.getElementById('delete_pathway_edge_modal');
|
|
||||||
modal.showModal();
|
|
||||||
window.dispatchEvent(new Event('modal-opened'));
|
|
||||||
return false;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
{# 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>
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -3,16 +3,14 @@
|
|||||||
{% block page_title %}Models{% endblock %}
|
{% block page_title %}Models{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
|
||||||
{% if meta.enabled_features.MODEL_BUILDING %}
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn btn-primary btn-sm"
|
||||||
class="btn btn-primary btn-sm"
|
onclick="document.getElementById('new_model_modal').showModal(); return false;"
|
||||||
onclick="document.getElementById('new_model_modal').showModal(); return false;"
|
>
|
||||||
>
|
New Model
|
||||||
New Model
|
</button>
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
{% block page_title %}Packages{% endblock %}
|
{% block page_title %}Packages{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
|
{% if meta.can_edit %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -70,6 +71,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
|
|||||||
@ -37,11 +37,7 @@
|
|||||||
perPage: {{ per_page|default:50 }}
|
perPage: {{ per_page|default:50 }}
|
||||||
})"
|
})"
|
||||||
>
|
>
|
||||||
{% if entity_type == 'joblog' %}
|
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||||
{% 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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Pathways{% endblock %}
|
{% block page_title %}Pathways{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Reactions{% endblock %}
|
{% block page_title %}Reactions{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Rules{% endblock %}
|
{% block page_title %}Rules{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -123,17 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 -->
|
<!-- Compound link widget -->
|
||||||
<template
|
<template
|
||||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
{# 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>
|
|
||||||
@ -44,7 +44,6 @@
|
|||||||
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||||
x-model="value"
|
x-model="value"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:required="isRequired"
|
|
||||||
>
|
>
|
||||||
<option value="" :selected="!value">Select...</option>
|
<option value="" :selected="!value">Select...</option>
|
||||||
|
|
||||||
|
|||||||
@ -65,11 +65,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
get showMlrr() {
|
get showMlrr() {
|
||||||
return this.selectedType === 'ml-relative-reasoning';
|
return this.selectedType === 'mlrr';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showRbrr() {
|
get showRbrr() {
|
||||||
return this.selectedType === 'rule-based-relative-reasoning';
|
return this.selectedType === 'rbrr';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showEnviformer() {
|
get showEnviformer() {
|
||||||
|
|||||||
@ -203,11 +203,11 @@
|
|||||||
id="model-based-prediction-setting-threshold"
|
id="model-based-prediction-setting-threshold"
|
||||||
name="model-based-prediction-setting-threshold"
|
name="model-based-prediction-setting-threshold"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
value="0.25"
|
placeholder="0.25"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="any"
|
step="0.05"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,25 +4,6 @@
|
|||||||
id="delete_pathway_edge_modal"
|
id="delete_pathway_edge_modal"
|
||||||
class="modal"
|
class="modal"
|
||||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
|
||||||
const links = d3.selectAll('path.highlighted');
|
|
||||||
|
|
||||||
if (!links.empty()) {
|
|
||||||
const el = links.node();
|
|
||||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
|
||||||
console.log(el);
|
|
||||||
console.log(el.__data__);
|
|
||||||
for (let option of selectElement.options) {
|
|
||||||
if (option.value === el.__data__.url) {
|
|
||||||
option.selected = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
|
||||||
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
>
|
>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
|
|||||||
@ -4,22 +4,6 @@
|
|||||||
id="delete_pathway_node_modal"
|
id="delete_pathway_node_modal"
|
||||||
class="modal"
|
class="modal"
|
||||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
|
||||||
const el = d3.select('circle.highlighted').node();
|
|
||||||
|
|
||||||
if (el !== null) {
|
|
||||||
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
|
||||||
|
|
||||||
for (let option of selectElement.options) {
|
|
||||||
if (option.value === el.__data__.url) {
|
|
||||||
option.selected = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
>
|
>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
|
|||||||
@ -56,8 +56,8 @@
|
|||||||
<ul class="menu bg-base-200 rounded-box">
|
<ul class="menu bg-base-200 rounded-box">
|
||||||
{% for um in group.user_member.all %}
|
{% for um in group.user_member.all %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% if not user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
|
<a href="{{ um.url }}" class="hover:bg-base-300"
|
||||||
>{{ um.username }}
|
>{{ um.username }}
|
||||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -77,6 +77,7 @@
|
|||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
{% include "modals/objects/add_pathway_node_modal.html" %}
|
{% include "modals/objects/add_pathway_node_modal.html" %}
|
||||||
|
{% include "modals/objects/add_pathway_pes_node_modal.html" %}
|
||||||
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||||
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
|
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
|
||||||
{% for tpl in add_templates %}
|
{% for tpl in add_templates %}
|
||||||
@ -106,12 +107,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Graphical Representation -->
|
<!-- Graphical Representation -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse overflow-y-auto">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Graphical Representation
|
Graphical Representation
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content ">
|
<div class="collapse-content">
|
||||||
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
||||||
<div class="navbar bg-base-100 rounded-lg">
|
<div class="navbar bg-base-100 rounded-lg">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@ -140,7 +141,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-96 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||||
>
|
>
|
||||||
{% include "actions/objects/pathway.html" %}
|
{% include "actions/objects/pathway.html" %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -367,18 +368,6 @@
|
|||||||
>
|
>
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||||
</marker>
|
</marker>
|
||||||
<marker
|
|
||||||
id="curve-arrow"
|
|
||||||
viewBox="0 0 10 10"
|
|
||||||
refX="10"
|
|
||||||
refY="5"
|
|
||||||
markerWidth="6"
|
|
||||||
markerHeight="6"
|
|
||||||
orient="auto"
|
|
||||||
markerUnits="strokeWidth"
|
|
||||||
>
|
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
<marker
|
||||||
id="doublearrow"
|
id="doublearrow"
|
||||||
viewBox="0 0 20 30"
|
viewBox="0 0 20 30"
|
||||||
|
|||||||
@ -105,7 +105,7 @@
|
|||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="select mb-8 w-full" id="prediction-setting-label">
|
<label class="select mb-8 w-full">
|
||||||
<span class="label">Predictor</span>
|
<span class="label">Predictor</span>
|
||||||
<select id="prediction-setting" name="prediction-setting">
|
<select id="prediction-setting" name="prediction-setting">
|
||||||
<option disabled>Select a Setting</option>
|
<option disabled>Select a Setting</option>
|
||||||
@ -148,22 +148,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{# prettier-ignore-start #}
|
{# prettier-ignore-start #}
|
||||||
<script>
|
<script>
|
||||||
// Hide predictor selection and update button text if mode is "build"
|
|
||||||
function radioChange(event) {
|
|
||||||
if (event.target.value === "build") {
|
|
||||||
document.getElementById("prediction-setting-label").hidden = true;
|
|
||||||
document.getElementById("predict-submit-button").innerText = "Build";
|
|
||||||
} else {
|
|
||||||
document.getElementById("prediction-setting-label").hidden = false;
|
|
||||||
document.getElementById("predict-submit-button").innerText = "Predict";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const radioButtons = document.querySelectorAll('input[name="predict"]');
|
|
||||||
radioButtons.forEach(radio => {
|
|
||||||
radio.addEventListener('change', radioChange);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to safely get Ketcher instance from iframe
|
// Helper function to safely get Ketcher instance from iframe
|
||||||
function getKetcherInstance(iframeId) {
|
function getKetcherInstance(iframeId) {
|
||||||
const ketcherFrame = document.getElementById(iframeId);
|
const ketcherFrame = document.getElementById(iframeId);
|
||||||
|
|||||||
@ -110,6 +110,8 @@
|
|||||||
<div
|
<div
|
||||||
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
||||||
>
|
>
|
||||||
|
<a href="/legal" class="link link-hover">Legal</a>
|
||||||
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/terms" class="link link-hover">Terms of Use</a>
|
<a href="/terms" class="link link-hover">Terms of Use</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
||||||
|
|||||||
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel
|
from envipy_additional_information import HalfLife, HalfLifeWS
|
||||||
from envipy_additional_information.information import Interval
|
from envipy_additional_information.information import Interval
|
||||||
from envipy_additional_information.parsers import (
|
from envipy_additional_information.parsers import (
|
||||||
AcidityParser,
|
AcidityParser,
|
||||||
@ -125,7 +125,6 @@ from envipy_additional_information.parsers import (
|
|||||||
LiquidMatrixSourceParser,
|
LiquidMatrixSourceParser,
|
||||||
OxygenUptakeRateParser,
|
OxygenUptakeRateParser,
|
||||||
InitiatingOrganismParser,
|
InitiatingOrganismParser,
|
||||||
PFASConfidenceParser,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -142,7 +141,7 @@ def get_parameter(request, paramname):
|
|||||||
res = request.POST.get(paramname)
|
res = request.POST.get(paramname)
|
||||||
if res is not None and res.strip() != "":
|
if res is not None and res.strip() != "":
|
||||||
return res
|
return res
|
||||||
raise ValueError("Not all parameters are set!")
|
return ValueError("Not all parameters are set!")
|
||||||
|
|
||||||
|
|
||||||
def get_parameter_or_empty_string(request, paramname):
|
def get_parameter_or_empty_string(request, paramname):
|
||||||
@ -474,12 +473,17 @@ def build_additional_information_from_request(request, type_):
|
|||||||
|
|
||||||
comment = get_parameter_or_empty_string(request, "comment")
|
comment = get_parameter_or_empty_string(request, "comment")
|
||||||
source = get_parameter_or_empty_string(request, "source")
|
source = get_parameter_or_empty_string(request, "source")
|
||||||
# first_order = get_parameter_or_empty_string(request, "firstOrder")
|
first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||||
model = get_parameter_or_empty_string(request, "model")
|
model = get_parameter_or_empty_string(request, "model")
|
||||||
fit = get_parameter_or_empty_string(request, "fit")
|
fit = get_parameter_or_empty_string(request, "fit")
|
||||||
|
|
||||||
if model:
|
if first_order != "":
|
||||||
model = HalfLifeModel(model.upper())
|
if model != "":
|
||||||
|
raise ValueError("not both, model and firstOrder can be set!")
|
||||||
|
if first_order == "true":
|
||||||
|
model = "SFO"
|
||||||
|
else:
|
||||||
|
logger.info("firstOrder is set to false which is not meaningful")
|
||||||
|
|
||||||
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
||||||
|
|
||||||
@ -504,10 +508,6 @@ def build_additional_information_from_request(request, type_):
|
|||||||
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
||||||
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
||||||
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
||||||
|
|
||||||
if model_ws:
|
|
||||||
model_ws = HalfLifeModel(model_ws.upper())
|
|
||||||
|
|
||||||
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
||||||
|
|
||||||
dt50_total = IntervalParser.from_string(hl_ws_total)
|
dt50_total = IntervalParser.from_string(hl_ws_total)
|
||||||
@ -674,8 +674,7 @@ def build_additional_information_from_request(request, type_):
|
|||||||
elif type_ == "studywst":
|
elif type_ == "studywst":
|
||||||
# study_wst_cond = get_parameter(request, "studywstcond")
|
# study_wst_cond = get_parameter(request, "studywstcond")
|
||||||
raise ValueError("studywstcond is not yet implemented")
|
raise ValueError("studywstcond is not yet implemented")
|
||||||
elif type_ == "pfasconfidence":
|
|
||||||
return PFASConfidenceParser.from_string(get_parameter(request, "level"))
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")
|
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")
|
||||||
|
|
||||||
|
|||||||
34
uv.lock
generated
34
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#0a608c85c73a6ef5c38afea87d2b57fb43f01a70" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
@ -2763,9 +2763,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54", upload-time = "2025-10-01T23:35:50Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58", upload-time = "2025-10-01T23:35:52Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2785,19 +2785,19 @@ dependencies = [
|
|||||||
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5", upload-time = "2025-10-01T23:33:41Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d", upload-time = "2025-10-01T23:33:45Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e", upload-time = "2025-10-01T23:33:48Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d", upload-time = "2025-10-01T23:33:52Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434", upload-time = "2025-10-01T23:34:10Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d", upload-time = "2025-10-01T23:34:15Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25", upload-time = "2025-10-01T23:34:19Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de", upload-time = "2025-10-01T23:34:23Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856", upload-time = "2025-10-01T23:34:28Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88", upload-time = "2025-10-01T23:34:45Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041", upload-time = "2025-10-01T23:34:50Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab", upload-time = "2025-10-01T23:34:53Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user