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.

@@ -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 row:
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, TypeVar, TypeAlias
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
- # CR-47717: Use TypeVars in the type hints of certain functions to prevent PyCharm from erroneously flagging certain
42
- # return type hints as incorrect.
43
- IsRecordModel = TypeVar('IsRecordModel', bound=RecordModel)
44
- """A PyRecordModel or AbstractRecordModel."""
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[IsSapioRecord]) -> dict[int, IsSapioRecord]:
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({AliasUtil.to_record_id(model): model})
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[IsSapioRecord], field_name: FieldIdentifier) \
579
- -> dict[FieldValue, list[IsSapioRecord]]:
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[IsSapioRecord], field_name: FieldIdentifier) \
597
- -> dict[FieldValue, IsSapioRecord]:
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[IsSapioRecord]) -> IsSapioRecord:
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[IsSapioRecord]) -> IsSapioRecord:
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[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
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[IsSapioRecord], field: FieldIdentifier) -> IsSapioRecord:
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.get(Parent.of_type_name(parent_dt))
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.get(Child.of_type_name(child_dt))
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.get(ForwardSideLink.of(side_link_field))
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[IsRecordModel], parent_type: type[WrappedType] | str) \
973
- -> dict[IsRecordModel, WrappedType | PyRecordModel]:
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 or data type name of the parents. If a data type name is
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[RecordModel, WrappedType | PyRecordModel] = {}
969
+ return_dict: dict[WrappedRecordModel, WrappedType] = {}
984
970
  for model in models:
985
- if isinstance(parent_type, str):
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[IsRecordModel], parent_type: type[WrappedType] | str) \
993
- -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
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 or data type name of the parents. If a data type name is
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] | list[PyRecordModel]] = {}
985
+ return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1004
986
  for model in models:
1005
- if isinstance(parent_type, str):
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[IsRecordModel], parent_type: type[WrappedType] | str) \
1013
- -> dict[WrappedType | PyRecordModel, IsRecordModel]:
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 or data type name of the parents. If a data type name is
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[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_parent(models, parent_type)
1025
- by_parent: dict[WrappedType | PyRecordModel, RecordModel] = {}
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[IsRecordModel], parent_type: type[WrappedType] | str) \
1037
- -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
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 or data type name of the parents. If a data type name is
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[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1049
- .map_to_parents(models, parent_type)
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[IsRecordModel], child_type: type[WrappedType] | str) \
1058
- -> dict[IsRecordModel, WrappedType | PyRecordModel]:
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 or data type name of the children. If a data type name is
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[RecordModel, WrappedType | PyRecordModel] = {}
1043
+ return_dict: dict[WrappedRecordModel, WrappedType] = {}
1069
1044
  for model in models:
1070
- if isinstance(child_type, str):
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[IsRecordModel], child_type: type[WrappedType] | str) \
1078
- -> dict[IsRecordModel, list[WrappedType] | PyRecordModel]:
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 or data type name of the children. If a data type name is
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[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1059
+ return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1089
1060
  for model in models:
1090
- if isinstance(child_type, str):
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[IsRecordModel], child_type: type[WrappedType] | str) \
1098
- -> dict[WrappedType | str, IsRecordModel]:
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 or data type name of the children. If a data type name is
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[RecordModel, WrappedType | PyRecordModel] = RecordHandler.map_to_child(models, child_type)
1110
- by_child: dict[WrappedType | PyRecordModel, RecordModel] = {}
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[IsRecordModel], child_type: type[WrappedType] | str) \
1122
- -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
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 or data type name of the children. If a data type name is
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[RecordModel, list[WrappedType] | list[PyRecordModel]] = RecordHandler\
1134
- .map_to_children(models, child_type)
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[IsRecordModel], field_name: FieldIdentifier,
1143
- side_link_type: type[WrappedType] | None) \
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. If None, the side links will
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[RecordModel, WrappedType | PyRecordModel] = {}
1119
+ return_dict: dict[WrappedRecordModel, WrappedType] = {}
1157
1120
  for model in models:
1158
- return_dict[model] = model.get(ForwardSideLink.of(field_name, side_link_type))
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[IsRecordModel], field_name: FieldIdentifier,
1163
- side_link_type: type[WrappedType] | None) \
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. If None, the side links will
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[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
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 | PyRecordModel, RecordModel] = {}
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[IsRecordModel], field_name: FieldIdentifier,
1192
- side_link_type: type[WrappedType] | None) \
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. If None, the side links will
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[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
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 | PyRecordModel, list[RecordModel]] = {}
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[IsRecordModel], field_name: FieldIdentifier,
1218
- side_link_type: type[WrappedType] | str) \
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 or data type name of the reverse side links. If a data type
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[RecordModel, WrappedType | PyRecordModel] = {}
1190
+ return_dict: dict[WrappedRecordModel, WrappedType] = {}
1234
1191
  for model in models:
1235
- if isinstance(side_link_type, str):
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[IsRecordModel], field_name: FieldIdentifier,
1247
- side_link_type: type[WrappedType] | str) \
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 or data type name of the reverse side links. If a data type
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[RecordModel, list[WrappedType] | list[PyRecordModel]] = {}
1214
+ return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
1263
1215
  for model in models:
1264
- if isinstance(side_link_type, str):
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[IsRecordModel], field_name: FieldIdentifier,
1272
- side_link_type: type[WrappedType] | str) \
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 or data type name of the reverse side links. If a data type
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[RecordModel, WrappedType | PyRecordModel] = RecordHandler\
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 | PyRecordModel, RecordModel] = {}
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[IsRecordModel], field_name: FieldIdentifier,
1302
- side_link_type: type[WrappedType] | str) -> dict[WrappedType | PyRecordModel, list[IsRecordModel]]:
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 or data type name of the reverse side links. If a data type
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[IsRecordModel], path: RelationshipPath,
1273
+ def get_linear_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1328
1274
  wrapper_type: type[WrappedType] | None = None) \
1329
- -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
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 | PyRecordModel | None] = {}
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[IsRecordModel], path: RelationshipPath,
1335
+ def get_branching_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1390
1336
  wrapper_type: type[WrappedType] | None = None)\
1391
- -> dict[IsRecordModel, list[WrappedType] | list[PyRecordModel]]:
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] | list[PyRecordModel]] = {}
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[IsRecordModel], path: RelationshipPath,
1386
+ def get_flat_path(self, models: Iterable[RecordModel], path: RelationshipPath,
1441
1387
  wrapper_type: type[WrappedType] | None = None) \
1442
- -> dict[IsRecordModel, WrappedType | PyRecordModel | None]:
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 | PyRecordModel | None] = {}
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: VeloxGroupManager
88
+ group_man: GroupManagerService
91
89
  """A class for making requests to the group management webservice endpoints."""
92
- messenger: SapioMessenger
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: VeloxUserManager
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.9a583
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=OuPJ1o6jcDQ7qV-dxrjAkJerGbVI9_9P-xu0r3ODaMM,153008
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=7ucCaRMLu1zfH2uPIvXwRTSdpNcS03O1P9p_O-5B4xQ,5110
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=WxmgrWQ3nX3eVZSHJY7e8fj7CI7azSyEyovmYcy9098,95021
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=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
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.9a583.dist-info/METADATA,sha256=zOjGEYY42rhigetoYC87X-jQ8rLzPNqEuP3stxwqarM,3142
66
- sapiopycommons-2025.7.9a583.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
67
- sapiopycommons-2025.7.9a583.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
68
- sapiopycommons-2025.7.9a583.dist-info/RECORD,,
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,,