17 Commits

Author SHA1 Message Date
57f5ce089c Make links thicker, reduce timeout trigger time
Some checks failed
CI / test (pull_request) Failing after 20s
API CI / api-tests (pull_request) Failing after 38s
2026-07-09 20:39:48 +02:00
28e5dbbcf5 Unconnected Nodes 2026-07-09 20:30:27 +02:00
8620f98082 Implemented Compound Names / Reaction Names View Option 2026-07-09 20:18:28 +02:00
4ee37b5c71 provide setting link and copy button 2026-07-09 13:55:10 +02:00
c65be30b6b Fix alter name/desc for node, make /node /edge funcitonal 2026-07-09 13:33:53 +02:00
b5e3c1957b underline pes link in templates 2026-07-08 20:24:53 +02:00
3bb9220b22 Fix Classification import, Fix PES Deserialization 2026-07-08 20:20:36 +02:00
d06ed33196 removed debug 2026-07-08 10:46:45 +02:00
ea672ec91a PES Fields in API Output 2026-07-08 10:46:27 +02:00
dfc1309fd3 Added pesLink field to PW Api output 2026-07-08 10:39:01 +02:00
6e2dbccd6c Harmonised proposed field in Json output 2026-07-08 10:32:05 +02:00
b77fbc9b90 implemented non persistent 2026-07-06 15:44:23 +02:00
ce1d709715 highlight Description links
Some checks failed
CI / test (pull_request) Failing after 18s
API CI / api-tests (pull_request) Failing after 36s
2026-07-06 15:17:38 +02:00
b0004ea66e Package Export/Import cycle 2026-07-06 15:17:16 +02:00
2fbffb4b3b Fixed molfile assignment, adjusted Export 2026-07-03 09:40:47 +02:00
365af14448 Fix PES Link HTML for other entities 2026-07-03 09:29:53 +02:00
8017070de8 Fixed error return, removed unused options 2026-07-02 22:57:30 +02:00
17 changed files with 820 additions and 593 deletions

View File

@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
def create(
compound: Compound,
pes_link: str,
mol_file: str,
molfile: str,
smiles: str,
name: str = None,
description: str = None,
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
cs.smiles = smiles
cs.mol_file = mol_file
cs.molfile = molfile
cs.pes_link = pes_link
cs.compound = compound

View File

@ -4,7 +4,9 @@
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
<p>
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
</p>
</div>
</div>

View File

@ -4,7 +4,9 @@
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
<p>
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
</p>
</div>
</div>

View File

@ -3,7 +3,11 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
<div class="collapse-content">
<p>
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
</p>
</div>
</div>
<!-- Image Representation -->

View File

@ -8,7 +8,7 @@ from django.shortcuts import redirect
from bayer.models import PESCompound
from epdb.logic import PackageManager
from epdb.models import Pathway, Node
from epdb.views import _anonymous_or_real
from epdb.views import _anonymous_or_real, error
from utilities.decorators import package_permission_required
Package = s.GET_PACKAGE_MODEL()
@ -22,7 +22,11 @@ def create_pes(request, package_uuid):
if request.method == "POST":
if current_package.classification_level == Package.Classification.INTERNAL:
raise HttpResponseBadRequest("Cannot create PESs for internal packages.")
return error(
request,
f'Creation of PESs for package {current_package.name} failed!',
"Creating PESs for internal packages is not allowed.",
)
compound_name = request.POST.get('compound-name')
compound_description = request.POST.get('compound-description')
@ -64,7 +68,11 @@ def create_pes_node(request, package_uuid, pathway_uuid):
if request.method == "POST":
if current_package.classification_level == Package.Classification.INTERNAL:
raise HttpResponseBadRequest("Cannot create PESs for internal packages.")
return error(
request,
f'Creation of PESs for package {current_package.name} failed!',
"Creating PESs for internal packages is not allowed.",
)
compound_name = request.POST.get('compound-name')
compound_description = request.POST.get('compound-description')

View File

@ -1,2 +1,5 @@
class InvalidSMILESException(Exception):
pass
class PackageImportException(Exception):
pass

View File

@ -633,9 +633,14 @@ class CompoundSchema(Schema):
reviewStatus: str = Field(False, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
structures: List["CompoundStructureSchema"] = []
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_review_status(obj: CompoundStructure):
def resolve_pes_link(obj: Compound):
return getattr(obj.default_structure, "pes_link", None)
@staticmethod
def resolve_review_status(obj: Compound):
return "reviewed" if obj.package.reviewed else "unreviewed"
@staticmethod
@ -709,6 +714,7 @@ class CompoundStructureSchema(Schema):
reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
smiles: str = Field(None, alias="smiles")
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_review_status(obj: CompoundStructure):
@ -1695,6 +1701,7 @@ class PathwayNode(Schema):
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
smiles: str = Field(None, alias="smiles")
pseudo: bool = Field(False, alias="pseudo")
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_atom_count(obj: Node):
@ -1707,24 +1714,10 @@ class PathwayNode(Schema):
# TODO
return []
@staticmethod
def resolve_engineered_intermediate(obj: Node):
# TODO
return False
@staticmethod
def resolve_image(obj: Node):
return f"{obj.default_node_label.url}?image=svg"
@staticmethod
def resolve_image_size(obj: Node):
return 400
@staticmethod
def resolve_proposed_intermediate(obj: Node):
# TODO
return []
class PathwaySchema(Schema):
aliases: List[str] = Field([], alias="aliases")
@ -2392,3 +2385,31 @@ def get_setting(request, setting_uuid):
return 403, {
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
}
########
# Util #
########
class NonPersistent(Schema):
smiles: str
setting_url: str = Field(..., alias="settingUri")
@router.post("/util", response={200: Any, 403: Error})
def predict(request, np: Form[NonPersistent]):
try:
print(request.user)
print(np.setting_url)
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
from epdb.logic import SPathway
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
spw.predict()
return spw.to_json()
except ValueError:
return 403, {
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
}

View File

@ -1078,10 +1078,8 @@ class PackageManager(object):
data: Dict[str, Any],
owner: User,
preserve_uuids=False,
add_import_timestamp=True,
trust_reviewed=False,
) -> Package:
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
importer = PackageImporter(data, preserve_uuids)
imported_package = importer.do_import()
up = UserPackagePermission()
@ -1898,6 +1896,12 @@ class SPathway(object):
"to": to_indices,
}
if edge.rule:
e["rule"] = edge.rule.simple_json()
if edge.probability:
e["probability"] = edge.probability
edges.append(e)
return {

View File

@ -2379,6 +2379,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
},
"predicted_properties": predicted_properties,
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
"proposed": self.is_proposed_intermediate(),
"timeseries": self.get_timeseries_data(),
**structure_data,
}
@ -2452,6 +2453,26 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
return data
def is_proposed_intermediate(self):
collected = defaultdict(dict)
for ai in self.additional_information.filter(
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"]
, scenario__isnull=False
):
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
if ai.type == "ProposedIntermediate":
collected[str(ai.scenario.uuid)]["proposed"] = True
if ai.type == "Confidence":
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
if ai.type == "TransformationProductImportance":
collected[str(ai.scenario.uuid)]["Transformation product importance"] = ai.get().importance.value
return list(collected.values())
def simple_json(self, include_description=False):
res = super().simple_json()
name = res.get("name", None)

