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): def structures(self):
return CompoundStructure.objects.filter(compound=self) return CompoundStructure.objects.filter(compound=self)
@property
def normalized_structure(self):
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
@property @property
def url(self): def url(self):
return '{}/compound/{}'.format(self.package.url, self.uuid) return '{}/compound/{}'.format(self.package.url, self.uuid)
# @property @transaction.atomic
# def related_pathways(self): def set_default_structure(self, cs: 'CompoundStructure'):
# pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True) if cs.compound != self:
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name') raise ValueError("Attempt to set a CompoundStructure stored in a different compound as default")
# @property self.default_structure = cs
# def related_reactions(self): self.save()
# return (
# Reaction.objects.filter(package=self.package, educts__in=[self.default_structure]) @property
# | def related_pathways(self):
# Reaction.objects.filter(package=self.package, products__in=[self.default_structure]) pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
# ).order_by('name') 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 @staticmethod
@transaction.atomic @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 if smiles is None or smiles == '':
# Validity of SMILES etc 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(): if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound 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 # Generate Compound
c = Compound() c = Compound()
c.package = package 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 c.name = name
if description is not None: if description is not None and description != '':
c.description = description c.description = description
c.save() c.save()
normalized_smiles = smiles # chem.normalize(smiles) is_standardized = standardized_smiles == smiles
if normalized_smiles != smiles: if not is_standardized:
_ = CompoundStructure.create(c, normalized_smiles, name='Normalized structure of {}'.format(name), _ = CompoundStructure.create(c, standardized_smiles, name='Normalized structure of {}'.format(name),
description='{} (in its normalized form)'.format(description), description='{} (in its normalized form)'.format(description),
normalized_structure=True) 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.default_structure = cs
c.save() c.save()
return c 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: class Meta:
unique_together = [('uuid', 'package')] unique_together = [('uuid', 'package')]
@ -391,6 +454,10 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin):
def InChIKey(self): def InChIKey(self):
return FormatConverter.InChIKey(self.smiles) return FormatConverter.InChIKey(self.smiles)
@property
def canonical_smiles(self):
return FormatConverter.canonicalize(self.smiles)
@property @property
def as_svg(self): def as_svg(self):
return IndigoUtils.mol_to_svg(self.smiles) 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 # I think this only affects Django Admin which we are barely using
# # https://github.com/django-polymorphic/django-polymorphic/issues/229 # # https://github.com/django-polymorphic/django-polymorphic/issues/229
# _non_polymorphic = models.Manager() _non_polymorphic = models.Manager()
#
# class Meta: class Meta:
# base_manager_name = '_non_polymorphic' base_manager_name = '_non_polymorphic'
@abc.abstractmethod @abc.abstractmethod
def apply(self, *args, **kwargs): def apply(self, *args, **kwargs):

View File

@ -684,7 +684,7 @@ def package_compound(request, package_uuid, compound_uuid):
return render(request, 'objects/compound.html', context) return render(request, 'objects/compound.html', context)
if request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-compound': if hidden == 'delete-compound':
current_compound.delete() current_compound.delete()
@ -706,6 +706,8 @@ def package_compound(request, package_uuid, compound_uuid):
return redirect(current_compound.url) return redirect(current_compound.url)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()
# https://envipath.org/package/<uuid>/compound/<uuid>/structure # https://envipath.org/package/<uuid>/compound/<uuid>/structure
@ -724,7 +726,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
reviewed_compound_structure_qs = CompoundStructure.objects.none() reviewed_compound_structure_qs = CompoundStructure.objects.none()
unreviewed_compound_structure_qs = CompoundStructure.objects.none() unreviewed_compound_structure_qs = CompoundStructure.objects.none()
if package.reviewed: if current_package.reviewed:
reviewed_compound_structure_qs = current_compound.structures.order_by('name') reviewed_compound_structure_qs = current_compound.structures.order_by('name')
else: else:
unreviewed_compound_structure_qs = current_compound.structures.order_by('name') unreviewed_compound_structure_qs = current_compound.structures.order_by('name')
@ -734,13 +736,24 @@ def package_compound_structures(request, package_uuid, compound_uuid):
return render(request, 'collections/objects_list.html', context) return render(request, 'collections/objects_list.html', context)
elif request.method == 'POST':
structure_name = request.POST.get('structure-name')
structure_smiles = request.POST.get('structure-smiles')
structure_description = request.POST.get('structure-description')
cs = current_compound.add_structure(structure_smiles, structure_name, structure_description)
return redirect(cs.url)
else:
return HttpResponseBadRequest()
# https://envipath.org/package/<id>/compound/<id>/structure/<id> # https://envipath.org/package/<id>/compound/<id>/structure/<id>
def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid): def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid) current_package = PackageManager.get_package_by_id(current_user, package_uuid)
current_compound = Compound.objects.get(package=current_package, uuid=compound_uuid) current_compound = Compound.objects.get(package=current_package, uuid=compound_uuid)
current_structure = CompoundStructure.get(compound=current_compound, uuid=structure_uuid) current_structure = CompoundStructure.objects.get(compound=current_compound, uuid=structure_uuid)
if request.method == 'GET': if request.method == 'GET':
context = get_base_context(request) context = get_base_context(request)
@ -818,6 +831,8 @@ def package_rules(request, package_uuid):
r = Rule.create(current_package, rule_type, name=rule_name, description=rule_description, **params) r = Rule.create(current_package, rule_type, name=rule_name, description=rule_description, **params)
return redirect(r.url) return redirect(r.url)
else:
return HttpResponseBadRequest()
# https://envipath.org/package/<id>/rule/<id> # https://envipath.org/package/<id>/rule/<id>
def package_rule(request, package_uuid, rule_uuid): def package_rule(request, package_uuid, rule_uuid):

