sapiopycommons 31.0.13.23.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. sapiopycommons-31.0.13.23.12/.gitignore +8 -0
  2. sapiopycommons-31.0.13.23.12/PKG-INFO +8 -0
  3. sapiopycommons-31.0.13.23.12/pyproject.toml +19 -0
  4. sapiopycommons-31.0.13.23.12/src/sapiopycommons/__init__.py +0 -0
  5. sapiopycommons-31.0.13.23.12/src/sapiopycommons/chem/IndigoMolecules.py +48 -0
  6. sapiopycommons-31.0.13.23.12/src/sapiopycommons/chem/Molecules.py +202 -0
  7. sapiopycommons-31.0.13.23.12/src/sapiopycommons/chem/__init__.py +0 -0
  8. sapiopycommons-31.0.13.23.12/src/sapiopycommons/datatype/__init__.py +0 -0
  9. sapiopycommons-31.0.13.23.12/src/sapiopycommons/datatype/attachment_util.py +65 -0
  10. sapiopycommons-31.0.13.23.12/src/sapiopycommons/eln/__init__.py +0 -0
  11. sapiopycommons-31.0.13.23.12/src/sapiopycommons/eln/experiment_handler.py +772 -0
  12. sapiopycommons-31.0.13.23.12/src/sapiopycommons/files/__init__.py +0 -0
  13. sapiopycommons-31.0.13.23.12/src/sapiopycommons/files/file_bridge.py +87 -0
  14. sapiopycommons-31.0.13.23.12/src/sapiopycommons/files/file_util.py +366 -0
  15. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/__init__.py +0 -0
  16. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/aliases.py +55 -0
  17. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/custom_report_util.py +64 -0
  18. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/exceptions.py +26 -0
  19. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/popup_util.py +447 -0
  20. sapiopycommons-31.0.13.23.12/src/sapiopycommons/general/time_util.py +128 -0
  21. sapiopycommons-31.0.13.23.12/src/sapiopycommons/recordmodel/__init__.py +0 -0
  22. sapiopycommons-31.0.13.23.12/src/sapiopycommons/recordmodel/record_handler.py +608 -0
  23. sapiopycommons-31.0.13.23.12/src/sapiopycommons/rules/__init__.py +0 -0
  24. sapiopycommons-31.0.13.23.12/src/sapiopycommons/rules/eln_rule_handler.py +95 -0
  25. sapiopycommons-31.0.13.23.12/src/sapiopycommons/rules/on_save_rule_handler.py +95 -0
  26. sapiopycommons-31.0.13.23.12/src/sapiopycommons/webhook/__init__.py +0 -0
  27. sapiopycommons-31.0.13.23.12/src/sapiopycommons/webhook/webhook_handlers.py +242 -0
@@ -0,0 +1,8 @@
1
+ **/.project
2
+ **/.classpath
3
+ **/*.iml
4
+ **/.idea
5
+ **/.settings
6
+ **/*.versionsBackup
7
+ **/target
8
+ /sapiopycommons/sapiopycommons.egg-info/
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: sapiopycommons
3
+ Version: 31.0.13.23.12
4
+ Summary: Sapio Commons for Python 3
5
+ Project-URL: Homepage, https://github.com/sapiosciences
6
+ Author-email: Yechen Qiao <yqiao@sapiosciences.com>
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: sapiopylib>=2023.12.13.174
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sapiopycommons"
7
+ version='31.0.13.23.12'
8
+ authors = [
9
+ { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
10
+ ]
11
+ description = "Sapio Commons for Python 3"
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ 'sapiopylib>=2023.12.13.174'
15
+ ]
16
+ homepage = "https://www.sapiosciences.com/"
17
+
18
+ [project.urls]
19
+ "Homepage" = "https://github.com/sapiosciences"
@@ -0,0 +1,48 @@
1
+ from indigo import Indigo, IndigoObject
2
+ from indigo.inchi import IndigoInchi
3
+ from indigo.renderer import IndigoRenderer
4
+
5
+ indigo = Indigo()
6
+ renderer = IndigoRenderer(indigo)
7
+ indigo.setOption("render-output-format", "svg")
8
+ indigo.setOption("ignore-stereochemistry-errors", True)
9
+ indigo.setOption("aromaticity-model", "generic")
10
+ indigo.setOption("render-coloring", True)
11
+ indigo_inchi = IndigoInchi(indigo);
12
+
13
+
14
+ def highlight_mol_substructure(query: IndigoObject, sub_match: IndigoObject):
15
+ """
16
+ Highlight the bonds and atoms for substructure search result
17
+ :param sub_match: The substructure search match obtained from indigo.substructureMatcher(mol).match(query)
18
+ :param query: The query we were running to match the original structure
19
+ """
20
+ for qatom in query.iterateAtoms():
21
+ atom = sub_match.mapAtom(qatom)
22
+ if atom is None:
23
+ continue
24
+ atom.highlight()
25
+
26
+ for nei in atom.iterateNeighbors():
27
+ if not nei.isPseudoatom() and not nei.isRSite() and nei.atomicNumber() == 1:
28
+ nei.highlight()
29
+ nei.bond().highlight()
30
+
31
+ for bond in query.iterateBonds():
32
+ bond = sub_match.mapBond(bond)
33
+ if bond is None:
34
+ continue
35
+ bond.highlight()
36
+
37
+
38
+ def highlight_reactions(query_reaction_smarts: IndigoObject, reaction_match: IndigoObject):
39
+ """
40
+ Highlight the bonds and atoms for substructure search result of reaction that's in the query and survived the mapping.
41
+ :param query_reaction_smarts: The query we ran substructure search on.
42
+ :param reaction_match: The substructure search match obtained from indigo.substructureMatcher(reaction).match(query)
43
+ :return:
44
+ """
45
+ for q_mol in query_reaction_smarts.iterateMolecules():
46
+ matched_mol = reaction_match.mapMolecule(q_mol)
47
+ sub_match = indigo.substructureMatcher(matched_mol).match(q_mol)
48
+ highlight_mol_substructure(q_mol, sub_match)
@@ -0,0 +1,202 @@
1
+ # Author Yechen Qiao
2
+ # Common Molecule Utilities for Molecule Transfers with Sapio
3
+
4
+ from rdkit import Chem
5
+ from rdkit.Chem import Crippen, MolToInchi
6
+ from rdkit.Chem import Descriptors
7
+ from rdkit.Chem import rdMolDescriptors
8
+ from rdkit.Chem.EnumerateStereoisomers import StereoEnumerationOptions, EnumerateStereoisomers
9
+ from rdkit.Chem.MolStandardize import rdMolStandardize
10
+ from rdkit.Chem.SaltRemover import SaltRemover
11
+ from rdkit.Chem.rdchem import Mol
12
+
13
+ from sapiopycommons.chem.IndigoMolecules import indigo, renderer, indigo_inchi
14
+
15
+ metal_disconnector = rdMolStandardize.MetalDisconnector()
16
+ tautomer_params = Chem.MolStandardize.rdMolStandardize.CleanupParameters()
17
+ tautomer_params.tautomerRemoveSp3Stereo = False
18
+ tautomer_params.tautomerRemoveBondStereo = False
19
+ tautomer_params.tautomerReassignStereo = False
20
+ tautomer_params.tautomerRemoveIsotopicHs = True
21
+ enumerator = rdMolStandardize.TautomerEnumerator(tautomer_params)
22
+
23
+ def neutralize_atoms(mol) -> Mol:
24
+ """
25
+ Neutralize atoms per https://baoilleach.blogspot.com/2019/12/no-charge-simple-approach-to.html
26
+ """
27
+ pattern = Chem.MolFromSmarts("[+1!h0!$([*]~[-1,-2,-3,-4]),-1!$([*]~[+1,+2,+3,+4])]")
28
+ at_matches = mol.GetSubstructMatches(pattern)
29
+ at_matches_list = [y[0] for y in at_matches]
30
+ if len(at_matches_list) > 0:
31
+ for at_idx in at_matches_list:
32
+ atom = mol.GetAtomWithIdx(at_idx)
33
+ chg = atom.GetFormalCharge()
34
+ hcount = atom.GetTotalNumHs()
35
+ atom.SetFormalCharge(0)
36
+ atom.SetNumExplicitHs(hcount - chg)
37
+ atom.UpdatePropertyCache()
38
+ return mol
39
+
40
+
41
+ def find_all_possible_stereoisomers(m: Mol, only_unassigned=True, try_embedding=True, unique=True, max_isomers=200) \
42
+ -> list[Mol]:
43
+ """
44
+ Find all possible candidates of stereoisomers given the current molecule
45
+ :param m: The molecule to search for
46
+ :param only_unassigned: Whether to only permute on unspecified stereocenter.
47
+ :param try_embedding: if set the process attempts to generate a standard RDKit distance geometry conformation for
48
+ the stereisomer.
49
+ If this fails, we assume that the stereoisomer is non-physical and don't return it
50
+ :param unique: whether to remove duplicates by isomer identity
51
+ :param max_isomers: Maximum number of search results to return.
52
+ """
53
+ # noinspection PyBroadException
54
+ try:
55
+ opts = StereoEnumerationOptions(tryEmbedding=try_embedding, unique=unique, onlyUnassigned=only_unassigned,
56
+ maxIsomers=max_isomers)
57
+ return list(EnumerateStereoisomers(m, options=opts))
58
+ except:
59
+ return []
60
+
61
+
62
+ def has_chiral_centers(m: Mol):
63
+ """
64
+ Returns true iff the molecule provided has at least 1 chiral centers (when stereochemistry is relevant)
65
+ :param m: The molecule to test.
66
+ """
67
+ # noinspection PyBroadException
68
+ try:
69
+ chiral_centers: list = Chem.FindMolChiralCenters(m, force=True, includeUnassigned=True,
70
+ useLegacyImplementation=False)
71
+ return len(chiral_centers) > 0
72
+ except:
73
+ return False
74
+
75
+
76
+ def mol_to_img(mol_str: str) -> str:
77
+ """
78
+ Convert molecule into image
79
+ :param mol_str: The molecule INCHI
80
+ :return: The SVG image text.
81
+ """
82
+ mol = indigo.loadMolecule(mol_str)
83
+ return renderer.renderToString(mol)
84
+
85
+
86
+
87
+ def mol_to_sapio_partial_pojo(mol: Mol):
88
+ """
89
+ Get the minimum information about molecule to Sapio, just its SMILES, V3000, and image data.
90
+ :param mol: The molecule to read the simplified data from
91
+ """
92
+ Chem.SanitizeMol(mol)
93
+ mol.UpdatePropertyCache()
94
+ smiles = Chem.MolToSmiles(mol)
95
+ molBlock = Chem.MolToMolBlock(mol)
96
+ img = mol_to_img(mol)
97
+ molecule = dict()
98
+ molecule["smiles"] = smiles
99
+ molecule["molBlock"] = molBlock
100
+ molecule["image"] = img
101
+ return molecule
102
+
103
+
104
+ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
105
+ normalize: bool = False, remove_salt: bool = False, make_images: bool = False,
106
+ salt_def: str | None = None, canonical_tautomer: bool = True):
107
+ """
108
+ Convert a molecule in RDKit to a molecule POJO in Sapio.
109
+ :param mol: The molecule in RDKit
110
+ :param include_stereoisomers: If true, will compute all stereoisomer permutations of this molecule.
111
+ :param normalize If true, will normalize the functional groups and return normalized result.
112
+ :param remove_salt If true, we will remove salts iteratively from the molecule before returning their data.
113
+ We will also populate desaltedList with molecules we deleted.
114
+ :param salt_def: if not none, specifies custom salt to be used during the desalt process.
115
+ :param canonical_tautomer: if True, we will attempt to compute canonical tautomer for the molecule. Slow!
116
+ This is needed for a registry. Note it stops after enumeration of 1000.
117
+ :return: The molecule POJO for Sapio.
118
+ """
119
+ molecule = dict()
120
+ Chem.SanitizeMol(mol)
121
+ mol.UpdatePropertyCache()
122
+ Chem.GetSymmSSSR(mol)
123
+ if normalize:
124
+ try:
125
+ mol = Chem.RemoveHs(mol)
126
+ mol = metal_disconnector.Disconnect(mol)
127
+ mol = rdMolStandardize.Normalize(mol)
128
+ molecule["normError"] = ""
129
+ except Exception as e:
130
+ molecule["normError"] = str(e)
131
+ if remove_salt:
132
+ try:
133
+ remover = SaltRemover(defnData=salt_def)
134
+ mol, deleted = remover.StripMolWithDeleted(mol)
135
+ molecule["desaltedList"] = [Chem.MolToSmarts(x) for x in deleted]
136
+ molecule["desaltError"] = ""
137
+ except Exception as e:
138
+ molecule["desaltError"] = str(e)
139
+ molecule["desaltedList"] = []
140
+ if normalize or remove_salt:
141
+ mol = neutralize_atoms(mol)
142
+ #//CR-46021 Jarvis: no canonicalize tautomers.
143
+ if canonical_tautomer:
144
+ mol = enumerator.Canonicalize(mol)
145
+ Chem.SanitizeMol(mol)
146
+ mol.UpdatePropertyCache()
147
+ Chem.GetSymmSSSR(mol)
148
+ smiles = Chem.MolToSmiles(mol)
149
+ cLogP = Crippen.MolLogP(mol)
150
+ tpsa = Descriptors.TPSA(mol)
151
+ amw = Descriptors.MolWt(mol)
152
+ exactMass = Descriptors.ExactMolWt(mol)
153
+ molFormula = rdMolDescriptors.CalcMolFormula(mol)
154
+ charge = Chem.GetFormalCharge(mol)
155
+ molBlock = Chem.MolToMolBlock(mol)
156
+
157
+ molecule["cLogP"] = cLogP
158
+ molecule["tpsa"] = tpsa
159
+ molecule["amw"] = amw
160
+ molecule["exactMass"] = exactMass
161
+ molecule["molFormula"] = molFormula
162
+ molecule["charge"] = charge
163
+ molecule["numHBondAcceptors"] = rdMolDescriptors.CalcNumHBA(mol)
164
+ # This is number of H-Bond Donor
165
+ molecule["numHBonds"] = rdMolDescriptors.CalcNumHBD(mol)
166
+ molecule["molBlock"] = molBlock
167
+ rdkit_inchi = MolToInchi(mol)
168
+ # If INCHI is completely invalid, we fail this molecule.
169
+ if not rdkit_inchi:
170
+ MolToInchi(mol, treatWarningAsError=True)
171
+ if make_images:
172
+ img = mol_to_img(smiles)
173
+ molecule["image"] = img
174
+ else:
175
+ molecule["image"] = None
176
+ # We need to test the INCHI can be loaded back to indigo.
177
+ indigo_mol = indigo.loadMolecule(molBlock)
178
+ indigo_mol.aromatize()
179
+ indigo_inchi_str = indigo_inchi.getInchi(indigo_mol)
180
+ molecule["inchi"] = indigo_inchi_str
181
+ indigo_inchi_key_str = indigo_inchi.getInchiKey(indigo_inchi_str)
182
+ molecule["inchiKey"] = indigo_inchi_key_str
183
+ molecule["smiles"] = indigo_mol.smiles()
184
+
185
+ if include_stereoisomers and has_chiral_centers(mol):
186
+ stereoisomers = find_all_possible_stereoisomers(mol, only_unassigned=False, try_embedding=False, unique=True)
187
+ molecule["stereoisomers"] = [mol_to_sapio_partial_pojo(x) for x in stereoisomers]
188
+ return molecule
189
+
190
+
191
+ def mol_to_sapio_compound(mol: Mol, include_stereoisomers: bool = False,
192
+ salt_def: str | None = None, resolve_canonical: bool = True,
193
+ make_images: bool = False, canonical_tautomer: bool = True):
194
+ ret = dict()
195
+ ret['originalMol'] = mol_to_sapio_substance(mol, include_stereoisomers,
196
+ normalize=False, remove_salt=False, make_images=make_images,
197
+ canonical_tautomer=canonical_tautomer)
198
+ if resolve_canonical:
199
+ ret['canonicalMol'] = mol_to_sapio_substance(mol, include_stereoisomers=False,
200
+ normalize=True, remove_salt=True, make_images=make_images,
201
+ salt_def=salt_def, canonical_tautomer=canonical_tautomer)
202
+ return ret
@@ -0,0 +1,65 @@
1
+ import io
2
+
3
+ from sapiopylib.rest.pojo.DataRecord import DataRecord
4
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
5
+ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager
6
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
7
+
8
+ from sapiopycommons.general.aliases import AliasUtil, SapioRecord
9
+ from sapiopycommons.general.exceptions import SapioException
10
+
11
+
12
+ # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
13
+ class AttachmentUtil:
14
+ @staticmethod
15
+ def get_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord) -> bytes:
16
+ """
17
+ Get the data bytes for the given attachment record. Makes a webservice call to retrieve the data.
18
+ :param context: The current webhook context.
19
+ :param attachment: The attachment record.
20
+ :return: The bytes for the attachment's file data.
21
+ """
22
+ attachment = AliasUtil.to_data_record(attachment)
23
+ with io.BytesIO() as data_sink:
24
+ def consume_data(chunk: bytes):
25
+ data_sink.write(chunk)
26
+ context.data_record_manager.get_attachment_data(attachment, consume_data)
27
+ data_sink.flush()
28
+ data_sink.seek(0)
29
+ file_bytes = data_sink.read()
30
+ return file_bytes
31
+
32
+ @staticmethod
33
+ def set_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord,
34
+ file_name: str, file_bytes: bytes) -> None:
35
+ """
36
+ Set the attachment data for a given attachment record. Makes a webservice call to set the data.
37
+ :param context: The current webhook context.
38
+ :param attachment: The attachment record. Must be an existing data record that is an attachment type.
39
+ :param file_name: The name of the attachment.
40
+ :param file_bytes: The bytes of the attachment data.
41
+ """
42
+ if attachment.record_id < 0:
43
+ raise SapioException("Provided record cannot have its attachment data set, as it does not exist in the "
44
+ "system yet.")
45
+ attachment = AliasUtil.to_data_record(attachment)
46
+ with io.BytesIO(file_bytes) as stream:
47
+ context.data_record_manager.set_attachment_data(attachment, file_name, stream)
48
+
49
+ @staticmethod
50
+ def create_attachment(context: SapioWebhookContext, file_name: str, file_bytes: bytes,
51
+ wrapper_type: type[WrappedType]) -> WrappedType:
52
+ """
53
+ Create an attachment data type and initialize its attachment bytes at the same time.
54
+ Makes a webservice call to create the attachment record and a second to set its bytes.
55
+ :param context: The current webhook context.
56
+ :param file_name: The name of the attachment.
57
+ :param file_bytes: THe bytes of the attachment data.
58
+ :param wrapper_type: The attachment type to create.
59
+ :return: A record model for the newly created attachment.
60
+ """
61
+ inst_man = RecordModelManager(context.user).instance_manager
62
+ attachment: DataRecord = context.data_record_manager.add_data_record(wrapper_type.DATA_TYPE_NAME)
63
+ attachment: WrappedType = inst_man.add_existing_record_of_type(attachment, wrapper_type)
64
+ AttachmentUtil.set_attachment_bytes(context, attachment, file_name, file_bytes)
65
+ return attachment