sapiopycommons 2024.8.15a304__py3-none-any.whl → 2024.8.20a306__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 (33) hide show
  1. sapiopycommons/callbacks/callback_util.py +130 -34
  2. sapiopycommons/customreport/__init__.py +0 -0
  3. sapiopycommons/customreport/column_builder.py +60 -0
  4. sapiopycommons/customreport/custom_report_builder.py +125 -0
  5. sapiopycommons/customreport/term_builder.py +299 -0
  6. sapiopycommons/datatype/attachment_util.py +11 -10
  7. sapiopycommons/eln/experiment_handler.py +209 -44
  8. sapiopycommons/eln/experiment_report_util.py +33 -129
  9. sapiopycommons/files/complex_data_loader.py +5 -4
  10. sapiopycommons/files/file_bridge.py +15 -14
  11. sapiopycommons/files/file_bridge_handler.py +26 -4
  12. sapiopycommons/files/file_data_handler.py +2 -5
  13. sapiopycommons/files/file_util.py +38 -5
  14. sapiopycommons/files/file_validator.py +26 -11
  15. sapiopycommons/files/file_writer.py +44 -15
  16. sapiopycommons/general/aliases.py +147 -3
  17. sapiopycommons/general/audit_log.py +196 -0
  18. sapiopycommons/general/custom_report_util.py +34 -32
  19. sapiopycommons/general/popup_util.py +17 -0
  20. sapiopycommons/general/sapio_links.py +50 -0
  21. sapiopycommons/general/time_util.py +40 -0
  22. sapiopycommons/multimodal/multimodal_data.py +0 -1
  23. sapiopycommons/processtracking/endpoints.py +22 -22
  24. sapiopycommons/recordmodel/record_handler.py +183 -61
  25. sapiopycommons/rules/eln_rule_handler.py +34 -25
  26. sapiopycommons/rules/on_save_rule_handler.py +34 -31
  27. sapiopycommons/webhook/webhook_handlers.py +90 -26
  28. sapiopycommons/webhook/webservice_handlers.py +67 -0
  29. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/METADATA +1 -1
  30. sapiopycommons-2024.8.20a306.dist-info/RECORD +50 -0
  31. sapiopycommons-2024.8.15a304.dist-info/RECORD +0 -43
  32. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/WHEEL +0 -0
  33. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,47 @@
1
1
  from collections.abc import Iterable
2
2
  from typing import Any
3
3
 
4
+ from sapiopylib.rest.User import SapioUser
4
5
  from sapiopylib.rest.pojo.DataRecord import DataRecord
6
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
5
7
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
8
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
6
9
  from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
7
10
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
8
- from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
11
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
9
12
 
13
+ from sapiopycommons.general.exceptions import SapioException
14
+
15
+ FieldValue = int | float | str | bool | None
16
+ """Allowable values for fields in the system."""
10
17
  RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
11
18
  """Different forms that a record model could take."""
12
19
  SapioRecord = DataRecord | RecordModel
13
20
  """A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
14
21
  RecordIdentifier = SapioRecord | int
15
22
  """A RecordIdentifier is either a record type or an integer for the record's record ID."""
23
+ DataTypeIdentifier = SapioRecord | type[WrappedType] | str
24
+ """A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
25
+ FieldIdentifier = WrapperField | str | tuple[str, FieldType]
26
+ """A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
27
+ and field type."""
28
+ FieldIdentifierKey = WrapperField | str
29
+ """A FieldIdentifierKey is a FieldIdentifier, except it can't be a tuple, s tuples can't be used as keys in
30
+ dictionaries.."""
31
+ HasFieldWrappers = type[WrappedType] | WrappedRecordModel
32
+ """An identifier for classes that have wrapper fields."""
16
33
  ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
17
34
  """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
18
35
  ID."""
19
- FieldMap = dict[str, Any]
36
+ FieldMap = dict[str, FieldValue]
20
37
  """A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
21
38
  any random dict in a webhook from one which is explicitly used for record fields."""
39
+ FieldIdentifierMap = dict[FieldIdentifierKey, FieldValue]
40
+ """A field identifier map is the same thing as a field map, except the keys can be field identifiers instead
41
+ of just strings. Note that although one of the allowed field identifiers is a tuple, you can't use tuples as
42
+ keys in a dictionary."""
43
+ UserIdentifier = SapioWebhookContext | SapioUser
44
+ """An identifier for classes from which a user object can be used for sending requests."""
22
45
 
23
46
 
24
47
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
@@ -50,7 +73,123 @@ class AliasUtil:
50
73
 
51
74
  :return: A list of record IDs for the input records.
52
75
  """
53
- return [(x if isinstance(x, int) else x.record_id) for x in records]
76
+ return [(AliasUtil.to_record_id(x)) for x in records]
77
+
78
+ @staticmethod
79
+ def to_record_id(record: RecordIdentifier):
80
+ """
81
+ Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
82
+ or WrappedRecordModel to just an integer (taking the record ID from the record).
83
+
84
+ :return: A record ID for the input record.
85
+ """
86
+ return record if isinstance(record, int) else record.record_id
87
+
88
+ @staticmethod
89
+ def to_data_type_name(value: DataTypeIdentifier) -> str:
90
+ """
91
+ Convert a given value to a data type name.
92
+
93
+ :param value: A value which is a string, record, or record model type.
94
+ :return: A string of the data type name of the input value.
95
+ """
96
+ if isinstance(value, str):
97
+ return value
98
+ if isinstance(value, SapioRecord):
99
+ return value.data_type_name
100
+ return value.get_wrapper_data_type_name()
101
+
102
+ @staticmethod
103
+ def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False) -> list[str] | set[str]:
104
+ """
105
+ Convert a given iterable of values to a list or set of data type names.
106
+
107
+ :param values: An iterable of values which are strings, records, or record model types.
108
+ :param return_set: If true, return a set instead of a list.
109
+ :return: A list or set of strings of the data type name of the input value.
110
+ """
111
+ values = [AliasUtil.to_data_type_name(x) for x in values]
112
+ return set(values) if return_set else values
113
+
114
+ @staticmethod
115
+ def to_singular_data_type_name(values: Iterable[DataTypeIdentifier]) -> str:
116
+ """
117
+ Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
118
+ than one data type name exists in the provided list of identifiers.
119
+
120
+ :param values: An iterable of values which are strings, records, or record model types.
121
+ :return: The single data type name that the input vales share.
122
+ """
123
+ data_types: set[str] = AliasUtil.to_data_type_names(values, True)
124
+ if len(data_types) > 1:
125
+ raise SapioException(f"Provided values contain multiple data types: {data_types}. "
126
+ f"Only expecting a single data type.")
127
+ return data_types.pop()
128
+
129
+ @staticmethod
130
+ def to_data_field_name(value: FieldIdentifier) -> str:
131
+ """
132
+ Convert a string or WrapperField to a data field name string.
133
+
134
+ :param value: A string or WrapperField.
135
+ :return: A string of the data field name of the input value.
136
+ """
137
+ if isinstance(value, tuple):
138
+ return value[0]
139
+ if isinstance(value, WrapperField):
140
+ return value.field_name
141
+ return value
142
+
143
+ @staticmethod
144
+ def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
145
+ """
146
+ Convert an iterable of strings or WrapperFields to a list of data field name strings.
147
+
148
+ :param values: An iterable of strings or WrapperFields.
149
+ :return: A list of strings of the data field names of the input values.
150
+ """
151
+ return [AliasUtil.to_data_field_name(x) for x in values]
152
+
153
+ @staticmethod
154
+ def to_data_field_names_dict(values: dict[FieldIdentifierKey, Any]) -> dict[str, Any]:
155
+ """
156
+ Take a dictionary whose keys are field identifiers and convert them all to strings for the data field name.
157
+
158
+ :param values: A dictionary of field identifiers to field values.
159
+ :return: A dictionary of strings of the data field names to field values for the input values.
160
+ """
161
+ ret_dict: dict[str, FieldValue] = {}
162
+ for field, value in values.items():
163
+ ret_dict[AliasUtil.to_data_field_name(field)] = value
164
+ return ret_dict
165
+
166
+ @staticmethod
167
+ def to_data_field_names_list_dict(values: list[dict[FieldIdentifierKey, Any]]) -> list[dict[str, Any]]:
168
+ ret_list: list[dict[str, Any]] = []
169
+ for field_map in values:
170
+ ret_list.append(AliasUtil.to_data_field_names_dict(field_map))
171
+ return ret_list
172
+
173
+ @staticmethod
174
+ def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
175
+ """
176
+ Convert a given field identifier to the field type for that field.
177
+
178
+ :param field: A string or WrapperField.
179
+ :param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
180
+ must be provided to determine the field type.
181
+ :return: The field type of the given field.
182
+ """
183
+ if isinstance(field, tuple):
184
+ return field[1]
185
+ if isinstance(field, WrapperField):
186
+ return field.field_type
187
+ for var in dir(data_type):
188
+ attr = getattr(data_type, var)
189
+ if isinstance(attr, WrapperField) and attr.field_name == field:
190
+ return attr.field_type
191
+ raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
192
+ f"field with the name \"{field}\",")
54
193
 
55
194
  @staticmethod
56
195
  def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
@@ -63,6 +202,7 @@ class AliasUtil:
63
202
  field_map_list: list[FieldMap] = []
64
203
  for record in records:
65
204
  if isinstance(record, DataRecord):
205
+ # noinspection PyTypeChecker
66
206
  field_map_list.append(record.get_fields())
67
207
  else:
68
208
  field_map_list.append(record.fields.copy_to_dict())
@@ -80,3 +220,7 @@ class AliasUtil:
80
220
  if isinstance(experiment, ElnExperiment):
81
221
  return experiment.notebook_experiment_id
82
222
  return experiment.get_id()
223
+
224
+ @staticmethod
225
+ def to_sapio_user(context: UserIdentifier) -> SapioUser:
226
+ return context if isinstance(context, SapioUser) else context.user
@@ -0,0 +1,196 @@
1
+ from enum import Enum
2
+
3
+ from sapiopylib.rest.User import SapioUser
4
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
5
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
6
+
7
+ from sapiopycommons.customreport.term_builder import TermBuilder
8
+ from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
9
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
10
+
11
+ EVENTTYPE_COLUMN = "EVENTTYPE"
12
+ TIMESTAMP_COLUMN = "TIMESTAMP"
13
+ DATATYPENAME_COLUMN = "DATATYPENAME"
14
+ RECORDID_COLUMN = "RECORDID"
15
+ DESCRIPTION_COLUMN = "DESCRIPTION"
16
+ USERNAME_COLUMN = "USERNAME"
17
+ USERCOMMENT_COLUMN = "USERCOMMENT"
18
+ RECORDNAME_COLUMN = "RECORDNAME"
19
+ DATAFIELDNAME_COLUMN = "DATAFIELDNAME"
20
+ ORIGINALVALUE_COLUMN = "ORIGINALVALUE"
21
+ NEWVALUE_COLUMN = "NEWVALUE"
22
+
23
+
24
+ class EventType(Enum):
25
+ """An enum to represent the possible event type values with the event type column in the audit log table."""
26
+ ADD = 0
27
+ DELETE = 1
28
+ MODIFY = 2
29
+ INFO = 3
30
+ ERROR = 4
31
+ WARNING = 5
32
+ IMPORT = 6
33
+ GENERATE = 7
34
+ EXPORT = 8
35
+ ADDREF = 9
36
+ REMOVEREF = 10
37
+ ESIGNATURE = 11
38
+ ROLEASSIGNMENT = 12
39
+
40
+
41
+ class AuditLogEntry:
42
+
43
+ __event_type: EventType
44
+ __date: int
45
+ __data_type_name: str
46
+ __record_id: int
47
+ __description: str
48
+ __users_login_name: str
49
+ __comment: str
50
+ __data_record_name: str
51
+ __data_field_name: str
52
+ __original_value: str
53
+ __new_value: str
54
+
55
+ @property
56
+ def event_type(self) -> EventType:
57
+ return self.__event_type
58
+
59
+ @property
60
+ def date(self) -> int:
61
+ return self.__date
62
+
63
+ @property
64
+ def data_type_name(self) -> str:
65
+ return self.__data_type_name
66
+
67
+ @property
68
+ def record_id(self) -> int:
69
+ return self.__record_id
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ return self.__description
74
+
75
+ @property
76
+ def users_login_name(self) -> str:
77
+ return self.__users_login_name
78
+
79
+ @property
80
+ def comment(self) -> str:
81
+ return self.__comment
82
+
83
+ @property
84
+ def data_record_name(self) -> str:
85
+ return self.__data_record_name
86
+
87
+ @property
88
+ def data_field_name(self) -> str:
89
+ return self.__data_field_name
90
+
91
+ @property
92
+ def original_value(self) -> str:
93
+ return self.__original_value
94
+
95
+ @property
96
+ def new_value(self) -> str:
97
+ return self.__new_value
98
+
99
+ def __init__(self, report_row: dict[str, FieldValue]):
100
+ self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
101
+ self.__date = report_row[TIMESTAMP_COLUMN]
102
+ self.__data_type_name = report_row[DATATYPENAME_COLUMN]
103
+ self.__record_id = report_row[RECORDID_COLUMN]
104
+ self.__description = report_row[DESCRIPTION_COLUMN]
105
+ self.__users_login_name = report_row[USERNAME_COLUMN]
106
+ self.__comment = report_row[USERCOMMENT_COLUMN]
107
+ self.__data_record_name = report_row[RECORDNAME_COLUMN]
108
+ self.__data_field_name = report_row[DATAFIELDNAME_COLUMN]
109
+ self.__original_value = report_row[ORIGINALVALUE_COLUMN]
110
+ self.__new_value = report_row[NEWVALUE_COLUMN]
111
+
112
+
113
+ class AuditLog:
114
+ AUDIT_LOG_PSEUDO_DATATYPE: str = "AUDITLOG"
115
+ EVENT_TYPE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, EVENTTYPE_COLUMN, FieldType.ENUM)
116
+ DATE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, TIMESTAMP_COLUMN, FieldType.DATE)
117
+ DATA_TYPE_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATATYPENAME_COLUMN, FieldType.STRING)
118
+ RECORD_ID: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, FieldType.LONG)
119
+ DESCRIPTION: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DESCRIPTION_COLUMN, FieldType.STRING)
120
+ USERS_LOGIN_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERNAME_COLUMN, FieldType.STRING)
121
+ COMMENT: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERCOMMENT_COLUMN, FieldType.STRING)
122
+ DATA_RECORD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDNAME_COLUMN, FieldType.STRING)
123
+ DATA_FIELD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, FieldType.STRING)
124
+ ORIGINAL_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, ORIGINALVALUE_COLUMN, FieldType.STRING)
125
+ NEW_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, NEWVALUE_COLUMN, FieldType.STRING)
126
+
127
+ AUDIT_LOG_COLUMNS = [EVENT_TYPE, DATE, DATA_TYPE_NAME, RECORD_ID, DESCRIPTION, USERS_LOGIN_NAME, COMMENT,
128
+ DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
129
+ user: SapioUser
130
+
131
+ def __init__(self, context: UserIdentifier):
132
+ self.user = AliasUtil.to_sapio_user(context)
133
+
134
+ @staticmethod
135
+ def create_data_record_audit_log_report(records: list[RecordIdentifier],
136
+ fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
137
+ """
138
+ This method creates a CustomReportCriteria object for running an audit log query based on data records.
139
+
140
+ Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
141
+ Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
142
+ parameter is not populated, the search will include results for all field changes.
143
+
144
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
145
+ :param fields: The data field names to include changes for.
146
+ :return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
147
+ """
148
+ # Build the raw report term querying for any entry with a matching record ID value to the record ID's
149
+ # passed in.
150
+ record_ids = AliasUtil.to_record_ids(records)
151
+ root_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, record_ids)
152
+
153
+ # If the user passed in any specific fields, then we should limit the query to those fields.
154
+ if fields:
155
+ fields: list[str] = AliasUtil.to_data_field_names(fields)
156
+ field_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, fields)
157
+ root_term = TermBuilder.and_terms(root_term, field_term)
158
+
159
+ return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
160
+
161
+ def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
162
+ fields: list[FieldIdentifier] | None = None) \
163
+ -> dict[RecordIdentifier, list[AuditLogEntry]]:
164
+ """
165
+ This method runs a custom report for changes made to the given data records using the audit log.
166
+ See "create_data_record_audit_log_report" for more details about the data record audit log report.
167
+
168
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
169
+ :param fields: The data field names to include changes for.
170
+ :return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
171
+ AuditLogEntry objects which match the record id value of those records.
172
+ """
173
+ fields: list[str] = AliasUtil.to_data_field_names(fields)
174
+ # First, we must build our report criteria for running the Custom Report.
175
+ criteria = AuditLog.create_data_record_audit_log_report(records, fields)
176
+
177
+ # Then we must run the custom report using that criteria.
178
+ raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
179
+
180
+ # This section will prepare a map matching the original RecordIdentifier by record id.
181
+ # This is because the audit log entries will have record ids, but we want the keys in our result map
182
+ # to match the record identifiers that the user passed in, for convenience.
183
+ record_identifier_mapping: dict[int, RecordIdentifier] = dict()
184
+ for record in records:
185
+ record_id = AliasUtil.to_record_id(record)
186
+ record_identifier_mapping[record_id] = record
187
+
188
+ # Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
189
+ # and the value is a list of applicable audit log entries.
190
+ final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
191
+ for audit_entry_data in raw_report_data:
192
+ audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
193
+ identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
194
+ final_audit_data.setdefault(identifier, []).append(audit_entry)
195
+
196
+ return final_audit_data
@@ -1,21 +1,21 @@
1
1
  from collections.abc import Iterable
2
- from typing import Any
3
2
 
4
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
5
4
  from sapiopylib.rest.User import SapioUser
6
5
  from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport, CustomReportCriteria, RawReportTerm
7
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
6
+
7
+ from sapiopycommons.general.aliases import UserIdentifier, FieldValue, AliasUtil, FieldIdentifierKey
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: SapioWebhookContext | SapioUser,
13
+ def run_system_report(context: UserIdentifier,
14
14
  report_name: str,
15
- filters: dict[str, Iterable[Any]] | None = None,
15
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
16
16
  page_limit: int | None = None,
17
17
  page_size: int | None = None,
18
- page_number: int | None = None) -> list[dict[str, Any]]:
18
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
19
19
  """
20
20
  Run a system report and return the results of that report as a list of dictionaries for the values of each
21
21
  column in each row.
@@ -41,16 +41,16 @@ class CustomReportUtil:
41
41
  results: tuple = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit,
42
42
  page_size, page_number)
43
43
  columns: list[ReportColumn] = results[0]
44
- rows: list[list[Any]] = results[1]
44
+ rows: list[list[FieldValue]] = results[1]
45
45
  return CustomReportUtil.__process_results(rows, columns, filters)
46
46
 
47
47
  @staticmethod
48
- def run_custom_report(context: SapioWebhookContext | SapioUser,
48
+ def run_custom_report(context: UserIdentifier,
49
49
  report_criteria: CustomReportCriteria,
50
- filters: dict[str, Iterable[Any]] | None = None,
50
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
51
51
  page_limit: int | None = None,
52
52
  page_size: int | None = None,
53
- page_number: int | None = None) -> list[dict[str, Any]]:
53
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
54
54
  """
