sapiopycommons 2024.8.29a317__py3-none-any.whl → 2024.8.30a320__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 +133 -37
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/eln/experiment_handler.py +209 -48
- sapiopycommons/eln/experiment_report_util.py +33 -129
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +15 -14
- sapiopycommons/files/file_bridge_handler.py +27 -5
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +38 -5
- sapiopycommons/files/file_validator.py +26 -11
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +34 -32
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal_data.py +0 -1
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +228 -77
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.30a320.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.29a317.dist-info/RECORD +0 -43
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.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,
|
|
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 [(
|
|
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
|
-
|
|
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:
|
|
13
|
+
def run_system_report(context: UserIdentifier,
|
|
14
14
|
report_name: str,
|
|
15
|
-
filters: dict[
|
|
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,
|
|
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[
|
|
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:
|
|
48
|
+
def run_custom_report(context: UserIdentifier,
|
|
49
49
|
report_criteria: CustomReportCriteria,
|
|
50
|
-
filters: dict[
|
|
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,
|
|
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[
|
|
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:
|
|
89
|
+
def run_quick_report(context: UserIdentifier,
|
|
90
90
|
report_term: RawReportTerm,
|
|
91
|
-
filters: dict[
|
|
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,
|
|
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[
|
|
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:
|
|
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 =
|
|
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:
|
|
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[
|
|
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 =
|
|
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[
|
|
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:
|
|
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[
|
|
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 =
|
|
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[
|
|
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:
|
|
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[
|
|
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 =
|
|
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[
|
|
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[
|
|
228
|
-
filters: dict[
|
|
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
|
-
|
|
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,
|
|
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
|