sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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.

Files changed (52) hide show
  1. sapiopycommons/callbacks/__init__.py +0 -0
  2. sapiopycommons/callbacks/callback_util.py +2041 -0
  3. sapiopycommons/callbacks/field_builder.py +545 -0
  4. sapiopycommons/chem/IndigoMolecules.py +52 -5
  5. sapiopycommons/chem/Molecules.py +114 -30
  6. sapiopycommons/customreport/__init__.py +0 -0
  7. sapiopycommons/customreport/column_builder.py +60 -0
  8. sapiopycommons/customreport/custom_report_builder.py +137 -0
  9. sapiopycommons/customreport/term_builder.py +315 -0
  10. sapiopycommons/datatype/attachment_util.py +17 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +390 -90
  14. sapiopycommons/eln/experiment_report_util.py +649 -0
  15. sapiopycommons/eln/plate_designer.py +152 -0
  16. sapiopycommons/files/complex_data_loader.py +31 -0
  17. sapiopycommons/files/file_bridge.py +153 -25
  18. sapiopycommons/files/file_bridge_handler.py +555 -0
  19. sapiopycommons/files/file_data_handler.py +633 -0
  20. sapiopycommons/files/file_util.py +270 -158
  21. sapiopycommons/files/file_validator.py +569 -0
  22. sapiopycommons/files/file_writer.py +377 -0
  23. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  24. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  25. sapiopycommons/general/accession_service.py +375 -0
  26. sapiopycommons/general/aliases.py +259 -18
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +252 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +85 -18
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +97 -7
  35. sapiopycommons/multimodal/multimodal.py +146 -0
  36. sapiopycommons/multimodal/multimodal_data.py +490 -0
  37. sapiopycommons/processtracking/__init__.py +0 -0
  38. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  39. sapiopycommons/processtracking/endpoints.py +192 -0
  40. sapiopycommons/recordmodel/record_handler.py +653 -149
  41. sapiopycommons/rules/eln_rule_handler.py +89 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +89 -12
  43. sapiopycommons/sftpconnect/__init__.py +0 -0
  44. sapiopycommons/sftpconnect/sftp_builder.py +70 -0
  45. sapiopycommons/webhook/webhook_context.py +39 -0
  46. sapiopycommons/webhook/webhook_handlers.py +617 -69
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
  49. sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
  50. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  # Author Yechen Qiao
2
2
  # Common Molecule Utilities for Molecule Transfers with Sapio
3
+ from typing import cast
3
4
 
4
5
  from rdkit import Chem
5
6
  from rdkit.Chem import Crippen, MolToInchi
@@ -8,7 +9,8 @@ from rdkit.Chem import rdMolDescriptors
8
9
  from rdkit.Chem.EnumerateStereoisomers import StereoEnumerationOptions, EnumerateStereoisomers
9
10
  from rdkit.Chem.MolStandardize import rdMolStandardize
10
11
  from rdkit.Chem.SaltRemover import SaltRemover
11
- from rdkit.Chem.rdchem import Mol
12
+ from rdkit.Chem.rdChemReactions import ChemicalReaction
13
+ from rdkit.Chem.rdchem import Mol, RWMol, Bond
12
14
 
13
15
  from sapiopycommons.chem.IndigoMolecules import indigo, renderer, indigo_inchi
14
16
 
@@ -20,6 +22,43 @@ tautomer_params.tautomerReassignStereo = False
20
22
  tautomer_params.tautomerRemoveIsotopicHs = True
21
23
  enumerator = rdMolStandardize.TautomerEnumerator(tautomer_params)
22
24
 
