sapiopycommons 2024.7.25a299__py3-none-any.whl → 2024.8.2a301__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 +59 -22
- sapiopycommons/datatype/attachment_util.py +15 -6
- sapiopycommons/eln/experiment_handler.py +66 -29
- sapiopycommons/files/complex_data_loader.py +1 -1
- sapiopycommons/files/file_bridge.py +1 -1
- sapiopycommons/files/file_util.py +13 -4
- sapiopycommons/files/file_validator.py +1 -2
- sapiopycommons/general/aliases.py +21 -1
- sapiopycommons/general/audit_log.py +200 -0
- sapiopycommons/general/sapio_links.py +48 -0
- sapiopycommons/recordmodel/record_handler.py +78 -16
- sapiopycommons/rules/eln_rule_handler.py +6 -22
- sapiopycommons/rules/on_save_rule_handler.py +6 -28
- sapiopycommons/webhook/webhook_handlers.py +58 -23
- {sapiopycommons-2024.7.25a299.dist-info → sapiopycommons-2024.8.2a301.dist-info}/METADATA +1 -1
- {sapiopycommons-2024.7.25a299.dist-info → sapiopycommons-2024.8.2a301.dist-info}/RECORD +18 -16
- {sapiopycommons-2024.7.25a299.dist-info → sapiopycommons-2024.8.2a301.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.7.25a299.dist-info → sapiopycommons-2024.8.2a301.dist-info}/licenses/LICENSE +0 -0
|
@@ -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.src.sapiopycommons.general.aliases import RecordIdentifier, AliasUtil
|
|
12
|
+
from sapiopycommons.src.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
|
|
@@ -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"
|
|
@@ -7,6 +7,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTer
|
|
|
7
7
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
8
8
|
from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
|
|
9
9
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
10
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
10
11
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
11
12
|
from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
|
|
12
13
|
QueryAllRecordsOfTypeAutoPager
|
|
@@ -16,6 +17,7 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
|
|
|
16
17
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
|
|
17
18
|
from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
|
|
18
19
|
RelationshipNodeType
|
|
20
|
+
from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
|
|
19
21
|
|
|
20
22
|
from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
|
|
21
23
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
@@ -32,6 +34,7 @@ class RecordHandler:
|
|
|
32
34
|
rec_man: RecordModelManager
|
|
33
35
|
inst_man: RecordModelInstanceManager
|
|
34
36
|
rel_man: RecordModelRelationshipManager
|
|
37
|
+
an_man: RecordModelAncestorManager
|
|
35
38
|
|
|
36
39
|
def __init__(self, context: SapioWebhookContext | SapioUser):
|
|
37
40
|
"""
|
|
@@ -42,6 +45,7 @@ class RecordHandler:
|
|
|
42
45
|
self.rec_man = RecordModelManager(self.user)
|
|
43
46
|
self.inst_man = self.rec_man.instance_manager
|
|
44
47
|
self.rel_man = self.rec_man.relationship_manager
|
|
48
|
+
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
45
49
|
|
|
46
50
|
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
47
51
|
"""
|
|
@@ -51,6 +55,7 @@ class RecordHandler:
|
|
|
51
55
|
:param wrapper_type: The record model wrapper to use.
|
|
52
56
|
:return: The record model for the input.
|
|
53
57
|
"""
|
|
58
|
+
self.__verify_data_type([record], wrapper_type)
|
|
54
59
|
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
55
60
|
|
|
56
61
|
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
@@ -61,6 +66,7 @@ class RecordHandler:
|
|
|
61
66
|
:param wrapper_type: The record model wrapper to use.
|
|
62
67
|
:return: The record models for the input.
|
|
63
68
|
"""
|
|
69
|
+
self.__verify_data_type(records, wrapper_type)
|
|
64
70
|
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
65
71
|
|
|
66
72
|
def query_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
|
|
@@ -831,8 +837,6 @@ class RecordHandler:
|
|
|
831
837
|
path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
|
|
832
838
|
relationship path must already be loaded.
|
|
833
839
|
|
|
834
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
835
|
-
|
|
836
840
|
:param models: A list of record models.
|
|
837
841
|
:param path: The relationship path to follow.
|
|
838
842
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -843,15 +847,44 @@ class RecordHandler:
|
|
|
843
847
|
# PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
|
|
844
848
|
path: list[RelationshipNode] = path.path
|
|
845
849
|
for model in models:
|
|
846
|
-
current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
850
|
+
current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
|
|
847
851
|
for node in path:
|
|
848
|
-
|
|
852
|
+
data_type: str = node.data_type_name
|
|
853
|
+
direction: RelationshipNodeType = node.direction
|
|
849
854
|
if current is None:
|
|
850
855
|
break
|
|
851
856
|
if direction == RelationshipNodeType.CHILD:
|
|
852
|
-
current = current.get_child_of_type(
|
|
857
|
+
current = current.get_child_of_type(data_type)
|
|
853
858
|
elif direction == RelationshipNodeType.PARENT:
|
|
854
|
-
current = current.get_parent_of_type(
|
|
859
|
+
current = current.get_parent_of_type(data_type)
|
|
860
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
861
|
+
ancestors: list[PyRecordModel] = list(self.an_man.get_ancestors_of_type(current, data_type))
|
|
862
|
+
if not ancestors:
|
|
863
|
+
current = None
|
|
864
|
+
elif len(ancestors) > 1:
|
|
865
|
+
raise SapioException(f"Hierarchy contains multiple ancestors of type {data_type}.")
|
|
866
|
+
else:
|
|
867
|
+
current = ancestors[0]
|
|
868
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
869
|
+
descendants: list[PyRecordModel] = list(self.an_man.get_descendant_of_type(current, data_type))
|
|
870
|
+
if not descendants:
|
|
871
|
+
current = None
|
|
872
|
+
elif len(descendants) > 1:
|
|
873
|
+
raise SapioException(f"Hierarchy contains multiple descendants of type {data_type}.")
|
|
874
|
+
else:
|
|
875
|
+
current = descendants[0]
|
|
876
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
877
|
+
current = current.get_forward_side_link(node.data_field_name)
|
|
878
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
879
|
+
field_name: str = node.data_field_name
|
|
880
|
+
reverse_links: list[PyRecordModel] = current.get_reverse_side_link(field_name, data_type)
|
|
881
|
+
if not reverse_links:
|
|
882
|
+
current = None
|
|
883
|
+
elif len(reverse_links) > 1:
|
|
884
|
+
raise SapioException(f"Hierarchy contains multiple reverse links of type {data_type} on field "
|
|
885
|
+
f"{field_name}.")
|
|
886
|
+
else:
|
|
887
|
+
current = reverse_links[0]
|
|
855
888
|
else:
|
|
856
889
|
raise SapioException("Unsupported path direction.")
|
|
857
890
|
ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
|
|
@@ -864,8 +897,6 @@ class RecordHandler:
|
|
|
864
897
|
path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
|
|
865
898
|
relationship path must already be loaded.
|
|
866
899
|
|
|
867
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
868
|
-
|
|
869
900
|
:param models: A list of record models.
|
|
870
901
|
:param path: The relationship path to follow.
|
|
871
902
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -880,14 +911,23 @@ class RecordHandler:
|
|
|
880
911
|
next_search: set[PyRecordModel] = set()
|
|
881
912
|
# Exhaust the records at each step in the path, then use those records for the next step.
|
|
882
913
|
for node in path:
|
|
883
|
-
|
|
914
|
+
data_type: str = node.data_type_name
|
|
915
|
+
direction: RelationshipNodeType = node.direction
|
|
884
916
|
if len(current_search) == 0:
|
|
885
917
|
break
|
|
886
918
|
for search in current_search:
|
|
887
919
|
if direction == RelationshipNodeType.CHILD:
|
|
888
|
-
next_search.update(search.get_children_of_type(
|
|
920
|
+
next_search.update(search.get_children_of_type(data_type))
|
|
889
921
|
elif direction == RelationshipNodeType.PARENT:
|
|
890
|
-
next_search.update(search.get_parents_of_type(
|
|
922
|
+
next_search.update(search.get_parents_of_type(data_type))
|
|
923
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
924
|
+
next_search.update(self.an_man.get_ancestors_of_type(search, data_type))
|
|
925
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
926
|
+
next_search.update(self.an_man.get_descendant_of_type(search, data_type))
|
|
927
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
928
|
+
next_search.add(search.get_forward_side_link(node.data_field_name))
|
|
929
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
930
|
+
next_search.update(search.get_reverse_side_link(node.data_field_name, data_type))
|
|
891
931
|
else:
|
|
892
932
|
raise SapioException("Unsupported path direction.")
|
|
893
933
|
current_search = next_search
|
|
@@ -908,8 +948,6 @@ class RecordHandler:
|
|
|
908
948
|
relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
|
|
909
949
|
together into a single sample).
|
|
910
950
|
|
|
911
|
-
Currently, the relationship path may only contain parent/child nodes.
|
|
912
|
-
|
|
913
951
|
:param models: A list of record models.
|
|
914
952
|
:param path: The relationship path to follow.
|
|
915
953
|
:param wrapper_type: The record model wrapper to use.
|
|
@@ -922,13 +960,22 @@ class RecordHandler:
|
|
|
922
960
|
for model in models:
|
|
923
961
|
current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
|
|
924
962
|
for node in path:
|
|
925
|
-
|
|
963
|
+
data_type: str = node.data_type_name
|
|
964
|
+
direction: RelationshipNodeType = node.direction
|
|
926
965
|
if len(current) == 0:
|
|
927
966
|
break
|
|
928
967
|
if direction == RelationshipNodeType.CHILD:
|
|
929
|
-
current = current[0].get_children_of_type(
|
|
968
|
+
current = current[0].get_children_of_type(data_type)
|
|
930
969
|
elif direction == RelationshipNodeType.PARENT:
|
|
931
|
-
current = current[0].get_parents_of_type(
|
|
970
|
+
current = current[0].get_parents_of_type(data_type)
|
|
971
|
+
elif direction == RelationshipNodeType.ANCESTOR:
|
|
972
|
+
current = list(self.an_man.get_ancestors_of_type(current[0], data_type))
|
|
973
|
+
elif direction == RelationshipNodeType.DESCENDANT:
|
|
974
|
+
current = list(self.an_man.get_descendant_of_type(current[0], data_type))
|
|
975
|
+
elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
|
|
976
|
+
current = [current[0].get_forward_side_link(node.data_field_name)]
|
|
977
|
+
elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
|
|
978
|
+
current = current[0].get_reverse_side_link(node.data_field_name, data_type)
|
|
932
979
|
else:
|
|
933
980
|
raise SapioException("Unsupported path direction.")
|
|
934
981
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
@@ -959,3 +1006,18 @@ class RecordHandler:
|
|
|
959
1006
|
f"encountered in system that matches all provided identifiers.")
|
|
960
1007
|
unique_record = result
|
|
961
1008
|
return unique_record
|
|
1009
|
+
|
|
1010
|
+
@staticmethod
|
|
1011
|
+
def __verify_data_type(records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> None:
|
|
1012
|
+
"""
|
|
1013
|
+
Throw an exception if the data type of the given records and wrapper don't match.
|
|
1014
|
+
"""
|
|
1015
|
+
model_type: str = wrapper_type.get_wrapper_data_type_name()
|
|
1016
|
+
for record in records:
|
|
1017
|
+
record_type: str = record.data_type_name
|
|
1018
|
+
# Account for ELN data type records.
|
|
1019
|
+
if ElnBaseDataType.is_eln_type(record_type):
|
|
1020
|
+
record_type = ElnBaseDataType.get_base_type(record_type).data_type_name
|
|
1021
|
+
if record_type != model_type:
|
|
1022
|
+
raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
|
|
1023
|
+
f"of type {model_type}")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
2
|
-
from sapiopylib.rest.pojo.
|
|
2
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
3
3
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
4
4
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
|
|
5
5
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
@@ -12,7 +12,6 @@ from sapiopycommons.general.exceptions import SapioException
|
|
|
12
12
|
class ElnRuleHandler:
|
|
13
13
|
"""
|
|
14
14
|
A class which helps with the parsing and navigation of the ELN rule result map of a webhook context.
|
|
15
|
-
TODO: Add functionality around the VeloxRuleType of the rule results.
|
|
16
15
|
"""
|
|
17
16
|
__context: SapioWebhookContext
|
|
18
17
|
"""The context that this handler is working from."""
|
|
@@ -64,13 +63,8 @@ class ElnRuleHandler:
|
|
|
64
63
|
# Get the data type of this record. If this is an ELN type, ignore the digits.
|
|
65
64
|
data_type: str = record.data_type_name
|
|
66
65
|
# PR-46331: Ensure that all ELN types are converted to their base data type name.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data_type = "ELNExperiment"
|
|
70
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
71
|
-
data_type = "ELNExperimentDetail"
|
|
72
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
73
|
-
data_type = "ELNSampleDetail"
|
|
66
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
67
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
74
68
|
# Update the list of records of this type that exist so far globally.
|
|
75
69
|
self.__records.setdefault(data_type, set()).add(record)
|
|
76
70
|
# Do the same for the list of records of this type for this specific entry.
|
|
@@ -85,19 +79,9 @@ class ElnRuleHandler:
|
|
|
85
79
|
entry_dict: dict[str, dict[int, FieldMap]] = {}
|
|
86
80
|
for record_result in entry_results:
|
|
87
81
|
for result in record_result.velox_type_rule_field_map_result_list:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if isinstance(velox_type, dict):
|
|
92
|
-
velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
|
|
93
|
-
data_type: str = velox_type.data_type_name
|
|
94
|
-
# TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
|
|
95
|
-
if data_type.startswith("ELNExperiment_"):
|
|
96
|
-
data_type = "ELNExperiment"
|
|
97
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
98
|
-
data_type = "ELNExperimentDetail"
|
|
99
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
100
|
-
data_type = "ELNSampleDetail"
|
|
82
|
+
data_type: str = result.velox_type_pojo.data_type_name
|
|
83
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
84
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
101
85
|
for field_map in result.field_map_list:
|
|
102
86
|
rec_id: int = field_map.get("RecordId")
|
|
103
87
|
self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
2
|
-
from sapiopylib.rest.pojo.
|
|
2
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
|
|
3
3
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
4
4
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
|
|
5
5
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
@@ -12,7 +12,6 @@ from sapiopycommons.general.exceptions import SapioException
|
|
|
12
12
|
class OnSaveRuleHandler:
|
|
13
13
|
"""
|
|
14
14
|
A class which helps with the parsing and navigation of the on save rule result map of a webhook context.
|
|
15
|
-
TODO: Add functionality around the VeloxRuleType of the rule results.
|
|
16
15
|
"""
|
|
17
16
|
__context: SapioWebhookContext
|
|
18
17
|
"""The context that this handler is working from."""
|
|
@@ -51,9 +50,6 @@ class OnSaveRuleHandler:
|
|
|
51
50
|
self.__base_id_to_records = {}
|
|
52
51
|
# Each record ID in the context has a list of results for that record.
|
|
53
52
|
for record_id, rule_results in self.__context.velox_on_save_result_map.items():
|
|
54
|
-
# TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
|
|
55
|
-
# once sapiopylib is fixed.
|
|
56
|
-
record_id = int(record_id)
|
|
57
53
|
# Keep track of the records for this specific record ID.
|
|
58
54
|
id_dict: dict[str, set[DataRecord]] = {}
|
|
59
55
|
# The list of results for a record consist of a list of data records and a VeloxType that specifies
|
|
@@ -64,13 +60,8 @@ class OnSaveRuleHandler:
|
|
|
64
60
|
# Get the data type of this record. If this is an ELN type, ignore the digits.
|
|
65
61
|
data_type: str = record.data_type_name
|
|
66
62
|
# PR-46331: Ensure that all ELN types are converted to their base data type name.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data_type = "ELNExperiment"
|
|
70
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
71
|
-
data_type = "ELNExperimentDetail"
|
|
72
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
73
|
-
data_type = "ELNSampleDetail"
|
|
63
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
64
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
74
65
|
# Update the list of records of this type that exist so far globally.
|
|
75
66
|
self.__records.setdefault(data_type, set()).add(record)
|
|
76
67
|
# Do the same for the list of records of this type that relate to this record ID.
|
|
@@ -82,24 +73,11 @@ class OnSaveRuleHandler:
|
|
|
82
73
|
self.__base_id_to_field_maps = {}
|
|
83
74
|
# Repeat the same thing for the field map results.
|
|
84
75
|
for record_id, rule_results in self.__context.velox_on_save_field_map_result_map.items():
|
|
85
|
-
# TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
|
|
86
|
-
# once sapiopylib is fixed.
|
|
87
|
-
record_id = int(record_id)
|
|
88
76
|
id_dict: dict[str, dict[int, FieldMap]] = {}
|
|
89
77
|
for record_result in rule_results:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if isinstance(velox_type, dict):
|
|
94
|
-
velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
|
|
95
|
-
data_type: str = velox_type.data_type_name
|
|
96
|
-
# TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
|
|
97
|
-
if data_type.startswith("ELNExperiment_"):
|
|
98
|
-
data_type = "ELNExperiment"
|
|
99
|
-
elif data_type.startswith("ELNExperimentDetail_"):
|
|
100
|
-
data_type = "ELNExperimentDetail"
|
|
101
|
-
elif data_type.startswith("ELNSampleDetail_"):
|
|
102
|
-
data_type = "ELNSampleDetail"
|
|
78
|
+
data_type: str = record_result.velox_type_pojo.data_type_name
|
|
79
|
+
if ElnBaseDataType.is_eln_type(data_type):
|
|
80
|
+
data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
|
|
103
81
|
for field_map in record_result.field_map_list:
|
|
104
82
|
rec_id: int = field_map.get("RecordId")
|
|
105
83
|
self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
|