This commit is contained in:
Tim Lorsbach
2026-05-05 13:04:03 +02:00
parent 5eb3ebac89
commit c92fccaf8e
17 changed files with 138 additions and 57 deletions

View File

@ -30,12 +30,13 @@ 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:1.0 . # docker build --ssh default -t envipath/envipy-bayer:1.0 .
RUN --mount=type=ssh \ RUN --mount=type=ssh \
uv sync --locked --extra ms-login --extra pepper-plugin uv sync --locked --extra ms-login --extra pepper-plugin
# Now copy source and do a final sync to install the project itself # Now copy source and do a final sync to install the project itself
# Ensure .dockerignore is reasonable # Ensure .dockerignore is reasonable
COPY bb4g bb4g
COPY biotransformer biotransformer COPY biotransformer biotransformer
COPY bayer bayer COPY bayer bayer
COPY bridge bridge COPY bridge bridge

View File

@ -103,20 +103,15 @@ 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 or True: if token:
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' or True: if pes_id == 'dummy':
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
@ -125,7 +120,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=proxies) res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
try: try:
res.raise_for_status() res.raise_for_status()

View File

@ -2,9 +2,11 @@ import json
import math import math
from datetime import datetime from datetime import datetime
from typing import List from typing import List
import enum
import requests import requests
from django.conf import settings as s from django.conf import settings as s
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
from envipy_additional_information import register
from bridge.contracts import Classifier # noqa: I001 from bridge.contracts import Classifier # noqa: I001
from bridge.dto import ( from bridge.dto import (
@ -15,12 +17,36 @@ from bridge.dto import (
TransformationProductPrediction, TransformationProductPrediction,
) # noqa: I001 ) # noqa: I001
class SamplingAlgorithm(enum.Enum):
EXACT = "exact"
@register("bb4gconfig")
class BB4GConfig(EnviPyModel):
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
cutoff: int = -5
class UI:
title = "BB4G Configuration"
sampling_algorithm = UIConfig(
widget=WidgetType.SELECT,
label="BB4G Sampling Algorithm",
order=1,
placeholder="If unset defaults to 'exact'"
)
cutoff = UIConfig(
widget=WidgetType.NUMBER,
label="BB4G Cutoff",
order=2,
placeholder="If unset defaults to -5"
)
# Once stable these will be exposed by enviPy-plugins lib # Once stable these will be exposed by enviPy-plugins lib
class BB4G(Classifier): class BB4G(Classifier):
Config = None Config = BB4GConfig
def __init__(self, config=None): def __init__(self, config: BB4GConfig | None = None):
super().__init__(config) super().__init__(config)
self.url = f"{s.BB4G_URL}" self.url = f"{s.BB4G_URL}"
@ -29,10 +55,6 @@ class BB4G(Classifier):
"Authorization": f"Bearer {self.token}", "Authorization": f"Bearer {self.token}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
self.proxies = {
"http": s.HTTP_PROXY,
"https": s.HTTPS_PROXY,
}
def acquire_token(self): def acquire_token(self):
BB4G_TENANT_ID = s.BB4G_TENANT_ID BB4G_TENANT_ID = s.BB4G_TENANT_ID
@ -65,7 +87,7 @@ class BB4G(Classifier):
started = False started = False
retries = 0 retries = 0
while not started and retries < 5: while not started and retries < 5:
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=self.proxies) res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
if res.status_code == 200: if res.status_code == 200:
started = True started = True
@ -140,11 +162,11 @@ class BB4G(Classifier):
for smi in smiles: for smi in smiles:
data = { data = {
"smiles": smi, "smiles": smi,
"sampling_alg": "exact", "sampling_alg": self.config.sampling_algorithm.value,
"cutoff": -5, "cutoff": self.config.cutoff,
} }
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=self.proxies) resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
resp.raise_for_status() resp.raise_for_status()

View File

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

View File

@ -143,6 +143,12 @@ 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
@ -474,3 +480,14 @@ if DATA_POOL_MAPPING:
else: else:
DATA_POOL_MAPPING = {} DATA_POOL_MAPPING = {}
PROXIES = {}
if os.environ.get("HTTP_PROXY"):
PROXIES["http"] = os.environ.get("HTTP_PROXY")
PROXIES["https"] = os.environ.get("HTTPS_PROXY")
# BB4g
BB4G_URL = os.environ.get("BB4G_URL")
BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")

View File

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

View File

@ -2,9 +2,11 @@ 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 from epdb.logic import UserManager, GroupManager
from epdb.models import Group
def get_msal_app_with_cache(request): def get_msal_app_with_cache(request):
@ -81,10 +83,12 @@ 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} ", uuid=id) g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
uuid=id)
else: else:
g = Group.objects.get(uuid=id) g = Group.objects.get(uuid=id)
# Ensure its secret # Ensure its secret
@ -93,7 +97,8 @@ 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} ", uuid=id) g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
uuid=id)
else: else:
g = Group.objects.get(uuid=id) g = Group.objects.get(uuid=id)
@ -152,3 +157,9 @@ def get_access_token_from_request(request, scopes=None):
return result return result
return None return None
def get_token(request):
token = get_access_token_from_request(request)
msg = f"{token}"
return HttpResponse(msg, content_type='text/plain')

View File

@ -1,19 +1,17 @@
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 requests
import nh3 import nh3
import requests
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
@ -51,6 +49,26 @@ 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):
@ -68,9 +86,7 @@ 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
# Fetch Microsoft's public keys jwks = get_cached_jwks(TENANT_ID)
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)
@ -78,13 +94,21 @@ 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"])
) )
claims = jwt.decode( # Handle V1 and V2 tokens
token, try:
public_key, claims = jwt.decode(
algorithms=["RS256"], token,
audience=[CLIENT_ID, f"api://{CLIENT_ID}"], public_key,
issuer=f"https://sts.windows.net/{TENANT_ID}/", algorithms=["RS256"],
) 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

View File

@ -937,12 +937,14 @@ 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():

View File

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

View File

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

View File

@ -3,7 +3,6 @@
{% 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"
@ -71,7 +70,6 @@
</ul> </ul>
</div> </div>
</div> </div>
{% endif %}
{% endblock action_button %} {% endblock action_button %}
{% block action_modals %} {% block action_modals %}

View File

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

View File

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

View File

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

View File

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

View File

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