25
+
26
+ def remove_dative_bonds_from_mol(mol: Mol) -> RWMol:
27
+ """
28
+ Create a new copy of RWMol molecule and remove all dative bonds in the molecule.
29
+ :param mol: The original molecule
30
+ :return: The new molecule with dative bonds removed.
31
+ """
32
+ ret: RWMol = Chem.RWMol(mol)
33
+ bonds_to_remove = []
34
+ bond: Bond
35
+ for bond in ret.GetBonds():
36
+ if bond.GetBondType() in [Chem.BondType.DATIVER, Chem.BondType.DATIVE, Chem.BondType.DATIVEL,
37
+ Chem.BondType.DATIVEONE]:
38
+ bonds_to_remove.append((bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()))
39
+ for atom1_idx, atom2_idx in bonds_to_remove:
40
+ ret.RemoveBond(atom1_idx, atom2_idx)
41
+ return ret
42
+
43
+
44
+ def get_enhanced_stereo_reg_hash(mol: Mol, enhanced_stereo: bool) -> str:
45
+ """
46
+ Get the Registration Hash for the molecule by the current registration configuration.
47
+ When we are running if we are canonicalization of tautomers or cleaning up any other way, do they first before calling.
48
+ :param mol: The molecule to obtain hash for.
49
+ :param canonical_tautomer: Whether the registry system canonicalize the tautomers.
50
+ :param enhanced_stereo: Whether we are computing enhanced stereo at all.
51
+ :return: The enhanced stereo hash.
52
+ """
53
+ if enhanced_stereo:
54
+ from rdkit.Chem.RegistrationHash import GetMolLayers, GetMolHash, HashScheme
55
+ layers = GetMolLayers(mol, enable_tautomer_hash_v2=True)
56
+ hash_scheme: HashScheme = HashScheme.TAUTOMER_INSENSITIVE_LAYERS
57
+ return GetMolHash(layers, hash_scheme=hash_scheme)
58
+ else:
59
+ return ""
60
+
61
+
23
62
  def neutralize_atoms(mol) -> Mol:
24
63
  """
25
64
  Neutralize atoms per https://baoilleach.blogspot.com/2019/12/no-charge-simple-approach-to.html
@@ -41,13 +80,14 @@ def neutralize_atoms(mol) -> Mol:
41
80
  def find_all_possible_stereoisomers(m: Mol, only_unassigned=True, try_embedding=True, unique=True, max_isomers=200) \
42
81
  -> list[Mol]:
43
82
  """
44
- Find all possible candidates of stereoisomers given the current molecule
45
- :param m: The molecule to search for
83
+ Find all possible candidates of stereoisomers given the current molecule.
84
+
85
+ :param m: The molecule to search for.
46
86
  :param only_unassigned: Whether to only permute on unspecified stereocenter.
47
87
  :param try_embedding: if set the process attempts to generate a standard RDKit distance geometry conformation for
48
88
  the stereisomer.
49
- If this fails, we assume that the stereoisomer is non-physical and don't return it
50
- :param unique: whether to remove duplicates by isomer identity
89
+ If this fails, we assume that the stereoisomer is non-physical and don't return it.
90
+ :param unique: whether to remove duplicates by isomer identity.
51
91
  :param max_isomers: Maximum number of search results to return.
52
92
  """
53
93
  # noinspection PyBroadException
@@ -61,7 +101,8 @@ def find_all_possible_stereoisomers(m: Mol, only_unassigned=True, try_embedding=
61
101
 
62
102
  def has_chiral_centers(m: Mol):
63
103
  """
64
- Returns true iff the molecule provided has at least 1 chiral centers (when stereochemistry is relevant)
104
+ Returns true iff the molecule provided has at least 1 chiral centers (when stereochemistry is relevant).
105
+
65
106
  :param m: The molecule to test.
66
107
  """
67
108
  # noinspection PyBroadException
@@ -75,24 +116,25 @@ def has_chiral_centers(m: Mol):
75
116
 
76
117
  def mol_to_img(mol_str: str) -> str:
77
118
  """
78
- Convert molecule into image
79
- :param mol_str: The molecule INCHI
119
+ Convert molecule into image.
120
+
121
+ :param mol_str: The molecule INCHI.
80
122
  :return: The SVG image text.
81
123
  """
82
124
  mol = indigo.loadMolecule(mol_str)
83
125
  return renderer.renderToString(mol)
84
126
 
85
127
 
86
-
87
128
  def mol_to_sapio_partial_pojo(mol: Mol):
88
129
  """
89
130
  Get the minimum information about molecule to Sapio, just its SMILES, V3000, and image data.