View File

@ -2482,7 +2482,22 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
return JsonResponse({"success": current_node.url})
return HttpResponseBadRequest()
new_node_name = request.POST.get("node-name")
new_node_description = request.POST.get("node-description")
if any([new_node_name, new_node_description]):
if new_node_name is not None and new_node_name.strip() != "":
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
current_node.name = new_node_name
if new_node_description is not None and new_node_description.strip() != "":
new_node_description = nh3.clean(new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
current_node.description = new_node_description
current_node.save()
return redirect(current_node.url)
return error(request, "Node update failed!", "No changes were made to the node")
else:
return HttpResponseNotAllowed(["GET", "POST"])

View File

@ -39,3 +39,7 @@ select.select[multiple] {
display: block;
white-space: normal;
}
p a {
@apply underline;
}

View File

@ -5,6 +5,125 @@
*/
document.addEventListener('alpine:init', () => {
const basePagination = (
items,
currentPage,
totalPages,
totalItems,
perPage,
isReviewed,
instanceId
) => ({
items,
currentPage,
totalPages,
totalItems,
perPage,
isReviewed,
instanceId,
get paginatedItems() {
return this.items;
},
get showingStart() {
if (this.totalItems === 0) return 0;
return (this.currentPage - 1) * this.perPage + 1;
},
get showingEnd() {
if (this.totalItems === 0) return 0;
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.fetchPage(this.currentPage + 1);
}
},
prevPage() {
if (this.currentPage > 1) {
this.fetchPage(this.currentPage - 1);
}
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.fetchPage(page);
}
},
get pageNumbers() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
if (total === 0) {
return pages;
}
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
} else {
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
let rangeStart;
let rangeEnd;
if (current <= 4) {
rangeStart = 2;
rangeEnd = 5;
} else if (current >= total - 3) {
rangeStart = total - 4;
rangeEnd = total - 1;
} else {
rangeStart = current - 1;
rangeEnd = current + 1;
}
if (rangeStart > 2) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
}
for (let i = rangeStart; i <= rangeEnd; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
if (rangeEnd < total - 1) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
}
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
}
return pages;
}
});
Alpine.data('paginatedList',
(items, options = {}) => ({
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
init() {
this.fetchPage(1);
},
async fetchPage(page) {
this.totalItems = this.items.length;
this.totalPages = Math.ceil(this.items.length / this.perPage);
this.currentPage = page
const start = page * this.perPage - this.perPage;
const end = page * this.perPage;
return this.items.slice(start, end);
}
}));
Alpine.data('remotePaginatedList', (options = {}) => ({
items: [],
currentPage: 1,

View File

@ -67,6 +67,9 @@ function draw(pathway, elem) {
const horizontalSpacing = 75; // horizontal space between nodes
const depthMap = new Map();
// Avoid leaving unconnected Nodes leaving the viewport
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
// Sort nodes by depth first to minimize crossings
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
@ -154,6 +157,10 @@ function draw(pathway, elem) {
});
node.attr("transform", d => `translate(${d.x},${d.y})`);
linkText
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
}
function dragstarted(event, d) {
@ -256,7 +263,7 @@ function draw(pathway, elem) {
}
// Wait before showing popup (ms)
var popupWaitBeforeShow = 1000;
var popupWaitBeforeShow = 500;
// Custom popover element
let popoverTimeout = null;
@ -647,21 +654,31 @@ function draw(pathway, elem) {
.on("tick", ticked);
// Kanten zeichnen
const link = zoomable.append("g")
.selectAll("path")
.data(links)
.enter().append("path")
// Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => {
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 linkGroup = zoomable.append("g")
.selectAll("g")
.data(links)
.enter()
.append("g")
.attr("class", "link-group");
const link = linkGroup
.append("path")
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
.attr("fill", "none")
// Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => {
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);
d3.selectAll("path").classed("highlighted", false);
if (!wasHighlighted) {
const toHighlight = [];
toHighlight.push(d.el);
@ -688,6 +705,18 @@ function draw(pathway, elem) {
}
});
const linkText = linkGroup
.append("text")
.attr("class", "link-label")
.attr("text-anchor", "middle")
.attr("dy", -6)
.style("font-size", "4px")
.style("pointer-events", "none")
.style("display", "none")
.text(d => d.name)
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
// add element to links array
link.each(function (d) {
d.el = this; // attach the DOM element to the data object

View File

@ -55,6 +55,15 @@
</div>
</div>
<!-- Description -->
{% if node.description and node.description != "no description" %}
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Description</div>
<div class="collapse-content">{{ node.description }}</div>
</div>
{% endif %}
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
{% for tpl in viz_templates %}

View File

@ -21,6 +21,7 @@
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
/* marker-end: url(#arrow); */
}
@ -177,6 +178,52 @@
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
>
<li>
<a id="compound-names-toggle-button" class="cursor-pointer">
<svg
id="compound-names-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Compound Names
</a>
</li>
<li>
<a id="reaction-names-toggle-button" class="cursor-pointer">
<svg
id="reaction-names-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Reaction Names
</a>
</li>
{% if pathway.setting.model.app_domain %}
<li>
<a id="app-domain-toggle-button" class="cursor-pointer">
@ -202,6 +249,7 @@
</a>
</li>
{% endif %}
{% if 1 == 0 %}
<li>
<a id="timeseries-toggle-button" class="cursor-pointer">
<svg
@ -252,6 +300,7 @@
Show Predicted Properties
</a>
</li>
{% endif %}
</ul>
</div>
</div>
@ -495,6 +544,10 @@
{{ pathway.d3_json|json_script:"pathway" }}
<script>
// Global switch for compound names view
var compoundNamesViewEnabled = false;
// Gloabl switch for reaction names view
var reactionNamesViewEnabled = false;
// Global switch for app domain view
var appDomainViewEnabled = false;
// Global switch for timeseries view
@ -530,6 +583,67 @@
descContent.innerHTML = newDesc;
}
const compoundNamesBtn = document.getElementById("compound-names-toggle-button");
if (compoundNamesBtn) {
compoundNamesBtn.addEventListener("click", function () {
compoundNamesViewEnabled = !compoundNamesViewEnabled;
const icon = document.getElementById("compound-names-icon");
if (compoundNamesViewEnabled) {
// Change to eye-off icon
icon.innerHTML =
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
nodes.forEach((x) => {
if (x.name) {
d3.select(x.el)
.append("text")
.text(d => d.name)
.attr("text-anchor", "middle")
.attr("y", -20)
.style("font-size", "4px");
}
});
} else {
// Change back to eye icon
icon.innerHTML =
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
nodes.forEach((x) => {
d3.select(x.el)
.select("text")
.remove()
})
}
});
}
const reactionNamesBtn = document.getElementById("reaction-names-toggle-button");
if (reactionNamesBtn) {
reactionNamesBtn.addEventListener("click", function () {
reactionNamesViewEnabled = !reactionNamesViewEnabled;
const icon = document.getElementById("reaction-names-icon");
if (reactionNamesViewEnabled) {
// Change to eye-off icon
icon.innerHTML =
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
d3.selectAll(".link-label")
.style("display", null);
} else {
// Change back to eye icon
icon.innerHTML =
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
d3.selectAll(".link-label")
.style("display", "none");
}
});
}
// App domain toggle
const appDomainBtn = document.getElementById("app-domain-toggle-button");
if (appDomainBtn) {

View File

@ -24,6 +24,76 @@
<td>Setting Name</td>
<td>{{ setting_to_render.name }}</td>
</tr>
<tr>
<td>Setting URL</td>
<td>
<a href="{{ setting_to_render.url }}" class="link link-primary">{{ setting_to_render.url }}</a>
<div
x-data="{
value: '{{ setting_to_render.url }}',
copied: false,
async copy() {
await navigator.clipboard.writeText(this.value)
this.copied = true
setTimeout(() => {
this.copied = false
}, 1500)
}
}"
class="join"
>
<button
type="button"
class="btn btn-ghost"
:data-tip="copied ? 'Copied!' : 'Copy'"
@click="copy"
aria-label="Copy to clipboard"
>
<svg
x-show="!copied"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2"
/>
<rect
width="12"
height="12"
x="8"
y="8"
rx="2"
ry="2"
/>
</svg>
<svg
x-show="copied"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</button>
</div>
</td>
</tr>
{% if setting_to_render.description %}
<tr>
<td>Setting Description</td>

View File

@ -11,8 +11,10 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING
from django.conf import settings as s
from django.db import transaction
from ninja import Schema
from pydantic import HttpUrl
from pydantic import HttpUrl, ValidationError
from bayer.models import PESCompound, PESStructure
from epdb.exceptions import PackageImportException
from epdb.models import (
AdditionalInformation,
Compound,
@ -34,7 +36,7 @@ from epdb.models import (
Setting,
SimpleAmbitRule,
SimpleRDKitRule,
SimpleRule,
SimpleRule, Group,
)
from utilities.chem import FormatConverter
@ -42,7 +44,7 @@ logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
if TYPE_CHECKING:
from epdb.logic import SPathway
from epdb.logic import SPathway, GroupManager
class LicenseExportSchema(Schema):
@ -64,6 +66,9 @@ class RefExportSchema(Schema):
return str(value)
class RefGroupExportSchema(RefExportSchema): ...
class RefCompoundExportSchema(RefExportSchema): ...
@ -88,6 +93,9 @@ class RefPathwayExportSchema(RefExportSchema): ...
class RefScenarioExportSchema(RefExportSchema): ...
class RefEnzymeExportSchema(RefExportSchema): ...
############
# Compound #
############
@ -110,6 +118,26 @@ class CompoundStructureExportSchema(RefCompoundStructureExportSchema):
scenarios: List[RefScenarioExportSchema]
class PESCompoundExportSchema(RefCompoundExportSchema):
name: str
description: str
aliases: List[str]
default_structure: RefCompoundStructureExportSchema
structures: List["PESCompoundStructureExportSchema"]
scenarios: List[RefScenarioExportSchema]
class PESCompoundStructureExportSchema(RefCompoundStructureExportSchema):
name: str
description: str
aliases: List[str]
smiles: str
molfile: Optional[str]
normalized_structure: bool
scenarios: List[RefScenarioExportSchema]
pes_link: HttpUrl
############
# Reaction #
############
@ -128,7 +156,28 @@ class ReactionExportSchema(RefReactionExportSchema):
#########
# Rules #
#########
class RuleExportSchema(RefRuleExportSchema):
class EnzymeExportSchema(RefEnzymeExportSchema):
ec_number: str
classification_level: int
linking_method: str
reaction_evidence: List[RefReactionExportSchema]
edge_evidence: List[RefEdgeExportSchema]
class EnzymeRuleExportSchema(RefRuleExportSchema):
enzymes: List[EnzymeExportSchema] | None = None
@staticmethod
def resolve_enzymes(obj):
if isinstance(obj, dict):
res = []
for e in obj.get("enzymes", []):
res.append(EnzymeExportSchema.model_validate(e))
return res
return obj.enzymelink_set.all()
class RuleExportSchema(EnzymeRuleExportSchema):
name: str
description: str
aliases: List[str]
@ -138,7 +187,7 @@ class RuleExportSchema(RefRuleExportSchema):
scenarios: List[RefScenarioExportSchema]
class ParallelRuleExportSchema(RefRuleExportSchema):
class ParallelRuleExportSchema(EnzymeRuleExportSchema):
name: str
description: str
aliases: List[str]
@ -210,8 +259,10 @@ class PackageExportSchema(Schema):
description: str
uuid: str
reviewed: bool
license: LicenseExportSchema | None
compounds: List[CompoundExportSchema]
license: LicenseExportSchema | None = None
classification_level: str = "Internal"
data_pool: RefGroupExportSchema | None = None
compounds: List[CompoundExportSchema | PESCompoundExportSchema]
reactions: List[ReactionExportSchema]
simple_rules: List[RuleExportSchema]
composite_rules: List[ParallelRuleExportSchema]
@ -224,6 +275,31 @@ class PackageExportSchema(Schema):
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
return str(value)
@staticmethod
def resolve_classification_level(obj):
if isinstance(obj, dict):
return obj["classification_level"]
return obj.Classification(obj.classification_level).name
@staticmethod
def resolve_compounds(obj):
res = []
if isinstance(obj, dict):
for c in obj.get("compounds", []):
is_pes = any([cs.get("pes_link", None) is not None for cs in c.get("structures", [])])
if is_pes:
res.append(PESCompoundExportSchema.model_validate(c))
else:
res.append(CompoundExportSchema.model_validate(c))
else:
for c in obj.compounds.all():
if isinstance(c, PESCompound):
res.append(PESCompoundExportSchema.from_orm(c))
else:
res.append(CompoundExportSchema.from_orm(c))
return res
@staticmethod
def resolve_simple_rules(obj):
if isinstance(obj, dict):
@ -277,51 +353,281 @@ class PackageExporter:
class PackageImporter:
"""
Imports package data from JSON export.
Handles object creation, relationship mapping, and dependency resolution.
"""
def __init__(
self,
package: Dict[str, Any],
preserve_uuids: bool = False,
add_import_timestamp=True,
trust_reviewed=False,
):
"""
Initialize the importer.
Args:
preserve_uuids: If True, preserve original UUIDs. If False, generate new ones.
"""
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
self.preserve_uuids = preserve_uuids
self.add_import_timestamp = add_import_timestamp
self.trust_reviewed = trust_reviewed
self.uuid_mapping = {}
self.object_cache = {}
self._raw_package = package
def _get_or_generate_uuid(self, original_uuid: str) -> str:
"""Get mapped UUID or generate new one if not preserving UUIDs."""
if self.preserve_uuids:
return original_uuid
if original_uuid not in self.uuid_mapping:
self.uuid_mapping[original_uuid] = str(uuid.uuid4())
return self.uuid_mapping[original_uuid]
def _cache_object(self, model_name: str, uuid_str: str, obj):
"""Cache a created object for later reference."""
self.object_cache[(model_name, uuid_str)] = obj
def _get_cached_object(self, model_name: str, uuid_str: str):
"""Get a cached object by model name and UUID."""
return self.object_cache.get((model_name, uuid_str))
self._cache = {}
def do_import(self) -> Package:
return self._import_package_from_json(self._raw_package)
return self._import_package_from_json()
def _import_compounds(
self,
package: Package,
compounds: List[CompoundExportSchema | PESCompoundExportSchema],
):
for c in compounds:
if isinstance(c, PESCompoundExportSchema):
c_cls = PESCompound
else:
c_cls = Compound
new_c = c_cls()
new_c.uuid = str(uuid.uuid4()) if not self.preserve_uuids else c.uuid
new_c.package = package
new_c.name = c.name
new_c.description = c.description
new_c.aliases = c.aliases
new_c.save()
self._cache[c.uuid] = new_c
for cs in c.structures:
if isinstance(cs, PESCompoundStructureExportSchema):
cs_cls = PESStructure
else:
cs_cls = CompoundStructure
new_cs = cs_cls()
new_cs.uuid = str(uuid.uuid4()) if not self.preserve_uuids else cs.uuid
new_cs.compound = new_c
new_cs.name = cs.name
new_cs.description = cs.description
new_cs.aliases = cs.aliases
new_cs.smiles = cs.smiles
new_cs.molfile = cs.molfile
if cs_cls == PESStructure:
new_cs.pes_link = cs.pes_link
new_cs.normalized_structure = cs.normalized_structure
new_cs.save()
self._cache[cs.uuid] = new_cs
# Now we can assigne the default_structure to the compound
new_c.default_structure = self._cache[c.default_structure.uuid]
new_c.save()
def _import_simple_rules(
self,
package: Package,
rules: List[RuleExportSchema]
):
for r in rules:
new_r = SimpleAmbitRule()
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
new_r.package = package
new_r.name = r.name
new_r.description = r.description
new_r.aliases = r.aliases
new_r.smirks = r.smirks
new_r.reactant_filter_smarts = r.reactant_filter_smarts
new_r.product_filter_smarts = r.product_filter_smarts
new_r.save()
self._cache[r.uuid] = new_r
def _import_composite_rules(
self,
package: Package,
rules: List[ParallelRuleExportSchema]
):
for r in rules:
new_r = ParallelRule()
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
new_r.package = package
new_r.name = r.name
new_r.description = r.description
new_r.aliases = r.aliases
new_r.save()
for sr in r.simple_rules:
new_r.simple_rules.add(self._cache[sr.uuid])
self._cache[r.uuid] = new_r
def _import_reactions(
self,
package: Package,
reactions: List[ReactionExportSchema],
):
for r in reactions:
new_r = Reaction()
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
new_r.package = package
new_r.name = r.name
new_r.description = r.description
new_r.aliases = r.aliases
new_r.multi_step = r.multi_step
new_r.medline_references = r.medline_references
new_r.save()
for educt in r.educts:
new_r.educts.add(self._cache[educt.uuid])
for product in r.products:
new_r.products.add(self._cache[product.uuid])
for rule in r.rules:
new_r.rules.add(self._cache[rule.uuid])
self._cache[r.uuid] = new_r
def _import_pathways(
self,
package: Package,
pathways: List[PathwayExportSchema]
):
for pw in pathways:
new_pw = Pathway()
new_pw.uuid = str(uuid.uuid4()) if not self.preserve_uuids else pw.uuid
new_pw.package = package
new_pw.name = pw.name
new_pw.description = pw.description
new_pw.aliases = pw.aliases
new_pw.predicted = pw.predicted
new_pw.save()
self._cache[pw.uuid] = new_pw
for n in pw.nodes:
new_n = Node()
new_n.uuid = str(uuid.uuid4()) if not self.preserve_uuids else n.uuid
new_n.pathway = new_pw
new_n.name = n.name
new_n.description = n.description
new_n.aliases = n.aliases
new_n.default_node_label = self._cache[n.default_node_label.uuid]
new_n.depth = n.depth
new_n.stereo_removed = n.stereo_removed
new_n.save()
for nl in n.node_labels:
new_n.node_labels.add(self._cache[nl.uuid])
self._cache[n.uuid] = new_n
for e in pw.edges:
new_e = Edge()
new_e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else e.uuid
new_e.pathway = new_pw
new_e.name = e.name
new_e.description = e.description
new_e.aliases = e.aliases
new_e.edge_label = self._cache[e.edge_label.uuid]
new_e.save()
for sn in e.start_nodes:
new_e.start_nodes.add(self._cache[sn.uuid])
for en in e.end_nodes:
new_e.end_nodes.add(self._cache[en.uuid])
self._cache[e.uuid] = new_e
def _import_scenarios(
self,
package: Package,
scenarios: List[ScenarioExportSchema]
):
for s in scenarios:
new_s = Scenario()
new_s.uuid = str(uuid.uuid4()) if not self.preserve_uuids else s.uuid
new_s.package = package
new_s.name = s.name
new_s.description = s.description
new_s.scenario_date = s.scenario_date
new_s.scenario_type = s.scenario_type
new_s.save()
self._cache[s.uuid] = new_s
def _import_additional_information(
self,
package: Package,
additional_information: List[AdditionalInformationExportSchema]
):
for ai in additional_information:
new_ai = AdditionalInformation()
new_ai.uuid = str(uuid.uuid4()) if not self.preserve_uuids else ai.uuid
new_ai.package = package
new_ai.type = ai.type
new_ai.data = ai.data
if ai.scenario:
new_ai.scenario = self._cache[ai.scenario.uuid]
if ai.attach_object:
new_ai.content_object = self._cache[ai.attach_object.uuid]
new_ai.save()
self._cache[ai.uuid] = new_ai
def _link_scenarios_after_import(self, data: PackageExportSchema):
collections = [
data.compounds,
data.simple_rules,
data.composite_rules,
data.reactions,
data.pathways,
[n for pw in data.pathways for n in pw.nodes],
[e for pw in data.pathways for e in pw.edges],
]
for coll in collections:
for elem in coll:
elem_obj = self._cache[elem.uuid]
for s in elem.scenarios:
elem_obj.scenarios.add(self._cache[s.uuid])
def _import_package_from_json(
self,
) -> Package:
try:
parsed = PackageExportSchema.model_validate(self._raw_package)
except ValidationError as e:
logger.error(f"Error validating package data: {e}")
raise PackageImportException("Deserialization failed")
package_uuid = str(uuid.uuid4())
if self.preserve_uuids:
package_uuid = parsed.uuid
package_license = License.objects.get(link=parsed.license.link) if parsed.license else None
package_classification_level = Package.Classification[parsed.classification_level.upper()]
print(parsed.classification_level, "->", package_classification_level)
package = Package()
package.uuid = package_uuid
package.name = f"{parsed.name} - Imported at {datetime.now()}"
package.description = parsed.description
package.reviewed = False
package.license = package_license
package.classification_level = package_classification_level
if package_classification_level == Package.Classification.SECRET:
package.data_pool = Group.objects.get(uuid=parsed.data_pool.uuid)
package.save()
# Import Elements
self._import_compounds(package, parsed.compounds)
self._import_simple_rules(package, parsed.simple_rules)
self._import_composite_rules(package, parsed.composite_rules)
self._import_reactions(package, parsed.reactions)
self._import_pathways(package, parsed.pathways)
self._import_scenarios(package, parsed.scenarios)
self._import_additional_information(package, parsed.additional_information)
self._link_scenarios_after_import(parsed)
return package
@staticmethod
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
@ -339,510 +645,6 @@ class PackageImporter:
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
return hmac.compare_digest(signature, expected)
@transaction.atomic
def _import_package_from_json(self, package_data: Dict[str, Any]) -> Package:
"""
Import a complete package from JSON data.
Args:
package_data: Dictionary containing the package export data
Returns:
The created Package instance
"""
print(f"Starting import of package: {package_data['name']}")
# Create the main package
package = self._create_package(package_data)
# Import in dependency order
self._import_compounds(package, package_data.get("compounds", []))
self._import_structures(package, package_data.get("structures", []))
self._import_rules(package, package_data.get("rules", {}))
self._import_reactions(package, package_data.get("reactions", []))
self._import_pathways(package, package_data.get("pathways", []))
self._import_nodes(package, package_data.get("nodes", []))
self._import_edges(package, package_data.get("edges", []))
self._import_scenarios(package, package_data.get("scenarios", []))
if package_data.get("models"):
self._import_models(package, package_data["models"])
# Set default structures for compounds (after all structures are created)
self._set_default_structures(package_data.get("compounds", []))
print(f"Package import completed: {package.name}")
return package
def _create_package(self, package_data: Dict[str, Any]) -> Package:
"""Create the main package object."""
package_uuid = self._get_or_generate_uuid(package_data["uuid"])
# Handle license
license_obj = None
if package_data.get("license"):
license_data = package_data["license"]
license_obj, _ = License.objects.get_or_create(
name=license_data["name"],
defaults={
"cc_string": license_data.get("cc_string", ""),
"link": license_data.get("link", ""),
"image_link": license_data.get("image_link", ""),
},
)
new_name = package_data.get("name")
if self.add_import_timestamp:
new_name = f"{new_name} - Imported at {datetime.now()}"
new_reviewed = False
if self.trust_reviewed:
new_reviewed = package_data.get("reviewed", False)
package = Package.objects.create(
uuid=package_uuid,
name=new_name,
description=package_data["description"],
kv=package_data.get("kv", {}),
reviewed=new_reviewed,
license=license_obj,
)
self._cache_object("Package", package_data["uuid"], package)
print(f"Created package: {package.name}")
return package
def _import_compounds(self, package: Package, compounds_data: List[Dict[str, Any]]):
"""Import compounds."""
print(f"Importing {len(compounds_data)} compounds...")
for compound_data in compounds_data:
compound_uuid = self._get_or_generate_uuid(compound_data["uuid"])
compound = Compound.objects.create(
uuid=compound_uuid,
package=package,
name=compound_data["name"],
description=compound_data["description"],
kv=compound_data.get("kv", {}),
# default_structure will be set later
)
# Set aliases if present
if compound_data.get("aliases"):
compound.aliases = compound_data["aliases"]
compound.save()
self._cache_object("Compound", compound_data["uuid"], compound)
# Handle external identifiers
self._create_external_identifiers(
compound, compound_data.get("external_identifiers", [])
)
def _import_structures(self, package: Package, structures_data: List[Dict[str, Any]]):
"""Import compound structures."""
print(f"Importing {len(structures_data)} compound structures...")
for structure_data in structures_data:
structure_uuid = self._get_or_generate_uuid(structure_data["uuid"])
compound_uuid = structure_data["compound"]["uuid"]
compound = self._get_cached_object("Compound", compound_uuid)
if not compound:
print(f"Warning: Compound with UUID {compound_uuid} not found for structure")
continue
structure = CompoundStructure.objects.create(
uuid=structure_uuid,
compound=compound,
name=structure_data["name"],
description=structure_data["description"],
kv=structure_data.get("kv", {}),
smiles=structure_data["smiles"],
canonical_smiles=structure_data["canonical_smiles"],
inchikey=structure_data["inchikey"],
normalized_structure=structure_data.get("normalized_structure", False),
)
# Set aliases if present
if structure_data.get("aliases"):
structure.aliases = structure_data["aliases"]
structure.save()
self._cache_object("CompoundStructure", structure_data["uuid"], structure)
# Handle external identifiers
self._create_external_identifiers(
structure, structure_data.get("external_identifiers", [])
)
def _import_rules(self, package: Package, rules_data: Dict[str, Any]):
"""Import all types of rules."""
print("Importing rules...")
# Import simple rules first
simple_rules_data = rules_data.get("simple_rules", [])
print(f"Importing {len(simple_rules_data)} simple rules...")
for rule_data in simple_rules_data:
self._create_simple_rule(package, rule_data)
# Import parallel rules
parallel_rules_data = rules_data.get("parallel_rules", [])
print(f"Importing {len(parallel_rules_data)} parallel rules...")
for rule_data in parallel_rules_data:
self._create_parallel_rule(package, rule_data)
def _create_simple_rule(self, package: Package, rule_data: Dict[str, Any]):
"""Create a simple rule (SimpleAmbitRule or SimpleRDKitRule)."""
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
rule_type = rule_data.get("rule_type", "SimpleRule")
common_fields = {
"uuid": rule_uuid,
"package": package,
"name": rule_data["name"],
"description": rule_data["description"],
"kv": rule_data.get("kv", {}),
}
if rule_type == "SimpleAmbitRule":
rule = SimpleAmbitRule.objects.create(
**common_fields,
smirks=rule_data.get("smirks", ""),
reactant_filter_smarts=rule_data.get("reactant_filter_smarts", ""),
product_filter_smarts=rule_data.get("product_filter_smarts", ""),
)
elif rule_type == "SimpleRDKitRule":
rule = SimpleRDKitRule.objects.create(
**common_fields, reaction_smarts=rule_data.get("reaction_smarts", "")
)
else:
rule = SimpleRule.objects.create(**common_fields)
# Set aliases if present
if rule_data.get("aliases"):
rule.aliases = rule_data["aliases"]
rule.save()
self._cache_object("SimpleRule", rule_data["uuid"], rule)
return rule
def _create_parallel_rule(self, package: Package, rule_data: Dict[str, Any]):
"""Create a parallel rule."""
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
rule = ParallelRule.objects.create(
uuid=rule_uuid,
package=package,
name=rule_data["name"],
description=rule_data["description"],
kv=rule_data.get("kv", {}),
)
# Set aliases if present
if rule_data.get("aliases"):
rule.aliases = rule_data["aliases"]
rule.save()
# Add simple rules
for simple_rule_ref in rule_data.get("simple_rules", []):
simple_rule = self._get_cached_object("SimpleRule", simple_rule_ref["uuid"])
if simple_rule:
rule.simple_rules.add(simple_rule)
self._cache_object("ParallelRule", rule_data["uuid"], rule)
return rule
def _import_reactions(self, package: Package, reactions_data: List[Dict[str, Any]]):
"""Import reactions."""
print(f"Importing {len(reactions_data)} reactions...")
for reaction_data in reactions_data:
reaction_uuid = self._get_or_generate_uuid(reaction_data["uuid"])
reaction = Reaction.objects.create(
uuid=reaction_uuid,
package=package,
name=reaction_data["name"],
description=reaction_data["description"],
kv=reaction_data.get("kv", {}),
multi_step=reaction_data.get("multi_step", False),
medline_references=reaction_data.get("medline_references", []),
)
# Set aliases if present
if reaction_data.get("aliases"):
reaction.aliases = reaction_data["aliases"]
reaction.save()
# Add educts and products
for educt_ref in reaction_data.get("educts", []):
compound = self._get_cached_object("CompoundStructure", educt_ref["uuid"])
if compound:
reaction.educts.add(compound)
for product_ref in reaction_data.get("products", []):
compound = self._get_cached_object("CompoundStructure", product_ref["uuid"])
if compound:
reaction.products.add(compound)
# Add rules
for rule_ref in reaction_data.get("rules", []):
# Try to find rule in different caches
rule = self._get_cached_object(
"SimpleRule", rule_ref["uuid"]
) or self._get_cached_object("ParallelRule", rule_ref["uuid"])
if rule:
reaction.rules.add(rule)
self._cache_object("Reaction", reaction_data["uuid"], reaction)
# Handle external identifiers
self._create_external_identifiers(
reaction, reaction_data.get("external_identifiers", [])
)
def _import_pathways(self, package: Package, pathways_data: List[Dict[str, Any]]):
"""Import pathways."""
print(f"Importing {len(pathways_data)} pathways...")
for pathway_data in pathways_data:
pathway_uuid = self._get_or_generate_uuid(pathway_data["uuid"])
pathway = Pathway.objects.create(
uuid=pathway_uuid,
package=package,
name=pathway_data["name"],
description=pathway_data["description"],
kv=pathway_data.get("kv", {}),
# setting will be handled separately if needed
)
# Set aliases if present
if pathway_data.get("aliases"):
pathway.aliases = pathway_data["aliases"]
pathway.save()
self._cache_object("Pathway", pathway_data["uuid"], pathway)
def _import_nodes(self, package: Package, nodes_data: List[Dict[str, Any]]):
"""Import pathway nodes."""
print(f"Importing {len(nodes_data)} nodes...")
for node_data in nodes_data:
node_uuid = self._get_or_generate_uuid(node_data["uuid"])
pathway_uuid = node_data["pathway"]["uuid"]
pathway = self._get_cached_object("Pathway", pathway_uuid)
if not pathway:
print(f"Warning: Pathway with UUID {pathway_uuid} not found for node")
continue
# For now, we'll set default_node_label to None and handle it later
# as it requires compound structures to be fully imported
node = Node.objects.create(
uuid=node_uuid,
pathway=pathway,
name=node_data["name"],
description=node_data["description"],
kv=node_data.get("kv", {}),
depth=node_data.get("depth", 0),
default_node_label=self._get_cached_object(
"CompoundStructure", node_data["default_node_label"]["uuid"]
),
)
# Set aliases if present
if node_data.get("aliases"):
node.aliases = node_data["aliases"]
node.save()
self._cache_object("Node", node_data["uuid"], node)
# Store node_data for later processing of relationships
node._import_data = node_data
def _import_edges(self, package: Package, edges_data: List[Dict[str, Any]]):
"""Import pathway edges."""
print(f"Importing {len(edges_data)} edges...")
for edge_data in edges_data:
edge_uuid = self._get_or_generate_uuid(edge_data["uuid"])
pathway_uuid = edge_data["pathway"]["uuid"]
pathway = self._get_cached_object("Pathway", pathway_uuid)
if not pathway:
print(f"Warning: Pathway with UUID {pathway_uuid} not found for edge")
continue
# For now, we'll set edge_label to None and handle it later
edge = Edge.objects.create(
uuid=edge_uuid,
pathway=pathway,
name=edge_data["name"],
description=edge_data["description"],
kv=edge_data.get("kv", {}),
edge_label=self._get_cached_object("Reaction", edge_data["edge_label"]["uuid"]),
)
# Set aliases if present
if edge_data.get("aliases"):
edge.aliases = edge_data["aliases"]
edge.save()
# Add start and end nodes
for start_node_ref in edge_data.get("start_nodes", []):
node = self._get_cached_object("Node", start_node_ref["uuid"])
if node:
edge.start_nodes.add(node)
for end_node_ref in edge_data.get("end_nodes", []):
node = self._get_cached_object("Node", end_node_ref["uuid"])
if node:
edge.end_nodes.add(node)
self._cache_object("Edge", edge_data["uuid"], edge)
def _import_scenarios(self, package: Package, scenarios_data: List[Dict[str, Any]]):
"""Import scenarios."""
print(f"Importing {len(scenarios_data)} scenarios...")
# First pass: create scenarios without parent relationships
for scenario_data in scenarios_data:
scenario_uuid = self._get_or_generate_uuid(scenario_data["uuid"])
scenario_date = None
if scenario_data.get("scenario_date"):
scenario_date = scenario_data["scenario_date"]
scenario = Scenario.objects.create(
uuid=scenario_uuid,
package=package,
name=scenario_data["name"],
description=scenario_data["description"],
kv=scenario_data.get("kv", {}),
scenario_date=scenario_date,
scenario_type=scenario_data.get("scenario_type"),
additional_information=scenario_data.get("additional_information", {}),
)
self._cache_object("Scenario", scenario_data["uuid"], scenario)
# Store scenario_data for later processing of parent relationships
scenario._import_data = scenario_data
# Second pass: set parent relationships
for scenario_data in scenarios_data:
if scenario_data.get("parent"):
scenario = self._get_cached_object("Scenario", scenario_data["uuid"])
parent = self._get_cached_object("Scenario", scenario_data["parent"]["uuid"])
if scenario and parent:
scenario.parent = parent
scenario.save()
def _import_models(self, package: Package, models_data: List[Dict[str, Any]]):
"""Import EPModels."""
print(f"Importing {len(models_data)} models...")
for model_data in models_data:
model_uuid = self._get_or_generate_uuid(model_data["uuid"])
model_type = model_data.get("model_type", "EPModel")
common_fields = {
"uuid": model_uuid,
"package": package,
"name": model_data["name"],
"description": model_data["description"],
"kv": model_data.get("kv", {}),
}
# Add PackageBasedModel fields if present
if "threshold" in model_data:
common_fields.update(
{
"threshold": model_data.get("threshold"),
"eval_results": model_data.get("eval_results", {}),
"model_status": model_data.get("model_status", "INITIAL"),
}
)
# Create the appropriate model type
if model_type == "RuleBasedRelativeReasoning":
model = RuleBasedRelativeReasoning.objects.create(
**common_fields,
min_count=model_data.get("min_count", 1),
max_count=model_data.get("max_count", 10),
)
elif model_type == "MLRelativeReasoning":
model = MLRelativeReasoning.objects.create(**common_fields)
elif model_type == "EnviFormer":
model = EnviFormer.objects.create(**common_fields)
else:
model = EPModel.objects.create(**common_fields)
# Set aliases if present
if model_data.get("aliases"):
model.aliases = model_data["aliases"]
model.save()
# Add package relationships for PackageBasedModel
if hasattr(model, "rule_packages"):
for pkg_ref in model_data.get("rule_packages", []):
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
if pkg:
model.rule_packages.add(pkg)
for pkg_ref in model_data.get("data_packages", []):
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
if pkg:
model.data_packages.add(pkg)
for pkg_ref in model_data.get("eval_packages", []):
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
if pkg:
model.eval_packages.add(pkg)
self._cache_object("EPModel", model_data["uuid"], model)
def _set_default_structures(self, compounds_data: List[Dict[str, Any]]):
"""Set default structures for compounds after all structures are created."""
print("Setting default structures for compounds...")
for compound_data in compounds_data:
if compound_data.get("default_structure"):
compound = self._get_cached_object("Compound", compound_data["uuid"])
structure = self._get_cached_object(
"CompoundStructure", compound_data["default_structure"]["uuid"]
)
if compound and structure:
compound.default_structure = structure
compound.save()
def _create_external_identifiers(self, obj, identifiers_data: List[Dict[str, Any]]):
"""Create external identifiers for an object."""
for identifier_data in identifiers_data:
# Get or create the external database
db_data = identifier_data["database"]
database, _ = ExternalDatabase.objects.get_or_create(
name=db_data["name"],
defaults={
"base_url": db_data.get("base_url", ""),
"full_name": db_data.get("name", ""),
"description": "",
"is_active": True,
},
)
# Create the external identifier
ExternalIdentifier.objects.create(
content_object=obj,
database=database,
identifier_value=identifier_data["identifier_value"],
url=identifier_data.get("url", ""),
is_primary=identifier_data.get("is_primary", False),
)
class PathwayUtils:
def __init__(self, pathway: "Pathway"):