Compare commits

..
Author SHA1 Message Date
Tim Lorsbach b12c2dda69 wip
CI / test (pull_request) Failing after 27s
API CI / api-tests (pull_request) Failing after 31s
2026-09-08 15:11:47 +02:00
Tim Lorsbach 0b325a30b7 updated add if dep
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 43s
2026-09-03 12:04:14 +02:00
Tim Lorsbach d8741c5375 wip
CI / test (pull_request) Failing after 29s
API CI / api-tests (pull_request) Failing after 42s
2026-09-02 21:34:26 +02:00
Tim Lorsbach 887de03a19 reactionSmarts 2026-09-02 21:30:53 +02:00
Tim Lorsbach b54c8eaab6 minor
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 44s
2026-08-28 13:14:43 +02:00
Tim Lorsbach f885745127 Fix import
CI / test (pull_request) Failing after 27s
API CI / api-tests (pull_request) Failing after 41s
2026-08-27 11:46:08 +02:00
Tim Lorsbach 48d2bce52a minor 2026-08-20 09:28:23 +02:00
Tim Lorsbach 67aa3731cb PES Adjustments, Check is User is in enviPath Entra Group
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 42s
2026-08-20 08:59:24 +02:00
14 changed files with 229 additions and 66 deletions
+1
View File
@@ -85,6 +85,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxext6 \ libxext6 \
libfontconfig1 \ libfontconfig1 \
nano \ nano \
openjdk-21-jre-headless \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash django RUN useradd -ms /bin/bash django
@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-09-08 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bayer', '0003_pescompound_pesstructure_package_data_pool'),
]
operations = [
migrations.AddField(
model_name='package',
name='shareable',
field=models.BooleanField(default=True, verbose_name='Shareable'),
),
]
+1
View File
@@ -23,6 +23,7 @@ class Package(EnviPathModel):
license = models.ForeignKey( license = models.ForeignKey(
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License" "epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
) )
shareable = models.BooleanField(verbose_name="Shareable", default=True)
class Classification(models.IntegerChoices): class Classification(models.IntegerChoices):
INTERNAL = 0, "Internal" INTERNAL = 0, "Internal"
+62 -16
View File
@@ -8,7 +8,7 @@ from django.shortcuts import redirect
from bayer.models import PESCompound from bayer.models import PESCompound
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Pathway, Node from epdb.models import Pathway, Node, Group
from epdb.views import _anonymous_or_real, error from epdb.views import _anonymous_or_real, error
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
@@ -18,6 +18,23 @@ Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def has_secret_group(user):
"""
Determines if the specified user belongs to any secret group.
This function checks whether the given user is a member of any group
that is marked as secret.
Args:
user: The user for whom the check is performed.
Returns:
bool: True if the user belongs to at least one secret group,
False otherwise.
"""
return Group.objects.filter(secret=True, user_member=user).exists()
@package_permission_required() @package_permission_required()
def create_pes(request, package_uuid): def create_pes(request, package_uuid):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
@@ -38,7 +55,7 @@ def create_pes(request, package_uuid):
if pes_link: if pes_link:
try: try:
pes_data = fetch_pes(request, pes_link) pes_data = fetch_pes(request, pes_link, current_user)
except ValueError as e: except ValueError as e:
return error( return error(
request, request,
@@ -98,12 +115,12 @@ def create_pes_node(request, package_uuid, pathway_uuid):
if pes_link: if pes_link:
try: try:
pes_data = fetch_pes(request, pes_link) pes_data = fetch_pes(request, pes_link, current_user)
except ValueError as e: except ValueError as e:
return error( return error(
request, request,
"Could not fetch PES", "Failed to fetch this PES",
f"Could not fetch PES data for {pes_link}" "Either the PES-ID is incorrect, or you're missing sufficient permissions to access this (potentially secret) item. In case you're missing permissions you can request the entitlement cs.u.enviPath_secret_data_user_group on go/idnow to gain access."
) )
classification = pes_data.get("classificationLevel", "") classification = pes_data.get("classificationLevel", "")
@@ -157,16 +174,33 @@ def create_pes_node(request, package_uuid, pathway_uuid):
return HttpResponseNotAllowed(["POST"]) return HttpResponseNotAllowed(["POST"])
def fetch_pes(request, pes_url) -> dict: def get_application_token(prod: bool) -> str:
from epauth.views import get_access_token_from_request scope = f"{s.PROD_PES_SCOPE if prod else s.NON_PROD_PES_SCOPE}/.default"
token = get_access_token_from_request(request)
if token is None: url = f"https://login.microsoftonline.com/{s.MS_ENTRA_TENANT_ID}/oauth2/v2.0/token"
token = pes_url.split('/')[-1] == 'dummy' data = {
"grant_type": "client_credentials",
"client_id": s.MS_ENTRA_CLIENT_ID,
"client_secret": s.MS_ENTRA_CLIENT_SECRET,
"scope": scope,
}
try:
response = requests.post(url, data=data)
response.raise_for_status()
return response.json()["access_token"]
except requests.exceptions.HTTPError as e:
logger.error(f"Could not fetch application token: {e}")
raise ValueError(f"Could not fetch application token!")
def fetch_pes(request, pes_url, user) -> dict:
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):
prod = "cropkey-np" not in pes_url
pes_id = pes_url.split('/')[-1] pes_id = pes_url.split('/')[-1]
if pes_id == 'dummy': if pes_id == 'dummy':
@@ -175,7 +209,16 @@ def fetch_pes(request, pes_url) -> dict:
res_data["pes_url"] = pes_url res_data["pes_url"] = pes_url
return res_data return res_data
else: else:
headers = {"Authorization": f"Bearer {token['access_token']}"} headers = {
"accept": "*/*",
"authorization": "Bearer " + get_application_token(prod),
}
# Restrict request if user is not part of any secret group
if not has_secret_group(user):
headers["app-classification-level-restriction"] = "restrict-pes-secret-structure-access"
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=s.PROXIES or None)
@@ -184,6 +227,11 @@ def fetch_pes(request, pes_url) -> dict:
res.raise_for_status() res.raise_for_status()
pes_data = res.json() pes_data = res.json()
# Handle missing response
if "detail" in pes_data and "The following PES Reg Entities Corporate Ids could not be found" in pes_data["detail"]:
raise ValueError(f"PES with id {pes_id} not found")
# Ensure we have a entity
if len(pes_data) == 0: if len(pes_data) == 0:
raise ValueError(f"PES with id {pes_id} not found") raise ValueError(f"PES with id {pes_id} not found")
@@ -193,17 +241,15 @@ def fetch_pes(request, pes_url) -> dict:
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise ValueError(f"Error fetching PES with id {pes_id}: {e}") raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
else:
raise ValueError(f"Unknown URL {pes_url}") raise ValueError(f"Unknown URL {pes_url}")
else:
raise ValueError("Could not fetch access token from request.")
def visualize_pes(request): def visualize_pes(request):
pes_link = request.GET.get('pesLink') pes_link = request.GET.get('pesLink')
if pes_link: if pes_link:
pes_data = fetch_pes(request, pes_link) pes_data = fetch_pes(request, pes_link, request.user)
representations = pes_data.get('representations') representations = pes_data.get('representations')
+2
View File
@@ -445,6 +445,8 @@ if MS_ENTRA_ENABLED:
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}" MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"] MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",") MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")
NON_PROD_PES_SCOPE = os.environ.get("NON_PROD_PES_SCOPE")
PROD_PES_SCOPE = os.environ.get("PROD_PES_SCOPE")
# Site ID 10 -> beta.envipath.org # Site ID 10 -> beta.envipath.org
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10") MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
+17 -2
View File
@@ -9,7 +9,7 @@ from django.shortcuts import redirect
from epdb.logic import UserManager, GroupManager from epdb.logic import UserManager, GroupManager
from epdb.models import Group from epdb.models import Group
from epdb.views import get_remote_address from epdb.views import get_remote_address, error
auth_log = logging.getLogger("auth") auth_log = logging.getLogger("auth")
@@ -72,6 +72,15 @@ def entra_callback(request):
claims = result["id_token_claims"] claims = result["id_token_claims"]
if claims.get("roles") is None or claims.get("roles") == [] or "envipath_registered_user" not in claims.get("roles"):
auth_log.error(f"Login attempt by {get_remote_address(request)} failed due to missing role")
return error(
request,
"Login Failed",
"Request access to role 1230/MON/APPS/Envipath/Envipath_Prod on go/idnow",
403,
)
user_name = claims.get("name") user_name = claims.get("name")
# preferred_username is a fallback for 2nd CWID # preferred_username is a fallback for 2nd CWID
user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username"))) user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username")))
@@ -130,10 +139,16 @@ def entra_callback(request):
auth_log.info(f"Login Group Sync: Adding {u.username} to Group {g.name} ({ uuid })") auth_log.info(f"Login Group Sync: Adding {u.username} to Group {g.name} ({ uuid })")
else: else:
g = Group.objects.get(uuid=uuid) g = Group.objects.get(uuid=uuid)
if g.user_member.contains(u): # Do not remove users from All enviPath Users
if g.user_member.contains(u) and g.name != 'All enviPath Users':
g.user_member.remove(u) g.user_member.remove(u)
auth_log.info(f"Login Group Sync: Removing {u.username} from Group {g.name} ({ uuid })") auth_log.info(f"Login Group Sync: Removing {u.username} from Group {g.name} ({ uuid })")
# Ensure people are part of All enviPath Users -> they have to as, envipath_registered_user is granted
all_envipath_users = Group.objects.get(name="All enviPath Users")
if not all_envipath_users.user_member.contains(u):
all_envipath_users.user_member.add(u)
if registered: if registered:
# #72 make package secret if user is part of a secret group # #72 make package secret if user is part of a secret group
for id, name in s.ENTRA_SECRET_GROUPS.items(): for id, name in s.ENTRA_SECRET_GROUPS.items():
+6
View File
@@ -11,6 +11,7 @@ from .models import (
CompoundStructure, CompoundStructure,
Edge, Edge,
EnviFormer, EnviFormer,
EnzymeLink,
ExternalDatabase, ExternalDatabase,
ExternalIdentifier, ExternalIdentifier,
Group, Group,
@@ -212,6 +213,10 @@ class CompoundStructureAdmin(EPAdmin):
pass pass
class EnzymeLinkAdmin(EPAdmin):
pass
class SimpleAmbitRuleAdmin(EPAdmin): class SimpleAmbitRuleAdmin(EPAdmin):
pass pass
@@ -266,6 +271,7 @@ admin.site.register(License, LicenseAdmin)
admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin) admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin)
admin.site.register(Compound, CompoundAdmin) admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin) admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(EnzymeLink, EnzymeLinkAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin) admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
admin.site.register(ParallelRule, ParallelRuleAdmin) admin.site.register(ParallelRule, ParallelRuleAdmin)
admin.site.register(Reaction, ReactionAdmin) admin.site.register(Reaction, ReactionAdmin)
+2 -2
View File
@@ -889,7 +889,7 @@ def create_package_compound(
from bayer.models import PESCompound from bayer.models import PESCompound
try: try:
pes_data = fetch_pes(request, c.pesLink) pes_data = fetch_pes(request, c.pesLink, request.user)
except ValueError as e: except ValueError as e:
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"} return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
@@ -2014,7 +2014,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
from bayer.models import PESCompound from bayer.models import PESCompound
try: try:
pes_data = fetch_pes(request, n.pesLink) pes_data = fetch_pes(request, n.pesLink, request.user)
except ValueError as e: except ValueError as e:
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"} return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
+19 -5
View File
@@ -211,7 +211,7 @@ class UserManager(object):
# Create package # Create package
package_name = f"{u.username}{'' if u.username[-1] in 'sxzß' else 's'} Package" package_name = f"{u.username}{'' if u.username[-1] in 'sxzß' else 's'} Package"
package_description = "This package was generated during registration." package_description = "This package was generated during registration."
p = PackageManager.create_package(u, package_name, package_description) p = PackageManager.create_package(u, package_name, package_description, shareable=False)
u.default_package = p u.default_package = p
u.save() u.save()
@@ -547,7 +547,7 @@ class PackageManager(object):
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create_package(current_user, name: str, description: str = None): def create_package(current_user, name: str, description: str = None, *args, **kwargs):
p = Package() p = Package()
# Clean for potential XSS # Clean for potential XSS
@@ -556,6 +556,9 @@ class PackageManager(object):
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip() p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
if "shareable" in kwargs:
p.shareable = kwargs["shareable"]
p.save() p.save()
up = UserPackagePermission() up = UserPackagePermission()
@@ -623,6 +626,10 @@ class PackageManager(object):
def grant_write(caller: User, package: Package, grantee: Union[User, Group]): def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0]) PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0])
@staticmethod
def grant_owner(caller: User, package: Package, grantee: Union[User, Group]):
PackageManager.update_permissions(caller, package, grantee, Permission.ALL[0])
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def import_legacy_package( def import_legacy_package(
@@ -665,11 +672,11 @@ class PackageManager(object):
# EDIT START # EDIT START
if data.get("classification"): if data.get("classification"):
if data["classification"] == "INTERNAL": if data["classification"] == "INTERNAL":
pack.classification = Package.Classification.RESTRICTED pack.classification_level = Package.Classification.RESTRICTED
elif data["classification"] == "RESTRICTED": elif data["classification"] == "RESTRICTED":
pack.classification = Package.Classification.RESTRICTED pack.classification_level = Package.Classification.RESTRICTED
elif data["classification"] == "SECRET": elif data["classification"] == "SECRET":
pack.classification = Package.Classification.SECRET pack.classification_level = Package.Classification.SECRET
if not "datapool" in data: if not "datapool" in data:
raise ValueError("Missing datapool in package") raise ValueError("Missing datapool in package")
@@ -821,7 +828,14 @@ class PackageManager(object):
r.name = rule["name"] r.name = rule["name"]
r.description = rule["description"] r.description = rule["description"]
r.aliases = rule.get("aliases", []) r.aliases = rule.get("aliases", [])
if rule.get("smirks") is not None:
r.smirks = rule["smirks"] r.smirks = rule["smirks"]
elif rule.get("reactionSmarts") is not None:
r.smirks = rule["reactionSmarts"]
else:
raise ValueError(f"No SMIRKS or reactionSmarts found for rule {rule['id']}")
r.reactant_filter_smarts = rule.get("reactantFilterSmarts", None) r.reactant_filter_smarts = rule.get("reactantFilterSmarts", None)
r.product_filter_smarts = rule.get("productFilterSmarts", None) r.product_filter_smarts = rule.get("productFilterSmarts", None)
r.save() r.save()
+3 -1
View File
@@ -7,6 +7,7 @@
<i class="glyphicon glyphicon-edit"></i> Edit Package</a <i class="glyphicon glyphicon-edit"></i> Edit Package</a
> >
</li> </li>
{% if meta.current_package.shareable %}
<li> <li>
<a <a
role="button" role="button"
@@ -15,6 +16,7 @@
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a <i class="glyphicon glyphicon-user"></i> Edit Permissions</a
> >
</li> </li>
{% endif %}
{% if meta.current_package.get_classification_level_display != "Secret" %} {% if meta.current_package.get_classification_level_display != "Secret" %}
<li> <li>
<a <a
@@ -52,7 +54,7 @@
> >
</li> </li>
{% endif %} {% endif %}
{% if not meta.can_edit %} {% if not meta.can_edit and meta.current_package.shareable %}
<li> <li>
<a <a
role="button" role="button"
+2
View File
@@ -7,6 +7,7 @@
<i class="glyphicon glyphicon-edit"></i> Update</a <i class="glyphicon glyphicon-edit"></i> Update</a
> >
</li> </li>
{% if 1 == 0 %}
<li> <li>
<a <a
role="button" role="button"
@@ -15,6 +16,7 @@
<i class="glyphicon glyphicon-lock"></i> Update Password</a <i class="glyphicon glyphicon-lock"></i> Update Password</a
> >
</li> </li>
{% endif %}
<li> <li>
<a <a
role="button" role="button"
+37 -7
View File
@@ -180,12 +180,26 @@
>Aspartame</a >Aspartame</a
> >
</div> </div>
<a
class="absolute top-0 left-[calc(100%-5.4rem)]"
href="/predict"
>Advanced</a
>
</div> </div>
{% if meta.current_package %}
<div
class="mt-4 rounded-lg border border-base-300 bg-base-50 px-4 py-2.5 text-sm text-base-content/70"
>
Prediction will be stored in
<strong class="text-base-content"
>{{ meta.current_package.name|safe }}</strong
>
{% if meta.user.default_setting %}
using setting
<strong class="text-base-content"
>{{ meta.user.default_setting.name|safe }}</strong
>
{% endif %}
<br />
To use a different setting click
<a class="label link" href="/predict">here</a>
</div>
{% endif %}
</div> </div>
<div <div
id="ketcher-container" id="ketcher-container"
@@ -208,9 +222,25 @@
> >
Predict! Predict!
</button> </button>
<div class="mt-1 flex w-full justify-end"> {% if meta.current_package %}
<a class="label justify-end" href="/predict">Advanced</a> <div
class="mt-4 rounded-lg border border-base-300 bg-base-50 px-4 py-2.5 text-sm text-base-content/70"
>
Prediction will be stored in
<strong class="text-base-content"
>{{ meta.current_package.name|safe }}</strong
>
{% if meta.user.default_setting %}
using setting
<strong class="text-base-content"
>{{ meta.user.default_setting.name|safe }}</strong
>
{% endif %}
<br />
To use a different setting click
<a class="label link" href="/predict">here</a>
</div> </div>
{% endif %}
</div> </div>
<input <input
type="hidden" type="hidden"
+32 -6
View File
@@ -21,6 +21,8 @@ from epdb.models import (
Compound, Compound,
CompoundStructure, CompoundStructure,
Edge, Edge,
EnzymeLink,
Group,
License, License,
Node, Node,
ParallelRule, ParallelRule,
@@ -150,6 +152,8 @@ class ReactionExportSchema(RefReactionExportSchema):
# Rules # # Rules #
######### #########
class EnzymeExportSchema(RefEnzymeExportSchema): class EnzymeExportSchema(RefEnzymeExportSchema):
name: str
description: str
ec_number: str ec_number: str
classification_level: int classification_level: int
linking_method: str linking_method: str
@@ -162,11 +166,11 @@ class EnzymeRuleExportSchema(RefRuleExportSchema):
@staticmethod @staticmethod
def resolve_enzymes(obj): def resolve_enzymes(obj):
if isinstance(obj, dict): if isinstance(obj, EnzymeRuleExportSchema):
res = [] return obj.enzymes
for e in obj.get("enzymes", []): elif isinstance(obj, dict):
res.append(EnzymeExportSchema.model_validate(e)) return obj.get("enzymes", [])
return res else:
return obj.enzymelink_set.all() return obj.enzymelink_set.all()
@@ -271,7 +275,7 @@ class PackageExportSchema(Schema):
@staticmethod @staticmethod
def resolve_classification_level(obj): def resolve_classification_level(obj):
if isinstance(obj, dict): if isinstance(obj, dict):
return obj["classification_level"] return obj.get("classification_level", "Internal")
return obj.Classification(obj.classification_level).name return obj.Classification(obj.classification_level).name
@@ -679,6 +683,27 @@ class PackageImporter:
for scen in elem.scenarios: for scen in elem.scenarios:
elem_obj.scenarios.add(self._cache[scen.uuid]) elem_obj.scenarios.add(self._cache[scen.uuid])
def _import_enzyme(self, rule: Rule, enzyme: EnzymeExportSchema):
e = EnzymeLink()
e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else enzyme.uuid
e.rule = rule
e.name = enzyme.name
e.description = enzyme.description
e.ec_number = enzyme.ec_number
e.classification_level = enzyme.classification_level
e.linking_method = enzyme.linking_method
e.save()
for reaction in enzyme.reaction_evidence:
e.reaction_evidence.add(self._cache[reaction.uuid])
for edge in enzyme.edge_evidence:
e.edge_evidence.add(self._cache[edge.uuid])
self._cache[enzyme.uuid] = e
def _import_and_link_enzymes(self, data: PackageExportSchema):
for rule in data.composite_rules:
for enzyme in rule.enzymes:
self._import_enzyme(self._cache[rule.uuid], enzyme)
def _import_package_from_json( def _import_package_from_json(
self, self,
) -> Package: ) -> Package:
@@ -717,6 +742,7 @@ class PackageImporter:
self._import_scenarios(package, parsed.scenarios) self._import_scenarios(package, parsed.scenarios)
self._import_additional_information(package, parsed.additional_information) self._import_additional_information(package, parsed.additional_information)
self._link_scenarios_after_import(parsed) self._link_scenarios_after_import(parsed)
self._import_and_link_enzymes(parsed)
return package return package
Generated
+1 -1
View File
@@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
[[package]] [[package]]
name = "envipy-additional-information" name = "envipy-additional-information"
version = "0.4.2" version = "0.4.2"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#ad825570480bbe2f1a35c04923fede756c450751" } source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#4909e35aac5a0a3969ac2a01f8d4ad8bdadae04c" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
] ]