90
- :param mol: The molecule to read the simplified data from
131
+
132
+ :param mol: The molecule to read the simplified data from.
91
133
  """
92
134
  Chem.SanitizeMol(mol)
93
135
  mol.UpdatePropertyCache()
94
136
  smiles = Chem.MolToSmiles(mol)
95
- molBlock = Chem.MolToMolBlock(mol)
137
+ molBlock = Chem.MolToMolBlock(mol, forceV3000=True)
96
138
  img = mol_to_img(mol)
97
139
  molecule = dict()
98
140
  molecule["smiles"] = smiles
@@ -101,22 +143,52 @@ def mol_to_sapio_partial_pojo(mol: Mol):
101
143
  return molecule
102
144
 
103
145
 
104
- def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
146
+ def get_cxs_smiles_hash(mol: Mol, enhanced_stereo: bool) -> str:
147
+ """
148
+ Return the SHA1 CXS Smiles hash for the canonical, isomeric CXS SMILES of the molecule.
149
+ """
150
+ if not enhanced_stereo:
151
+ return ""
152
+ import hashlib
153
+ return hashlib.sha1(Chem.MolToCXSmiles(mol, canonical=True, isomericSmiles=True).encode()).hexdigest()
154
+
155
+
156
+ def get_has_or_group(mol: Mol, enhanced_stereo: bool) -> bool:
157
+ """
158
+ Return true if and only if: enhanced stereochemistry is enabled and there is at least one OR group in mol.
159
+ """
160
+ if not enhanced_stereo:
161
+ return False
162
+ from rdkit.Chem import StereoGroup_vect, STEREO_OR
163
+ stereo_groups: StereoGroup_vect = mol.GetStereoGroups()
164
+ for stereo_group in stereo_groups:
165
+ if stereo_group.GetGroupType() == STEREO_OR:
166
+ return True
167
+ return False
168
+
169
+
170
+ def mol_to_sapio_substance(mol: Mol, include_stereoisomers=False,
105
171
  normalize: bool = False, remove_salt: bool = False, make_images: bool = False,
106
- salt_def: str | None = None, canonical_tautomer: bool = True):
172
+ salt_def: str | None = None, canonical_tautomer: bool = True,
173
+ enhanced_stereo: bool = False, remove_atom_map: bool = True):
107
174
  """
108
175
  Convert a molecule in RDKit to a molecule POJO in Sapio.
109
- :param mol: The molecule in RDKit
110
- :param include_stereoisomers: If true, will compute all stereoisomer permutations of this molecule.
176
+
177
+ :param mol: The molecule in RDKit.
111
178
  :param normalize If true, will normalize the functional groups and return normalized result.
112
179
  :param remove_salt If true, we will remove salts iteratively from the molecule before returning their data.
113
180
  We will also populate desaltedList with molecules we deleted.
181
+ :param make_images Whether to make images as part of the result without having another script to resolve it.
114
182
  :param salt_def: if not none, specifies custom salt to be used during the desalt process.
115
183
  :param canonical_tautomer: if True, we will attempt to compute canonical tautomer for the molecule. Slow!
116
184
  This is needed for a registry. Note it stops after enumeration of 1000.
185
+ :param enhanced_stereo: If enabled, enhanced stereo hash will be produced.
186
+ :param remove_atom_map: When set, clear all atom AAM maps that were set had it been merged into some reactions earlier.
117
187
  :return: The molecule POJO for Sapio.
