sapiopycommons 2025.7.10a595__py3-none-any.whl → 2025.7.15a611__py3-none-any.whl

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.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

@@ -7,7 +7,6 @@ import traceback
7
7
  from abc import abstractmethod
8
8
  from logging import Logger
9
9
 
10
- from sapiopylib.rest import UserManagerService, GroupManagerService, MessengerService
11
10
  from sapiopylib.rest.AccessionService import AccessionManager
12
11
  from sapiopylib.rest.CustomReportService import CustomReportManager
13
12
  from sapiopylib.rest.DashboardManager import DashboardManager
@@ -16,10 +15,13 @@ from sapiopylib.rest.DataRecordManagerService import DataRecordManager
16
15
  from sapiopylib.rest.DataService import DataManager
17
16
  from sapiopylib.rest.DataTypeService import DataTypeManager
18
17
  from sapiopylib.rest.ELNService import ElnManager
18
+ from sapiopylib.rest.GroupManagerService import VeloxGroupManager
19
+ from sapiopylib.rest.MessengerService import SapioMessenger
19
20
  from sapiopylib.rest.PicklistService import PickListManager
20
21
  from sapiopylib.rest.ReportManager import ReportManager
21
22
  from sapiopylib.rest.SesssionManagerService import SessionManager
22
23
  from sapiopylib.rest.User import SapioUser
24
+ from sapiopylib.rest.UserManagerService import VeloxUserManager
23
25
  from sapiopylib.rest.WebhookService import AbstractWebhookHandler
24
26
  from sapiopylib.rest.pojo.Message import VeloxLogMessage, VeloxLogLevel
25
27
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import PopupType
@@ -85,9 +87,9 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
85
87
  """A class for making requests to the data type webservice endpoints."""
86
88
  eln_man: ElnManager
87
89
  """A class for making requests to the ELN management webservice endpoints."""
88
- group_man: GroupManagerService
90
+ group_man: VeloxGroupManager
89
91
  """A class for making requests to the group management webservice endpoints."""
90
- messenger: MessengerService
92
+ messenger: SapioMessenger
91
93
  """A class for making requests to the message webservice endpoints."""
92
94
  list_man: PickListManager
93
95
  """A class for making requests to the pick list webservice endpoints."""
@@ -95,7 +97,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
95
97
  """A class for making requests to the report webservice endpoints."""
96
98
  session_man: SessionManager
97
99
  """A class for making requests to the session management webservice endpoints."""
98
- user_man: UserManagerService
100
+ user_man: VeloxUserManager
99
101
  """A class for making requests to the user management webservice endpoints."""
100
102
 
101
103
  rec_man: RecordModelManager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.7.10a595
3
+ Version: 2025.7.15a611
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -1,11 +1,12 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ sapiopycommons/ai/tool_of_tools.py,sha256=zYmQ4rNX-qYQnc-vNDnYZjtv9JgmQAmVVuHfVOdBF3w,46984
2
4
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/callbacks/callback_util.py,sha256=rps6RA6lmzCOwiBqPQAe2Mkf0CIF4RjHPQTYgduMAgE,153011
5
+ sapiopycommons/callbacks/callback_util.py,sha256=RCMTf_ix7C_FkToy7NZMwjQz8cZZ84X09E4mpIVev7s,153559
4
6
  sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
5
- sapiopycommons/chem/IndigoMolecules.py,sha256=30bsnZ2o4fJXUV6kUTI-I6fDa7bQj7zfE3rOQQ7WD5M,5287
7
+ sapiopycommons/chem/IndigoMolecules.py,sha256=7ucCaRMLu1zfH2uPIvXwRTSdpNcS03O1P9p_O-5B4xQ,5110
6
8
  sapiopycommons/chem/Molecules.py,sha256=mVqPn32MPMjF0iZas-5MFkS-upIdoW5OB72KKZmJRJA,12523
7
9
  sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sapiopycommons/chem/ps_commons.py,sha256=TobN8V9FW32x7o1C6i_WsqGkdldbyVTUfhG5W0xAxMM,23598
9
10
  sapiopycommons/customreport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  sapiopycommons/customreport/auto_pagers.py,sha256=89p-tik0MhsOplYje6LbAW4WClldpAmb8YXFDoXhIlY,17144
11
12
  sapiopycommons/customreport/column_builder.py,sha256=0RO53e9rKPZ07C--KcepN6_tpRw_FxF3O9vdG0ilKG8,3014
@@ -28,6 +29,7 @@ sapiopycommons/files/complex_data_loader.py,sha256=T39veNhvYl6j_uZjIIJ8Mk5Aa7otR
28
29
  sapiopycommons/files/file_bridge.py,sha256=vKbqxPexi15epr_-_qLrEfYoxNxB031mXN92iVtOMqE,9511
29
30
  sapiopycommons/files/file_bridge_handler.py,sha256=SEYDIQhSCmjI6qyLdDJE8JVKSd0WYvF7JvAq_Ahp9Do,25503
30
31
  sapiopycommons/files/file_data_handler.py,sha256=f96MlkMuQhUCi4oLnzJK5AiuElCp5jLI8_sJkZVwpws,36779
32
+ sapiopycommons/files/file_text_converter.py,sha256=Gaj_divTiKXWd6flDOgrxNXpcn9fDWqxX6LUG0joePk,7516
31
33
  sapiopycommons/files/file_util.py,sha256=djouyGjsYgWzjz2OBRnSeMDgj6NrsJUm1a2J93J8Wco,31915
32
34
  sapiopycommons/files/file_validator.py,sha256=ryg22-93csmRO_Pv0ZpWphNkB74xWZnHyJ23K56qLj0,28761
33
35
  sapiopycommons/files/file_writer.py,sha256=hACVl0duCjP28gJ1NPljkjagNCLod0ygUlPbvUmRDNM,17605
@@ -52,7 +54,7 @@ sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
52
54
  sapiopycommons/processtracking/custom_workflow_handler.py,sha256=eYKdYlwo8xx-6AkB_iPUBNV9yDoNvW2h_Sm3i8JpmRU,25844
53
55
  sapiopycommons/processtracking/endpoints.py,sha256=5AJLbhRKQsOeeOdQa888xcCJZD5aavxD-DHZ36Qob_M,12548
54
56
  sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- sapiopycommons/recordmodel/record_handler.py,sha256=HfYOl_dDHFd0SEQS3g48_a4zsm36ODWkvZunwzCFDos,90666
57
+ sapiopycommons/recordmodel/record_handler.py,sha256=WxmgrWQ3nX3eVZSHJY7e8fj7CI7azSyEyovmYcy9098,95021
56
58
  sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
59
  sapiopycommons/rules/eln_rule_handler.py,sha256=MnE-eSl1kNfaXWFi9elTOC9V2fdUzrwWTvCHUprC8_I,11388
58
60
  sapiopycommons/rules/on_save_rule_handler.py,sha256=fkNIlslAZZ0BUrRiwecyvf42JBR8FpCCQ6DBNKXP2jE,11155
@@ -61,9 +63,9 @@ sapiopycommons/sftpconnect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
61
63
  sapiopycommons/sftpconnect/sftp_builder.py,sha256=lFK3FeXk-sFLefW0hqY8WGUQDeYiGaT6yDACzT_zFgQ,3015
62
64
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
65
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
64
- sapiopycommons/webhook/webhook_handlers.py,sha256=tUVNCw05CDGu1gFDm2g558hX_O203WVm_n__ojjoRRM,39841
66
+ sapiopycommons/webhook/webhook_handlers.py,sha256=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
65
67
  sapiopycommons/webhook/webservice_handlers.py,sha256=tyaYGG1-v_JJrJHZ6cy5mGCxX9z1foLw7pM4MDJlFxs,14297
66
- sapiopycommons-2025.7.10a595.dist-info/METADATA,sha256=LBGGU5Is2VuGiTokZ-wlD74iKgZnbtr-_0Yp5-R4Z1A,3143
67
- sapiopycommons-2025.7.10a595.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
68
- sapiopycommons-2025.7.10a595.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
69
- sapiopycommons-2025.7.10a595.dist-info/RECORD,,
68
+ sapiopycommons-2025.7.15a611.dist-info/METADATA,sha256=ad1RZT1C8Wl0AsDh77kH6ZxFNeY2o5qUGOVy0yWI3LQ,3143
69
+ sapiopycommons-2025.7.15a611.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
70
+ sapiopycommons-2025.7.15a611.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
71
+ sapiopycommons-2025.7.15a611.dist-info/RECORD,,
@@ -1,523 +0,0 @@
1
- """
2
- Parallel Synthesis Commons
3
- Author: Yechen Qiao
4
- """
5
- import json
6
- from dataclasses import dataclass
7
- from typing import Any
8
-
9
- from indigo import IndigoObject
10
- from sapiopycommons.chem.IndigoMolecules import indigo, get_aromatic_dearomatic_forms, renderer
11
-
12
-
13
- class SerializableQueryMolecule:
14
- mol_block: str
15
- smarts: str
16
- render_svg: str
17
-
18
- @staticmethod
19
- def create(query_molecule: IndigoObject):
20
- aromatic, dearomatic = get_aromatic_dearomatic_forms(query_molecule)
21
- ret: SerializableQueryMolecule = SerializableQueryMolecule()
22
- ret.mol_block = aromatic.molfile()
23
- ret.smarts = aromatic.smarts()
24
- ret.render_svg = renderer.renderToString(dearomatic)
25
- return ret
26
-
27
- def to_json(self) -> dict[str, Any]:
28
- """
29
- Save the SerializableQueryMolecule to a JSON string.
30
- :return: A JSON string representation of the query molecule.
31
- """
32
- return {
33
- "mol_block": self.mol_block,
34
- "smarts": self.smarts,
35
- "render_svg": self.render_svg
36
- }
37
-
38
-
39
- class SerializableMoleculeMatch:
40
- """
41
- A serializable match that stores and loads a match that can be serialized to JSON.
42
- """
43
- _query_atom_to_atom: dict[int, int]
44
- _query_bond_to_bond: dict[int, int]
45
- _query_molecule_file: str
46
- _matching_molecule_file: str
47
- _query_molecule: IndigoObject
48
- _matching_molecule: IndigoObject
49
- _record_id: int # Only when received from Sapio.
50
-
51
- @property
52
- def record_id(self) -> int:
53
- """
54
- Get the record ID of the match.
55
- :return: The record ID.
56
- """
57
- return self._record_id
58
-
59
- def __str__(self):
60
- return json.dumps(self.to_json())
61
-
62
- def __hash__(self):
63
- return hash(self._query_molecule.smarts())
64
-
65
- def __eq__(self, other):
66
- if not isinstance(other, SerializableMoleculeMatch):
67
- return False
68
- if self._query_atom_to_atom == other._query_atom_to_atom and \
69
- self._query_bond_to_bond == other._query_bond_to_bond and \
70
- self._query_molecule_file == other._query_molecule_file and \
71
- self._matching_molecule_file == other._matching_molecule_file and \
72
- self._record_id == other._record_id:
73
- return True
74
- if self._query_molecule.smarts() != other._query_molecule.smarts():
75
- return False
76
- return are_symmetrical_subs(self, other)
77
-
78
- def mapAtom(self, atom: IndigoObject) -> IndigoObject | None:
79
- if not self._query_atom_to_atom or atom.index() not in self._query_atom_to_atom:
80
- return None
81
- index = self._query_atom_to_atom[atom.index()]
82
- return self._matching_molecule.getAtom(index)
83
-
84
- def mapBond(self, bond: IndigoObject) -> IndigoObject | None:
85
- if not self._query_bond_to_bond or bond.index() not in self._query_bond_to_bond:
86
- return None
87
- index = self._query_bond_to_bond[bond.index()]
88
- return self._matching_molecule.getBond(index)
89
-
90
- def to_json(self) -> dict[str, Any]:
91
- """
92
- Save the SerializableMoleculeMatch to a JSON string.
93
- :return: A JSON string representation of the match.
94
- """
95
- return {
96
- "query_molecule_file": self._query_molecule_file,
97
- "matching_molecule_file": self._matching_molecule_file,
98
- "query_atom_to_atom": self._query_atom_to_atom,
99
- "query_bond_to_bond": self._query_bond_to_bond,
100
- "record_id": self._record_id
101
- }
102
-
103
- @staticmethod
104
- def from_json(json_dct: dict[str, Any]) -> 'SerializableMoleculeMatch':
105
- """
106
- Load a SerializableMoleculeMatch from a JSON string.
107
- :param json_dct: A JSON string representation of the match.
108
- :return: A new SerializableMoleculeMatch instance.
109
- """
110
- smm = SerializableMoleculeMatch()
111
- smm._query_atom_to_atom = {}
112
- for key, value in json_dct.get("query_atom_to_atom", {}).items():
113
- smm._query_atom_to_atom[int(key)] = int(value)
114
- smm._query_bond_to_bond = {}
115
- for key, value in json_dct.get("query_bond_to_bond", {}).items():
116
- smm._query_bond_to_bond[int(key)] = int(value)
117
- smm._query_molecule_file = json_dct.get("query_molecule_file")
118
- smm._matching_molecule_file = json_dct.get("matching_molecule_file")
119
- smm._query_molecule = indigo.loadQueryMolecule(smm._query_molecule_file)
120
- smm._matching_molecule = indigo.loadMolecule(smm._matching_molecule_file)
121
- smm._record_id = json_dct.get("record_id", 0) # Default to 0 if not present
122
- return smm
123
-
124
- @staticmethod
125
- def create(query_molecule: IndigoObject, matching_molecule: IndigoObject,
126
- match: IndigoObject) -> 'SerializableMoleculeMatch':
127
- """
128
- Create a SerializableMoleculeMatch from a query molecule, matching molecule, and match.
129
- :param query_molecule: The query molecule.
130
- :param matching_molecule: The matching molecule.
131
- :param match: The match object containing atom mappings.
132
- :return: A new SerializableMoleculeMatch instance.
133
- """
134
- smm = SerializableMoleculeMatch()
135
- smm._query_atom_to_atom = {}
136
- smm._query_bond_to_bond = {}
137
- smm._query_molecule = query_molecule.clone()
138
- smm._matching_molecule = matching_molecule.clone()
139
- smm._query_molecule_file = query_molecule.molfile()
140
- smm._matching_molecule_file = matching_molecule.molfile()
141
- smm._record_id = 0
142
-
143
- for qatom in query_molecule.iterateAtoms():
144
- concrete_atom = match.mapAtom(qatom)
145
- if concrete_atom is None:
146
- continue
147
- smm._query_atom_to_atom[qatom.index()] = concrete_atom.index()
148
-
149
- for qbond in query_molecule.iterateBonds():
150
- concrete_bond = match.mapBond(qbond)
151
- if concrete_bond is None:
152
- continue
153
- smm._query_bond_to_bond[qbond.index()] = concrete_bond.index()
154
- return smm
155
-
156
- def get_matched_molecule_copy(self):
157
- return self._matching_molecule.clone()
158
-
159
-
160
- @dataclass
161
- class ReplacementReaction:
162
- """
163
- A replacement reaction stores reactio template with 1 reactant replaced by specific user match.
164
- """
165
- reaction: IndigoObject
166
- reaction_reactant: IndigoObject
167
- replacement_reactant: IndigoObject
168
- replacement_query_reaction_match: SerializableMoleculeMatch
169
-
170
-
171
- # noinspection PyProtectedMember
172
- def highlight_mol_substructure_serial_match(molecule: IndigoObject, serializable_match: SerializableMoleculeMatch):
173
- """
174
- Highlight the substructure in the molecule based on the SerializableMoleculeMatch.
175
- :param molecule: The molecule to highlight.
176
- :param serializable_match: The SerializableMoleculeMatch containing atom mappings.
177
- """
178
- for qatom in serializable_match._query_molecule.iterateAtoms():
179
- atom = serializable_match.mapAtom(qatom)
180
- if atom is None:
181
- continue
182
- atom.highlight()
183
-
184
- for nei in atom.iterateNeighbors():
185
- if not nei.isPseudoatom() and not nei.isRSite() and nei.atomicNumber() == 1:
186
- nei.highlight()
187
- nei.bond().highlight()
188
-
189
- for bond in serializable_match._query_molecule.iterateBonds():
190
- bond = serializable_match.mapBond(bond)
191
- if bond is None:
192
- continue
193
- bond.highlight()
194
-
195
-
196
- def clear_highlights(molecule: IndigoObject):
197
- """
198
- Clear all highlights in the molecule.
199
- :param molecule: The molecule to clear highlights from.
200
- """
201
- for atom in molecule.iterateAtoms():
202
- atom.unhighlight()
203
- for bond in molecule.iterateBonds():
204
- bond.unhighlight()
205
-
206
-
207
- def clear_reaction_highlights(reaction: IndigoObject):
208
- """
209
- Clear all highlights in the reaction.
210
- :param reaction: The reaction to clear highlights from.
211
- """
212
- for reactant in reaction.iterateReactants():
213
- clear_highlights(reactant)
214
- for product in reaction.iterateProducts():
215
- clear_highlights(product)
216
-
217
-
218
- def reserve_atom_mapping_number_of_search_result(q_reaction: IndigoObject, q_reactant: IndigoObject,
219
- new_reaction_reactant: IndigoObject, new_reaction: IndigoObject,
220
- sub_match: SerializableMoleculeMatch) -> None:
221
- """
222
- Set the atom mapping number on the query molecule based on the atom mapping number of the sub_match molecule, if it exists.
223
- :param new_reaction: The new reaction where the new reaction's reactant is found. This will be the target reaciton to write AAM to.
224
- :param new_reaction_reactant: The new reaction's reactant where the AAM will be written to.
225
- :param q_reactant: The query reactant from the query reaction that is being matched.
226
- :param q_reaction: The query reaction that contains the query reactant for the sub_match.
227
- :param sub_match: The substructure search match obtained from indigo.substructureMatcher(mol).match(query).
228
- """
229
- for query_atom in q_reactant.iterateAtoms():
230
- concrete_atom = sub_match.mapAtom(query_atom)
231
- if concrete_atom is None:
232
- continue
233
- reaction_atom = q_reactant.getAtom(query_atom.index())
234
- map_num = q_reaction.atomMappingNumber(reaction_atom)
235
- if map_num:
236
- concrete_atom = new_reaction_reactant.getAtom(concrete_atom.index())
237
- new_reaction.setAtomMappingNumber(concrete_atom, map_num)
238
-
239
-
240
- def clean_product_aam(reaction: IndigoObject):
241
- """
242
- Remove atom mappings from product that are not present in the reactants.
243
- """
244
- existing_mapping_numbers = set()
245
- for reactant in reaction.iterateReactants():
246
- for atom in reactant.iterateAtoms():
247
- map_num = reaction.atomMappingNumber(atom)
248
- if map_num:
249
- existing_mapping_numbers.add(map_num)
250
-
251
- for product in reaction.iterateProducts():
252
- for atom in product.iterateAtoms():
253
- map_num = reaction.atomMappingNumber(atom)
254
- if map_num and map_num not in existing_mapping_numbers:
255
- reaction.setAtomMappingNumber(atom, 0) # YQ: atom number 0 means no mapping number in Indigo
256
-
257
-
258
- def make_concrete_reaction(reactants: list[IndigoObject], products: list[IndigoObject], replacement: IndigoObject,
259
- replacement_index: int) -> tuple[IndigoObject, IndigoObject]:
260
- """
261
- Create a concrete reaction from the given reactants and products, replacing the specified reactant with the replacement molecule.
262
- :param reactants: List of reactant molecules.
263
- :param products: List of product molecules.
264
- :param replacement: The molecule to replace in the reactants.
265
- :param replacement_index: The index of the reactant to replace.
266
- :return: A new IndigoObject representing the concrete reaction.
267
- """
268
- concrete_reaction = indigo.createQueryReaction()
269
- for i, reactant in enumerate(reactants):
270
- if i == replacement_index:
271
- concrete_reaction.addReactant(indigo.loadQueryMolecule(replacement.molfile()))
272
- else:
273
- concrete_reaction.addReactant(reactant.clone())
274
- for product in products:
275
- concrete_reaction.addProduct(product.clone())
276
- return concrete_reaction, concrete_reaction.getMolecule(replacement_index)
277
-
278
-
279
- def is_ambiguous_atom(atom: IndigoObject) -> bool:
280
- """
281
- Test whether the symbol is an adjacent matching wildcard.
282
- """
283
- if atom.isPseudoatom() or atom.isRSite():
284
- return True
285
- symbol = atom.symbol()
286
- if symbol in {'A', 'Q', 'X', 'M', 'AH', 'QH', 'XH', 'MH', 'NOT', 'R', '*'}:
287
- return True
288
- return "[" in symbol and "]" in symbol
289
-
290
-
291
- def get_react_site_highlights(product, ignored_atom_indexes):
292
- """
293
- Get the highlights for the reaction site in the product, ignoring the atoms that are not part of the reaction site.
294
- :param product: The product molecule.
295
- :param ignored_atom_indexes: A set of atom indexes to ignore.
296
- :return: An IndigoObject with highlighted atoms and bonds that are part of the reaction site.
297
- """
298
- highlight = product.clone()
299
- for atom in highlight.iterateAtoms():
300
- if atom.index() not in ignored_atom_indexes:
301
- atom.highlight()
302
- for nei in atom.iterateNeighbors():
303
- if nei.index() not in ignored_atom_indexes:
304
- nei.highlight()
305
- nei.bond().highlight()
306
- return highlight
307
-
308
-
309
- def inherit_auto_map_by_match(target_reaction: IndigoObject, source_reaction: IndigoObject,
310
- reaction_match: IndigoObject):
311
- """
312
- Inherit the auto-mapping from the source reaction to the target reaction based on the reaction match.
313
- :param target_reaction: The target reaction to inherit auto-mapping to.
314
- :param source_reaction: The source reaction to inherit auto-mapping from.
315
- :param reaction_match: The match object that maps atoms and bonds between the source and target reactions.
316
- """
317
- source_molecules = []
318
- for q_reactant in source_reaction.iterateReactants():
319
- source_molecules.append(q_reactant)
320
- for q_product in source_reaction.iterateProducts():
321
- source_molecules.append(q_product)
322
- for source_molecule in source_molecules:
323
- for source_atom in source_molecule.iterateAtoms():
324
- source_atom_map_number = source_reaction.atomMappingNumber(source_atom)
325
- if source_atom_map_number == 0:
326
- continue
327
- target_atom = reaction_match.mapAtom(source_atom)
328
- if target_atom:
329
- target_reaction.setAtomMappingNumber(target_atom, source_atom_map_number)
330
- target_reaction.automap("keep")
331
-
332
-
333
- def get_used_reactants_for_match(
334
- reaction: IndigoObject, q_reaction: IndigoObject, reaction_match: IndigoObject,
335
- kept_replacement_reaction_list_list: list[list[ReplacementReaction]]) -> list[ReplacementReaction]:
336
- """
337
- Find the replacement reactions that correspond to the reactants in reaction that also matches the query reaction.
338
- Return None if any of the reactants do not have a corresponding replacement reaction, even though reaction may have matches directly to the query reaction.
339
- Otherwise, return a list of ReplacementReaction objects that correspond to the reactants in the reaction ordered by the reactants in the query reaction.
340
- """
341
- q_reactants = []
342
- for q_reactant in q_reaction.iterateReactants():
343
- q_reactants.append(q_reactant)
344
- q_products = []
345
- for rr_product in q_reaction.iterateProducts():
346
- q_products.append(rr_product)
347
- reactants = []
348
- for enum_r in reaction.iterateReactants():
349
- reactants.append(enum_r)
350
- products = []
351
- for enum_p in reaction.iterateProducts():
352
- products.append(enum_p)
353
- q_reactant: IndigoObject
354
- ret: list[ReplacementReaction] = []
355
- for reactant_index, q_reactant in enumerate(q_reactants):
356
- replacement_list = kept_replacement_reaction_list_list[reactant_index]
357
- enum_r = reactants[reactant_index]
358
- useful_enumr_atom_indexes = set()
359
- for q_atom in q_reactant.iterateAtoms():
360
- enum_atom = reaction_match.mapAtom(q_atom)
361
- if enum_atom:
362
- useful_enumr_atom_indexes.add(enum_atom.index())
363
- found: ReplacementReaction | None = None
364
- for rr_index, rr in enumerate(replacement_list):
365
- exact_match = indigo.exactMatch(rr.replacement_reactant, enum_r)
366
- if not exact_match:
367
- # YQ Skip if this enumeration is not meant to be the same reactant as replacement we are iterating.
368
- continue
369
- query_reactant_atom_by_index: dict[int, IndigoObject] = {}
370
- rr_reactant_atom_by_index: dict[int, IndigoObject] = {}
371
- query_reactant_index_to_rr_reactant_index: dict[int, int] = {}
372
- rr_reactant_index_to_query_reactant_index: dict[int, int] = {}
373
- enum_r_atom_mapping_number_to_rr_atom: dict[int, IndigoObject] = {}
374
- q_reaction_atom_mapping_number_to_rr_atom: dict[int, IndigoObject] = {}
375
- q_r_site_to_rr_atom: dict[str, IndigoObject] = {}
376
- for q_atom in q_reactant.iterateAtoms():
377
- query_reactant_atom_by_index[q_atom.index()] = q_atom
378
- rr_atom = rr.replacement_query_reaction_match.mapAtom(q_atom)
379
- if rr_atom:
380
- query_reactant_index_to_rr_reactant_index[q_atom.index()] = rr_atom.index()
381
- rr_reactant_index_to_query_reactant_index[rr_atom.index()] = q_atom.index()
382
- q_reaction_atom_mapping_number = q_reaction.atomMappingNumber(q_atom)
383
- if q_reaction_atom_mapping_number > 0:
384
- q_reaction_atom_mapping_number_to_rr_atom[q_reaction_atom_mapping_number] = rr_atom
385
- if q_atom.isRSite():
386
- r_site = q_atom.symbol()
387
- q_r_site_to_rr_atom[r_site] = rr_atom
388
- for rr_atom in rr.replacement_reactant.iterateAtoms():
389
- rr_reactant_atom_by_index[rr_atom.index()] = rr_atom
390
- enum_r_atom = exact_match.mapAtom(rr_atom)
391
- if enum_r_atom:
392
- enum_r_atom_mapping_number = reaction.atomMappingNumber(enum_r_atom)
393
- if enum_r_atom_mapping_number > 0:
394
- enum_r_atom_mapping_number_to_rr_atom[enum_r_atom_mapping_number] = rr_atom
395
-
396
- rr_products = []
397
- for rr_product in rr.reaction.iterateProducts():
398
- rr_products.append(rr_product)
399
- still_valid_rr = True
400
- for product_index, enum_product in enumerate(products):
401
- if not still_valid_rr:
402
- break
403
- query_product = q_products[product_index]
404
- enum_r_atom_mapping_number_to_q_product_atom = {}
405
- for q_atom in query_product.iterateAtoms():
406
- enum_atom = reaction_match.mapAtom(q_atom)
407
- if enum_atom:
408
- enum_mapping_number = reaction.atomMappingNumber(enum_atom)
409
- if enum_mapping_number > 0:
410
- enum_r_atom_mapping_number_to_q_product_atom[enum_mapping_number] = q_atom
411
-
412
- for enum_atom in enum_product.iterateAtoms():
413
- enum_mapping_number = reaction.atomMappingNumber(enum_atom)
414
- if enum_mapping_number == 0:
415
- continue
416
- rr_atom = enum_r_atom_mapping_number_to_rr_atom.get(enum_mapping_number)
417
- if not rr_atom:
418
- continue
419
- q_product_atom: IndigoObject = enum_r_atom_mapping_number_to_q_product_atom.get(enum_mapping_number)
420
- if not q_product_atom:
421
- continue
422
- if q_product_atom.isRSite():
423
- r_site = q_product_atom.symbol()
424
- rr_atom_r_site = q_r_site_to_rr_atom.get(r_site)
425
- if not rr_atom_r_site:
426
- still_valid_rr = False
427
- break
428
- if rr_atom.index() != rr_atom_r_site.index():
429
- still_valid_rr = False
430
- break
431
- else:
432
- q_product_atom_mapping_number = q_reaction.atomMappingNumber(q_product_atom)
433
- if q_product_atom_mapping_number == 0:
434
- continue
435
- query_reactant_atom_index = rr_reactant_index_to_query_reactant_index.get(rr_atom.index())
436
- if query_reactant_atom_index is None:
437
- still_valid_rr = False
438
- break
439
- query_reactant_atom = query_reactant_atom_by_index.get(query_reactant_atom_index)
440
- query_reactant_atom_mapping_number = q_reaction.atomMappingNumber(query_reactant_atom)
441
- if q_product_atom_mapping_number != query_reactant_atom_mapping_number:
442
- still_valid_rr = False
443
- break
444
- if still_valid_rr:
445
- found = rr
446
- break
447
- if found:
448
- ret.append(found)
449
- else:
450
- return []
451
- return ret
452
-
453
-
454
- def are_symmetrical_subs(match1: SerializableMoleculeMatch, match2: SerializableMoleculeMatch) -> bool:
455
- """
456
- Check if two SerializableMoleculeMatch objects are symmetrical.
457
- That is, if we only get the atoms and bonds in the mapping, the two molecules are identical.
458
- :param match1: The first SerializableMoleculeMatch object.
459
- :param match2: The second SerializableMoleculeMatch object.
460
- :return: True if the matches are symmetrical, False otherwise.
461
- """
462
- match1_test = match1.get_matched_molecule_copy()
463
- match1_atom_indexes = set(match1._query_atom_to_atom.values())
464
- match1_bond_indexes = set(match1._query_bond_to_bond.values())
465
- atom_delete_list: list[int] = []
466
- atom_mirror_list: list[int] = []
467
- bond_delete_list: list[int] = []
468
- bond_mirror_list: list[int] = []
469
- for atom in match1_test.iterateAtoms():
470
- if atom.index() not in match1_atom_indexes:
471
- atom_delete_list.append(atom.index())
472
- else:
473
- atom_mirror_list.append(atom.index())
474
- for bond in match1_test.iterateBonds():
475
- if bond.index() not in match1_bond_indexes:
476
- bond_delete_list.append(bond.index())
477
- else:
478
- bond_mirror_list.append(bond.index())
479
- match1_test.removeBonds(bond_delete_list)
480
- match1_test.removeAtoms(atom_delete_list)
481
- match1_mirror_test = match1.get_matched_molecule_copy()
482
- match1_mirror_test.removeBonds(bond_mirror_list)
483
- match1_mirror_test.removeAtoms(atom_mirror_list)
484
-
485
- match2_test = match2.get_matched_molecule_copy()
486
- match2_atom_indexes = set(match2._query_atom_to_atom.values())
487
- match2_bond_indexes = set(match2._query_bond_to_bond.values())
488
- atom_delete_list = []
489
- bond_delete_list = []
490
- atom_mirror_list = []
491
- bond_mirror_list = []
492
- for atom in match2_test.iterateAtoms():
493
- if atom.index() not in match2_atom_indexes:
494
- atom_delete_list.append(atom.index())
495
- else:
496
- atom_mirror_list.append(atom.index())
497
- for bond in match2_test.iterateBonds():
498
- if bond.index() not in match2_bond_indexes:
499
- bond_delete_list.append(bond.index())
500
- else:
501
- bond_mirror_list.append(bond.index())
502
- match2_test.removeBonds(bond_delete_list)
503
- match2_test.removeAtoms(atom_delete_list)
504
- match2_mirror_test = match2.get_matched_molecule_copy()
505
- match2_mirror_test.removeBonds(bond_mirror_list)
506
- match2_mirror_test.removeAtoms(atom_mirror_list)
507
-
508
- return match1_test.canonicalSmiles() == match2_test.canonicalSmiles() and \
509
- match1_mirror_test.canonicalSmiles() == match2_mirror_test.canonicalSmiles()
510
-
511
-
512
- def replace_r_site_with_wildcards(mol: IndigoObject) -> IndigoObject:
513
- """
514
- This will be used to replace molecule's R sites with wildcard *.
515
- The substructure matcher at molecular level will not touch R sites. Therefore if we are to preserve mapping with bonds we need to replace R sites with wildcards.
516
- :param mol: The molecule to process.
517
- :return: A cloned molecule with R sites replaced by wildcards.
518
- """
519
- ret = mol.clone()
520
- for atom in ret.iterateAtoms():
521
- if atom.isRSite():
522
- atom.resetAtom("*")
523
- return ret