View File

@ -2,6 +2,10 @@
<a role="button" data-toggle="modal" data-target="#edit_compound_modal"> <a role="button" data-toggle="modal" data-target="#edit_compound_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a> <i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#add_structure_modal">
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
</li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#delete_compound_modal"> <a class="button" data-toggle="modal" data-target="#delete_compound_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a> <i class="glyphicon glyphicon-trash"></i> Delete Compound</a>

View File

@ -16,12 +16,13 @@
<input id="compound-name" class="form-control" name="compound-name" placeholder="Name"/> <input id="compound-name" class="form-control" name="compound-name" placeholder="Name"/>
<label for="compound-description">Description</label> <label for="compound-description">Description</label>
<input id="compound-description" class="form-control" name="compound-description" placeholder="Description"/> <input id="compound-description" class="form-control" name="compound-description" placeholder="Description"/>
<label for="compound-smiles">SMILES</label>
<input type="text" class="form-control" name="compound-smiles" placeholder="SMILES" id="compound-smiles">
<p></p> <p></p>
<div> <div>
<iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%" <iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
height="510"></iframe> height="510"></iframe>
</div> </div>
<input type="hidden" name="compound-smiles" id="compound-smiles">
<p></p> <p></p>
</form> </form>
</div> </div>
@ -35,16 +36,43 @@
</div> </div>
<script> <script>
function newCompoundModalketcherToNewCompoundModalTextInput() {
$('#compound-smiles').val(this.ketcher.getSmiles());
}
$(function() { $(function() {
$('#new_compound_modal_form_submit').on('click', function(e) {
e.preventDefault();
$(this).prop("disabled",true);
k = getKetcher('new_compound_ketcher'); $('#new_compound_ketcher').on('load', function() {
$('#compound-smiles').val(k.getSmiles()); const checkKetcherReady = () => {
win = this.contentWindow
if (win.ketcher && 'editor' in win.ketcher) {
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: newCompoundModalketcherToNewCompoundModalTextInput,
ketcher: win.ketcher
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
// submit form checkKetcherReady();
$('#new_compound_modal_form').submit(); })
$(function() {
$('#new_compound_modal_form_submit').on('click', function(e) {
e.preventDefault();
$(this).prop("disabled",true);
// submit form
$('#new_compound_modal_form').submit();
});
}); });
}); });
</script> </script>

View File

@ -0,0 +1,78 @@
{% load static %}
<div class="modal fade bs-modal-lg" id="add_structure_modal" tabindex="-1" aria-labelledby="add_structure_modal" aria-modal="true"
role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">Create a new Structure</h4>
</div>
<div class="modal-body">
<form id="add_structure_modal_form" accept-charset="UTF-8" action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}" data-remote="true" method="post">
{% csrf_token %}
<label for="structure-name">Name</label>
<input id="structure-name" class="form-control" name="structure-name" placeholder="Name"/>
<label for="structure-description">Description</label>
<input id="structure-description" class="form-control" name="structure-description" placeholder="Description"/>
<label for="structure-smiles">SMILES</label>
<input type="text" class="form-control" name="structure-smiles" placeholder="SMILES" id="structure-smiles">
<p></p>
<div>
<iframe id="add_structure_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
height="510"></iframe>
</div>
<p></p>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close
</button>
<button type="button" class="btn btn-primary" id="add_structure_modal_form_submit">Submit</button>
</div>
</div>
</div>
</div>
<script>
function newStructureModalketcherToNewStructureModalTextInput() {
$('#structure-smiles').val(this.ketcher.getSmiles());
}
$(function() {
$('#add_structure_ketcher').on('load', function() {
const checkKetcherReady = () => {
win = this.contentWindow
if (win.ketcher && 'editor' in win.ketcher) {
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: newStructureModalketcherToNewStructureModalTextInput,
ketcher: win.ketcher
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
})
$(function() {
$('#add_structure_modal_form_submit').on('click', function(e) {
e.preventDefault();
$(this).prop("disabled",true);
// submit form
$('#add_structure_modal_form').submit();
});
});
});
</script>

View File

@ -4,6 +4,7 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_compound_modal.html" %} {% include "modals/objects/edit_compound_modal.html" %}
{% include "modals/objects/add_structure_modal.html" %}
{% include "modals/objects/delete_compound_modal.html" %} {% include "modals/objects/delete_compound_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
@ -71,7 +72,20 @@
</div> </div>
</div> </div>
<!-- SMILES --> <!-- Canonical SMILES -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-canonical-smiles-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-canonical-smiles">Canonical SMILES Representation</a>
</h4>
</div>
<div id="compound-canonical-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ compound.default_structure.canonical_smiles }}
</div>
</div>
<!-- InChiKey -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
<a id="compound-inchi-link" data-toggle="collapse" data-parent="#compound-detail" <a id="compound-inchi-link" data-toggle="collapse" data-parent="#compound-detail"
@ -84,10 +98,35 @@
</div> </div>
</div> </div>
<!-- Reactions --> <!-- Reactions -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-reaction-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-reaction">Reactions</a>
</h4>
</div>
<div id="compound-reaction" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for r in compound.related_reactions %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
<!-- Pathways --> <!-- Pathways -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-pathway-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-pathway">Pathways</a>
</h4>
</div>
<div id="compound-pathway" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for r in compound.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
<!-- External Identifiers --> <!-- External Identifiers -->

View File

@ -0,0 +1,196 @@
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure
class CompoundTest(TestCase):
fixtures = ["test_fixture.json.gz"]
def setUp(self):
pass
@classmethod
def setUpClass(cls):
super(CompoundTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
@classmethod
def tearDownClass(cls):
pass
def tearDown(self):
pass
def test_smoke(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
)
self.assertEqual(c.default_structure.smiles,
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
self.assertEqual(c.name, 'Afoxolaner')
self.assertEqual(c.description, 'No Desc')
def test_missing_smiles(self):
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles=None,
name='Afoxolaner',
description='No Desc'
)
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles='',
name='Afoxolaner',
description='No Desc'
)
def test_smiles_are_trimmed(self):
c = Compound.create(
self.package,
smiles=' C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ',
name='Afoxolaner',
description='No Desc'
)
self.assertEqual(c.default_structure.smiles,
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
def test_name_and_description_optional(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
)
self.assertEqual(c.name, 'no name')
self.assertEqual(c.description, 'no description')
def test_empty_name_and_description_are_ignored(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='',
description='',
)
self.assertEqual(c.name, 'no name')
self.assertEqual(c.description, 'no description')
def test_deduplication(self):
c1 = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
)
c2 = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
)
# Check if create detects that this Compound already exist
# In this case the existing object should be returned
self.assertEqual(c1.pk, c2.pk)
self.assertEqual(len(self.package.compounds), 1)
def test_wrong_smiles(self):
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
)
def test_create_with_standardized_smiles(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
)
self.assertEqual(len(c.structures.all()), 1)
cs = c.structures.all()[0]
self.assertEqual(cs.normalized_structure, True)
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
def test_create_with_non_standardized_smiles(self):
c = Compound.create(
self.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1',
name='Non Standardized SMILES',
description='No Desc'
)
self.assertEqual(len(c.structures.all()), 2)
for cs in c.structures.all():
if cs.normalized_structure:
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
break
else:
# Loop finished without break, lets fail...
self.assertEqual(1, 2)
def test_add_structure_smoke(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
)
c.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
self.assertEqual(len(c.structures.all()), 2)
def test_add_structure_with_different_normalized_smiles(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
)
with self.assertRaises(ValueError):
c.add_structure(
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
'Different Standardized SMILES')
def test_delete(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardization Test',
description='No Desc'
)
c.delete()
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0)
self.assertEqual(CompoundStructure.objects.filter(compound__package=self.package).count(), 0)
def test_set_as_default_structure(self):
c1 = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
)
default_structure = c1.default_structure
c2 = c1.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
c1.set_default_structure(c2)
self.assertNotEqual(default_structure, c2)

View File

@ -70,13 +70,17 @@ class FormatConverter(object):
return Chem.MolFromSmiles(smiles) return Chem.MolFromSmiles(smiles)
@staticmethod @staticmethod
def to_smiles(mol): def to_smiles(mol, canonical=False):
return Chem.MolToSmiles(mol) return Chem.MolToSmiles(mol, canonical=canonical)
@staticmethod @staticmethod
def InChIKey(smiles): def InChIKey(smiles):
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles)) return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
@staticmethod
def canonicalize(smiles: str):
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
@staticmethod @staticmethod
def maccs(smiles): def maccs(smiles):
mol = Chem.MolFromSmiles(smiles) mol = Chem.MolFromSmiles(smiles)

View File

@ -36,7 +36,8 @@ def ensure_plugins_installed():
if not is_installed(package_name): if not is_installed(package_name):
install_wheel(wheel_path) install_wheel(wheel_path)
else: else:
print(f"Plugin already installed: {package_name}") # print(f"Plugin already installed: {package_name}")
pass
def discover_plugins(_cls: Type = None) -> Dict[str, Type]: def discover_plugins(_cls: Type = None) -> Dict[str, Type]: