sapiopycommons 2025.7.9a583__py3-none-any.whl → 2025.7.10a595__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.
- sapiopycommons/callbacks/callback_util.py +1 -1
- sapiopycommons/chem/IndigoMolecules.py +2 -0
- sapiopycommons/chem/ps_commons.py +523 -0
- sapiopycommons/recordmodel/record_handler.py +107 -161
- sapiopycommons/webhook/webhook_handlers.py +4 -6
- {sapiopycommons-2025.7.9a583.dist-info → sapiopycommons-2025.7.10a595.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.7.9a583.dist-info → sapiopycommons-2025.7.10a595.dist-info}/RECORD +9 -8
- {sapiopycommons-2025.7.9a583.dist-info → sapiopycommons-2025.7.10a595.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.7.9a583.dist-info → sapiopycommons-2025.7.10a595.dist-info}/licenses/LICENSE +0 -0
|
@@ -780,7 +780,7 @@ class CallbackUtil:
|
|
|
780
780
|
# FR-47690: Set default values for fields that aren't present.
|
|
781
781
|
for row in values:
|
|
782
782
|
for field in fields:
|
|
783
|
-
if field.data_field_name not in
|
|
783
|
+
if field.data_field_name not in values:
|
|
784
784
|
row[field.data_field_name] = field.default_value
|
|
785
785
|
|
|
786
786
|
# Convert the group_by parameter to a field name.
|
|
@@ -6,6 +6,8 @@ indigo = Indigo()
|
|
|
6
6
|
renderer = IndigoRenderer(indigo)
|
|
7
7
|
indigo.setOption("render-output-format", "svg")
|
|
8
8
|
indigo.setOption("ignore-stereochemistry-errors", True)
|
|
9
|
+
# Ignore only if loading as non-query object. That is the meaning of this flag. Does nothing if it's query molecule.
|
|
10
|
+
indigo.setOption("ignore-noncritical-query-features", True)
|
|
9
11
|
indigo.setOption("render-stereo-style", "ext")
|
|
10
12
|
indigo.setOption("aromaticity-model", "generic")
|
|
11
13
|
indigo.setOption("render-coloring", True)
|
|
@@ -0,0 +1,523 @@
|
|
|
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
|
|
@@ -3,13 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import io
|
|
4
4
|
import warnings
|
|
5
5
|
from collections.abc import Iterable
|
|
6
|
-
from typing import Collection
|
|
6
|
+
from typing import Collection
|
|
7
7
|
from weakref import WeakValueDictionary
|
|
8
8
|
|
|
9
|
-
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
10
|
-
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
|
|
11
|
-
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
12
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
13
9
|
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
14
10
|
from sapiopylib.rest.User import SapioUser
|
|
15
11
|
from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTerm, ReportColumn
|
|
@@ -28,23 +24,19 @@ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, Wr
|
|
|
28
24
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
29
25
|
RelationshipNodeType
|
|
30
26
|
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
31
|
-
from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
|
|
32
|
-
ReverseSideLink
|
|
33
|
-
|
|
34
|
-
# Aliases for longer name.
|
|
35
|
-
_PropertyGetter: TypeAlias = AbstractRecordModelPropertyGetter
|
|
36
|
-
_PropertyAdder: TypeAlias = AbstractRecordModelPropertyAdder
|
|
37
|
-
_PropertyRemover: TypeAlias = AbstractRecordModelPropertyRemover
|
|
38
|
-
_PropertySetter: TypeAlias = AbstractRecordModelPropertySetter
|
|
39
|
-
_PropertyType: TypeAlias = RecordModelPropertyType
|
|
27
|
+
from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
IsSapioRecord = TypeVar('IsSapioRecord', bound=SapioRecord)
|
|
46
|
-
"""A DataRecord, PyRecordModel, or AbstractRecordModel."""
|
|
29
|
+
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
|
|
30
|
+
FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey, DataTypeIdentifier
|
|
31
|
+
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
32
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
47
33
|
|
|
34
|
+
# Aliases for longer name.
|
|
35
|
+
_PropertyGetter = AbstractRecordModelPropertyGetter
|
|
36
|
+
_PropertyAdder = AbstractRecordModelPropertyAdder
|
|
37
|
+
_PropertyRemover = AbstractRecordModelPropertyRemover
|
|
38
|
+
_PropertySetter = AbstractRecordModelPropertySetter
|
|
39
|
+
_PropertyType = RecordModelPropertyType
|
|
48
40
|
|
|
49
41
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
50
42
|
# FR-47575 - Reordered functions so that the Java and Python versions are as close to each other as possible.
|
|
@@ -532,11 +524,9 @@ class RecordHandler:
|
|
|
532
524
|
"""
|
|
533
525
|
warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
|
|
534
526
|
if isinstance(report_name, str):
|
|
535
|
-
# noinspection PyDeprecation
|
|
536
527
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
537
528
|
page_limit, page_size, page_number)
|
|
538
529
|
elif isinstance(report_name, RawReportTerm):
|
|
539
|
-
# noinspection PyDeprecation
|
|
540
530
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
541
531
|
page_limit, page_size, page_number)
|
|
542
532
|
elif isinstance(report_name, CustomReportCriteria):
|
|
@@ -549,7 +539,6 @@ class RecordHandler:
|
|
|
549
539
|
# Enforce that the given custom report has a record ID column.
|
|
550
540
|
if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
|
|
551
541
|
report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
|
|
552
|
-
# noinspection PyDeprecation
|
|
553
542
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
|
|
554
543
|
page_limit, page_size, page_number)
|
|
555
544
|
else:
|
|
@@ -562,7 +551,7 @@ class RecordHandler:
|
|
|
562
551
|
return self.query_models_by_id(wrapper_type, ids)
|
|
563
552
|
|
|
564
553
|
@staticmethod
|
|
565
|
-
def map_by_id(models: Iterable[
|
|
554
|
+
def map_by_id(models: Iterable[SapioRecord]) -> dict[int, SapioRecord]:
|
|
566
555
|
"""
|
|
567
556
|
Map the given records their record IDs.
|
|
568
557
|
|
|
@@ -571,12 +560,12 @@ class RecordHandler:
|
|
|
571
560
|
"""
|
|
572
561
|
ret_dict: dict[int, SapioRecord] = {}
|
|
573
562
|
for model in models:
|
|
574
|
-
ret_dict.update({
|
|
563
|
+
ret_dict.update({model.record_id: model})
|
|
575
564
|
return ret_dict
|
|
576
565
|
|
|
577
566
|
@staticmethod
|
|
578
|
-
def map_by_field(models: Iterable[
|
|
579
|
-
-> dict[FieldValue, list[
|
|
567
|
+
def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
568
|
+
-> dict[FieldValue, list[SapioRecord]]:
|
|
580
569
|
"""
|
|
581
570
|
Map the given records by one of their fields. If any two records share the same field value, they'll appear in
|
|
582
571
|
the same value list.
|
|
@@ -593,8 +582,8 @@ class RecordHandler:
|
|
|
593
582
|
return ret_dict
|
|
594
583
|
|
|
595
584
|
@staticmethod
|
|
596
|
-
def map_by_unique_field(models: Iterable[
|
|
597
|
-
-> dict[FieldValue,
|
|
585
|
+
def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
|
|
586
|
+
-> dict[FieldValue, SapioRecord]:
|
|
598
587
|
"""
|
|
599
588
|
Uniquely map the given records by one of their fields. If any two records share the same field value, throws
|
|
600
589
|
an exception.
|
|
@@ -673,7 +662,7 @@ class RecordHandler:
|
|
|
673
662
|
return RecordHandler.sum_of_field(models, field_name) / len(models)
|
|
674
663
|
|
|
675
664
|
@staticmethod
|
|
676
|
-
def get_newest_record(records: Iterable[
|
|
665
|
+
def get_newest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
677
666
|
"""
|
|
678
667
|
Get the newest record from a list of records.
|
|
679
668
|
|
|
@@ -684,7 +673,7 @@ class RecordHandler:
|
|
|
684
673
|
|
|
685
674
|
# FR-46696: Add a function for getting the oldest record in a list, just like we have one for the newest record.
|
|
686
675
|
@staticmethod
|
|
687
|
-
def get_oldest_record(records: Iterable[
|
|
676
|
+
def get_oldest_record(records: Iterable[SapioRecord]) -> SapioRecord:
|
|
688
677
|
"""
|
|
689
678
|
Get the oldest record from a list of records.
|
|
690
679
|
|
|
@@ -694,7 +683,7 @@ class RecordHandler:
|
|
|
694
683
|
return min(records, key=lambda x: x.record_id)
|
|
695
684
|
|
|
696
685
|
@staticmethod
|
|
697
|
-
def get_min_record(records: list[
|
|
686
|
+
def get_min_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
698
687
|
"""
|
|
699
688
|
Get the record model with the minimum value of a given field from a list of record models.
|
|
700
689
|
|
|
@@ -706,7 +695,7 @@ class RecordHandler:
|
|
|
706
695
|
return min(records, key=lambda x: x.get_field_value(field))
|
|
707
696
|
|
|
708
697
|
@staticmethod
|
|
709
|
-
def get_max_record(records: list[
|
|
698
|
+
def get_max_record(records: list[RecordModel], field: FieldIdentifier) -> RecordModel:
|
|
710
699
|
"""
|
|
711
700
|
Get the record model with the maximum value of a given field from a list of record models.
|
|
712
701
|
|
|
@@ -881,7 +870,7 @@ class RecordHandler:
|
|
|
881
870
|
parent_dt: str = AliasUtil.to_data_type_name(parent_type)
|
|
882
871
|
wrapper: type[WrappedType] | None = parent_type if isinstance(parent_type, type) else None
|
|
883
872
|
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
884
|
-
parent: PyRecordModel | None = record.
|
|
873
|
+
parent: PyRecordModel | None = record.get_parent_of_type(parent_dt)
|
|
885
874
|
if parent is not None:
|
|
886
875
|
return self.wrap_model(parent, wrapper) if wrapper else parent
|
|
887
876
|
return record.add(Parent.create(wrapper)) if wrapper else record.add(Parent.create_by_name(parent_dt))
|
|
@@ -899,7 +888,7 @@ class RecordHandler:
|
|
|
899
888
|
child_dt: str = AliasUtil.to_data_type_name(child_type)
|
|
900
889
|
wrapper: type[WrappedType] | None = child_type if isinstance(child_type, type) else None
|
|
901
890
|
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
902
|
-
child: PyRecordModel | None = record.
|
|
891
|
+
child: PyRecordModel | None = record.get_child_of_type(child_dt)
|
|
903
892
|
if child is not None:
|
|
904
893
|
return self.wrap_model(child, wrapper) if wrapper else child
|
|
905
894
|
return record.add(Child.create(wrapper)) if wrapper else record.add(Child.create_by_name(child_dt))
|
|
@@ -919,7 +908,7 @@ class RecordHandler:
|
|
|
919
908
|
side_link_field: str = AliasUtil.to_data_field_name(side_link_field)
|
|
920
909
|
wrapper: type[WrappedType] | None = side_link_type if isinstance(side_link_type, type) else None
|
|
921
910
|
record: PyRecordModel = RecordModelInstanceManager.unwrap(record)
|
|
922
|
-
side_link: PyRecordModel | None = record.
|
|
911
|
+
side_link: PyRecordModel | None = record.get_forward_side_link(side_link_field)
|
|
923
912
|
if side_link is not None:
|
|
924
913
|
return self.wrap_model(side_link, wrapper) if wrapper else side_link
|
|
925
914
|
side_link: WrappedType | PyRecordModel = self.add_model(side_link_type)
|
|
@@ -966,63 +955,52 @@ class RecordHandler:
|
|
|
966
955
|
if child not in children:
|
|
967
956
|
record.remove(Child.ref(child))
|
|
968
957
|
|
|
969
|
-
# CR-47717: Update the map_[to/by]_[relationship] functions to allow PyRecordModels to be provided and returned
|
|
970
|
-
# instead of only using WrappedRecordModels and wrapper types.
|
|
971
958
|
@staticmethod
|
|
972
|
-
def map_to_parent(models: Iterable[
|
|
973
|
-
-> dict[
|
|
959
|
+
def map_to_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType])\
|
|
960
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
974
961
|
"""
|
|
975
962
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
976
963
|
|
|
977
964
|
:param models: A list of record models.
|
|
978
|
-
:param parent_type: The record model wrapper
|
|
979
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
965
|
+
:param parent_type: The record model wrapper of the parent.
|
|
980
966
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
981
967
|
it will map to None.
|
|
982
968
|
"""
|
|
983
|
-
return_dict: dict[
|
|
969
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
984
970
|
for model in models:
|
|
985
|
-
|
|
986
|
-
return_dict[model] = model.get(Parent.of_type_name(parent_type))
|
|
987
|
-
else:
|
|
988
|
-
return_dict[model] = model.get(Parent.of_type(parent_type))
|
|
971
|
+
return_dict[model] = model.get_parent_of_type(parent_type)
|
|
989
972
|
return return_dict
|
|
990
973
|
|
|
991
974
|
@staticmethod
|
|
992
|
-
def map_to_parents(models: Iterable[
|
|
993
|
-
-> dict[
|
|
975
|
+
def map_to_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
976
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
994
977
|
"""
|
|
995
978
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
996
979
|
|
|
997
980
|
:param models: A list of record models.
|
|
998
|
-
:param parent_type: The record model wrapper
|
|
999
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
981
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1000
982
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
1001
983
|
then it will map to an empty list.
|
|
1002
984
|
"""
|
|
1003
|
-
return_dict: dict[WrappedRecordModel, list[WrappedType]
|
|
985
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1004
986
|
for model in models:
|
|
1005
|
-
|
|
1006
|
-
return_dict[model] = model.get(Parents.of_type_name(parent_type))
|
|
1007
|
-
else:
|
|
1008
|
-
return_dict[model] = model.get(Parents.of_type(parent_type))
|
|
987
|
+
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
1009
988
|
return return_dict
|
|
1010
989
|
|
|
1011
990
|
@staticmethod
|
|
1012
|
-
def map_by_parent(models: Iterable[
|
|
1013
|
-
-> dict[WrappedType
|
|
991
|
+
def map_by_parent(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
992
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
1014
993
|
"""
|
|
1015
994
|
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
1016
995
|
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
1017
996
|
|
|
1018
997
|
:param models: A list of record models.
|
|
1019
|
-
:param parent_type: The record model wrapper
|
|
1020
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
998
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1021
999
|
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
1022
1000
|
then it will not be in the resulting dictionary.
|
|
1023
1001
|
"""
|
|
1024
|
-
to_parent: dict[
|
|
1025
|
-
by_parent: dict[WrappedType
|
|
1002
|
+
to_parent: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
|
|
1003
|
+
by_parent: dict[WrappedType, WrappedRecordModel] = {}
|
|
1026
1004
|
for record, parent in to_parent.items():
|
|
1027
1005
|
if parent is None:
|
|
1028
1006
|
continue
|
|
@@ -1033,81 +1011,70 @@ class RecordHandler:
|
|
|
1033
1011
|
return by_parent
|
|
1034
1012
|
|
|
1035
1013
|
@staticmethod
|
|
1036
|
-
def map_by_parents(models: Iterable[
|
|
1037
|
-
-> dict[WrappedType
|
|
1014
|
+
def map_by_parents(models: Iterable[WrappedRecordModel], parent_type: type[WrappedType]) \
|
|
1015
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1038
1016
|
"""
|
|
1039
1017
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
1040
1018
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
1041
1019
|
|
|
1042
1020
|
:param models: A list of record models.
|
|
1043
|
-
:param parent_type: The record model wrapper
|
|
1044
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1021
|
+
:param parent_type: The record model wrapper of the parents.
|
|
1045
1022
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
1046
1023
|
then it will not be in the resulting dictionary.
|
|
1047
1024
|
"""
|
|
1048
|
-
to_parents: dict[
|
|
1049
|
-
|
|
1050
|
-
by_parents: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
1025
|
+
to_parents: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
|
|
1026
|
+
by_parents: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1051
1027
|
for record, parents in to_parents.items():
|
|
1052
1028
|
for parent in parents:
|
|
1053
1029
|
by_parents.setdefault(parent, []).append(record)
|
|
1054
1030
|
return by_parents
|
|
1055
1031
|
|
|
1056
1032
|
@staticmethod
|
|
1057
|
-
def map_to_child(models: Iterable[
|
|
1058
|
-
-> dict[
|
|
1033
|
+
def map_to_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType])\
|
|
1034
|
+
-> dict[WrappedRecordModel, WrappedType]:
|
|
1059
1035
|
"""
|
|
1060
1036
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
1061
1037
|
|
|
1062
1038
|
:param models: A list of record models.
|
|
1063
|
-
:param child_type: The record model wrapper
|
|
1064
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1039
|
+
:param child_type: The record model wrapper of the child.
|
|
1065
1040
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
1066
1041
|
it will map to None.
|
|
1067
1042
|
"""
|
|
1068
|
-
return_dict: dict[
|
|
1043
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1069
1044
|
for model in models:
|
|
1070
|
-
|
|
1071
|
-
return_dict[model] = model.get(Child.of_type_name(child_type))
|
|
1072
|
-
else:
|
|
1073
|
-
return_dict[model] = model.get(Child.of_type(child_type))
|
|
1045
|
+
return_dict[model] = model.get_child_of_type(child_type)
|
|
1074
1046
|
return return_dict
|
|
1075
1047
|
|
|
1076
1048
|
@staticmethod
|
|
1077
|
-
def map_to_children(models: Iterable[
|
|
1078
|
-
-> dict[
|
|
1049
|
+
def map_to_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
1050
|
+
-> dict[WrappedRecordModel, list[WrappedType]]:
|
|
1079
1051
|
"""
|
|
1080
1052
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
1081
1053
|
|
|
1082
1054
|
:param models: A list of record models.
|
|
1083
|
-
:param child_type: The record model wrapper
|
|
1084
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1055
|
+
:param child_type: The record model wrapper of the children.
|
|
1085
1056
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
1086
1057
|
then it will map to an empty list.
|
|
1087
1058
|
"""
|
|
1088
|
-
return_dict: dict[
|
|
1059
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1089
1060
|
for model in models:
|
|
1090
|
-
|
|
1091
|
-
return_dict[model] = model.get(Children.of_type_name(child_type))
|
|
1092
|
-
else:
|
|
1093
|
-
return_dict[model] = model.get(Children.of_type(child_type))
|
|
1061
|
+
return_dict[model] = model.get_children_of_type(child_type)
|
|
1094
1062
|
return return_dict
|
|
1095
1063
|
|
|
1096
1064
|
@staticmethod
|
|
1097
|
-
def map_by_child(models: Iterable[
|
|
1098
|
-
-> dict[WrappedType
|
|
1065
|
+
def map_by_child(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
1066
|
+
-> dict[WrappedType, WrappedRecordModel]:
|
|
1099
1067
|
"""
|
|
1100
1068
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
1101
1069
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
1102
1070
|
|
|
1103
1071
|
:param models: A list of record models.
|
|
1104
|
-
:param child_type: The record model wrapper
|
|
1105
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1072
|
+
:param child_type: The record model wrapper of the children.
|
|
1106
1073
|
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
1107
1074
|
then it will not be in the resulting dictionary.
|
|
1108
1075
|
"""
|
|
1109
|
-
to_child: dict[
|
|
1110
|
-
by_child: dict[WrappedType
|
|
1076
|
+
to_child: dict[WrappedRecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
|
|
1077
|
+
by_child: dict[WrappedType, WrappedRecordModel] = {}
|
|
1111
1078
|
for record, child in to_child.items():
|
|
1112
1079
|
if child is None:
|
|
1113
1080
|
continue
|
|
@@ -1118,50 +1085,45 @@ class RecordHandler:
|
|
|
1118
1085
|
return by_child
|
|
1119
1086
|
|
|
1120
1087
|
@staticmethod
|
|
1121
|
-
def map_by_children(models: Iterable[
|
|
1122
|
-
-> dict[WrappedType
|
|
1088
|
+
def map_by_children(models: Iterable[WrappedRecordModel], child_type: type[WrappedType]) \
|
|
1089
|
+
-> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1123
1090
|
"""
|
|
1124
1091
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
1125
1092
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
1126
1093
|
|
|
1127
1094
|
:param models: A list of record models.
|
|
1128
|
-
:param child_type: The record model wrapper
|
|
1129
|
-
provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1095
|
+
:param child_type: The record model wrapper of the children.
|
|
1130
1096
|
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
1131
1097
|
then it will not be in the resulting dictionary.
|
|
1132
1098
|
"""
|
|
1133
|
-
to_children: dict[
|
|
1134
|
-
|
|
1135
|
-
by_children: dict[WrappedType | PyRecordModel, list[RecordModel]] = {}
|
|
1099
|
+
to_children: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
|
|
1100
|
+
by_children: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1136
1101
|
for record, children in to_children.items():
|
|
1137
1102
|
for child in children:
|
|
1138
1103
|
by_children.setdefault(child, []).append(record)
|
|
1139
1104
|
return by_children
|
|
1140
1105
|
|
|
1141
1106
|
@staticmethod
|
|
1142
|
-
def map_to_forward_side_link(models: Iterable[
|
|
1143
|
-
side_link_type: type[WrappedType]
|
|
1144
|
-
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
1107
|
+
def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1108
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
1145
1109
|
"""
|
|
1146
1110
|
Map a list of record models to their forward side link. The forward side link must already be loaded.
|
|
1147
1111
|
|
|
1148
1112
|
:param models: A list of record models.
|
|
1149
1113
|
:param field_name: The field name on the record models where the side link is located.
|
|
1150
|
-
:param side_link_type: The record model wrapper of the forward side link.
|
|
1151
|
-
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
1114
|
+
:param side_link_type: The record model wrapper of the forward side link.
|
|
1152
1115
|
:return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
|
|
1153
1116
|
then it will map to None.
|
|
1154
1117
|
"""
|
|
1155
1118
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1156
|
-
return_dict: dict[
|
|
1119
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1157
1120
|
for model in models:
|
|
1158
|
-
return_dict[model] = model.
|
|
1121
|
+
return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
|
|
1159
1122
|
return return_dict
|
|
1160
1123
|
|
|
1161
1124
|
@staticmethod
|
|
1162
|
-
def map_by_forward_side_link(models: Iterable[
|
|
1163
|
-
side_link_type: type[WrappedType]
|
|
1164
|
-
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
1125
|
+
def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1126
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
1165
1127
|
"""
|
|
1166
1128
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
1167
1129
|
map_to_forward_side_link, but if two records share the same forward link, an exception is thrown.
|
|
@@ -1169,15 +1131,14 @@ class RecordHandler:
|
|
|
1169
1131
|
|
|
1170
1132
|
:param models: A list of record models.
|
|
1171
1133
|
:param field_name: The field name on the record models where the side link is located.
|
|
1172
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
1173
|
-
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
1134
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
1174
1135
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
|
|
1175
1136
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1176
1137
|
"""
|
|
1177
1138
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1178
|
-
to_side_link: dict[
|
|
1139
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1179
1140
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1180
|
-
by_side_link: dict[WrappedType
|
|
1141
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1181
1142
|
for record, side_link in to_side_link.items():
|
|
1182
1143
|
if side_link is None:
|
|
1183
1144
|
continue
|
|
@@ -1188,9 +1149,8 @@ class RecordHandler:
|
|
|
1188
1149
|
return by_side_link
|
|
1189
1150
|
|
|
1190
1151
|
@staticmethod
|
|
1191
|
-
def map_by_forward_side_links(models: Iterable[
|
|
1192
|
-
side_link_type: type[WrappedType]
|
|
1193
|
-
-> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
|
|
1152
|
+
def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1153
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1194
1154
|
"""
|
|
1195
1155
|
Take a list of record models and map them by their forward side link. Essentially an inversion of
|
|
1196
1156
|
map_to_forward_side_link. Input models that share a forward side link will end up in the same list.
|
|
@@ -1198,15 +1158,14 @@ class RecordHandler:
|
|
|
1198
1158
|
|
|
1199
1159
|
:param models: A list of record models.
|
|
1200
1160
|
:param field_name: The field name on the record models where the side link is located.
|
|
1201
|
-
:param side_link_type: The record model wrapper of the forward side links.
|
|
1202
|
-
be returned as PyRecordModels instead of WrappedRecordModels.
|
|
1161
|
+
:param side_link_type: The record model wrapper of the forward side links.
|
|
1203
1162
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
|
|
1204
1163
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1205
1164
|
"""
|
|
1206
1165
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1207
|
-
to_side_link: dict[
|
|
1166
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1208
1167
|
.map_to_forward_side_link(models, field_name, side_link_type)
|
|
1209
|
-
by_side_link: dict[WrappedType
|
|
1168
|
+
by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
|
|
1210
1169
|
for record, side_link in to_side_link.items():
|
|
1211
1170
|
if side_link is None:
|
|
1212
1171
|
continue
|
|
@@ -1214,9 +1173,8 @@ class RecordHandler:
|
|
|
1214
1173
|
return by_side_link
|
|
1215
1174
|
|
|
1216
1175
|
@staticmethod
|
|
1217
|
-
def map_to_reverse_side_link(models: Iterable[
|
|
1218
|
-
side_link_type: type[WrappedType]
|
|
1219
|
-
-> dict[IsRecordModel, WrappedType | PyRecordModel]:
|
|
1176
|
+
def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1177
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
|
|
1220
1178
|
"""
|
|
1221
1179
|
Map a list of record models to the reverse side link of a given type. If a given record has more than one
|
|
1222
1180
|
reverse side link of this type, an exception is thrown. The reverse side links must already be loaded.
|
|
@@ -1224,18 +1182,14 @@ class RecordHandler:
|
|
|
1224
1182
|
:param models: A list of record models.
|
|
1225
1183
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1226
1184
|
located.
|
|
1227
|
-
:param side_link_type: The record model wrapper
|
|
1228
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1185
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1229
1186
|
:return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
|
|
1230
1187
|
then it will map to None.
|
|
1231
1188
|
"""
|
|
1232
1189
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1233
|
-
return_dict: dict[
|
|
1190
|
+
return_dict: dict[WrappedRecordModel, WrappedType] = {}
|
|
1234
1191
|
for model in models:
|
|
1235
|
-
|
|
1236
|
-
links: list[WrappedType] = model.get(ReverseSideLink.of(side_link_type, field_name))
|
|
1237
|
-
else:
|
|
1238
|
-
links: list[WrappedType] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
|
|
1192
|
+
links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
|
|
1239
1193
|
if len(links) > 1:
|
|
1240
1194
|
raise SapioException(f"Model {model.data_type_name} {model.record_id} has more than one reverse link "
|
|
1241
1195
|
f"of type {side_link_type.get_wrapper_data_type_name()}.")
|
|
@@ -1243,9 +1197,8 @@ class RecordHandler:
|
|
|
1243
1197
|
return return_dict
|
|
1244
1198
|
|
|
1245
1199
|
@staticmethod
|
|
1246
|
-
def map_to_reverse_side_links(models: Iterable[
|
|
1247
|
-
side_link_type: type[WrappedType]
|
|
1248
|
-
-> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
1200
|
+
def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1201
|
+
side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
|
|
1249
1202
|
"""
|
|
1250
1203
|
Map a list of record models to a list reverse side links of a given type. The reverse side links must already
|
|
1251
1204
|
be loaded.
|
|
@@ -1253,24 +1206,19 @@ class RecordHandler:
|
|
|
1253
1206
|
:param models: A list of record models.
|
|
1254
1207
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1255
1208
|
located.
|
|
1256
|
-
:param side_link_type: The record model wrapper
|
|
1257
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1209
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1258
1210
|
:return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
|
|
1259
1211
|
then it will map to an empty list.
|
|
1260
1212
|
"""
|
|
1261
1213
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1262
|
-
return_dict: dict[
|
|
1214
|
+
return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
|
|
1263
1215
|
for model in models:
|
|
1264
|
-
|
|
1265
|
-
return_dict[model] = model.get(ReverseSideLink.of(side_link_type, field_name))
|
|
1266
|
-
else:
|
|
1267
|
-
return_dict[model] = model.get(ReverseSideLink.of_type(side_link_type, field_name))
|
|
1216
|
+
return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
|
|
1268
1217
|
return return_dict
|
|
1269
1218
|
|
|
1270
1219
|
@staticmethod
|
|
1271
|
-
def map_by_reverse_side_link(models: Iterable[
|
|
1272
|
-
side_link_type: type[WrappedType]
|
|
1273
|
-
-> dict[WrappedType | PyRecordModel, IsRecordModel]:
|
|
1220
|
+
def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1221
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
|
|
1274
1222
|
"""
|
|
1275
1223
|
Take a list of record models and map them by their reverse side link. Essentially an inversion of
|
|
1276
1224
|
map_to_reverse_side_link. If two records share the same reverse side link, an exception is thrown.
|
|
@@ -1279,15 +1227,14 @@ class RecordHandler:
|
|
|
1279
1227
|
:param models: A list of record models.
|
|
1280
1228
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1281
1229
|
located.
|
|
1282
|
-
:param side_link_type: The record model wrapper
|
|
1283
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1230
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1284
1231
|
:return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
|
|
1285
1232
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1286
1233
|
"""
|
|
1287
1234
|
field_name: str = AliasUtil.to_data_field_name(field_name)
|
|
1288
|
-
to_side_link: dict[
|
|
1235
|
+
to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
|
|
1289
1236
|
.map_to_reverse_side_link(models, field_name, side_link_type)
|
|
1290
|
-
by_side_link: dict[WrappedType
|
|
1237
|
+
by_side_link: dict[WrappedType, WrappedRecordModel] = {}
|
|
1291
1238
|
for record, side_link in to_side_link.items():
|
|
1292
1239
|
if side_link is None:
|
|
1293
1240
|
continue
|
|
@@ -1298,8 +1245,8 @@ class RecordHandler:
|
|
|
1298
1245
|
return by_side_link
|
|
1299
1246
|
|
|
1300
1247
|
@staticmethod
|
|
1301
|
-
def map_by_reverse_side_links(models: Iterable[
|
|
1302
|
-
side_link_type: type[WrappedType]
|
|
1248
|
+
def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
|
|
1249
|
+
side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
|
|
1303
1250
|
"""
|
|
1304
1251
|
Take a list of record models and map them by their reverse side links. Essentially an inversion of
|
|
1305
1252
|
map_to_reverse_side_links. Input models that share a reverse side link will end up in the same list.
|
|
@@ -1308,8 +1255,7 @@ class RecordHandler:
|
|
|
1308
1255
|
:param models: A list of record models.
|
|
1309
1256
|
:param field_name: The field name on the side linked model where the side link to the given record models is
|
|
1310
1257
|
located.
|
|
1311
|
-
:param side_link_type: The record model wrapper
|
|
1312
|
-
name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1258
|
+
:param side_link_type: The record model wrapper of the reverse side links.
|
|
1313
1259
|
:return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
|
|
1314
1260
|
pointing to it, then it will not be in the resulting dictionary.
|
|
1315
1261
|
"""
|
|
@@ -1324,9 +1270,9 @@ class RecordHandler:
|
|
|
1324
1270
|
|
|
1325
1271
|
# FR-46155: Update relationship path traversing functions to be non-static and take in a wrapper type so that the
|
|
1326
1272
|
# output can be wrapped instead of requiring the user to wrap the output.
|
|
1327
|
-
def get_linear_path(self, models: Iterable[
|
|
1273
|
+
def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1328
1274
|
wrapper_type: type[WrappedType] | None = None) \
|
|
1329
|
-
-> dict[
|
|
1275
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
1330
1276
|
"""
|
|
1331
1277
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1332
1278
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
@@ -1339,7 +1285,7 @@ class RecordHandler:
|
|
|
1339
1285
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1340
1286
|
path couldn't be reached, the record will map to None.
|
|
1341
1287
|
"""
|
|
1342
|
-
ret_dict: dict[RecordModel, WrappedType |
|
|
1288
|
+
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1343
1289
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1344
1290
|
path: list[RelationshipNode] = path.path
|
|
1345
1291
|
for model in models:
|
|
@@ -1386,9 +1332,9 @@ class RecordHandler:
|
|
|
1386
1332
|
ret_dict.update({model: self.wrap_model(current, wrapper_type) if current else None})
|
|
1387
1333
|
return ret_dict
|
|
1388
1334
|
|
|
1389
|
-
def get_branching_path(self, models: Iterable[
|
|
1335
|
+
def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1390
1336
|
wrapper_type: type[WrappedType] | None = None)\
|
|
1391
|
-
-> dict[
|
|
1337
|
+
-> dict[RecordModel, list[WrappedType] | list[PyRecordModel]]:
|
|
1392
1338
|
"""
|
|
1393
1339
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1394
1340
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1401,7 +1347,7 @@ class RecordHandler:
|
|
|
1401
1347
|
:return: Each record model mapped to the records at the end of the path starting from itself. If the end of the
|
|
1402
1348
|
path couldn't be reached, the record will map to an empty list.
|
|
1403
1349
|
"""
|
|
1404
|
-
ret_dict: dict[RecordModel, list[WrappedType]
|
|
1350
|
+
ret_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
1405
1351
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1406
1352
|
path: list[RelationshipNode] = path.path
|
|
1407
1353
|
for model in models:
|
|
@@ -1437,9 +1383,9 @@ class RecordHandler:
|
|
|
1437
1383
|
|
|
1438
1384
|
# FR-46155: Create a relationship traversing function that returns a single function at the end of the path like
|
|
1439
1385
|
# get_linear_path but can handle branching paths in the middle of the search like get_branching_path.
|
|
1440
|
-
def get_flat_path(self, models: Iterable[
|
|
1386
|
+
def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
|
|
1441
1387
|
wrapper_type: type[WrappedType] | None = None) \
|
|
1442
|
-
-> dict[
|
|
1388
|
+
-> dict[RecordModel, WrappedType | PyRecordModel | None]:
|
|
1443
1389
|
"""
|
|
1444
1390
|
Given a relationship path, travel the path starting from the input models. Returns the record at the end of the
|
|
1445
1391
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
@@ -1456,7 +1402,7 @@ class RecordHandler:
|
|
|
1456
1402
|
:return: Each record model mapped to the record at the end of the path starting from itself. If the end of the
|
|
1457
1403
|
path couldn't be reached, the record will map to None.
|
|
1458
1404
|
"""
|
|
1459
|
-
ret_dict: dict[RecordModel, WrappedType |
|
|
1405
|
+
ret_dict: dict[RecordModel, WrappedType | None] = {}
|
|
1460
1406
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
1461
1407
|
path: list[RelationshipNode] = path.path
|
|
1462
1408
|
for model in models:
|
|
@@ -7,6 +7,7 @@ import traceback
|
|
|
7
7
|
from abc import abstractmethod
|
|
8
8
|
from logging import Logger
|
|
9
9
|
|
|
10
|
+
from sapiopylib.rest import UserManagerService, GroupManagerService, MessengerService
|
|
10
11
|
from sapiopylib.rest.AccessionService import AccessionManager
|
|
11
12
|
from sapiopylib.rest.CustomReportService import CustomReportManager
|
|
12
13
|
from sapiopylib.rest.DashboardManager import DashboardManager
|
|
@@ -15,13 +16,10 @@ from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
|
15
16
|
from sapiopylib.rest.DataService import DataManager
|
|
16
17
|
from sapiopylib.rest.DataTypeService import DataTypeManager
|
|
17
18
|
from sapiopylib.rest.ELNService import ElnManager
|
|
18
|
-
from sapiopylib.rest.GroupManagerService import VeloxGroupManager
|
|
19
|
-
from sapiopylib.rest.MessengerService import SapioMessenger
|
|
20
19
|
from sapiopylib.rest.PicklistService import PickListManager
|
|
21
20
|
from sapiopylib.rest.ReportManager import ReportManager
|
|
22
21
|
from sapiopylib.rest.SesssionManagerService import SessionManager
|
|
23
22
|
from sapiopylib.rest.User import SapioUser
|
|
24
|
-
from sapiopylib.rest.UserManagerService import VeloxUserManager
|
|
25
23
|
from sapiopylib.rest.WebhookService import AbstractWebhookHandler
|
|
26
24
|
from sapiopylib.rest.pojo.Message import VeloxLogMessage, VeloxLogLevel
|
|
27
25
|
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import PopupType
|
|
@@ -87,9 +85,9 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
87
85
|
"""A class for making requests to the data type webservice endpoints."""
|
|
88
86
|
eln_man: ElnManager
|
|
89
87
|
"""A class for making requests to the ELN management webservice endpoints."""
|
|
90
|
-
group_man:
|
|
88
|
+
group_man: GroupManagerService
|
|
91
89
|
"""A class for making requests to the group management webservice endpoints."""
|
|
92
|
-
messenger:
|
|
90
|
+
messenger: MessengerService
|
|
93
91
|
"""A class for making requests to the message webservice endpoints."""
|
|
94
92
|
list_man: PickListManager
|
|
95
93
|
"""A class for making requests to the pick list webservice endpoints."""
|
|
@@ -97,7 +95,7 @@ class CommonsWebhookHandler(AbstractWebhookHandler):
|
|
|
97
95
|
"""A class for making requests to the report webservice endpoints."""
|
|
98
96
|
session_man: SessionManager
|
|
99
97
|
"""A class for making requests to the session management webservice endpoints."""
|
|
100
|
-
user_man:
|
|
98
|
+
user_man: UserManagerService
|
|
101
99
|
"""A class for making requests to the user management webservice endpoints."""
|
|
102
100
|
|
|
103
101
|
rec_man: RecordModelManager
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2025.7.
|
|
3
|
+
Version: 2025.7.10a595
|
|
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,10 +1,11 @@
|
|
|
1
1
|
sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
sapiopycommons/callbacks/callback_util.py,sha256=
|
|
3
|
+
sapiopycommons/callbacks/callback_util.py,sha256=rps6RA6lmzCOwiBqPQAe2Mkf0CIF4RjHPQTYgduMAgE,153011
|
|
4
4
|
sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
|
|
5
|
-
sapiopycommons/chem/IndigoMolecules.py,sha256=
|
|
5
|
+
sapiopycommons/chem/IndigoMolecules.py,sha256=30bsnZ2o4fJXUV6kUTI-I6fDa7bQj7zfE3rOQQ7WD5M,5287
|
|
6
6
|
sapiopycommons/chem/Molecules.py,sha256=mVqPn32MPMjF0iZas-5MFkS-upIdoW5OB72KKZmJRJA,12523
|
|
7
7
|
sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
sapiopycommons/chem/ps_commons.py,sha256=TobN8V9FW32x7o1C6i_WsqGkdldbyVTUfhG5W0xAxMM,23598
|
|
8
9
|
sapiopycommons/customreport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
10
|
sapiopycommons/customreport/auto_pagers.py,sha256=89p-tik0MhsOplYje6LbAW4WClldpAmb8YXFDoXhIlY,17144
|
|
10
11
|
sapiopycommons/customreport/column_builder.py,sha256=0RO53e9rKPZ07C--KcepN6_tpRw_FxF3O9vdG0ilKG8,3014
|
|
@@ -51,7 +52,7 @@ sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
51
52
|
sapiopycommons/processtracking/custom_workflow_handler.py,sha256=eYKdYlwo8xx-6AkB_iPUBNV9yDoNvW2h_Sm3i8JpmRU,25844
|
|
52
53
|
sapiopycommons/processtracking/endpoints.py,sha256=5AJLbhRKQsOeeOdQa888xcCJZD5aavxD-DHZ36Qob_M,12548
|
|
53
54
|
sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
sapiopycommons/recordmodel/record_handler.py,sha256=
|
|
55
|
+
sapiopycommons/recordmodel/record_handler.py,sha256=HfYOl_dDHFd0SEQS3g48_a4zsm36ODWkvZunwzCFDos,90666
|
|
55
56
|
sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
57
|
sapiopycommons/rules/eln_rule_handler.py,sha256=MnE-eSl1kNfaXWFi9elTOC9V2fdUzrwWTvCHUprC8_I,11388
|
|
57
58
|
sapiopycommons/rules/on_save_rule_handler.py,sha256=fkNIlslAZZ0BUrRiwecyvf42JBR8FpCCQ6DBNKXP2jE,11155
|
|
@@ -60,9 +61,9 @@ sapiopycommons/sftpconnect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
60
61
|
sapiopycommons/sftpconnect/sftp_builder.py,sha256=lFK3FeXk-sFLefW0hqY8WGUQDeYiGaT6yDACzT_zFgQ,3015
|
|
61
62
|
sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
63
|
sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
|
|
63
|
-
sapiopycommons/webhook/webhook_handlers.py,sha256=
|
|
64
|
+
sapiopycommons/webhook/webhook_handlers.py,sha256=tUVNCw05CDGu1gFDm2g558hX_O203WVm_n__ojjoRRM,39841
|
|
64
65
|
sapiopycommons/webhook/webservice_handlers.py,sha256=tyaYGG1-v_JJrJHZ6cy5mGCxX9z1foLw7pM4MDJlFxs,14297
|
|
65
|
-
sapiopycommons-2025.7.
|
|
66
|
-
sapiopycommons-2025.7.
|
|
67
|
-
sapiopycommons-2025.7.
|
|
68
|
-
sapiopycommons-2025.7.
|
|
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,,
|
|
File without changes
|
{sapiopycommons-2025.7.9a583.dist-info → sapiopycommons-2025.7.10a595.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|