Implement Compound CRUD (#22)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#22
This commit is contained in:
2025-07-03 07:17:04 +12:00
parent 4e58a1fad7
commit 9323a9f7d7
9 changed files with 472 additions and 40 deletions

View File

@ -281,58 +281,121 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin):
def structures(self):
return CompoundStructure.objects.filter(compound=self)
@property
def normalized_structure(self):
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
@property
def url(self):
return '{}/compound/{}'.format(self.package.url, self.uuid)
# @property
# def related_pathways(self):
# pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
@transaction.atomic
def set_default_structure(self, cs: 'CompoundStructure'):
if cs.compound != self:
raise ValueError("Attempt to set a CompoundStructure stored in a different compound as default")
# @property
# def related_reactions(self):
# return (
# Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
# |
# Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
# ).order_by('name')
self.default_structure = cs
self.save()
@property
def related_pathways(self):
pathways = Node.objects.filter(node_labels__in=[self.default_structure]).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, educts__in=[self.default_structure])
|
Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
).order_by('name')
@staticmethod
@transaction.atomic
def create(package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs):
def create(package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs) -> 'Compound':
# Pre check
# Validity of SMILES etc
if smiles is None or smiles == '':
raise ValueError('SMILES is required')
smiles = smiles.strip()
parsed = FormatConverter.from_smiles(smiles)
if parsed is None:
raise ValueError('Given SMILES is invalid')
standardized_smiles = FormatConverter.standardize(smiles)
# Check if we find a direct match for a given SMILES
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
# Check if we can find the standardized one
if CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package).exists():
# TODO should we add a structure?
return CompoundStructure.objects.get(smiles=standardized_smiles, compound__package=package).compound
# Generate Compound
c = Compound()
c.package = package
if name is not None:
# For name and description we have defaults so only set them if they carry a real value
if name is not None and name != '':
c.name = name
if description is not None:
if description is not None and description != '':
c.description = description
c.save()
normalized_smiles = smiles # chem.normalize(smiles)
is_standardized = standardized_smiles == smiles
if normalized_smiles != smiles:
_ = CompoundStructure.create(c, normalized_smiles, name='Normalized structure of {}'.format(name),
if not is_standardized:
_ = CompoundStructure.create(c, standardized_smiles, name='Normalized structure of {}'.format(name),
description='{} (in its normalized form)'.format(description),
normalized_structure=True)
cs = CompoundStructure.create(c, smiles, name=name, description=description)
cs = CompoundStructure.create(c, smiles, name=name, description=description, normalized_structure=is_standardized)
c.default_structure = cs
c.save()
return c
@transaction.atomic
def add_structure(self, smiles: str, name: str = None, description: str = None, default_structure: bool = False,
*args, **kwargs) -> 'CompoundStructure':
if smiles is None or smiles == '':
raise ValueError('SMILES is required')
smiles = smiles.strip()
parsed = FormatConverter.from_smiles(smiles)
if parsed is None:
raise ValueError('Given SMILES is invalid')
standardized_smiles = FormatConverter.standardize(smiles)
is_standardized = standardized_smiles == smiles
if self.normalized_structure.smiles != standardized_smiles:
raise ValueError('The standardized SMILES does not match the compounds standardized one!')
if is_standardized:
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
if CompoundStructure.objects.filter(smiles__in=smiles, compound__package=self.package).exists():
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
cs = CompoundStructure.create(self, smiles, name=name, description=description, normalized_structure=is_standardized)
if default_structure:
self.default_structure = cs
self.save()
return cs
class Meta:
unique_together = [('uuid', 'package')]
@ -391,6 +454,10 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin):
def InChIKey(self):
return FormatConverter.InChIKey(self.smiles)
@property
def canonical_smiles(self):
return FormatConverter.canonicalize(self.smiles)
@property
def as_svg(self):
return IndigoUtils.mol_to_svg(self.smiles)
@ -401,10 +468,10 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
# I think this only affects Django Admin which we are barely using
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
# _non_polymorphic = models.Manager()
#
# class Meta:
# base_manager_name = '_non_polymorphic'
_non_polymorphic = models.Manager()
class Meta:
base_manager_name = '_non_polymorphic'
@abc.abstractmethod
def apply(self, *args, **kwargs):