118
188
  """
119
189
  molecule = dict()
190
+ if remove_atom_map:
191
+ [a.SetAtomMapNum(0) for a in mol.GetAtoms()]
120
192
  Chem.SanitizeMol(mol)
121
193
  mol.UpdatePropertyCache()
122
194
  Chem.GetSymmSSSR(mol)
@@ -152,7 +224,7 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
152
224
  exactMass = Descriptors.ExactMolWt(mol)
153
225
  molFormula = rdMolDescriptors.CalcMolFormula(mol)
154
226
  charge = Chem.GetFormalCharge(mol)
155
- molBlock = Chem.MolToMolBlock(mol)
227
+ molBlock = Chem.MolToMolBlock(mol, forceV3000=True)
156
228
 
157
229
  molecule["cLogP"] = cLogP
158
230
  molecule["tpsa"] = tpsa
@@ -164,7 +236,9 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
164
236
  # This is number of H-Bond Donor
165
237
  molecule["numHBonds"] = rdMolDescriptors.CalcNumHBD(mol)
166
238
  molecule["molBlock"] = molBlock
167
- rdkit_inchi = MolToInchi(mol)
239
+ # Create a copy of molecule before modifying it for InChI generation.
240
+ inchi_mol: Mol = remove_dative_bonds_from_mol(mol)
241
+ rdkit_inchi = MolToInchi(inchi_mol)
168
242
  # If INCHI is completely invalid, we fail this molecule.
169
243
  if not rdkit_inchi:
170
244
  MolToInchi(mol, treatWarningAsError=True)
@@ -176,27 +250,37 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
176
250
  # We need to test the INCHI can be loaded back to indigo.
177
251
  indigo_mol = indigo.loadMolecule(molBlock)
178
252
  indigo_mol.aromatize()
179
- indigo_inchi_str = indigo_inchi.getInchi(indigo_mol)
180
- molecule["inchi"] = indigo_inchi_str
181
- indigo_inchi_key_str = indigo_inchi.getInchiKey(indigo_inchi_str)
182
- molecule["inchiKey"] = indigo_inchi_key_str
253
+ if enhanced_stereo:
254
+ # Remove enhanced stereo layer when generating InChI as the stereo hash is generated separately for reg.
255
+ Chem.CanonicalizeEnhancedStereo(inchi_mol)
256
+ molecule["inchi"] = Chem.MolToInchi(inchi_mol)
257
+ molecule["inchiKey"] = Chem.MolToInchiKey(inchi_mol)
258
+ else:
259
+ indigo_inchi.resetOptions()
260
+ indigo_inchi_mol = indigo.loadMolecule(Chem.MolToMolBlock(inchi_mol, forceV3000=True))
261
+ indigo_inchi_str = indigo_inchi.getInchi(indigo_inchi_mol)
262
+ molecule["inchi"] = indigo_inchi_str
263
+ indigo_inchi_key_str = indigo_inchi.getInchiKey(indigo_inchi_str)
264
+ molecule["inchiKey"] = indigo_inchi_key_str
183
265
  molecule["smiles"] = indigo_mol.smiles()
184
-
185
- if include_stereoisomers and has_chiral_centers(mol):
186
- stereoisomers = find_all_possible_stereoisomers(mol, only_unassigned=False, try_embedding=False, unique=True)
187
- molecule["stereoisomers"] = [mol_to_sapio_partial_pojo(x) for x in stereoisomers]
266
+ molecule["reg_hash"] = get_enhanced_stereo_reg_hash(mol, enhanced_stereo=enhanced_stereo)
267
+ molecule["cxsmiles_hash"] = get_cxs_smiles_hash(mol, enhanced_stereo=enhanced_stereo)
268
+ molecule["has_or_group"] = get_has_or_group(mol, enhanced_stereo=enhanced_stereo)
188
269
  return molecule
189
270
 
190
271
 
191
- def mol_to_sapio_compound(mol: Mol, include_stereoisomers: bool = False,
272
+ def mol_to_sapio_compound(mol: Mol, include_stereoisomers=False, enhanced_stereo: bool = False,
192
273
  salt_def: str | None = None, resolve_canonical: bool = True,
193
- make_images: bool = False, canonical_tautomer: bool = True):
274
+ make_images: bool = False, canonical_tautomer: bool = True,
275
+ remove_atom_map: bool = True):
194
276
  ret = dict()
195
- ret['originalMol'] = mol_to_sapio_substance(mol, include_stereoisomers,
277
+ ret['originalMol'] = mol_to_sapio_substance(mol, include_stereoisomers=False,
196
278
  normalize=False, remove_salt=False, make_images=make_images,
197
- canonical_tautomer=canonical_tautomer)
279
+ canonical_tautomer=canonical_tautomer,
280
+ enhanced_stereo=enhanced_stereo, remove_atom_map=remove_atom_map)
198
281
  if resolve_canonical:
199
282
  ret['canonicalMol'] = mol_to_sapio_substance(mol, include_stereoisomers=False,
200
283
  normalize=True, remove_salt=True, make_images=make_images,
201
- salt_def=salt_def, canonical_tautomer=canonical_tautomer)
284
+ salt_def=salt_def, canonical_tautomer=canonical_tautomer,
285
+ enhanced_stereo=enhanced_stereo, remove_atom_map=remove_atom_map)
202
286
  return ret
File without changes
@@ -0,0 +1,60 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn
2
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
3
+
4
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil
5
+ from sapiopycommons.general.exceptions import SapioException
6
+
7
+ # The system fields that every record has and their field types. System fields aren't generated as record model fields
8
+ # for all platform version, hence the need to create a dict for them in the off chance that they're not present on
9
+ # the model wrapper.
10
+ SYSTEM_FIELDS: dict[str, FieldType] = {
11
+ "DataRecordName": FieldType.IDENTIFIER,
12
+ "RecordId": FieldType.LONG,
13
+ "DateCreated": FieldType.DATE,
14
+ "CreatedBy": FieldType.STRING,
15
+ "VeloxLastModifiedDate": FieldType.DATE,
16
+ "VeloxLastModifiedBy": FieldType.STRING
17
+ }
18
+
19
+
20
+ class ColumnBuilder:
21
+ """
22
+ A class for building report columns for custom reports.
23
+ """
24
+ @staticmethod
25
+ def build_column(data_type: DataTypeIdentifier, field: FieldIdentifier, field_type: FieldType | None = None) \
26
+ -> ReportColumn:
27
+ """
28
+ Build a ReportColumn from a variety of possible inputs.
29
+
30
+ :param data_type: An object that can be used to identify a data type.
31
+ :param field: An object that can be used to identify a data field.
32
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
33
+ determined from the given data type and field, which occurs when the given field is a string and the
34
+ given data type is not a wrapped record model or record model wrapper.
35
+ :return: A ReportColumn for the inputs.
36
+ """
37
+ # Get the data type and field names from the inputs.
38
+ data_type_name = AliasUtil.to_data_type_name(data_type)
39
+ field_name = AliasUtil.to_data_field_name(field)
40
+ if field_type is None:
41
+ field_type = ColumnBuilder.__field_type(data_type, field)
42
+ if field_type is None:
43
+ raise SapioException("The field_type parameter is required for the provided data_type and field inputs.")
44
+ return ReportColumn(data_type_name, field_name, field_type)
45
+
46
+ @staticmethod
47
+ def __field_type(data_type: DataTypeIdentifier, field: FieldIdentifier) -> FieldType | None:
48
+ """
49
+ Given a record model wrapper and a field name, return the field type for that field. Accounts for system fields.
50
+
51
+ :param data_type: The record model wrapper that the field is on.
52
+ :param field: The field name to return the type of.
53
+ :return: The field type of the given field name.
54
+ """
55
+ # Check if the field name is a system field. If it is, use the field type defined in this file.
56
+ field_name: str = AliasUtil.to_data_field_name(field)
57
+ if field_name in SYSTEM_FIELDS:
58
+ return SYSTEM_FIELDS.get(field_name)
59
+ # Otherwise, check if the field type can be found from the wrapper.
60
+ return AliasUtil.to_field_type(field, data_type)
@@ -0,0 +1,137 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria, AbstractReportTerm, \
2
+ ExplicitJoinDefinition, RelatedRecordCriteria, QueryRestriction, FieldCompareReportTerm
3
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
4
+
5
+ from sapiopycommons.customreport.column_builder import ColumnBuilder
6
+ from sapiopycommons.customreport.term_builder import TermBuilder
7
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil, SapioRecord
8
+ from sapiopycommons.general.exceptions import SapioException
9
+
10
+
11
+ class CustomReportBuilder:
12
+ """
13
+ A class used for building custom reports. Look into using the TermBuilder and ColumnBuilder classes for building
14
+ parts of a custom report.
15
+ """
16
+ root_data_type: DataTypeIdentifier
17
+ data_type_name: str
18
+ root_term: AbstractReportTerm | None
19
+ record_criteria: RelatedRecordCriteria
20
+ column_list: list[ReportColumn]
21
+ join_list: list[ExplicitJoinDefinition]
22
+
23
+ def __init__(self, root_data_type: DataTypeIdentifier):
24
+ """
25
+ :param root_data_type: An object that can be used to identify a data type name. Used as the root data type name
26
+ of this search.
27
+ """
28
+ self.root_data_type = root_data_type
29
+ self.data_type_name = AliasUtil.to_data_type_name(root_data_type)
30
+ self.root_term = None
31
+ self.record_criteria = RelatedRecordCriteria(QueryRestriction.QUERY_ALL)
32
+ self.column_list = []
33
+ self.join_list = []
34
+
35
+ def get_term_builder(self) -> TermBuilder:
36
+ """
37
+ :return: A TermBuilder with a data type matching this report builder's root data type.
38
+ """
39
+ return TermBuilder(self.root_data_type)
40
+
41
+ def has_root_term(self) -> bool:
42
+ """
43
+ :return: Whether this report builder has had its root term set.
44
+ """
45
+ return self.root_term is not None
46
+
47
+ def set_root_term(self, term: AbstractReportTerm) -> None:
48
+ """
49
+ Set the root term of the report. Use the TermBuilder class to construct the report terms.
50
+
51
+ :param term: The term to set as the root term.
52
+ """
53
+ self.root_term = term
54
+
55
+ def has_columns(self) -> bool:
56
+ """
57
+ :return: Whether this report builder has any report columns.
58
+ """
59
+ return bool(self.column_list)
60
+
61
+ def add_column(self, field: FieldIdentifier, field_type: FieldType = None,
62
+ *, data_type: DataTypeIdentifier | None = None) -> None:
63
+ """
64
+ Add a column to this report builder.
65
+
66
+ :param field: An object that can be used to identify a data field.
67
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
68
+ determined from the given data type and field, which occurs when the given field is a string and the
69
+ given data type is not a wrapped record model or record model wrapper.
70
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
71
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
72
+ that is from a different data type than the root data type.
73
+ """
74
+ if data_type is None:
75
+ data_type = self.root_data_type
76
+ self.column_list.append(ColumnBuilder.build_column(data_type, field, field_type))
77
+
78
+ def add_columns(self, fields: list[FieldIdentifier], *, data_type: DataTypeIdentifier | None = None) -> None:
79
+ """
80
+ Add columns to this report builder.
81
+
82
+ :param fields: A list of objects that can be used to identify data fields.
83
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
84
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
85
+ that is from a different data type than the root data type.
86
+ """
87
+ for field in fields:
88
+ self.add_column(field, data_type=data_type)
89
+
90
+ def set_query_restriction(self, base_record: SapioRecord, search_related: QueryRestriction) -> None:
91
+ """
92
+ Set a restriction on the report for this report builder such that the returned results must be related in
93
+ some way to the provided base record. Without this, the report searches all records in the system that match the
94
+ root term.
95
+
96
+ :param base_record: The base record to run the search from.
97
+ :param search_related: Determine the relationship of the related records that can appear in the search, be those
98
+ children, parents, descendants, or ancestors.
99
+ """
100
+ if search_related == QueryRestriction.QUERY_ALL:
101
+ raise SapioException("The search_related must be something other than QUERY_ALL when setting a query restriction.")
102
+ self.record_criteria = RelatedRecordCriteria(search_related,
103
+ AliasUtil.to_record_id(base_record),
104
+ AliasUtil.to_data_type_name(base_record))
105
+
106
+ def add_join(self, comparison_term: FieldCompareReportTerm, data_type: DataTypeIdentifier | None = None) -> None:
107
+ """
108
+ Add a join statement to this report builder.
109
+
110
+ :param comparison_term: The field comparison term to join with.
111
+ :param data_type: The data type name that this join is on. If not provided, then the left side data type name
112
+ of the comparison term will be the data type that is joined against.
113
+ """
114
+ if data_type is None:
115
+ data_type: str = comparison_term.left_data_type_name
116
+ else:
117
+ data_type: str = AliasUtil.to_data_type_name(data_type)
118
+ self.join_list.append(ExplicitJoinDefinition(data_type, comparison_term))
119
+
120
+ def build_report_criteria(self, page_size: int = 0, page_number: int = -1, case_sensitive: bool = False,
121
+ owner_restriction_set: list[str] = None) -> CustomReportCriteria:
122
+ """
123
+ Generate a CustomReportCriteria using the column list, root term, and root data type from this report builder.
124
+ You can use the CustomReportManager or CustomReportUtil to run the constructed report.
125
+
126
+ :param page_size: The page size of the custom report.
127
+ :param page_number: The page number of the current report.
128
+ :param case_sensitive: When searching texts, should the search be case-sensitive?
129
+ :param owner_restriction_set: Specifies to only return records if the record is owned by this list of usernames.
130
+ :return: A CustomReportCriteria from this report builder.
131
+ """
132
+ if not self.has_root_term():
133
+ raise SapioException("Cannot build a report with no root term.")
134
+ if not self.has_columns():
135
+ raise SapioException("Cannot build a report with no columns.")
136
+ return CustomReportCriteria(self.column_list, self.root_term, self.record_criteria, self.data_type_name,
137
+ case_sensitive, page_size, page_number, owner_restriction_set, self.join_list)