sapiopycommons 2024.8.15a304__py3-none-any.whl → 2024.8.19a305__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 +122 -25
- 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 +296 -0
- sapiopycommons/datatype/attachment_util.py +15 -6
- sapiopycommons/eln/experiment_handler.py +193 -39
- sapiopycommons/files/complex_data_loader.py +1 -1
- sapiopycommons/files/file_bridge.py +1 -1
- sapiopycommons/files/file_bridge_handler.py +21 -0
- sapiopycommons/files/file_util.py +38 -5
- sapiopycommons/files/file_validator.py +21 -6
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/aliases.py +93 -2
- sapiopycommons/general/audit_log.py +200 -0
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +48 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/recordmodel/record_handler.py +114 -17
- sapiopycommons/rules/eln_rule_handler.py +29 -22
- sapiopycommons/rules/on_save_rule_handler.py +29 -28
- sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.19a305.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.15a304.dist-info/RECORD +0 -43
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from abc import abstractmethod
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from typing import Any
|
|
@@ -18,7 +19,7 @@ class FileWriter:
|
|
|
18
19
|
body: list[list[Any]]
|
|
19
20
|
delimiter: str
|
|
20
21
|
line_break: str
|
|
21
|
-
column_definitions:
|
|
22
|
+
column_definitions: dict[str, ColumnDef]
|
|
22
23
|
|
|
23
24
|
def __init__(self, headers: list[str], delimiter: str = ",", line_break: str = "\r\n"):
|
|
24
25
|
"""
|
|
@@ -30,7 +31,7 @@ class FileWriter:
|
|
|
30
31
|
self.delimiter = delimiter
|
|
31
32
|
self.line_break = line_break
|
|
32
33
|
self.body = []
|
|
33
|
-
self.column_definitions =
|
|
34
|
+
self.column_definitions = {}
|
|
34
35
|
|
|
35
36
|
def add_row_list(self, row: list[Any]) -> None:
|
|
36
37
|
"""
|
|
@@ -65,21 +66,49 @@ class FileWriter:
|
|
|
65
66
|
new_row.append(row.get(header, ""))
|
|
66
67
|
self.body.append(new_row)
|
|
67
68
|
|
|
68
|
-
def
|
|
69
|
+
def add_column_definition(self, header: str, column_def: ColumnDef) -> None:
|
|
69
70
|
"""
|
|
70
|
-
Add new column
|
|
71
|
-
meaning that they map to the header with the equivalent index. Before the file is built, the number of column
|
|
72
|
-
definitions must equal the number of headers if any column definition is provided.
|
|
71
|
+
Add a new column definition to this FileWriter for a specific header.
|
|
73
72
|
|
|
74
|
-
ColumnDefs are only used if the build_file function is provided with a list of RowBundles.
|
|
73
|
+
ColumnDefs are only used if the build_file function is provided with a list of RowBundles. Every header must
|
|
74
|
+
have a column definition if this is the case.
|
|
75
75
|
|
|
76
76
|
Custom column definitions can be created by defining a class that extends ColumnDef and implements the print
|
|
77
77
|
method.
|
|
78
78
|
|
|
79
|
-
:param
|
|
79
|
+
:param column_def: A column definitions to be used to construct the file when build_file is
|
|
80
80
|
called.
|
|
81
|
+
:param header: The header that this column definition is for. If a header is provided that isn't in the headers
|
|
82
|
+
list, the header is appended to the end of the list.
|
|
81
83
|
"""
|
|
82
|
-
self.
|
|
84
|
+
if header not in self.headers:
|
|
85
|
+
self.headers.append(header)
|
|
86
|
+
self.column_definitions[header] = column_def
|
|
87
|
+
|
|
88
|
+
def add_column_definitions(self, column_defs: dict[str, ColumnDef]) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Add new column definitions to this FileWriter.
|
|
91
|
+
|
|
92
|
+
ColumnDefs are only used if the build_file function is provided with a list of RowBundles. Every header must
|
|
93
|
+
have a column definition if this is the case.
|
|
94
|
+
|
|
95
|
+
Custom column definitions can be created by defining a class that extends ColumnDef and implements the print
|
|
96
|
+
method.
|
|
97
|
+
|
|
98
|
+
:param column_defs: A dictionary of header names to column definitions to be used to construct the file when
|
|
99
|
+
build_file is called.
|
|
100
|
+
"""
|
|
101
|
+
# For backwards compatibility purposes, if column definitions are provided as a list,
|
|
102
|
+
# add them in order of appearance of the headers. This will only work if the headers are defined first, though.
|
|
103
|
+
if isinstance(column_defs, list):
|
|
104
|
+
warnings.warn("Adding column definitions is no longer expected as a list. Continuing to provide a list to "
|
|
105
|
+
"this function may result in undesirable behavior.", UserWarning)
|
|
106
|
+
if not self.headers:
|
|
107
|
+
raise SapioException("No headers provided to FileWriter before the column definitions were added.")
|
|
108
|
+
for header, column_def in zip(self.headers, column_defs):
|
|
109
|
+
self.column_definitions[header] = column_def
|
|
110
|
+
for header, column_def in column_defs.items():
|
|
111
|
+
self.add_column_definition(header, column_def)
|
|
83
112
|
|
|
84
113
|
def build_file(self, rows: list[RowBundle] | None = None, sorter=None, reverse: bool = False) -> str:
|
|
85
114
|
"""
|
|
@@ -100,11 +129,10 @@ class FileWriter:
|
|
|
100
129
|
"""
|
|
101
130
|
# If any column definitions have been provided, the number of column definitions and headers must be equal.
|
|
102
131
|
if self.column_definitions:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
f"headers. The number of column definitions must equal the number of headers.")
|
|
132
|
+
for header in self.headers:
|
|
133
|
+
if header not in self.column_definitions:
|
|
134
|
+
raise SapioException(f"FileWriter has no column definition for the header {header}. If any column "
|
|
135
|
+
f"definitions are provided, then all headers must have a column definition.")
|
|
108
136
|
# If any RowBundles have been provided, there must be column definitions for mapping them to the file.
|
|
109
137
|
elif rows:
|
|
110
138
|
raise SapioException(f"FileWriter was given RowBundles but contains no column definitions for mapping "
|
|
@@ -130,7 +158,8 @@ class FileWriter:
|
|
|
130
158
|
rows.sort(key=lambda x: x.index)
|
|
131
159
|
for row in rows:
|
|
132
160
|
new_row: list[Any] = []
|
|
133
|
-
for
|
|
161
|
+
for header in self.headers:
|
|
162
|
+
column = self.column_definitions[header]
|
|
134
163
|
if column.may_skip and row.may_skip:
|
|
135
164
|
new_row.append("")
|
|
136
165
|
else:
|
|
@@ -2,10 +2,13 @@ from collections.abc import Iterable
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
5
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
5
6
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
6
7
|
from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
|
|
7
8
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
8
|
-
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
|
|
9
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
|
|
10
|
+
|
|
11
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
9
12
|
|
|
10
13
|
RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
|
|
11
14
|
"""Different forms that a record model could take."""
|
|
@@ -13,6 +16,13 @@ SapioRecord = DataRecord | RecordModel
|
|
|
13
16
|
"""A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
|
|
14
17
|
RecordIdentifier = SapioRecord | int
|
|
15
18
|
"""A RecordIdentifier is either a record type or an integer for the record's record ID."""
|
|
19
|
+
DataTypeIdentifier = SapioRecord | type[WrappedType] | str
|
|
20
|
+
"""A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
|
|
21
|
+
FieldIdentifier = WrapperField | str | tuple[str, FieldType]
|
|
22
|
+
"""A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
|
|
23
|
+
and field type."""
|
|
24
|
+
HasFieldWrappers = type[WrappedType] | WrappedRecordModel
|
|
25
|
+
"""An identifier for classes that have wrapper fields."""
|
|
16
26
|
ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
|
|
17
27
|
"""An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
|
|
18
28
|
ID."""
|
|
@@ -50,7 +60,88 @@ class AliasUtil:
|
|
|
50
60
|
|
|
51
61
|
:return: A list of record IDs for the input records.
|
|
52
62
|
"""
|
|
53
|
-
return [(
|
|
63
|
+
return [(AliasUtil.to_record_id(x)) for x in records]
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def to_record_id(record: RecordIdentifier):
|
|
67
|
+
"""
|
|
68
|
+
Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
|
|
69
|
+
or WrappedRecordModel to just an integer (taking the record ID from the record).
|
|
70
|
+
|
|
71
|
+
:return: A record ID for the input record.
|
|
72
|
+
"""
|
|
73
|
+
return record if isinstance(record, int) else record.record_id
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def to_data_type_name(value: DataTypeIdentifier) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Convert a given value to a data type name.
|
|
79
|
+
|
|
80
|
+
:param value: A value which is a string, record, or record model type.
|
|
81
|
+
:return: A string of the data type name of the input value.
|
|
82
|
+
"""
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
return value
|
|
85
|
+
if isinstance(value, SapioRecord):
|
|
86
|
+
return value.data_type_name
|
|
87
|
+
return value.get_wrapper_data_type_name()
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False) -> list[str] | set[str]:
|
|
91
|
+
"""
|
|
92
|
+
Convert a given iterable of values to a list or set of data type names.
|
|
93
|
+
|
|
94
|
+
:param values: An iterable of values which are strings, records, or record model types.
|
|
95
|
+
:param return_set: If true, return a set instead of a list.
|
|
96
|
+
:return: A list or set of strings of the data type name of the input value.
|
|
97
|
+
"""
|
|
98
|
+
values = [AliasUtil.to_data_type_name(x) for x in values]
|
|
99
|
+
return set(values) if return_set else values
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def to_data_field_name(value: FieldIdentifier) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Convert a string or WrapperField to a data field name string.
|
|
105
|
+
|
|
106
|
+
:param value: A string or WrapperField.
|
|
107
|
+
:return: A string of the data field name of the input value.
|
|
108
|
+
"""
|
|
109
|
+
if isinstance(value, tuple):
|
|
110
|
+
return value[0]
|
|
111
|
+
if isinstance(value, WrapperField):
|
|
112
|
+
return value.field_name
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
|
|
117
|
+
"""
|
|
118
|
+
Convert an iterable of strings or WrapperFields to a list of data field name strings.
|
|
119
|
+
|
|
120
|
+
:param values: An iterable of strings or WrapperFields.
|
|
121
|
+
:return: A list of strings of the data field names of the input values.
|
|
122
|
+
"""
|
|
123
|
+
return [AliasUtil.to_data_field_name(x) for x in values]
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
|
|
127
|
+
"""
|
|
128
|
+
Convert a given field identifier to the field type for that field.
|
|
129
|
+
|
|
130
|
+
:param field: A string or WrapperField.
|
|
131
|
+
:param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
|
|
132
|
+
must be provided to determine the field type.
|
|
133
|
+
:return: The field type of the given field.
|
|
134
|
+
"""
|
|
135
|
+
if isinstance(field, tuple):
|
|
136
|
+
return field[1]
|
|
137
|
+
if isinstance(field, WrapperField):
|
|
138
|
+
return field.field_type
|
|
139
|
+
for var in dir(data_type):
|
|
140
|
+
attr = getattr(data_type, var)
|
|
141
|
+
if isinstance(attr, WrapperField) and attr.field_name == field:
|
|
142
|
+
return attr.field_type
|
|
143
|
+
raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
|
|
144
|
+
f"field with the name \"{field}\",")
|
|
54
145
|
|
|
55
146
|
@staticmethod
|
|
56
147
|
def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from sapiopylib.rest.DataRecordManagerService import DataRecordManager
|
|
5
|
+
from sapiopylib.rest.User import SapioUser
|
|
6
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn, RawReportTerm, CustomReportCriteria, RawTermOperation, \
|
|
7
|
+
CompositeReportTerm, CompositeTermOperation
|
|
8
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
9
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
10
|
+
|
|
11
|
+
from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil
|
|
12
|
+
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
13
|
+
|
|
14
|
+
EVENTTYPE_COLUMN = "EVENTTYPE"
|
|
15
|
+
TIMESTAMP_COLUMN = "TIMESTAMP"
|
|
16
|
+
DATATYPENAME_COLUMN = "DATATYPENAME"
|
|
17
|
+
RECORDID_COLUMN = "RECORDID"
|
|
18
|
+
DESCRIPTION_COLUMN = "DESCRIPTION"
|
|
19
|
+
USERNAME_COLUMN = "USERNAME"
|
|
20
|
+
USERCOMMENT_COLUMN = "USERCOMMENT"
|
|
21
|
+
RECORDNAME_COLUMN = "RECORDNAME"
|
|
22
|
+
DATAFIELDNAME_COLUMN = "DATAFIELDNAME"
|
|
23
|
+
ORIGINALVALUE_COLUMN = "ORIGINALVALUE"
|
|
24
|
+
NEWVALUE_COLUMN = "NEWVALUE"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EventType(Enum):
|
|
28
|
+
"""An enum to represent the possible event type values with the event type column in the audit log table."""
|
|
29
|
+
ADD = 0
|
|
30
|
+
DELETE = 1
|
|
31
|
+
MODIFY = 2
|
|
32
|
+
INFO = 3
|
|
33
|
+
ERROR = 4
|
|
34
|
+
WARNING = 5
|
|
35
|
+
IMPORT = 6
|
|
36
|
+
GENERATE = 7
|
|
37
|
+
EXPORT = 8
|
|
38
|
+
ADDREF = 9
|
|
39
|
+
REMOVEREF = 10
|
|
40
|
+
ESIGNATURE = 11
|
|
41
|
+
ROLEASSIGNMENT = 12
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuditLogEntry:
|
|
45
|
+
|
|
46
|
+
__event_type: EventType
|
|
47
|
+
__date: int
|
|
48
|
+
__data_type_name: str
|
|
49
|
+
__record_id: int
|
|
50
|
+
__description: str
|
|
51
|
+
__users_login_name: str
|
|
52
|
+
__comment: str
|
|
53
|
+
__data_record_name: str
|
|
54
|
+
__data_field_name: str
|
|
55
|
+
__original_value: str
|
|
56
|
+
__new_value: str
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def event_type(self) -> EventType:
|
|
60
|
+
return self.__event_type
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def date(self) -> int:
|
|
64
|
+
return self.__date
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def data_type_name(self) -> str:
|
|
68
|
+
return self.__data_type_name
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def record_id(self) -> int:
|
|
72
|
+
return self.__record_id
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
return self.__description
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def users_login_name(self) -> str:
|
|
80
|
+
return self.__users_login_name
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def comment(self) -> str:
|
|
84
|
+
return self.__comment
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def data_record_name(self) -> str:
|
|
88
|
+
return self.__data_record_name
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def data_field_name(self) -> str:
|
|
92
|
+
return self.__data_field_name
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def original_value(self) -> str:
|
|
96
|
+
return self.__original_value
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def new_value(self) -> str:
|
|
100
|
+
return self.__new_value
|
|
101
|
+
|
|
102
|
+
def __init__(self, report_row: dict[str, Any]):
|
|
103
|
+
self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
|
|
104
|
+
self.__date = report_row[TIMESTAMP_COLUMN]
|
|
105
|
+
self.__data_type_name = report_row[DATATYPENAME_COLUMN]
|
|
106
|
+
self.__record_id = report_row[RECORDID_COLUMN]
|
|
107
|
+
self.__description = report_row[DESCRIPTION_COLUMN]
|
|
108
|
+
self.__users_login_name = report_row[USERNAME_COLUMN]
|
|
109
|
+
self.__comment = report_row[USERCOMMENT_COLUMN]
|
|
110
|
+
self.__data_record_name = report_row[RECORDNAME_COLUMN]
|
|
111
|
+
self.__data_field_name = report_row[DATAFIELDNAME_COLUMN]
|
|
112
|
+
self.__original_value = report_row[ORIGINALVALUE_COLUMN]
|
|
113
|
+
self.__new_value = report_row[NEWVALUE_COLUMN]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AuditLog:
|
|
117
|
+
AUDIT_LOG_PSEUDO_DATATYPE: str = "AUDITLOG"
|
|
118
|
+
EVENT_TYPE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, EVENTTYPE_COLUMN, FieldType.ENUM)
|
|
119
|
+
DATE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, TIMESTAMP_COLUMN, FieldType.DATE)
|
|
120
|
+
DATA_TYPE_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATATYPENAME_COLUMN, FieldType.STRING)
|
|
121
|
+
RECORD_ID: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, FieldType.LONG)
|
|
122
|
+
DESCRIPTION: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DESCRIPTION_COLUMN, FieldType.STRING)
|
|
123
|
+
USERS_LOGIN_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERNAME_COLUMN, FieldType.STRING)
|
|
124
|
+
COMMENT: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERCOMMENT_COLUMN, FieldType.STRING)
|
|
125
|
+
DATA_RECORD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDNAME_COLUMN, FieldType.STRING)
|
|
126
|
+
DATA_FIELD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, FieldType.STRING)
|
|
127
|
+
ORIGINAL_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, ORIGINALVALUE_COLUMN, FieldType.STRING)
|
|
128
|
+
NEW_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, NEWVALUE_COLUMN, FieldType.STRING)
|
|
129
|
+
|
|
130
|
+
AUDIT_LOG_COLUMNS = [EVENT_TYPE, DATE, DATA_TYPE_NAME, RECORD_ID, DESCRIPTION, USERS_LOGIN_NAME, COMMENT,
|
|
131
|
+
DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
|
|
132
|
+
user: SapioUser
|
|
133
|
+
|
|
134
|
+
def __init__(self, context: SapioWebhookContext | SapioUser):
|
|
135
|
+
self.user = context if isinstance(context, SapioUser) else context.user
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def create_data_record_audit_log_report(records: list[RecordIdentifier], fields: list[str] | None = None) -> CustomReportCriteria:
|
|
139
|
+
"""
|
|
140
|
+
This method creates a CustomReportCriteria object for running an audit log query based on data records.
|
|
141
|
+
|
|
142
|
+
Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
|
|
143
|
+
Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
|
|
144
|
+
parameter is not populated, the search will include results for all field changes.
|
|
145
|
+
|
|
146
|
+
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
147
|
+
:param fields: The data field names to include changes for.
|
|
148
|
+
:return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
|
|
149
|
+
"""
|
|
150
|
+
# We need to compile the record ids from these record identifiers as "str" variables, so they can be
|
|
151
|
+
# concatenated.
|
|
152
|
+
record_ids = AliasUtil.to_record_ids(records)
|
|
153
|
+
id_strs = [str(id_int) for id_int in record_ids]
|
|
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) + "}")
|
|
159
|
+
|
|
160
|
+
# If the user passed in any specific fields, then we should limit the query to those fields.
|
|
161
|
+
if fields:
|
|
162
|
+
field_term = RawReportTerm(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN,
|
|
163
|
+
RawTermOperation.EQUAL_TO_OPERATOR, "{" + ",".join(fields) + "}")
|
|
164
|
+
root_term = CompositeReportTerm(root_term, CompositeTermOperation.AND_OPERATOR, field_term)
|
|
165
|
+
|
|
166
|
+
return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
|
|
167
|
+
|
|
168
|
+
def run_data_record_audit_log_report(self, records: list[RecordIdentifier], fields: list[str] | None = None) -> dict[RecordIdentifier, list[AuditLogEntry]]:
|
|
169
|
+
"""
|
|
170
|
+
This method runs a custom report for changes made to the given data records using the audit log.
|
|
171
|
+
See "create_data_record_audit_log_report" for more details about the data record audit log report.
|
|
172
|
+
|
|
173
|
+
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
174
|
+
:param fields: The data field names to include changes for.
|
|
175
|
+
:return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
|
|
176
|
+
AuditLogEntry objects which match the record id value of those records.
|
|
177
|
+
"""
|
|
178
|
+
# First, we must build our report criteria for running the Custom Report.
|
|
179
|
+
criteria = AuditLog.create_data_record_audit_log_report(records, fields)
|
|
180
|
+
|
|
181
|
+
# Then we must run the custom report using that criteria.
|
|
182
|
+
raw_report_data: list[dict[str, Any]] = CustomReportUtil.run_custom_report(self.user, criteria)
|
|
183
|
+
|
|
184
|
+
# This section will prepare a map matching the original RecordIdentifier by record id.
|
|
185
|
+
# This is because the audit log entries will have record ids, but we want the keys in our result map
|
|
186
|
+
# to match the record identifiers that the user passed in, for convenience.
|
|
187
|
+
record_identifier_mapping: dict[int, RecordIdentifier] = dict()
|
|
188
|
+
for record in records:
|
|
189
|
+
record_id = AliasUtil.to_record_id(record)
|
|
190
|
+
record_identifier_mapping[record_id] = record
|
|
191
|
+
|
|
192
|
+
# Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
|
|
193
|
+
# and the value is a list of applicable audit log entries.
|
|
194
|
+
final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
|
|
195
|
+
for audit_entry_data in raw_report_data:
|
|
196
|
+
audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
|
|
197
|
+
identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
|
|
198
|
+
final_audit_data.setdefault(identifier, []).append(audit_entry)
|
|
199
|
+
|
|
200
|
+
return final_audit_data
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
2
4
|
from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
|
|
3
5
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxStringFieldDefinition, AbstractVeloxFieldDefinition, \
|
|
@@ -51,6 +53,7 @@ class PopupUtil:
|
|
|
51
53
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
52
54
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
53
55
|
"""
|
|
56
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
54
57
|
if display_name is None:
|
|
55
58
|
display_name = data_type
|
|
56
59
|
if plural_display_name is None:
|
|
@@ -97,6 +100,7 @@ class PopupUtil:
|
|
|
97
100
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
98
101
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
99
102
|
"""
|
|
103
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
100
104
|
# Get the field definitions of the data type.
|
|
101
105
|
data_type: str = record.data_type_name
|
|
102
106
|
type_man = DataMgmtServer.get_data_type_manager(context.user)
|
|
@@ -155,6 +159,7 @@ class PopupUtil:
|
|
|
155
159
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
156
160
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
157
161
|
"""
|
|
162
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
158
163
|
if max_length is None:
|
|
159
164
|
max_length = len(default_value) if default_value else 100
|
|
160
165
|
string_field = VeloxStringFieldDefinition(data_type, field_name, field_name, default_value=default_value,
|
|
@@ -191,6 +196,7 @@ class PopupUtil:
|
|
|
191
196
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
192
197
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
193
198
|
"""
|
|
199
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
194
200
|
if default_value is None:
|
|
195
201
|
default_value = max(0, min_value)
|
|
196
202
|
integer_field = VeloxIntegerFieldDefinition(data_type, field_name, field_name, default_value=default_value,
|
|
@@ -229,6 +235,7 @@ class PopupUtil:
|
|
|
229
235
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
230
236
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
231
237
|
"""
|
|
238
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
232
239
|
if default_value is None:
|
|
233
240
|
default_value = min_value
|
|
234
241
|
double_field = VeloxDoubleFieldDefinition(data_type, field_name, field_name, default_value=default_value,
|
|
@@ -260,6 +267,7 @@ class PopupUtil:
|
|
|
260
267
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
261
268
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
262
269
|
"""
|
|
270
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
263
271
|
if display_name is None:
|
|
264
272
|
display_name = data_type
|
|
265
273
|
if plural_display_name is None:
|
|
@@ -295,6 +303,7 @@ class PopupUtil:
|
|
|
295
303
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
296
304
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
297
305
|
"""
|
|
306
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
298
307
|
data_types: set[str] = {x.data_type_name for x in records}
|
|
299
308
|
if len(data_types) > 1:
|
|
300
309
|
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
@@ -347,6 +356,7 @@ class PopupUtil:
|
|
|
347
356
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
348
357
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
349
358
|
"""
|
|
359
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
350
360
|
data_types: set[str] = {x.data_type_name for x in records}
|
|
351
361
|
if len(data_types) > 1:
|
|
352
362
|
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
@@ -391,6 +401,7 @@ class PopupUtil:
|
|
|
391
401
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
392
402
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
393
403
|
"""
|
|
404
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
394
405
|
callback = ListDialogRequest(title, multi_select, options,
|
|
395
406
|
callback_context_data=request_context)
|
|
396
407
|
return SapioWebhookResult(True, client_callback_request=callback)
|
|
@@ -415,6 +426,7 @@ class PopupUtil:
|
|
|
415
426
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
416
427
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
417
428
|
"""
|
|
429
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
418
430
|
callback = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
419
431
|
callback_context_data=request_context)
|
|
420
432
|
return SapioWebhookResult(True, client_callback_request=callback)
|
|
@@ -437,6 +449,7 @@ class PopupUtil:
|
|
|
437
449
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
438
450
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
439
451
|
"""
|
|
452
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
440
453
|
return PopupUtil.option_popup(title, msg, ["OK"], 0, user_can_cancel, request_context=request_context)
|
|
441
454
|
|
|
442
455
|
@staticmethod
|
|
@@ -458,6 +471,7 @@ class PopupUtil:
|
|
|
458
471
|
:param request_context: Context that will be returned to the webhook server in the client callback result.
|
|
459
472
|
:return: A SapioWebhookResult with the popup as its client callback request.
|
|
460
473
|
"""
|
|
474
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
461
475
|
return PopupUtil.option_popup(title, msg, ["Yes", "No"], 0 if default_yes else 1, user_can_cancel,
|
|
462
476
|
request_context=request_context)
|
|
463
477
|
|
|
@@ -470,6 +484,7 @@ class PopupUtil:
|
|
|
470
484
|
|
|
471
485
|
Deprecated for PopupUtil.text_field_popup.
|
|
472
486
|
"""
|
|
487
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
473
488
|
return PopupUtil.string_field_popup(title, "", field_name, msg, len(msg), False, data_type,
|
|
474
489
|
request_context=request_context, auto_size=True)
|
|
475
490
|
|
|
@@ -481,6 +496,7 @@ class PopupUtil:
|
|
|
481
496
|
|
|
482
497
|
Deprecated for PopupUtil.option_popup.
|
|
483
498
|
"""
|
|
499
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
484
500
|
return PopupUtil.option_popup(title, msg, options, 0, user_can_cancel, request_context=request_context)
|
|
485
501
|
|
|
486
502
|
@staticmethod
|
|
@@ -490,4 +506,5 @@ class PopupUtil:
|
|
|
490
506
|
|
|
491
507
|
Deprecated for PopupUtil.ok_popup.
|
|
492
508
|
"""
|
|
509
|
+
warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
|
|
493
510
|
return PopupUtil.ok_popup(title, msg, False, request_context=request_context)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from sapiopylib.rest.User import SapioUser
|
|
2
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
3
|
+
|
|
4
|
+
from sapiopycommons.general.aliases import RecordIdentifier, ExperimentIdentifier, AliasUtil
|
|
5
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SapioNavigationLinker:
|
|
9
|
+
"""
|
|
10
|
+
Given a URL to a system's webservice API (example: https://company.exemplareln.com/webservice/api), construct
|
|
11
|
+
URLs for navigation links to various locations in the system.
|
|
12
|
+
"""
|
|
13
|
+
base_url: str
|
|
14
|
+
|
|
15
|
+
def __init__(self, url: str | SapioUser | SapioWebhookContext):
|
|
16
|
+
"""
|
|
17
|
+
:param url: A user or context object that is being used to send requests to a Sapio system, or a URL to a
|
|
18
|
+
system's webservice API.
|
|
19
|
+
"""
|
|
20
|
+
if isinstance(url, SapioWebhookContext):
|
|
21
|
+
url = url.user.url
|
|
22
|
+
elif isinstance(url, SapioUser):
|
|
23
|
+
url = url.url
|
|
24
|
+
self.base_url = url.rstrip("/").replace('webservice/api', 'veloxClient')
|
|
25
|
+
|
|
26
|
+
def data_record(self, record_identifier: RecordIdentifier, data_type_name: str | None = None) -> str:
|
|
27
|
+
"""
|
|
28
|
+
:param record_identifier: An object that can be used to identify a record in the system, be that a record ID,
|
|
29
|
+
a data record, or a record model.
|
|
30
|
+
:param data_type_name: If the provided record identifier is a record ID, then the data type name of the record
|
|
31
|
+
must be provided in this parameter. Otherwise, this parameter is ignored.
|
|
32
|
+
:return: A URL for navigating to the input record.
|
|
33
|
+
"""
|
|
34
|
+
record_id: int = AliasUtil.to_record_id(record_identifier)
|
|
35
|
+
if not isinstance(record_identifier, int):
|
|
36
|
+
data_type_name = AliasUtil.to_data_type_name(record_identifier)
|
|
37
|
+
if not data_type_name:
|
|
38
|
+
raise SapioException("Unable to create a data record link without a data type name. "
|
|
39
|
+
"Only a record ID was provided.")
|
|
40
|
+
return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
|
|
41
|
+
|
|
42
|
+
def experiment(self, experiment: ExperimentIdentifier) -> str:
|
|
43
|
+
"""
|
|
44
|
+
:param experiment: An object that can be used to identify an experiment in the system, be that an experiment
|
|
45
|
+
object, experiment protocol, or a notebook ID.
|
|
46
|
+
:return: A URL for navigating to the input experiment.
|
|
47
|
+
"""
|
|
48
|
+
return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import time
|
|
2
4
|
from datetime import datetime
|
|
3
5
|
|
|
4
6
|
import pytz
|
|
5
7
|
|
|
8
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
9
|
+
|
|
6
10
|
__timezone = None
|
|
7
11
|
"""The default timezone. Use TimeUtil.set_default_timezone in a global context before making use of TimeUtil."""
|
|
8
12
|
|
|
@@ -137,3 +141,39 @@ class TimeUtil:
|
|
|
137
141
|
return True
|
|
138
142
|
except Exception:
|
|
139
143
|
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DateRange:
|
|
147
|
+
start: int | None
|
|
148
|
+
end: int | None
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def from_string(value: str | None) -> DateRange:
|
|
152
|
+
"""
|
|
153
|
+
Construct a DateRange object from a string. The field value of date range fields is a string of the form
|
|
154
|
+
<start timestamp>/<end timestamp>.
|
|
155
|
+
|
|
156
|
+
:param value: A date range field value.
|
|
157
|
+
:return: A DateRange object matching the input field value.
|
|
158
|
+
"""
|
|
159
|
+
if not value:
|
|
160
|
+
return DateRange(None, None)
|
|
161
|
+
values: list[str] = value.split("/")
|
|
162
|
+
return DateRange(int(values[0]), int(values[1]))
|
|
163
|
+
|
|
164
|
+
def __init__(self, start: int | None, end: int | None):
|
|
165
|
+
"""
|
|
166
|
+
:param start: The timestamp for the start of the date range.
|
|
167
|
+
:param end: The timestamp for the end of the date rate.
|
|
168
|
+
"""
|
|
169
|
+
if (start and end is None) or (end and start is None):
|
|
170
|
+
raise SapioException("Both start and end values must be present in a date range.")
|
|
171
|
+
if start and end and end < start:
|
|
172
|
+
raise SapioException(f"End timestamp {end} is earlier than the start timestamp {start}.")
|
|
173
|
+
self.start = start
|
|
174
|
+
self.end = end
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str | None:
|
|
177
|
+
if not self.start and not self.end:
|
|
178
|
+
return None
|
|
179
|
+
return f"{self.start}/{self.end}"
|