sapiopycommons 2024.8.19a305__py3-none-any.whl → 2024.8.26a307__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 +14 -15
- sapiopycommons/customreport/term_builder.py +5 -2
- sapiopycommons/datatype/attachment_util.py +6 -14
- sapiopycommons/eln/experiment_handler.py +24 -17
- sapiopycommons/eln/experiment_report_util.py +33 -129
- sapiopycommons/files/complex_data_loader.py +4 -3
- sapiopycommons/files/file_bridge.py +14 -13
- sapiopycommons/files/file_bridge_handler.py +8 -7
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_validator.py +7 -7
- sapiopycommons/general/aliases.py +54 -1
- sapiopycommons/general/audit_log.py +19 -23
- sapiopycommons/general/custom_report_util.py +34 -32
- sapiopycommons/general/sapio_links.py +4 -2
- sapiopycommons/multimodal/multimodal_data.py +0 -1
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +119 -65
- sapiopycommons/rules/eln_rule_handler.py +5 -3
- sapiopycommons/rules/on_save_rule_handler.py +5 -3
- {sapiopycommons-2024.8.19a305.dist-info → sapiopycommons-2024.8.26a307.dist-info}/METADATA +1 -1
- {sapiopycommons-2024.8.19a305.dist-info → sapiopycommons-2024.8.26a307.dist-info}/RECORD +23 -23
- {sapiopycommons-2024.8.19a305.dist-info → sapiopycommons-2024.8.26a307.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.19a305.dist-info → sapiopycommons-2024.8.26a307.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,13 +4,14 @@ import urllib.parse
|
|
|
4
4
|
|
|
5
5
|
from requests import Response
|
|
6
6
|
from sapiopylib.rest.User import SapioUser
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
11
12
|
class FileBridge:
|
|
12
13
|
@staticmethod
|
|
13
|
-
def read_file(context:
|
|
14
|
+
def read_file(context: UserIdentifier, bridge_name: str, file_path: str,
|
|
14
15
|
base64_decode: bool = True) -> bytes:
|
|
15
16
|
"""
|
|
16
17
|
Read a file from FileBridge.
|
|
@@ -27,7 +28,7 @@ class FileBridge:
|
|
|
27
28
|
params = {
|
|
28
29
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
29
30
|
}
|
|
30
|
-
user: SapioUser =
|
|
31
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
31
32
|
response = user.get(sub_path, params)
|
|
32
33
|
user.raise_for_status(response)
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ class FileBridge:
|
|
|
37
38
|
return ret_val
|
|
38
39
|
|
|
39
40
|
@staticmethod
|
|
40
|
-
def write_file(context:
|
|
41
|
+
def write_file(context: UserIdentifier, bridge_name: str, file_path: str,
|
|
41
42
|
file_data: bytes | str) -> None:
|
|
42
43
|
"""
|
|
43
44
|
Write a file to FileBridge.
|
|
@@ -53,13 +54,13 @@ class FileBridge:
|
|
|
53
54
|
params = {
|
|
54
55
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
55
56
|
}
|
|
56
|
-
user: SapioUser =
|
|
57
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
57
58
|
with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
|
|
58
59
|
response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
|
|
59
60
|
user.raise_for_status(response)
|
|
60
61
|
|
|
61
62
|
@staticmethod
|
|
62
|
-
def list_directory(context:
|
|
63
|
+
def list_directory(context: UserIdentifier, bridge_name: str,
|
|
63
64
|
file_path: str | None = "") -> list[str]:
|
|
64
65
|
"""
|
|
65
66
|
List the contents of a FileBridge directory.
|
|
@@ -74,7 +75,7 @@ class FileBridge:
|
|
|
74
75
|
params = {
|
|
75
76
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
76
77
|
}
|
|
77
|
-
user: SapioUser =
|
|
78
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
78
79
|
response: Response = user.get(sub_path, params=params)
|
|
79
80
|
user.raise_for_status(response)
|
|
80
81
|
|
|
@@ -83,7 +84,7 @@ class FileBridge:
|
|
|
83
84
|
return [urllib.parse.unquote(value)[path_length:] for value in response_body]
|
|
84
85
|
|
|
85
86
|
@staticmethod
|
|
86
|
-
def create_directory(context:
|
|
87
|
+
def create_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
|
|
87
88
|
"""
|
|
88
89
|
Create a new directory in FileBridge.
|
|
89
90
|
|
|
@@ -97,12 +98,12 @@ class FileBridge:
|
|
|
97
98
|
params = {
|
|
98
99
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
99
100
|
}
|
|
100
|
-
user: SapioUser =
|
|
101
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
101
102
|
response = user.post(sub_path, params=params)
|
|
102
103
|
user.raise_for_status(response)
|
|
103
104
|
|
|
104
105
|
@staticmethod
|
|
105
|
-
def delete_file(context:
|
|
106
|
+
def delete_file(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
|
|
106
107
|
"""
|
|
107
108
|
Delete an existing file in FileBridge.
|
|
108
109
|
|
|
@@ -115,12 +116,12 @@ class FileBridge:
|
|
|
115
116
|
params = {
|
|
116
117
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
117
118
|
}
|
|
118
|
-
user: SapioUser =
|
|
119
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
119
120
|
response = user.delete(sub_path, params=params)
|
|
120
121
|
user.raise_for_status(response)
|
|
121
122
|
|
|
122
123
|
@staticmethod
|
|
123
|
-
def delete_directory(context:
|
|
124
|
+
def delete_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
|
|
124
125
|
"""
|
|
125
126
|
Delete an existing directory in FileBridge.
|
|
126
127
|
|
|
@@ -133,6 +134,6 @@ class FileBridge:
|
|
|
133
134
|
params = {
|
|
134
135
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
135
136
|
}
|
|
136
|
-
user: SapioUser =
|
|
137
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
137
138
|
response = user.delete(sub_path, params=params)
|
|
138
139
|
user.raise_for_status(response)
|
|
@@ -3,9 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import abstractmethod, ABC
|
|
4
4
|
from weakref import WeakValueDictionary
|
|
5
5
|
|
|
6
|
-
from sapiopycommons.files.file_bridge import FileBridge
|
|
7
6
|
from sapiopylib.rest.User import SapioUser
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
from sapiopycommons.files.file_bridge import FileBridge
|
|
9
|
+
from sapiopycommons.general.aliases import AliasUtil, UserIdentifier
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class FileBridgeHandler:
|
|
@@ -27,11 +28,11 @@ class FileBridgeHandler:
|
|
|
27
28
|
__instances: WeakValueDictionary[str, FileBridgeHandler] = WeakValueDictionary()
|
|
28
29
|
__initialized: bool
|
|
29
30
|
|
|
30
|
-
def __new__(cls, context:
|
|
31
|
+
def __new__(cls, context: UserIdentifier, bridge_name: str):
|
|
31
32
|
"""
|
|
32
33
|
:param context: The current webhook context or a user object to send requests from.
|
|
33
34
|
"""
|
|
34
|
-
user =
|
|
35
|
+
user = AliasUtil.to_sapio_user(context)
|
|
35
36
|
key = f"{user.__hash__()}:{bridge_name}"
|
|
36
37
|
obj = cls.__instances.get(key)
|
|
37
38
|
if not obj:
|
|
@@ -40,7 +41,7 @@ class FileBridgeHandler:
|
|
|
40
41
|
cls.__instances[key] = obj
|
|
41
42
|
return obj
|
|
42
43
|
|
|
43
|
-
def __init__(self, context:
|
|
44
|
+
def __init__(self, context: UserIdentifier, bridge_name: str):
|
|
44
45
|
"""
|
|
45
46
|
:param context: The current webhook context or a user object to send requests from.
|
|
46
47
|
:param bridge_name: The name of the bridge to communicate with. This is the "connection name" in the
|
|
@@ -50,7 +51,7 @@ class FileBridgeHandler:
|
|
|
50
51
|
return
|
|
51
52
|
self.__initialized = True
|
|
52
53
|
|
|
53
|
-
self.user =
|
|
54
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
54
55
|
self.__bridge = bridge_name
|
|
55
56
|
self.__file_cache = {}
|
|
56
57
|
self.__files = {}
|
|
@@ -327,7 +328,7 @@ class Directory(FileBridgeObject):
|
|
|
327
328
|
return {x: y for x, y in self.contents.items() if not y.is_file()}
|
|
328
329
|
|
|
329
330
|
|
|
330
|
-
def split_path(file_path: str) ->
|
|
331
|
+
def split_path(file_path: str) -> tuple[str, str]:
|
|
331
332
|
"""
|
|
332
333
|
:param file_path: A file path where directories are separated the "/" characters.
|
|
333
334
|
:return: A tuple of two strings that splits the path on its last slash. The first string is the name of the
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Any, Callable, Iterable
|
|
3
3
|
|
|
4
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
5
|
-
|
|
6
|
-
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
7
|
-
|
|
8
4
|
from sapiopycommons.general.aliases import SapioRecord
|
|
9
|
-
|
|
5
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
10
6
|
from sapiopycommons.general.time_util import TimeUtil
|
|
7
|
+
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
11
8
|
|
|
12
9
|
FilterList = Iterable[int] | range | Callable[[int, dict[str, Any]], bool] | None
|
|
13
10
|
"""A FilterList is an object used to determine if a row in the file data should be skipped over. This can take the
|
|
@@ -7,10 +7,10 @@ from sapiopylib.rest.User import SapioUser
|
|
|
7
7
|
from sapiopylib.rest.pojo.CustomReport import RawReportTerm, RawTermOperation
|
|
8
8
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxIntegerFieldDefinition, VeloxStringFieldDefinition, \
|
|
9
9
|
AbstractVeloxFieldDefinition
|
|
10
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
10
|
|
|
12
11
|
from sapiopycommons.callbacks.callback_util import CallbackUtil
|
|
13
12
|
from sapiopycommons.files.file_data_handler import FileDataHandler, FilterList
|
|
13
|
+
from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
|
|
14
14
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
15
15
|
from sapiopycommons.general.exceptions import SapioUserCancelledException
|
|
16
16
|
from sapiopycommons.general.time_util import TimeUtil
|
|
@@ -80,7 +80,7 @@ class FileValidator:
|
|
|
80
80
|
|
|
81
81
|
return failed_rows
|
|
82
82
|
|
|
83
|
-
def build_violation_report(self, context:
|
|
83
|
+
def build_violation_report(self, context: UserIdentifier,
|
|
84
84
|
rule_violations: dict[int, list[ValidationRule]]) -> None:
|
|
85
85
|
"""
|
|
86
86
|
Display a simple report of any rule violations in the file to the user as a table dialog.
|
|
@@ -125,7 +125,7 @@ class FileValidator:
|
|
|
125
125
|
callback.table_dialog("Errors", "The following rule violations were encountered in the provided file.",
|
|
126
126
|
columns, rows)
|
|
127
127
|
|
|
128
|
-
def validate_and_report_errors(self, context:
|
|
128
|
+
def validate_and_report_errors(self, context: UserIdentifier) -> None:
|
|
129
129
|
"""
|
|
130
130
|
Validate the file. If any rule violations are found, display a simple report of any rule violations in the file
|
|
131
131
|
to the user as a table dialog and throw a SapioUserCancelled exception after the user acknowledges the dialog
|
|
@@ -509,7 +509,7 @@ class UniqueSystemValueRule(ColumnRule):
|
|
|
509
509
|
data_type_name: str
|
|
510
510
|
data_field_name: str
|
|
511
511
|
|
|
512
|
-
def __init__(self, context:
|
|
512
|
+
def __init__(self, context: UserIdentifier, header: str, data_type_name: str,
|
|
513
513
|
data_field_name: str):
|
|
514
514
|
"""
|
|
515
515
|
:param context: The current webhook context or a user object to send requests from.
|
|
@@ -517,7 +517,7 @@ class UniqueSystemValueRule(ColumnRule):
|
|
|
517
517
|
:param data_type_name: The data type name to search on.
|
|
518
518
|
:param data_field_name: The data field name to search on. This is expected to be a string field.
|
|
519
519
|
"""
|
|
520
|
-
self.user =
|
|
520
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
521
521
|
self.data_type_name = data_type_name
|
|
522
522
|
self.data_field_name = data_field_name
|
|
523
523
|
super().__init__(header, f"This value already exists in the system.")
|
|
@@ -543,7 +543,7 @@ class ExistingSystemValueRule(ColumnRule):
|
|
|
543
543
|
data_type_name: str
|
|
544
544
|
data_field_name: str
|
|
545
545
|
|
|
546
|
-
def __init__(self, context:
|
|
546
|
+
def __init__(self, context: UserIdentifier, header: str, data_type_name: str,
|
|
547
547
|
data_field_name: str):
|
|
548
548
|
"""
|
|
549
549
|
:param context: The current webhook context or a user object to send requests from.
|
|
@@ -551,7 +551,7 @@ class ExistingSystemValueRule(ColumnRule):
|
|
|
551
551
|
:param data_type_name: The data type name to search on.
|
|
552
552
|
:param data_field_name: The data field name to search on. This is expected to be a string field.
|
|
553
553
|
"""
|
|
554
|
-
self.user =
|
|
554
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
555
555
|
self.data_type_name = data_type_name
|
|
556
556
|
self.data_field_name = data_field_name
|
|
557
557
|
super().__init__(header, f"This value doesn't exist in the system.")
|
|
@@ -1,15 +1,19 @@
|
|
|
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
|
|
5
6
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
6
7
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
8
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
7
9
|
from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
|
|
8
10
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
9
11
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
|
|
10
12
|
|
|
11
13
|
from sapiopycommons.general.exceptions import SapioException
|
|
12
14
|
|
|
15
|
+
FieldValue = int | float | str | bool | None
|
|
16
|
+
"""Allowable values for fields in the system."""
|
|
13
17
|
RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
|
|
14
18
|
"""Different forms that a record model could take."""
|
|
15
19
|
SapioRecord = DataRecord | RecordModel
|
|
@@ -21,14 +25,23 @@ DataTypeIdentifier = SapioRecord | type[WrappedType] | str
|
|
|
21
25
|
FieldIdentifier = WrapperField | str | tuple[str, FieldType]
|
|
22
26
|
"""A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
|
|
23
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.."""
|
|
24
31
|
HasFieldWrappers = type[WrappedType] | WrappedRecordModel
|
|
25
32
|
"""An identifier for classes that have wrapper fields."""
|
|
26
33
|
ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
|
|
27
34
|
"""An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
|
|
28
35
|
ID."""
|
|
29
|
-
FieldMap = dict[str,
|
|
36
|
+
FieldMap = dict[str, FieldValue]
|
|
30
37
|
"""A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
|
|
31
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."""
|
|
32
45
|
|
|
33
46
|
|
|
34
47
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
@@ -98,6 +111,21 @@ class AliasUtil:
|
|
|
98
111
|
values = [AliasUtil.to_data_type_name(x) for x in values]
|
|
99
112
|
return set(values) if return_set else values
|
|
100
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
|
+
|
|
101
129
|
@staticmethod
|
|
102
130
|
def to_data_field_name(value: FieldIdentifier) -> str:
|
|
103
131
|
"""
|
|
@@ -122,6 +150,26 @@ class AliasUtil:
|
|
|
122
150
|
"""
|
|
123
151
|
return [AliasUtil.to_data_field_name(x) for x in values]
|
|
124
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
|
+
|
|
125
173
|
@staticmethod
|
|
126
174
|
def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
|
|
127
175
|
"""
|
|
@@ -154,6 +202,7 @@ class AliasUtil:
|
|
|
154
202
|
field_map_list: list[FieldMap] = []
|
|
155
203
|
for record in records:
|
|
156
204
|
if isinstance(record, DataRecord):
|
|
205
|
+
# noinspection PyTypeChecker
|
|
157
206
|
field_map_list.append(record.get_fields())
|
|
158
207
|
else:
|
|
159
208
|
field_map_list.append(record.fields.copy_to_dict())
|
|
@@ -171,3 +220,7 @@ class AliasUtil:
|
|
|
171
220
|
if isinstance(experiment, ElnExperiment):
|
|
172
221
|
return experiment.notebook_experiment_id
|
|
173
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
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Any
|
|
3
2
|
|
|
4
|
-
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
5
3
|
from sapiopylib.rest.User import SapioUser
|
|
6
|
-
from sapiopylib.rest.pojo.CustomReport import ReportColumn,
|
|
7
|
-
CompositeReportTerm, CompositeTermOperation
|
|
4
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
|
|
8
5
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
9
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
10
6
|
|
|
11
|
-
from sapiopycommons.
|
|
7
|
+
from sapiopycommons.customreport.term_builder import TermBuilder
|
|
8
|
+
from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
|
|
12
9
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
13
10
|
|
|
14
11
|
EVENTTYPE_COLUMN = "EVENTTYPE"
|
|
@@ -99,7 +96,7 @@ class AuditLogEntry:
|
|
|
99
96
|
def new_value(self) -> str:
|
|
100
97
|
return self.__new_value
|
|
101
98
|
|
|
102
|
-
def __init__(self, report_row: dict[str,
|
|
99
|
+
def __init__(self, report_row: dict[str, FieldValue]):
|
|
103
100
|
self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
|
|
104
101
|
self.__date = report_row[TIMESTAMP_COLUMN]
|
|
105
102
|
self.__data_type_name = report_row[DATATYPENAME_COLUMN]
|
|
@@ -131,11 +128,12 @@ class AuditLog:
|
|
|
131
128
|
DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
|
|
132
129
|
user: SapioUser
|
|
133
130
|
|
|
134
|
-
def __init__(self, context:
|
|
135
|
-
self.user =
|
|
131
|
+
def __init__(self, context: UserIdentifier):
|
|
132
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
136
133
|
|
|
137
134
|
@staticmethod
|
|
138
|
-
def create_data_record_audit_log_report(records: list[RecordIdentifier],
|
|
135
|
+
def create_data_record_audit_log_report(records: list[RecordIdentifier],
|
|
136
|
+
fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
|
|
139
137
|
"""
|
|
140
138
|
This method creates a CustomReportCriteria object for running an audit log query based on data records.
|
|
141
139
|
|
|
@@ -147,25 +145,22 @@ class AuditLog:
|
|
|
147
145
|
:param fields: The data field names to include changes for.
|
|
148
146
|
:return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
|
|
149
147
|
"""
|
|
150
|
-
#
|
|
151
|
-
#
|
|
148
|
+
# Build the raw report term querying for any entry with a matching record ID value to the record ID's
|
|
149
|
+
# passed in.
|
|
152
150
|
record_ids = AliasUtil.to_record_ids(records)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Next we'll build the raw report term querying for any entry with a matching record id value to the record ID's
|
|
156
|
-
# passed in
|
|
157
|
-
root_term = RawReportTerm(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN,
|
|
158
|
-
RawTermOperation.EQUAL_TO_OPERATOR, "{" + ",".join(id_strs) + "}")
|
|
151
|
+
root_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, record_ids)
|
|
159
152
|
|
|
160
153
|
# If the user passed in any specific fields, then we should limit the query to those fields.
|
|
161
154
|
if fields:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
root_term =
|
|
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)
|
|
165
158
|
|
|
166
159
|
return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
|
|
167
160
|
|
|
168
|
-
def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
|
|
161
|
+
def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
|
|
162
|
+
fields: list[FieldIdentifier] | None = None) \
|
|
163
|
+
-> dict[RecordIdentifier, list[AuditLogEntry]]:
|
|
169
164
|
"""
|
|
170
165
|
This method runs a custom report for changes made to the given data records using the audit log.
|
|
171
166
|
See "create_data_record_audit_log_report" for more details about the data record audit log report.
|
|
@@ -175,11 +170,12 @@ class AuditLog:
|
|
|
175
170
|
:return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
|
|
176
171
|
AuditLogEntry objects which match the record id value of those records.
|
|
177
172
|
"""
|
|
173
|
+
fields: list[str] = AliasUtil.to_data_field_names(fields)
|
|
178
174
|
# First, we must build our report criteria for running the Custom Report.
|
|
179
175
|
criteria = AuditLog.create_data_record_audit_log_report(records, fields)
|
|
180
176
|
|
|
181
177
|
# Then we must run the custom report using that criteria.
|
|
182
|
-
raw_report_data: list[dict[str,
|
|
178
|
+
raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
|
|
183
179
|
|
|
184
180
|
# This section will prepare a map matching the original RecordIdentifier by record id.
|
|
185
181
|
# This is because the audit log entries will have record ids, but we want the keys in our result map
|
|
@@ -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
|