Copy Objects between Packages (#59)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#59
This commit is contained in:
2025-08-28 06:27:11 +12:00
parent 13816ecaf3
commit 00d9188c0c
16 changed files with 696 additions and 24 deletions

View File

@ -533,15 +533,16 @@ class AliasMixin(models.Model):
@transaction.atomic
def add_alias(self, new_alias, set_as_default=False):
if set_as_default:
self.aliases.add(self.name)
self.aliases.append(self.name)
self.name = new_alias
if new_alias in self.aliases:
self.aliases.remove(new_alias)
else:
if new_alias not in self.aliases:
self.aliases.add(new_alias)
self.aliases.append(new_alias)
self.aliases = sorted(list(set(self.aliases)), key=lambda x: x.lower())
self.save()
class Meta:
@ -762,6 +763,64 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
return cs
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict):
if self in mapping:
return mapping[self]
new_compound = Compound.objects.create(
package=target,
name=self.name,
description=self.description,
kv=self.kv.copy() if self.kv else {}
)
mapping[self] = new_compound
# Copy compound structures
for structure in self.structures.all():
if structure not in mapping:
new_structure = CompoundStructure.objects.create(
compound=new_compound,
smiles=structure.smiles,
canonical_smiles=structure.canonical_smiles,
inchikey=structure.inchikey,
normalized_structure=structure.normalized_structure,
name=structure.name,
description=structure.description,
kv=structure.kv.copy() if structure.kv else {}
)
mapping[structure] = new_structure
# Copy external identifiers for structure
for ext_id in structure.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_structure,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
if self.default_structure:
new_compound.default_structure = mapping.get(self.default_structure)
new_compound.save()
for a in self.aliases:
new_compound.add_alias(a)
new_compound.save()
# Copy external identifiers for compound
for ext_id in self.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_compound,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
return new_compound
class Meta:
unique_together = [('uuid', 'package')]
@ -817,6 +876,14 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
return cs
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict):
if self in mapping:
return mapping[self]
self.compound.copy(target, mapping)
return mapping[self]
@property
def as_svg(self, width: int = 800, height: int = 400):
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
@ -856,22 +923,54 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
return cls.create(*args, **kwargs)
#
# @property
# def related_pathways(self):
# reaction_ids = self.related_reactions.values_list('id', flat=True)
# pathways = Edge.objects.filter(edge_label__in=reaction_ids).values_list('pathway', flat=True)
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
#
# @property
# def related_reactions(self):
# return (
# Reaction.objects.filter(package=self.package, rules__in=[self])
# |
# Reaction.objects.filter(package=self.package, rules__in=[self])
# ).order_by('name')
#
#
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict):
"""Copy a rule to the target package."""
if self in mapping:
return mapping[self]
# Get the specific rule type and copy accordingly
rule_type = type(self)
if rule_type == SimpleAmbitRule:
new_rule = SimpleAmbitRule.objects.create(
package=target,
name=self.name,
description=self.description,
smirks=self.smirks,
reactant_filter_smarts=self.reactant_filter_smarts,
product_filter_smarts=self.product_filter_smarts,
kv=self.kv.copy() if self.kv else {}
)
elif rule_type == SimpleRDKitRule:
new_rule = SimpleRDKitRule.objects.create(
package=target,
name=self.name,
description=self.description,
reaction_smarts=self.reaction_smarts,
kv=self.kv.copy() if self.kv else {}
)
elif rule_type == ParallelRule:
new_rule = ParallelRule.objects.create(
package=target,
name=self.name,
description=self.description,
kv=self.kv.copy() if self.kv else {}
)
# Copy simple rules relationships
for simple_rule in self.simple_rules.all():
copied_simple_rule = simple_rule.copy(target, mapping)
new_rule.simple_rules.add(copied_simple_rule)
elif rule_type == SequentialRule:
raise ValueError("SequentialRule copy not implemented!")
else:
raise ValueError(f"Unknown rule type: {rule_type}")
mapping[self] = new_rule
return new_rule
class SimpleRule(Rule):
pass
@ -1141,6 +1240,50 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r.save()
return r
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict ) -> 'Reaction':
"""Copy a reaction to the target package."""
if self in mapping:
return mapping[self]
# Create new reaction
new_reaction = Reaction.objects.create(
package=target,
name=self.name,
description=self.description,
multi_step=self.multi_step,
medline_references=self.medline_references,
kv=self.kv.copy() if self.kv else {}
)
mapping[self] = new_reaction
# Copy educts (reactant compounds)
for educt in self.educts.all():
copied_educt = educt.copy(target, mapping)
new_reaction.educts.add(copied_educt)
# Copy products
for product in self.products.all():
copied_product = product.copy(target, mapping)
new_reaction.products.add(copied_product)
# Copy rules
for rule in self.rules.all():
copied_rule = rule.copy(target, mapping)
new_reaction.rules.add(copied_rule)
# Copy external identifiers
for ext_id in self.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_reaction,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
return new_reaction
def smirks(self):
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
@ -1375,6 +1518,87 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
return pw
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict) -> 'Pathway':
if self in mapping:
return mapping[self]
# Start copying the pathway
new_pathway = Pathway.objects.create(
package=target,
name=self.name,
description=self.description,
setting=self.setting, # TODO copy settings?
kv=self.kv.copy() if self.kv else {}
)
# # Copy aliases if they exist
# if hasattr(self, 'aliases'):
# new_pathway.aliases.set(self.aliases.all())
#
# # Copy scenarios if they exist
# if hasattr(self, 'scenarios'):
# new_pathway.scenarios.set(self.scenarios.all())
# Copy all nodes first
for node in self.nodes.all():
# Copy the compound structure for the node label
copied_structure = None
if node.default_node_label:
copied_compound = node.default_node_label.compound.copy(target, mapping)
# Find the corresponding structure in the copied compound
for structure in copied_compound.structures.all():
if structure.smiles == node.default_node_label.smiles:
copied_structure = structure
break
new_node = Node.objects.create(
pathway=new_pathway,
default_node_label=copied_structure,
depth=node.depth,
name=node.name,
description=node.description,
kv=node.kv.copy() if node.kv else {}
)
mapping[node] = new_node
# Copy node labels (many-to-many relationship)
for label in node.node_labels.all():
copied_label_compound = label.compound.copy(target, mapping)
for structure in copied_label_compound.structures.all():
if structure.smiles == label.smiles:
new_node.node_labels.add(structure)
break
# Copy all edges
for edge in self.edges.all():
# Copy the reaction for edge label if it exists
copied_reaction = None
if edge.edge_label:
copied_reaction = edge.edge_label.copy(target, mapping)
new_edge = Edge.objects.create(
pathway=new_pathway,
edge_label=copied_reaction,
name=edge.name,
description=edge.description,
kv=edge.kv.copy() if edge.kv else {}
)
# Copy start and end nodes relationships
for start_node in edge.start_nodes.all():
if start_node in mapping:
new_edge.start_nodes.add(mapping[start_node])
for end_node in edge.end_nodes.all():
if end_node in mapping:
new_edge.end_nodes.add(mapping[end_node])
mapping[self] = new_pathway
return new_pathway
@transaction.atomic
def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None):
return Node.create(self, smiles, 0)