sapiopycommons 2024.8.28a313__py3-none-any.whl → 2024.8.28a315__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 +407 -69
- sapiopycommons/chem/IndigoMolecules.py +1 -0
- sapiopycommons/chem/Molecules.py +1 -0
- 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 +118 -0
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +31 -24
- sapiopycommons/files/file_bridge_handler.py +340 -0
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +50 -10
- sapiopycommons/files/file_validator.py +92 -6
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +211 -37
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +486 -0
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +481 -97
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +147 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/METADATA +4 -2
- sapiopycommons-2024.8.28a315.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.28a313.dist-info/RECORD +0 -38
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.28a313.dist-info → sapiopycommons-2024.8.28a315.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,19 +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
|
-
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport
|
|
7
|
-
|
|
5
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport, CustomReportCriteria, RawReportTerm
|
|
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[
|
|
16
|
-
page_limit: int | None = None
|
|
15
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
16
|
+
page_limit: int | None = None,
|
|
17
|
+
page_size: int | None = None,
|
|
18
|
+
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
17
19
|
"""
|
|
18
20
|
Run a system report and return the results of that report as a list of dictionaries for the values of each
|
|
19
21
|
column in each row.
|
|
@@ -27,29 +29,97 @@ class CustomReportUtil:
|
|
|
27
29
|
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
28
30
|
any filters that have a header name that isn't in the custom report will be ignored.
|
|
29
31
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
32
|
+
:param page_size: The size of each page of results in the search. If None, the page size is set by the server.
|
|
33
|
+
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
30
34
|
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
31
35
|
values in the dicts are the data field names of the columns.
|
|
36
|
+
If two columns in the search have the same data field name but differing data type names, then the
|
|
37
|
+
dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
|
|
38
|
+
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
39
|
+
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
32
40
|
"""
|
|
33
|
-
results = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit
|
|
41
|
+
results: tuple = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit,
|
|
42
|
+
page_size, page_number)
|
|
34
43
|
columns: list[ReportColumn] = results[0]
|
|
35
|
-
rows: list[list[
|
|
44
|
+
rows: list[list[FieldValue]] = results[1]
|
|
45
|
+
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
@staticmethod
|
|
48
|
+
def run_custom_report(context: UserIdentifier,
|
|
49
|
+
report_criteria: CustomReportCriteria,
|
|
50
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
51
|
+
page_limit: int | None = None,
|
|
52
|
+
page_size: int | None = None,
|
|
53
|
+
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
54
|
+
"""
|
|
55
|
+
Run a custom report and return the results of that report as a list of dictionaries for the values of each
|
|
56
|
+
column in each row.
|
|
57
|
+
|
|
58
|
+
Custom reports are constructed by the caller, specifying the report terms and the columns that will be in the
|
|
59
|
+
results. They are like advanced or predefined searches from the system, except they are constructed from
|
|
60
|
+
within the webhook instead of from within the system.
|
|
61
|
+
|
|
62
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
63
|
+
:param report_criteria: The custom report criteria to run.
|
|
64
|
+
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
65
|
+
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
66
|
+
any filters that have a header name that isn't in the custom report will be ignored.
|
|
67
|
+
Note that this parameter is only provided for parity with the other run report functions. If you need to
|
|
68
|
+
filter the results of a search, it would likely be more beneficial to have just added a new term to the
|
|
69
|
+
input report criteria that corresponds to the filter.
|
|
70
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
71
|
+
:param page_size: The size of each page of results in the search. If None, uses the value from the given report
|
|
72
|
+
criteria. If not None, overwrites the value from the given report criteria.
|
|
73
|
+
:param page_number: The page number to start the search from, If None, uses the value from the given report
|
|
74
|
+
criteria. If not None, overwrites the value from the given report criteria.
|
|
75
|
+
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
76
|
+
values in the dicts are the data field names of the columns.
|
|
77
|
+
If two columns in the search have the same data field name but differing data type names, then the
|
|
78
|
+
dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
|
|
79
|
+
had a Sample column with a data field name of Identifier and a Request column with the same data field name,
|
|
80
|
+
then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
|
|
81
|
+
"""
|
|
82
|
+
results: tuple = CustomReportUtil.__exhaust_custom_report(context, report_criteria, page_limit,
|
|
83
|
+
page_size, page_number)
|
|
84
|
+
columns: list[ReportColumn] = results[0]
|
|
85
|
+
rows: list[list[FieldValue]] = results[1]
|
|
86
|
+
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
50
87
|
|
|
51
88
|
@staticmethod
|
|
52
|
-
def
|
|
89
|
+
def run_quick_report(context: UserIdentifier,
|
|
90
|
+
report_term: RawReportTerm,
|
|
91
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
92
|
+
page_limit: int | None = None,
|
|
93
|
+
page_size: int | None = None,
|
|
94
|
+
page_number: int | None = None) -> list[dict[str, FieldValue]]:
|
|
95
|
+
"""
|
|
96
|
+
Run a quick report and return the results of that report as a list of dictionaries for the values of each
|
|
97
|
+
column in each row.
|
|
98
|
+
|
|
99
|
+
Quick reports are helpful for cases where you need to query record field values in a more complex manner than
|
|
100
|
+
the data record manager allows, but still simpler than a full-blown custom report. The columns that are returned
|
|
101
|
+
in a quick search are every visible field from the data type that corresponds to the given report term. (Fields
|
|
102
|
+
which are not marked as visible in the data designer will be excluded.)
|
|
103
|
+
|
|
104
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
105
|
+
:param report_term: The raw report term to use for the quick report.
|
|
106
|
+
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
107
|
+
filter on. Only those headers that both the filters and the custom report share will take effect. That is,
|
|
108
|
+
any filters that have a header name that isn't in the custom report will be ignored.
|
|
109
|
+
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
|
|
110
|
+
:param page_size: The size of each page of results in the search. If None, the page size is set by the server.
|
|
111
|
+
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
112
|
+
:return: The results of the report listed row by row, mapping each cell to the header it is under. The header
|
|
113
|
+
values in the dicts are the data field names of the columns.
|
|
114
|
+
"""
|
|
115
|
+
results: tuple = CustomReportUtil.__exhaust_quick_report(context, report_term, page_limit,
|
|
116
|
+
page_size, page_number)
|
|
117
|
+
columns: list[ReportColumn] = results[0]
|
|
118
|
+
rows: list[list[FieldValue]] = results[1]
|
|
119
|
+
return CustomReportUtil.__process_results(rows, columns, filters)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_system_report_criteria(context: UserIdentifier, report_name: str) -> CustomReport:
|
|
53
123
|
"""
|
|
54
124
|
Retrieve a custom report from the system given the name of the report. This works by querying the system report
|
|
55
125
|
with a page number and size of 1 to minimize the amount of data transfer needed to retrieve the report's config.
|
|
@@ -64,27 +134,131 @@ class CustomReportUtil:
|
|
|
64
134
|
:param report_name: The name of the system report to run.
|
|
65
135
|
:return: The CustomReport object for the given system report name.
|
|
66
136
|
"""
|
|
67
|
-
user: SapioUser =
|
|
137
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
68
138
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
69
139
|
return report_man.run_system_report_by_name(report_name, 1, 1)
|
|
70
140
|
|
|
71
141
|
@staticmethod
|
|
72
|
-
def __exhaust_system_report(context:
|
|
73
|
-
|
|
74
|
-
|
|
142
|
+
def __exhaust_system_report(context: UserIdentifier,
|
|
143
|
+
report_name: str,
|
|
144
|
+
page_limit: int | None,
|
|
145
|
+
page_size: int | None,
|
|
146
|
+
page_number: int | None) \
|
|
147
|
+
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
148
|
+
"""
|
|
149
|
+
Given a system report, iterate over every page of the report and collect the results
|
|
150
|
+
until there are no remaining pages.
|
|
151
|
+
"""
|
|
152
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
153
|
+
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
154
|
+
|
|
155
|
+
result = None
|
|
156
|
+
has_next_page: bool = True
|
|
157
|
+
rows: list[list[FieldValue]] = []
|
|
158
|
+
cur_page: int = 1
|
|
159
|
+
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
160
|
+
result = report_man.run_system_report_by_name(report_name, page_size, page_number)
|
|
161
|
+
page_size = result.page_size
|
|
162
|
+
page_number = result.page_number
|
|
163
|
+
has_next_page = result.has_next_page
|
|
164
|
+
rows.extend(result.result_table)
|
|
165
|
+
cur_page += 1
|
|
166
|
+
return result.column_list, rows
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def __exhaust_custom_report(context: UserIdentifier,
|
|
170
|
+
report: CustomReportCriteria,
|
|
171
|
+
page_limit: int | None,
|
|
172
|
+
page_size: int | None,
|
|
173
|
+
page_number: int | None) \
|
|
174
|
+
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
175
|
+
"""
|
|
176
|
+
Given a custom report, iterate over every page of the report and collect the results
|
|
177
|
+
until there are no remaining pages.
|
|
178
|
+
"""
|
|
179
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
75
180
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
76
181
|
|
|
77
|
-
|
|
78
|
-
page_size
|
|
79
|
-
|
|
182
|
+
result = None
|
|
183
|
+
if page_size is not None:
|
|
184
|
+
report.page_size = page_size
|
|
185
|
+
if page_number is not None:
|
|
186
|
+
report.page_number = page_number
|
|
80
187
|
has_next_page: bool = True
|
|
81
|
-
rows: list[list[
|
|
188
|
+
rows: list[list[FieldValue]] = []
|
|
82
189
|
cur_page: int = 1
|
|
83
|
-
while has_next_page and (not page_limit or cur_page
|
|
84
|
-
|
|
85
|
-
page_size =
|
|
86
|
-
page_number =
|
|
87
|
-
has_next_page =
|
|
88
|
-
rows.extend(
|
|
190
|
+
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
191
|
+
result = report_man.run_custom_report(report)
|
|
192
|
+
report.page_size = result.page_size
|
|
193
|
+
report.page_number = result.page_number
|
|
194
|
+
has_next_page = result.has_next_page
|
|
195
|
+
rows.extend(result.result_table)
|
|
89
196
|
cur_page += 1
|
|
90
|
-
return
|
|
197
|
+
return result.column_list, rows
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def __exhaust_quick_report(context: UserIdentifier,
|
|
201
|
+
report_term: RawReportTerm,
|
|
202
|
+
page_limit: int | None,
|
|
203
|
+
page_size: int | None,
|
|
204
|
+
page_number: int | None) \
|
|
205
|
+
-> tuple[list[ReportColumn], list[list[FieldValue]]]:
|
|
206
|
+
"""
|
|
207
|
+
Given a quick report, iterate over every page of the report and collect the results
|
|
208
|
+
until there are no remaining pages.
|
|
209
|
+
"""
|
|
210
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
211
|
+
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
212
|
+
|
|
213
|
+
result = None
|
|
214
|
+
has_next_page: bool = True
|
|
215
|
+
rows: list[list[FieldValue]] = []
|
|
216
|
+
cur_page: int = 1
|
|
217
|
+
while has_next_page and (not page_limit or cur_page <= page_limit):
|
|
218
|
+
result = report_man.run_quick_report(report_term, page_size, page_number)
|
|
219
|
+
page_size = result.page_size
|
|
220
|
+
page_number = result.page_number
|
|
221
|
+
has_next_page = result.has_next_page
|
|
222
|
+
rows.extend(result.result_table)
|
|
223
|
+
cur_page += 1
|
|
224
|
+
return result.column_list, rows
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def __process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
|
|
228
|
+
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
|
|
229
|
+
"""
|
|
230
|
+
Given the results of a report as a list of row values and the report's columns, combine these lists to
|
|
231
|
+
result in a singular list of dictionaries for each row in the results.
|
|
232
|
+
|
|
233
|
+
If any filter criteria has been provided, also use that to filter the row.
|
|
234
|
+
"""
|
|
235
|
+
# It may be the case that two columns have the same data field name but differing data type names.
|
|
236
|
+
# If this occurs, then we need to be able to differentiate these columns in the resulting dictionary.
|
|
237
|
+
prepend_dt: set[str] = set()
|
|
238
|
+
encountered_names: list[str] = []
|
|
239
|
+
for column in columns:
|
|
240
|
+
field_name: str = column.data_field_name
|
|
241
|
+
if field_name in encountered_names:
|
|
242
|
+
prepend_dt.add(field_name)
|
|
243
|
+
else:
|
|
244
|
+
encountered_names.append(field_name)
|
|
245
|
+
|
|
246
|
+
filters: dict[str, Iterable[FieldValue]] = AliasUtil.to_data_field_names_dict(filters)
|
|
247
|
+
|
|
248
|
+
ret: list[dict[str, FieldValue]] = []
|
|
249
|
+
for row in rows:
|
|
250
|
+
row_data: dict[str, FieldValue] = {}
|
|
251
|
+
filter_row: bool = False
|
|
252
|
+
for value, column in zip(row, columns):
|
|
253
|
+
header: str = column.data_field_name
|
|
254
|
+
# If two columns share the same data field name, prepend the data type name of the column to the
|
|
255
|
+
# data field name.
|
|
256
|
+
if header in prepend_dt:
|
|
257
|
+
header = column.data_type_name + "." + header
|
|
258
|
+
if filters is not None and header in filters and value not in filters.get(header):
|
|
259
|
+
filter_row = True
|
|
260
|
+
break
|
|
261
|
+
row_data.update({header: value})
|
|
262
|
+
if filter_row is False:
|
|
263
|
+
ret.append(row_data)
|
|
264
|
+
return ret
|
|
@@ -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,50 @@
|
|
|
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, DataTypeIdentifier
|
|
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: DataTypeIdentifier | 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 data_type_name:
|
|
36
|
+
data_type_name = AliasUtil.to_data_type_name(data_type_name)
|
|
37
|
+
if not isinstance(record_identifier, int):
|
|
38
|
+
data_type_name = AliasUtil.to_data_type_name(record_identifier)
|
|
39
|
+
if not data_type_name:
|
|
40
|
+
raise SapioException("Unable to create a data record link without a data type name. "
|
|
41
|
+
"Only a record ID was provided.")
|
|
42
|
+
return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
|
|
43
|
+
|
|
44
|
+
def experiment(self, experiment: ExperimentIdentifier) -> str:
|
|
45
|
+
"""
|
|
46
|
+
:param experiment: An object that can be used to identify an experiment in the system, be that an experiment
|
|
47
|
+
object, experiment protocol, or a notebook ID.
|
|
48
|
+
:return: A URL for navigating to the input experiment.
|
|
49
|
+
"""
|
|
50
|
+
return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
|