sapiopycommons 2024.11.8a355__py3-none-any.whl → 2024.11.8a359__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/callbacks/callback_util.py +83 -532
- sapiopycommons/chem/IndigoMolecules.py +0 -2
- sapiopycommons/chem/Molecules.py +18 -77
- sapiopycommons/datatype/attachment_util.py +10 -11
- sapiopycommons/eln/experiment_handler.py +70 -272
- sapiopycommons/files/complex_data_loader.py +4 -5
- sapiopycommons/files/file_bridge.py +24 -31
- sapiopycommons/files/file_data_handler.py +5 -2
- sapiopycommons/files/file_util.py +9 -59
- sapiopycommons/files/file_validator.py +6 -92
- sapiopycommons/files/file_writer.py +15 -44
- sapiopycommons/general/aliases.py +6 -207
- sapiopycommons/general/custom_report_util.py +37 -212
- sapiopycommons/general/exceptions.py +8 -21
- sapiopycommons/general/popup_util.py +0 -21
- sapiopycommons/general/time_util.py +2 -8
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +97 -481
- sapiopycommons/rules/eln_rule_handler.py +25 -34
- sapiopycommons/rules/on_save_rule_handler.py +31 -34
- sapiopycommons/webhook/webhook_handlers.py +42 -201
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/METADATA +2 -4
- sapiopycommons-2024.11.8a359.dist-info/RECORD +38 -0
- sapiopycommons/callbacks/field_builder.py +0 -537
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +0 -60
- sapiopycommons/customreport/custom_report_builder.py +0 -130
- sapiopycommons/customreport/term_builder.py +0 -299
- sapiopycommons/datatype/data_fields.py +0 -61
- sapiopycommons/datatype/pseudo_data_types.py +0 -440
- sapiopycommons/eln/experiment_report_util.py +0 -653
- sapiopycommons/files/file_bridge_handler.py +0 -340
- sapiopycommons/flowcyto/flow_cyto.py +0 -77
- sapiopycommons/flowcyto/flowcyto_data.py +0 -75
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/general/audit_log.py +0 -189
- sapiopycommons/general/sapio_links.py +0 -50
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -489
- sapiopycommons/processtracking/custom_workflow_handler.py +0 -406
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +0 -69
- sapiopycommons/webhook/webhook_context.py +0 -39
- sapiopycommons/webhook/webservice_handlers.py +0 -67
- sapiopycommons-2024.11.8a355.dist-info/RECORD +0 -59
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.11.8a355.dist-info → sapiopycommons-2024.11.8a359.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,51 +1,24 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from sapiopylib.rest.User import SapioUser
|
|
5
4
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
6
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
7
5
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
8
|
-
from sapiopylib.rest.
|
|
9
|
-
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
10
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
|
-
from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
|
|
6
|
+
from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
|
|
12
7
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
13
|
-
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
|
|
8
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
FieldValue = int | float | str | bool | None
|
|
18
|
-
"""Allowable values for fields in the system."""
|
|
19
|
-
RecordModel = PyRecordModel | WrappedRecordModel
|
|
10
|
+
RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
|
|
20
11
|
"""Different forms that a record model could take."""
|
|
21
12
|
SapioRecord = DataRecord | RecordModel
|
|
22
13
|
"""A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
|
|
23
14
|
RecordIdentifier = SapioRecord | int
|
|
24
15
|
"""A RecordIdentifier is either a record type or an integer for the record's record ID."""
|
|
25
|
-
DataTypeIdentifier = SapioRecord | type[WrappedType] | str
|
|
26
|
-
"""A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
|
|
27
|
-
FieldIdentifier = WrapperField | str | tuple[str, FieldType]
|
|
28
|
-
"""A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
|
|
29
|
-
and field type."""
|
|
30
|
-
FieldIdentifierKey = WrapperField | str
|
|
31
|
-
"""A FieldIdentifierKey is a FieldIdentifier, except it can't be a tuple, s tuples can't be used as keys in
|
|
32
|
-
dictionaries.."""
|
|
33
|
-
HasFieldWrappers = type[WrappedType] | WrappedRecordModel
|
|
34
|
-
"""An identifier for classes that have wrapper fields."""
|
|
35
16
|
ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
|
|
36
|
-
"""An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for
|
|
17
|
+
"""An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
|
|
37
18
|
ID."""
|
|
38
|
-
|
|
39
|
-
"""An ExperimentEntryIdentifier is either an ELN entry step, experiment entry, or an integer for the entry's ID."""
|
|
40
|
-
FieldMap = dict[str, FieldValue]
|
|
19
|
+
FieldMap = dict[str, Any]
|
|
41
20
|
"""A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
|
|
42
21
|
any random dict in a webhook from one which is explicitly used for record fields."""
|
|
43
|
-
FieldIdentifierMap = dict[FieldIdentifierKey, FieldValue]
|
|
44
|
-
"""A field identifier map is the same thing as a field map, except the keys can be field identifiers instead
|
|
45
|
-
of just strings. Note that although one of the allowed field identifiers is a tuple, you can't use tuples as
|
|
46
|
-
keys in a dictionary."""
|
|
47
|
-
UserIdentifier = SapioWebhookContext | SapioUser
|
|
48
|
-
"""An identifier for classes from which a user object can be used for sending requests."""
|
|
49
22
|
|
|
50
23
|
|
|
51
24
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -77,132 +50,7 @@ class AliasUtil:
|
|
|
77
50
|
|
|
78
51
|
:return: A list of record IDs for the input records.
|
|
79
52
|
"""
|
|
80
|
-
return [(
|
|
81
|
-
|
|
82
|
-
@staticmethod
|
|
83
|
-
def to_record_id(record: RecordIdentifier):
|
|
84
|
-
"""
|
|
85
|
-
Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
|
|
86
|
-
or WrappedRecordModel to just an integer (taking the record ID from the record).
|
|
87
|
-
|
|
88
|
-
:return: A record ID for the input record.
|
|
89
|
-
"""
|
|
90
|
-
return record if isinstance(record, int) else record.record_id
|
|
91
|
-
|
|
92
|
-
@staticmethod
|
|
93
|
-
def to_data_type_name(value: DataTypeIdentifier, convert_eln_dts: bool = True) -> str:
|
|
94
|
-
"""
|
|
95
|
-
Convert a given value to a data type name.
|
|
96
|
-
|
|
97
|
-
:param value: A value which is a string, record, or record model type.
|
|
98
|
-
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
99
|
-
:return: A string of the data type name of the input value.
|
|
100
|
-
"""
|
|
101
|
-
if isinstance(value, SapioRecord):
|
|
102
|
-
value = value.data_type_name
|
|
103
|
-
elif not isinstance(value, str):
|
|
104
|
-
value = value.get_wrapper_data_type_name()
|
|
105
|
-
if convert_eln_dts and ElnBaseDataType.is_eln_type(value):
|
|
106
|
-
return ElnBaseDataType.get_base_type(value).data_type_name
|
|
107
|
-
return value
|
|
108
|
-
|
|
109
|
-
@staticmethod
|
|
110
|
-
def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False,
|
|
111
|
-
convert_eln_dts: bool = True) -> list[str] | set[str]:
|
|
112
|
-
"""
|
|
113
|
-
Convert a given iterable of values to a list or set of data type names.
|
|
114
|
-
|
|
115
|
-
:param values: An iterable of values which are strings, records, or record model types.
|
|
116
|
-
:param return_set: If true, return a set instead of a list.
|
|
117
|
-
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
118
|
-
:return: A list or set of strings of the data type name of the input value.
|
|
119
|
-
"""
|
|
120
|
-
values = [AliasUtil.to_data_type_name(x, convert_eln_dts) for x in values]
|
|
121
|
-
return set(values) if return_set else values
|
|
122
|
-
|
|
123
|
-
@staticmethod
|
|
124
|
-
def to_singular_data_type_name(values: Iterable[DataTypeIdentifier], convert_eln_dts: bool = True) -> str:
|
|
125
|
-
"""
|
|
126
|
-
Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
|
|
127
|
-
than one data type name exists in the provided list of identifiers.
|
|
128
|
-
|
|
129
|
-
:param values: An iterable of values which are strings, records, or record model types.
|
|
130
|
-
:param convert_eln_dts: If true, convert ELN data types to their base data type name.
|
|
131
|
-
:return: The single data type name that the input vales share. Returns an empty string if an empty iterable
|
|
132
|
-
was provided.
|
|
133
|
-
"""
|
|
134
|
-
if not values:
|
|
135
|
-
return ""
|
|
136
|
-
data_types: set[str] = AliasUtil.to_data_type_names(values, True, convert_eln_dts)
|
|
137
|
-
if len(data_types) > 1:
|
|
138
|
-
raise SapioException(f"Provided values contain multiple data types: {data_types}. "
|
|
139
|
-
f"Only expecting a single data type.")
|
|
140
|
-
return data_types.pop()
|
|
141
|
-
|
|
142
|
-
@staticmethod
|
|
143
|
-
def to_data_field_name(value: FieldIdentifier) -> str:
|
|
144
|
-
"""
|
|
145
|
-
Convert a string or WrapperField to a data field name string.
|
|
146
|
-
|
|
147
|
-
:param value: A string or WrapperField.
|
|
148
|
-
:return: A string of the data field name of the input value.
|
|
149
|
-
"""
|
|
150
|
-
if isinstance(value, tuple):
|
|
151
|
-
return value[0]
|
|
152
|
-
if isinstance(value, WrapperField):
|
|
153
|
-
return value.field_name
|
|
154
|
-
return value
|
|
155
|
-
|
|
156
|
-
@staticmethod
|
|
157
|
-
def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
|
|
158
|
-
"""
|
|
159
|
-
Convert an iterable of strings or WrapperFields to a list of data field name strings.
|
|
160
|
-
|
|
161
|
-
:param values: An iterable of strings or WrapperFields.
|
|
162
|
-
:return: A list of strings of the data field names of the input values.
|
|
163
|
-
"""
|
|
164
|
-
return [AliasUtil.to_data_field_name(x) for x in values]
|
|
165
|
-
|
|
166
|
-
@staticmethod
|
|
167
|
-
def to_data_field_names_dict(values: dict[FieldIdentifierKey, Any]) -> dict[str, Any]:
|
|
168
|
-
"""
|
|
169
|
-
Take a dictionary whose keys are field identifiers and convert them all to strings for the data field name.
|
|
170
|
-
|
|
171
|
-
:param values: A dictionary of field identifiers to field values.
|
|
172
|
-
:return: A dictionary of strings of the data field names to field values for the input values.
|
|
173
|
-
"""
|
|
174
|
-
ret_dict: dict[str, FieldValue] = {}
|
|
175
|
-
for field, value in values.items():
|
|
176
|
-
ret_dict[AliasUtil.to_data_field_name(field)] = value
|
|
177
|
-
return ret_dict
|
|
178
|
-
|
|
179
|
-
@staticmethod
|
|
180
|
-
def to_data_field_names_list_dict(values: list[dict[FieldIdentifierKey, Any]]) -> list[dict[str, Any]]:
|
|
181
|
-
ret_list: list[dict[str, Any]] = []
|
|
182
|
-
for field_map in values:
|
|
183
|
-
ret_list.append(AliasUtil.to_data_field_names_dict(field_map))
|
|
184
|
-
return ret_list
|
|
185
|
-
|
|
186
|
-
@staticmethod
|
|
187
|
-
def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
|
|
188
|
-
"""
|
|
189
|
-
Convert a given field identifier to the field type for that field.
|
|
190
|
-
|
|
191
|
-
:param field: A string or WrapperField.
|
|
192
|
-
:param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
|
|
193
|
-
must be provided to determine the field type.
|
|
194
|
-
:return: The field type of the given field.
|
|
195
|
-
"""
|
|
196
|
-
if isinstance(field, tuple):
|
|
197
|
-
return field[1]
|
|
198
|
-
if isinstance(field, WrapperField):
|
|
199
|
-
return field.field_type
|
|
200
|
-
for var in dir(data_type):
|
|
201
|
-
attr = getattr(data_type, var)
|
|
202
|
-
if isinstance(attr, WrapperField) and attr.field_name == field:
|
|
203
|
-
return attr.field_type
|
|
204
|
-
raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
|
|
205
|
-
f"field with the name \"{field}\",")
|
|
53
|
+
return [(x if isinstance(x, int) else x.record_id) for x in records]
|
|
206
54
|
|
|
207
55
|
@staticmethod
|
|
208
56
|
def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
|
|
@@ -215,7 +63,6 @@ class AliasUtil:
|
|
|
215
63
|
field_map_list: list[FieldMap] = []
|
|
216
64
|
for record in records:
|
|
217
65
|
if isinstance(record, DataRecord):
|
|
218
|
-
# noinspection PyTypeChecker
|
|
219
66
|
field_map_list.append(record.get_fields())
|
|
220
67
|
else:
|
|
221
68
|
field_map_list.append(record.fields.copy_to_dict())
|
|
@@ -233,51 +80,3 @@ class AliasUtil:
|
|
|
233
80
|
if isinstance(experiment, ElnExperiment):
|
|
234
81
|
return experiment.notebook_experiment_id
|
|
235
82
|
return experiment.get_id()
|
|
236
|
-
|
|
237
|
-
@staticmethod
|
|
238
|
-
def to_notebook_ids(experiments: list[ExperimentIdentifier]) -> list[int]:
|
|
239
|
-
"""
|
|
240
|
-
Convert a list of objects that identify ELN experiments to their notebook IDs.
|
|
241
|
-
|
|
242
|
-
:return: The list of notebook IDs for the experiment identifiers.
|
|
243
|
-
"""
|
|
244
|
-
notebook_ids: list[int] = []
|
|
245
|
-
for experiment in experiments:
|
|
246
|
-
notebook_ids.append(AliasUtil.to_notebook_id(experiment))
|
|
247
|
-
return notebook_ids
|
|
248
|
-
|
|
249
|
-
@staticmethod
|
|
250
|
-
def to_entry_id(entry: ExperimentEntryIdentifier) -> int:
|
|
251
|
-
"""
|
|
252
|
-
Convert an object that identifies an experiment entry to its entry ID.
|
|
253
|
-
|
|
254
|
-
:return: The entry ID for the entry identifier.
|
|
255
|
-
"""
|
|
256
|
-
if isinstance(entry, int):
|
|
257
|
-
return entry
|
|
258
|
-
elif isinstance(entry, ExperimentEntry):
|
|
259
|
-
return entry.entry_id
|
|
260
|
-
elif isinstance(entry, ElnEntryStep):
|
|
261
|
-
return entry.get_id()
|
|
262
|
-
raise SapioException(f"Unrecognized entry identifier of type {type(entry)}")
|
|
263
|
-
|
|
264
|
-
@staticmethod
|
|
265
|
-
def to_entry_ids(entries: list[ExperimentEntryIdentifier]) -> list[int]:
|
|
266
|
-
"""
|
|
267
|
-
Convert a list of objects that identify experiment entries to their entry IDs.
|
|
268
|
-
|
|
269
|
-
:return: The list of entry IDs for the entry identifiers.
|
|
270
|
-
"""
|
|
271
|
-
entry_ids: list[int] = []
|
|
272
|
-
for entry in entries:
|
|
273
|
-
entry_ids.append(AliasUtil.to_entry_id(entry))
|
|
274
|
-
return entry_ids
|
|
275
|
-
|
|
276
|
-
@staticmethod
|
|
277
|
-
def to_sapio_user(context: UserIdentifier) -> SapioUser:
|
|
278
|
-
"""
|
|
279
|
-
Convert an object that could be either a SapioUser or SapioWebhookContext to just a SapioUser.
|
|
280
|
-
|
|
281
|
-
:return: A SapioUser object.
|
|
282
|
-
"""
|
|
283
|
-
return context if isinstance(context, SapioUser) else context.user
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
4
5
|
from sapiopylib.rest.User import SapioUser
|
|
5
|
-
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport
|
|
6
|
-
|
|
7
|
-
from sapiopycommons.general.aliases import UserIdentifier, FieldValue, AliasUtil, FieldIdentifierKey
|
|
6
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport
|
|
7
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
11
11
|
class CustomReportUtil:
|
|
12
12
|
@staticmethod
|
|
13
|
-
def run_system_report(context:
|
|
13
|
+
def run_system_report(context: SapioWebhookContext | SapioUser,
|
|
14
14
|
report_name: str,
|
|
15
|
-
filters: dict[
|
|
16
|
-
page_limit: int | None = None,
|
|
17
|
-
page_size: int | None = None,
|
|
18
|
-
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
15
|
+
filters: dict[str, Iterable[Any]] | None = None,
|
|
16
|
+
page_limit: int | None = None) -> list[dict[str, Any]]:
|
|
19
17
|
"""
|
|
20
18
|
Run a system report and return the results of that report as a list of dictionaries for the values of each
|
|
21
19
|
column in each row.
|
|
@@ -29,97 +27,29 @@ class CustomReportUtil:
|
|
|
29
27
|
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
30
28
|
any filters that have a header name that isn't in the custom report will be ignored.
|
|
31
29
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
32
|
-
:param page_size: The size of each page of results in the search. If None, the page size is set by the server.
|
|
33
|
-
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
34
|
-
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
35
|
-
values in the dicts are the data field names of the columns.
|
|
36
|
-
If two columns in the search have the same data field name but differing data type names, then the
|
|
37
|
-
dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
|
|
38
|
-
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
39
|
-
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
40
|
-
"""
|
|
41
|
-
results: tuple = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit,
|
|
42
|
-
page_size, page_number)
|
|
43
|
-
columns: list[ReportColumn] = results[0]
|
|
44
|
-
rows: list[list[FieldValue]] = results[1]
|
|
45
|
-
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def run_custom_report(context: UserIdentifier,
|
|
49
|
-
report_criteria: CustomReportCriteria,
|
|
50
|
-
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
51
|
-
page_limit: int | None = None,
|
|
52
|
-
page_size: int | None = None,
|
|
53
|
-
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
54
|
-
"""
|
|
55
|
-
Run a custom report and return the results of that report as a list of dictionaries for the values of each
|
|
56
|
-
column in each row.
|
|
57
|
-
|
|
58
|
-
Custom reports are constructed by the caller, specifying the report terms and the columns that will be in the
|
|
59
|
-
results. They are like advanced or predefined searches from the system, except they are constructed from
|
|
60
|
-
within the webhook instead of from within the system.
|
|
61
|
-
|
|
62
|
-
:param context: The current webhook context or a user object to send requests from.
|
|
63
|
-
:param report_criteria: The custom report criteria to run.
|
|
64
|
-
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
65
|
-
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
66
|
-
any filters that have a header name that isn't in the custom report will be ignored.
|
|
67
|
-
Note that this parameter is only provided for parity with the other run report functions. If you need to
|
|
68
|
-
filter the results of a search, it would likely be more beneficial to have just added a new term to the
|
|
69
|
-
input report criteria that corresponds to the filter.
|
|
70
|
-
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
71
|
-
:param page_size: The size of each page of results in the search. If None, uses the value from the given report
|
|
72
|
-
criteria. If not None, overwrites the value from the given report criteria.
|
|
73
|
-
:param page_number: The page number to start the search from, If None, uses the value from the given report
|
|
74
|
-
criteria. If not None, overwrites the value from the given report criteria.
|
|
75
30
|
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
76
31
|
values in the dicts are the data field names of the columns.
|
|
77
|
-
If two columns in the search have the same data field name but differing data type names, then the
|
|
78
|
-
dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
|
|
79
|
-
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
80
|
-
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
81
32
|
"""
|
|
82
|
-
results
|
|
83
|
-
page_size, page_number)
|
|
33
|
+
results = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit)
|
|
84
34
|
columns: list[ReportColumn] = results[0]
|
|
85
|
-
rows: list[list[
|
|
86
|
-
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
87
|
-
|
|
88
|
-
@staticmethod
|
|
89
|
-
def run_quick_report(context: UserIdentifier,
|
|
90
|
-
report_term: RawReportTerm,
|
|
91
|
-
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
92
|
-
page_limit: int | None = None,
|
|
93
|
-
page_size: int | None = None,
|
|
94
|
-
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
95
|
-
"""
|
|
96
|
-
Run a quick report and return the results of that report as a list of dictionaries for the values of each
|
|
97
|
-
column in each row.
|
|
35
|
+
rows: list[list[Any]] = results[1]
|
|
98
36
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
113
|
-
values in the dicts are the data field names of the columns.
|
|
114
|
-
"""
|
|
115
|
-
results: tuple = CustomReportUtil.__exhaust_quick_report(context, report_term, page_limit,
|
|
116
|
-
page_size, page_number)
|
|
117
|
-
columns: list[ReportColumn] = results[0]
|
|
118
|
-
rows: list[list[FieldValue]] = results[1]
|
|
119
|
-
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
37
|
+
ret: list[dict[str, Any]] = []
|
|
38
|
+
for row in rows:
|
|
39
|
+
row_data: dict[str, Any] = {}
|
|
40
|
+
filter_row: bool = False
|
|
41
|
+
for value, column in zip(row, columns):
|
|
42
|
+
header: str = column.data_field_name
|
|
43
|
+
if filters is not None and header in filters and value not in filters.get(header):
|
|
44
|
+
filter_row = True
|
|
45
|
+
break
|
|
46
|
+
row_data.update({header: value})
|
|
47
|
+
if filter_row is False:
|
|
48
|
+
ret.append(row_data)
|
|
49
|
+
return ret
|
|
120
50
|
|
|
121
51
|
@staticmethod
|
|
122
|
-
def get_system_report_criteria(context:
|
|
52
|
+
def get_system_report_criteria(context: SapioWebhookContext | SapioUser, report_name: str) -> CustomReport:
|
|
123
53
|
"""
|
|
124
54
|
Retrieve a custom report from the system given the name of the report. This works by querying the system report
|
|
125
55
|
with a page number and size of 1 to minimize the amount of data transfer needed to retrieve the report's config.
|
|
@@ -134,132 +64,27 @@ class CustomReportUtil:
|
|
|
134
64
|
:param report_name: The name of the system report to run.
|
|
135
65
|
:return: The CustomReport object for the given system report name.
|
|
136
66
|
"""
|
|
137
|
-
user: SapioUser =
|
|
67
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
138
68
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
139
69
|
return report_man.run_system_report_by_name(report_name, 1, 1)
|
|
140
70
|
|
|
141
71
|
@staticmethod
|
|
142
|
-
def __exhaust_system_report(context:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
page_size: int | None,
|
|
146
|
-
page_number: int | None) \
|
|
147
|
-
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
148
|
-
"""
|
|
149
|
-
Given a system report, iterate over every page of the report and collect the results
|
|
150
|
-
until there are no remaining pages.
|
|
151
|
-
"""
|
|
152
|
-
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
153
|
-
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
154
|
-
|
|
155
|
-
result = None
|
|
156
|
-
has_next_page: bool = True
|
|
157
|
-
rows: list[list[FieldValue]] = []
|
|
158
|
-
cur_page: int = 1
|
|
159
|
-
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
160
|
-
result = report_man.run_system_report_by_name(report_name, page_size, page_number)
|
|
161
|
-
page_size = result.page_size
|
|
162
|
-
page_number = result.page_number
|
|
163
|
-
has_next_page = result.has_next_page
|
|
164
|
-
rows.extend(result.result_table)
|
|
165
|
-
cur_page += 1
|
|
166
|
-
return result.column_list, rows
|
|
167
|
-
|
|
168
|
-
@staticmethod
|
|
169
|
-
def __exhaust_custom_report(context: UserIdentifier,
|
|
170
|
-
report: CustomReportCriteria,
|
|
171
|
-
page_limit: int | None,
|
|
172
|
-
page_size: int | None,
|
|
173
|
-
page_number: int | None) \
|
|
174
|
-
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
175
|
-
"""
|
|
176
|
-
Given a custom report, iterate over every page of the report and collect the results
|
|
177
|
-
until there are no remaining pages.
|
|
178
|
-
"""
|
|
179
|
-
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
72
|
+
def __exhaust_system_report(context: SapioWebhookContext | SapioUser, report_name: str, page_limit: int | None = None) \
|
|
73
|
+
-> tuple[list[ReportColumn], list[list[Any]]]:
|
|
74
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
180
75
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
181
76
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if page_number is not None:
|
|
186
|
-
report.page_number = page_number
|
|
77
|
+
report = None
|
|
78
|
+
page_size: int | None = None
|
|
79
|
+
page_number: int | None = None
|
|
187
80
|
has_next_page: bool = True
|
|
188
|
-
rows: list[list[
|
|
81
|
+
rows: list[list[Any]] = []
|
|
189
82
|
cur_page: int = 1
|
|
190
|
-
while has_next_page and (not page_limit or cur_page
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
has_next_page =
|
|
195
|
-
rows.extend(
|
|
83
|
+
while has_next_page and (not page_limit or cur_page < page_limit):
|
|
84
|
+
report = report_man.run_system_report_by_name(report_name, page_size, page_number)
|
|
85
|
+
page_size = report.page_size
|
|
86
|
+
page_number = report.page_number
|
|
87
|
+
has_next_page = report.has_next_page
|
|
88
|
+
rows.extend(report.result_table)
|
|
196
89
|
cur_page += 1
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
@staticmethod
|
|
200
|
-
def __exhaust_quick_report(context: UserIdentifier,
|
|
201
|
-
report_term: RawReportTerm,
|
|
202
|
-
page_limit: int | None,
|
|
203
|
-
page_size: int | None,
|
|
204
|
-
page_number: int | None) \
|
|
205
|
-
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
206
|
-
"""
|
|
207
|
-
Given a quick report, iterate over every page of the report and collect the results
|
|
208
|
-
until there are no remaining pages.
|
|
209
|
-
"""
|
|
210
|
-
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
211
|
-
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
212
|
-
|
|
213
|
-
result = None
|
|
214
|
-
has_next_page: bool = True
|
|
215
|
-
rows: list[list[FieldValue]] = []
|
|
216
|
-
cur_page: int = 1
|
|
217
|
-
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
218
|
-
result = report_man.run_quick_report(report_term, page_size, page_number)
|
|
219
|
-
page_size = result.page_size
|
|
220
|
-
page_number = result.page_number
|
|
221
|
-
has_next_page = result.has_next_page
|
|
222
|
-
rows.extend(result.result_table)
|
|
223
|
-
cur_page += 1
|
|
224
|
-
return result.column_list, rows
|
|
225
|
-
|
|
226
|
-
@staticmethod
|
|
227
|
-
def __process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
|
|
228
|
-
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
|
|
229
|
-
"""
|
|
230
|
-
Given the results of a report as a list of row values and the report's columns, combine these lists to
|
|
231
|
-
result in a singular list of dictionaries for each row in the results.
|
|
232
|
-
|
|
233
|
-
If any filter criteria has been provided, also use that to filter the row.
|
|
234
|
-
"""
|
|
235
|
-
# It may be the case that two columns have the same data field name but differing data type names.
|
|
236
|
-
# If this occurs, then we need to be able to differentiate these columns in the resulting dictionary.
|
|
237
|
-
prepend_dt: set[str] = set()
|
|
238
|
-
encountered_names: list[str] = []
|
|
239
|
-
for column in columns:
|
|
240
|
-
field_name: str = column.data_field_name
|
|
241
|
-
if field_name in encountered_names:
|
|
242
|
-
prepend_dt.add(field_name)
|
|
243
|
-
else:
|
|
244
|
-
encountered_names.append(field_name)
|
|
245
|
-
|
|
246
|
-
if filters:
|
|
247
|
-
filters: dict[str, Iterable[FieldValue]] = AliasUtil.to_data_field_names_dict(filters)
|
|
248
|
-
|
|
249
|
-
ret: list[dict[str, FieldValue]] = []
|
|
250
|
-
for row in rows:
|
|
251
|
-
row_data: dict[str, FieldValue] = {}
|
|
252
|
-
filter_row: bool = False
|
|
253
|
-
for value, column in zip(row, columns):
|
|
254
|
-
header: str = column.data_field_name
|
|
255
|
-
# If two columns share the same data field name, prepend the data type name of the column to the
|
|
256
|
-
# data field name.
|
|
257
|
-
if header in prepend_dt:
|
|
258
|
-
header = column.data_type_name + "." + header
|
|
259
|
-
if filters is not None and header in filters and value not in filters.get(header):
|
|
260
|
-
filter_row = True
|
|
261
|
-
break
|
|
262
|
-
row_data.update({header: value})
|
|
263
|
-
if filter_row is False:
|
|
264
|
-
ret.append(row_data)
|
|
265
|
-
return ret
|
|
90
|
+
return report.column_list, rows
|
|
@@ -3,47 +3,34 @@ class SapioException(Exception):
|
|
|
3
3
|
"""
|
|
4
4
|
A generic exception thrown by sapiopycommons methods. Typically caused by programmer error, but may also be from
|
|
5
5
|
extremely edge case user errors. For expected user errors, use SapioUserErrorException.
|
|
6
|
-
|
|
7
|
-
CommonsWebhookHandler's default behavior for this and any other exception that doesn't extend SapioException is
|
|
8
|
-
to return a generic toaster message saying that an unexpected error has occurred.
|
|
9
6
|
"""
|
|
10
7
|
pass
|
|
11
8
|
|
|
12
9
|
|
|
10
|
+
# CommonsWebhookHandler catches this exception and returns "User Cancelled."
|
|
13
11
|
class SapioUserCancelledException(SapioException):
|
|
14
12
|
"""
|
|
15
13
|
An exception thrown when the user cancels a client callback.
|
|
16
|
-
|
|
17
|
-
CommonsWebhookHandler's default behavior is to simply end the webhook session with a true result without logging
|
|
18
|
-
the exception.
|
|
19
|
-
"""
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SapioDialogTimeoutException(SapioException):
|
|
24
|
-
"""
|
|
25
|
-
An exception thrown when the user leaves a client callback open for too long.
|
|
26
|
-
|
|
27
|
-
CommonsWebhookHandler's default behavior is to display an OK popup notifying the user that the dialog has timed out.
|
|
28
14
|
"""
|
|
29
15
|
pass
|
|
30
16
|
|
|
31
17
|
|
|
18
|
+
# CommonsWebhookHandler catches this exception and returns the text to the user as display text in a webhook result.
|
|
32
19
|
class SapioUserErrorException(SapioException):
|
|
33
20
|
"""
|
|
34
21
|
An exception caused by user error (e.g. user provided a CSV when an XLSX was expected), which promises to return a
|
|
35
|
-
user-friendly message explaining the error that should be displayed to the user.
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
user-friendly message explaining the error that should be displayed to the user. It is the responsibility of the
|
|
23
|
+
programmer to catch any such exceptions and return the value in e.args[0] as text for the user to see (such as
|
|
24
|
+
through the display text of a webhook result).
|
|
38
25
|
"""
|
|
39
26
|
pass
|
|
40
27
|
|
|
41
28
|
|
|
29
|
+
# CommonsWebhookHandler catches this exception and returns the text in a display_error client callback.
|
|
42
30
|
class SapioCriticalErrorException(SapioException):
|
|
43
31
|
"""
|
|
44
32
|
A critical exception caused by user error, which promises to return a user-friendly message explaining the error
|
|
45
|
-
that should be displayed to the user.
|
|
46
|
-
|
|
47
|
-
CommonsWebhookHandler's default behavior is to return the error message in a display_error callback.
|
|
33
|
+
that should be displayed to the user. It is the responsibility of the programmer to catch any such exceptions and
|
|
34
|
+
return the value in e.args[0] as text for the user to see (such as through a dialog form client callback request).
|
|
48
35
|
"""
|
|
49
36
|
pass
|