From 386098b8a62080a5fef7cf1e0de9f86e2513e41a Mon Sep 17 00:00:00 2001 From: jebus Date: Wed, 15 Oct 2025 19:35:26 +1300 Subject: [PATCH] [Feature] EnzymeLink Annotations (#152) Co-authored-by: Tim Lorsbach Reviewed-on: https://git.envipath.com/enviPath/enviPy/pulls/152 --- epdb/logic.py | 49 +++++++++- epdb/management/commands/localize_urls.py | 1 + epdb/models.py | 56 ++++++++++++ epdb/urls.py | 34 +++++-- epdb/views.py | 27 ++++++ pyproject.toml | 5 +- templates/objects/composite_rule.html | 48 +++++++--- templates/objects/enzymelink.html | 105 ++++++++++++++++++++++ templates/objects/reaction.html | 17 ++++ templates/objects/simple_rule.html | 37 ++++++++ 10 files changed, 352 insertions(+), 27 deletions(-) create mode 100644 templates/objects/enzymelink.html diff --git a/epdb/logic.py b/epdb/logic.py index 530ebc51..19f03ae2 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -26,6 +26,7 @@ from epdb.models import ( Compound, Reaction, CompoundStructure, + EnzymeLink, ) from utilities.chem import FormatConverter from utilities.misc import PackageImporter, PackageExporter @@ -617,6 +618,8 @@ class PackageManager(object): parent_mapping = {} # Mapping old scen_id to old_obj_id scen_mapping = defaultdict(list) + # Enzymelink Mapping rule_id to enzymelink objects + enzyme_mapping = defaultdict(list) # Store Scenarios for scenario in data["scenarios"]: @@ -648,9 +651,7 @@ class PackageManager(object): # Broken eP Data if name == "initialmasssediment" and addinf_data == "missing data": continue - - # TODO Enzymes arent ready yet - if name == "enzyme": + if name == "columnheight" and addinf_data == "(2)-(2.5);(6)-(8)": continue try: @@ -740,6 +741,9 @@ class PackageManager(object): for scen in rule["scenarios"]: scen_mapping[scen["id"]].append(r) + for enzyme_link in rule.get("enzymeLinks", []): + enzyme_mapping[r.uuid].append(enzyme_link) + print("Par: ", len(par_rules)) print("Seq: ", len(seq_rules)) @@ -757,6 +761,9 @@ class PackageManager(object): for scen in par_rule["scenarios"]: scen_mapping[scen["id"]].append(r) + for enzyme_link in par_rule.get("enzymeLinks", []): + enzyme_mapping[r.uuid].append(enzyme_link) + for simple_rule in par_rule["simpleRules"]: if simple_rule["id"] in mapping: r.simple_rules.add(SimpleRule.objects.get(uuid=mapping[simple_rule["id"]])) @@ -777,6 +784,9 @@ class PackageManager(object): for scen in seq_rule["scenarios"]: scen_mapping[scen["id"]].append(r) + for enzyme_link in seq_rule.get("enzymeLinks", []): + enzyme_mapping[r.uuid].append(enzyme_link) + for i, simple_rule in enumerate(seq_rule["simpleRules"]): sro = SequentialRuleOrdering() sro.simple_rule = simple_rule @@ -910,6 +920,39 @@ class PackageManager(object): print("Scenarios linked...") + # Import Enzyme Links + for rule_uuid, enzyme_links in enzyme_mapping.items(): + r = Rule.objects.get(uuid=rule_uuid) + for enzyme in enzyme_links: + e = EnzymeLink() + e.uuid = UUID(enzyme["id"].split("/")[-1]) if keep_ids else uuid4() + e.rule = r + e.name = enzyme["name"] + e.ec_number = enzyme["ecNumber"] + e.classification_level = enzyme["classificationLevel"] + e.linking_method = enzyme["linkingMethod"] + e.save() + + for reaction in enzyme["reactionLinkEvidence"]: + reaction = Reaction.objects.get(uuid=mapping[reaction["id"]]) + e.reaction_evidence.add(reaction) + + for edge in enzyme["edgeLinkEvidence"]: + edge = Edge.objects.get(uuid=mapping[edge["id"]]) + e.reaction_evidence.add(edge) + + for evidence in enzyme["linkEvidence"]: + matches = re.findall(r">(R[0-9]+)<", evidence["evidence"]) + if not matches or len(matches) != 1: + logger.warning(f"Could not find reaction id in {evidence['evidence']}") + continue + + e.add_kegg_reaction_id(matches[0]) + + e.save() + + print("Enzyme links imported...") + print("Import statistics:") print("Package {} stored".format(pack.url)) print("Imported {} compounds".format(Compound.objects.filter(package=pack).count())) diff --git a/epdb/management/commands/localize_urls.py b/epdb/management/commands/localize_urls.py index b9f95b11..91afb0a6 100644 --- a/epdb/management/commands/localize_urls.py +++ b/epdb/management/commands/localize_urls.py @@ -41,6 +41,7 @@ class Command(BaseCommand): "RuleBasedRelativeReasoning", "EnviFormer", "ApplicabilityDomain", + "EnzymeLink", ] for model in MODELS: obj_cls = apps.get_model("epdb", model) diff --git a/epdb/models.py b/epdb/models.py index 83b29925..4b8f4198 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -494,6 +494,20 @@ class ChemicalIdentifierMixin(ExternalIdentifierMixin): return self.get_external_identifier("CAS") +class KEGGIdentifierMixin(ExternalIdentifierMixin): + @property + def kegg_reaction_links(self): + return self.get_external_identifier("KEGG Reaction") + + def add_kegg_reaction_id(self, kegg_id): + return self.add_external_identifier( + "KEGG Reaction", kegg_id, f"https://www.genome.jp/entry/{kegg_id}" + ) + + class Meta: + abstract = True + + class ReactionIdentifierMixin(ExternalIdentifierMixin): class Meta: abstract = True @@ -1014,6 +1028,26 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti return self.compound.default_structure == self +class EnzymeLink(EnviPathModel, KEGGIdentifierMixin): + rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True) + ec_number = models.TextField(blank=False, null=False, verbose_name="EC Number") + classification_level = models.IntegerField( + blank=False, null=False, verbose_name="Classification Level" + ) + linking_method = models.TextField(blank=False, null=False, verbose_name="Linking Method") + + reaction_evidence = models.ManyToManyField("epdb.Reaction") + edge_evidence = models.ManyToManyField("epdb.Edge") + + external_identifiers = GenericRelation("ExternalIdentifier") + + def _url(self): + return "{}/enzymelink/{}".format(self.rule.url, self.uuid) + + def get_group(self) -> str: + return ".".join(self.ec_number.split(".")[:3]) + ".-" + + class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): package = models.ForeignKey( "epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True @@ -1095,6 +1129,18 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): return new_rule + def enzymelinks(self): + return self.enzymelink_set.all() + + def get_grouped_enzymelinks(self): + res = defaultdict(list) + + for el in self.enzymelinks(): + key = ".".join(el.ec_number.split(".")[:3]) + ".-" + res[key].append(el) + + return dict(res) + class SimpleRule(Rule): pass @@ -1437,6 +1483,16 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin id__in=Edge.objects.filter(edge_label=self).values("pathway_id") ).order_by("name") + def get_related_enzymes(self): + res = [] + edges = Edge.objects.filter(edge_label=self) + for e in edges: + for scen in e.scenarios.all(): + for ai in scen.additional_information.keys(): + if ai == "Enzyme": + res.extend(scen.additional_information[ai]) + return res + class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): package = models.ForeignKey( diff --git a/epdb/urls.py b/epdb/urls.py index 16f0f2ba..391a2f32 100644 --- a/epdb/urls.py +++ b/epdb/urls.py @@ -1,5 +1,5 @@ -from django.urls import path, re_path from django.contrib.auth import views as auth_views +from django.urls import path, re_path from . import views as v @@ -88,20 +88,36 @@ urlpatterns = [ v.package_rule, name="package rule detail", ), - re_path( - rf"^package/(?P{UUID})/simple-rdkit-rule/(?P{UUID})$", - v.package_rule, - name="package rule detail", - ), + # re_path( + # rf"^package/(?P{UUID})/simple-rdkit-rule/(?P{UUID})$", + # v.package_rule, + # name="package rule detail", + # ), re_path( rf"^package/(?P{UUID})/parallel-rule/(?P{UUID})$", v.package_rule, name="package rule detail", ), + # re_path( + # rf"^package/(?P{UUID})/sequential-rule/(?P{UUID})$", + # v.package_rule, + # name="package rule detail", + # ), + # EnzymeLinks re_path( - rf"^package/(?P{UUID})/sequential-rule/(?P{UUID})$", - v.package_rule, - name="package rule detail", + rf"^package/(?P{UUID})/rule/(?P{UUID})/enzymelink/(?P{UUID})$", + v.package_rule_enzymelink, + name="package rule enzymelink detail", + ), + re_path( + rf"^package/(?P{UUID})/simple-ambit-rule/(?P{UUID})/enzymelink/(?P{UUID})$", + v.package_rule_enzymelink, + name="package rule enzymelink detail", + ), + re_path( + rf"^package/(?P{UUID})/parallel-rule/(?P{UUID})/enzymelink/(?P{UUID})$", + v.package_rule_enzymelink, + name="package rule enzymelink detail", ), # Reaction re_path( diff --git a/epdb/views.py b/epdb/views.py index b0ff39dc..dd13d21d 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -46,6 +46,7 @@ from .models import ( Edge, ExternalDatabase, ExternalIdentifier, + EnzymeLink, ) logger = logging.getLogger(__name__) @@ -1528,6 +1529,32 @@ def package_rule(request, package_uuid, rule_uuid): return HttpResponseNotAllowed(["GET", "POST"]) +@package_permission_required() +def package_rule_enzymelink(request, package_uuid, rule_uuid, enzymelink_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + current_rule = Rule.objects.get(package=current_package, uuid=rule_uuid) + current_enzymelink = EnzymeLink.objects.get(rule=current_rule, uuid=enzymelink_uuid) + + if request.method == "GET": + context = get_base_context(request) + + context["title"] = f"enviPath - {current_package.name} - {current_rule.name}" + + context["meta"]["current_package"] = current_package + context["object_type"] = "enzyme" + context["breadcrumbs"] = breadcrumbs( + current_package, "rule", current_rule, "enzymelink", current_enzymelink + ) + + context["enzymelink"] = current_enzymelink + context["current_object"] = current_enzymelink + + return render(request, "objects/enzymelink.html", context) + + return HttpResponseNotAllowed(["GET"]) + + @package_permission_required() def package_reactions(request, package_uuid): current_user = _anonymous_or_real(request) diff --git a/pyproject.toml b/pyproject.toml index b6e191bb..1fba9371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "django-ninja>=1.4.1", "django-oauth-toolkit>=3.0.1", "django-polymorphic>=4.1.0", - "django-stubs>=5.2.4", "enviformer", "envipy-additional-information", "envipy-ambit>=0.1.0", @@ -33,12 +32,14 @@ dependencies = [ [tool.uv.sources] enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } -envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"} +envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"} envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } [project.optional-dependencies] ms-login = ["msal>=1.33.0"] dev = [ + "celery-stubs==0.1.3", + "django-stubs>=5.2.4", "poethepoet>=0.37.0", "pre-commit>=4.3.0", "ruff>=0.13.3", diff --git a/templates/objects/composite_rule.html b/templates/objects/composite_rule.html index 4e5aaafe..e27e8a00 100644 --- a/templates/objects/composite_rule.html +++ b/templates/objects/composite_rule.html @@ -29,7 +29,7 @@

- {{ rule.description }} + {{ rule.description|safe }}

@@ -87,19 +87,41 @@ {% endif %} - -
-

- EC Numbers -

-
-
-
- + {% if rule.enzymelinks %} + +
+

+ EC Numbers +

-
- +
+
+ {% for k, v in rule.get_grouped_enzymelinks.items %} + + + {% endfor %} +
+
+ {% endif %}
{% endblock content %} diff --git a/templates/objects/enzymelink.html b/templates/objects/enzymelink.html new file mode 100644 index 00000000..464af8ae --- /dev/null +++ b/templates/objects/enzymelink.html @@ -0,0 +1,105 @@ +{% extends "framework.html" %} + +{% block content %} + +
+
+
+ {{ enzymelink.ec_number }} +
+ + +
+

+ Enzyme Name +

+
+
+
+ {{ enzymelink.name }} +
+
+ + +
+

+ Linking Method +

+
+
+
+ {{ enzymelink.linking_method }}.  Learn more >> +
+
+ + {% if enzymelink.kegg_reaction_links %} + +
+
+ {% for kl in enzymelink.kegg_reaction_links %} + {{ kl.identifier_value }} + {% endfor %} +
+
+ {% endif %} + + {% if enzymelink.reaction_evidence.all %} + +
+
+ {% for r in enzymelink.reaction_evidence.all %} + {{ r.name }} ({{ r.package.name }}) + {% endfor %} +
+
+ {% endif %} + + {% if enzymelink.edge_evidence.all %} + +
+
+ {% for e in enzymelink.edge_evidence.all %} + {{ e.pathway.name }} + ({{ r.package.name }}) + {% endfor %} +
+
+ {% endif %} + + + + + +
+
+{% endblock content %} diff --git a/templates/objects/reaction.html b/templates/objects/reaction.html index 2a026d22..1335a6d7 100644 --- a/templates/objects/reaction.html +++ b/templates/objects/reaction.html @@ -124,6 +124,23 @@ {% endif %} + {% if reaction.get_related_enzymes %} + +
+

+ EC Numbers +

+
+
+
+ {% for e in reaction.get_related_enzymes %} + {{ e.name }} + {% endfor %} +
+
+ {% endif %} + {% if reaction.related_pathways %}
diff --git a/templates/objects/simple_rule.html b/templates/objects/simple_rule.html index cc55d856..a84751a8 100644 --- a/templates/objects/simple_rule.html +++ b/templates/objects/simple_rule.html @@ -201,6 +201,43 @@
{% endif %} + + {% if rule.enzymelinks %} + +
+

+ EC Numbers +

+
+
+
+ {% for k, v in rule.get_grouped_enzymelinks.items %} + + + {% endfor %} +
+
+ {% endif %} {% endblock content %}