55
55
  Run a custom report and return the results of that report as a list of dictionaries for the values of each
56
56
  column in each row.
@@ -82,16 +82,16 @@ class CustomReportUtil:
82
82
  results: tuple = CustomReportUtil.__exhaust_custom_report(context, report_criteria, page_limit,
83
83
  page_size, page_number)
84
84
  columns: list[ReportColumn] = results[0]
85
- rows: list[list[Any]] = results[1]
85
+ rows: list[list[FieldValue]] = results[1]
86
86
  return CustomReportUtil.__process_results(rows, columns, filters)
87
87
 
88
88
  @staticmethod
89
- def run_quick_report(context: SapioWebhookContext | SapioUser,
89
+ def run_quick_report(context: UserIdentifier,
90
90
  report_term: RawReportTerm,
91
- filters: dict[str, Iterable[Any]] | None = None,
91
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
92
92
  page_limit: int | None = None,
93
93
  page_size: int | None = None,
94
- page_number: int | None = None) -> list[dict[str, Any]]:
94
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
95
95
  """
96
96
  Run a quick report and return the results of that report as a list of dictionaries for the values of each
97
97
  column in each row.
@@ -115,11 +115,11 @@ class CustomReportUtil:
115
115
  results: tuple = CustomReportUtil.__exhaust_quick_report(context, report_term, page_limit,
116
116
  page_size, page_number)
117
117
  columns: list[ReportColumn] = results[0]
118
- rows: list[list[Any]] = results[1]
118
+ rows: list[list[FieldValue]] = results[1]
119
119
  return CustomReportUtil.__process_results(rows, columns, filters)
120
120
 
121
121
  @staticmethod
122
- def get_system_report_criteria(context: SapioWebhookContext | SapioUser, report_name: str) -> CustomReport:
122
+ def get_system_report_criteria(context: UserIdentifier, report_name: str) -> CustomReport:
123
123
  """
124
124
  Retrieve a custom report from the system given the name of the report. This works by querying the system report
125
125
  with a page number and size of 1 to minimize the amount of data transfer needed to retrieve the report's config.
@@ -134,27 +134,27 @@ class CustomReportUtil:
134
134
  :param report_name: The name of the system report to run.
135
135
  :return: The CustomReport object for the given system report name.
136
136
  """
137
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
137
+ user: SapioUser = AliasUtil.to_sapio_user(context)
138
138
  report_man = DataMgmtServer.get_custom_report_manager(user)
139
139
  return report_man.run_system_report_by_name(report_name, 1, 1)
140
140
 
141
141
  @staticmethod
142
- def __exhaust_system_report(context: SapioWebhookContext | SapioUser,
142
+ def __exhaust_system_report(context: UserIdentifier,
143
143
  report_name: str,
144
144
  page_limit: int | None,
145
145
  page_size: int | None,
146
146
  page_number: int | None) \
147
- -> tuple[list[ReportColumn], list[list[Any]]]:
147
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
148
148
  """
149
149
  Given a system report, iterate over every page of the report and collect the results
150
150
  until there are no remaining pages.
151
151
  """
152
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
152
+ user: SapioUser = AliasUtil.to_sapio_user(context)
153
153
  report_man = DataMgmtServer.get_custom_report_manager(user)
154
154
 
155
155
  result = None
156
156
  has_next_page: bool = True
157
- rows: list[list[Any]] = []
157
+ rows: list[list[FieldValue]] = []
158
158
  cur_page: int = 1
159
159
  while has_next_page and (not page_limit or cur_page <= page_limit):
160
160
  result = report_man.run_system_report_by_name(report_name, page_size, page_number)
@@ -166,17 +166,17 @@ class CustomReportUtil:
166
166
  return result.column_list, rows
167
167
 
168
168
  @staticmethod
169
- def __exhaust_custom_report(context: SapioWebhookContext | SapioUser,
169
+ def __exhaust_custom_report(context: UserIdentifier,
170
170
  report: CustomReportCriteria,
171
171
  page_limit: int | None,
172
172
  page_size: int | None,
173
173
  page_number: int | None) \
174
- -> tuple[list[ReportColumn], list[list[Any]]]:
174
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
175
175
  """
176
176
  Given a custom report, iterate over every page of the report and collect the results
177
177
  until there are no remaining pages.
178
178
  """
179
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
179
+ user: SapioUser = AliasUtil.to_sapio_user(context)
180
180
  report_man = DataMgmtServer.get_custom_report_manager(user)
181
181
 
182
182
  result = None
@@ -185,7 +185,7 @@ class CustomReportUtil:
185
185
  if page_number is not None:
186
186
  report.page_number = page_number
187
187
  has_next_page: bool = True
188
- rows: list[list[Any]] = []
188
+ rows: list[list[FieldValue]] = []
189
189
  cur_page: int = 1
190
190
  while has_next_page and (not page_limit or cur_page <= page_limit):
191
191
  result = report_man.run_custom_report(report)
@@ -197,22 +197,22 @@ class CustomReportUtil:
197
197
  return result.column_list, rows
198
198
 
199
199
  @staticmethod
200
- def __exhaust_quick_report(context: SapioWebhookContext | SapioUser,
200
+ def __exhaust_quick_report(context: UserIdentifier,
201
201
  report_term: RawReportTerm,
202
202
  page_limit: int | None,
203
203
  page_size: int | None,
204
204
  page_number: int | None) \
205
- -> tuple[list[ReportColumn], list[list[Any]]]:
205
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
206
206
  """
207
207
  Given a quick report, iterate over every page of the report and collect the results
208
208
  until there are no remaining pages.
209
209
  """
210
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
210
+ user: SapioUser = AliasUtil.to_sapio_user(context)
211
211
  report_man = DataMgmtServer.get_custom_report_manager(user)
212
212
 
213
213
  result = None
214
214
  has_next_page: bool = True
215
- rows: list[list[Any]] = []
215
+ rows: list[list[FieldValue]] = []
216
216
  cur_page: int = 1
217
217
  while has_next_page and (not page_limit or cur_page <= page_limit):
218
218
  result = report_man.run_quick_report(report_term, page_size, page_number)
@@ -224,8 +224,8 @@ class CustomReportUtil:
224
224
  return result.column_list, rows
225
225
 
226
226
  @staticmethod
227
- def __process_results(rows: list[list[Any]], columns: list[ReportColumn],
228
- filters: dict[str, Iterable[Any]] | None) -> list[dict[str, Any]]:
227
+ def __process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
228
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
229
229
  """
230
230
  Given the results of a report as a list of row values and the report's columns, combine these lists to
231
231
  result in a singular list of dictionaries for each row in the results.
@@ -243,9 +243,11 @@ class CustomReportUtil:
243
243
  else:
244
244
  encountered_names.append(field_name)
245
245
 
246
- ret: list[dict[str, Any]] = []
246
+ filters: dict[str, Iterable[FieldValue]] = AliasUtil.to_data_field_names_dict(filters)
247
+
248
+ ret: list[dict[str, FieldValue]] = []
247
249
  for row in rows:
248
- row_data: dict[str, Any] = {}
250
+ row_data: dict[str, FieldValue] = {}
249
251
  filter_row: bool = False
250
252
  for value, column in zip(row, columns):
251
253
  header: str = column.data_field_name