sapiopycommons 2024.8.27a310__py3-none-any.whl → 2024.8.27a312__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.

Files changed (33) hide show
  1. sapiopycommons/callbacks/callback_util.py +37 -133
  2. sapiopycommons/datatype/attachment_util.py +10 -11
  3. sapiopycommons/eln/experiment_handler.py +48 -209
  4. sapiopycommons/eln/experiment_report_util.py +129 -33
  5. sapiopycommons/files/complex_data_loader.py +4 -5
  6. sapiopycommons/files/file_bridge.py +14 -15
  7. sapiopycommons/files/file_bridge_handler.py +5 -27
  8. sapiopycommons/files/file_data_handler.py +5 -2
  9. sapiopycommons/files/file_util.py +5 -38
  10. sapiopycommons/files/file_validator.py +11 -26
  11. sapiopycommons/files/file_writer.py +15 -44
  12. sapiopycommons/general/aliases.py +3 -147
  13. sapiopycommons/general/custom_report_util.py +32 -34
  14. sapiopycommons/general/popup_util.py +0 -17
  15. sapiopycommons/general/time_util.py +0 -40
  16. sapiopycommons/multimodal/multimodal_data.py +1 -0
  17. sapiopycommons/processtracking/endpoints.py +22 -22
  18. sapiopycommons/recordmodel/record_handler.py +77 -228
  19. sapiopycommons/rules/eln_rule_handler.py +25 -34
  20. sapiopycommons/rules/on_save_rule_handler.py +31 -34
  21. sapiopycommons/webhook/webhook_handlers.py +26 -90
  22. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.27a312.dist-info}/METADATA +1 -1
  23. sapiopycommons-2024.8.27a312.dist-info/RECORD +43 -0
  24. sapiopycommons/customreport/__init__.py +0 -0
  25. sapiopycommons/customreport/column_builder.py +0 -60
  26. sapiopycommons/customreport/custom_report_builder.py +0 -125
  27. sapiopycommons/customreport/term_builder.py +0 -299
  28. sapiopycommons/general/audit_log.py +0 -196
  29. sapiopycommons/general/sapio_links.py +0 -50
  30. sapiopycommons/webhook/webservice_handlers.py +0 -67
  31. sapiopycommons-2024.8.27a310.dist-info/RECORD +0 -50
  32. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.27a312.dist-info}/WHEEL +0 -0
  33. {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.27a312.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,5 @@
1
- from __future__ import annotations
2
-
3
1
  from collections.abc import Iterable
4
- from weakref import WeakValueDictionary
2
+ from typing import Any
5
3
 
6
4
  from sapiopylib.rest.DataRecordManagerService import DataRecordManager
7
5
  from sapiopylib.rest.User import SapioUser
@@ -9,7 +7,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, RawReportTer
9
7
  from sapiopylib.rest.pojo.DataRecord import DataRecord
10
8
  from sapiopylib.rest.pojo.DataRecordPaging import DataRecordPojoPageCriteria
11
9
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
12
- from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
10
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
13
11
  from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
14
12
  QueryAllRecordsOfTypeAutoPager
15
13
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
@@ -18,10 +16,8 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
18
16
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
19
17
  from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
20
18
  RelationshipNodeType
21
- from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
22
19
 
23
- from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
24
- FieldIdentifierMap, FieldValue, UserIdentifier, FieldIdentifierKey
20
+ from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
25
21
  from sapiopycommons.general.custom_report_util import CustomReportUtil
26
22
  from sapiopycommons.general.exceptions import SapioException
27
23
 
@@ -36,38 +32,16 @@ class RecordHandler:
36
32
  rec_man: RecordModelManager
37
33
  inst_man: RecordModelInstanceManager
38
34
  rel_man: RecordModelRelationshipManager
39
- an_man: RecordModelAncestorManager
40
-
41
- __instances: WeakValueDictionary[SapioUser, RecordHandler] = WeakValueDictionary()
42
- __initialized: bool
43
35
 
44
- def __new__(cls, context: UserIdentifier):
36
+ def __init__(self, context: SapioWebhookContext | SapioUser):
45
37
  """
46
38
  :param context: The current webhook context or a user object to send requests from.
47
39
  """
48
- user = AliasUtil.to_sapio_user(context)
49
- obj = cls.__instances.get(user)
50
- if not obj:
51
- obj = object.__new__(cls)
52
- obj.__initialized = False
53
- cls.__instances[user] = obj
54
- return obj
55
-
56
- def __init__(self, context: UserIdentifier):
57
- """
58
- :param context: The current webhook context or a user object to send requests from.
59
- """
60
- self.user = AliasUtil.to_sapio_user(context)
61
- if self.__initialized:
62
- return
63
- self.__initialized = True
64
-
65
40
  self.user = context if isinstance(context, SapioUser) else context.user
66
41
  self.dr_man = DataRecordManager(self.user)
67
42
  self.rec_man = RecordModelManager(self.user)
68
43
  self.inst_man = self.rec_man.instance_manager
69
44
  self.rel_man = self.rec_man.relationship_manager
70
- self.an_man = RecordModelAncestorManager(self.rec_man)
71
45
 
72
46
  def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
73
47
  """
@@ -77,7 +51,6 @@ class RecordHandler:
77
51
  :param wrapper_type: The record model wrapper to use.
78
52
  :return: The record model for the input.
79
53
  """
80
- self.__verify_data_type([record], wrapper_type)
81
54
  return self.inst_man.add_existing_record_of_type(record, wrapper_type)
82
55
 
83
56
  def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
@@ -88,11 +61,10 @@ class RecordHandler:
88
61
  :param wrapper_type: The record model wrapper to use.
89
62
  :return: The record models for the input.
90
63
  """
91
- self.__verify_data_type(records, wrapper_type)
92
64
  return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
93
65
 
94
- def query_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier, value_list: Iterable[FieldValue],
95
- page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
66
+ def query_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
67
+ page_limit: int | None = None) -> list[WrappedType]:
96
68
  """
97
69
  Shorthand for using the data record manager to query for a list of data records by field value
98
70
  and then converting the results into a list of record models.
@@ -100,20 +72,14 @@ class RecordHandler:
100
72
  :param wrapper_type: The record model wrapper to use.
101
73
  :param field: The field to query on.
102
74
  :param value_list: The values of the field to query on.
103
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
104
- only functions if you set a page size or the platform enforces a page size.
105
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
75
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
106
76
  :return: The record models for the queried records.
107
77
  """
108
- criteria: DataRecordPojoPageCriteria | None = None
109
- if page_size is not None:
110
- criteria = DataRecordPojoPageCriteria(page_size=page_size)
111
- return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
78
+ return self.query_models_with_criteria(wrapper_type, field, value_list, None, page_limit)[0]
112
79
 
113
- def query_and_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
114
- value_list: Iterable[FieldValue], page_limit: int | None = None,
115
- page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
116
- -> dict[FieldValue, list[WrappedType]]:
80
+ def query_and_map_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
81
+ page_limit: int | None = None, *, mapping_field: str | None = None) \
82
+ -> dict[Any, list[WrappedType]]:
117
83
  """
118
84
  Shorthand for using query_models to search for records given values on a specific field and then using
119
85
  map_by_field to turn the returned list into a dictionary mapping field values to records.
@@ -121,21 +87,17 @@ class RecordHandler:
121
87
  :param wrapper_type: The record model wrapper to use.
122
88
  :param field: The field to query and map on.
123
89
  :param value_list: The values of the field to query on.
124
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
125
- only functions if you set a page size or the platform enforces a page size.
126
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
90
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
127
91
  :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
128
92
  :return: The record models for the queried records mapped by field values to the records with that value.
129
93
  """
130
94
  if mapping_field is None:
131
95
  mapping_field = field
132
- return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
133
- mapping_field)
96
+ return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
134
97
 
135
- def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
136
- value_list: Iterable[FieldValue], page_limit: int | None = None,
137
- page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
138
- -> dict[FieldValue, WrappedType]:
98
+ def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
99
+ page_limit: int | None = None, *, mapping_field: str | None = None) \
100
+ -> dict[Any, WrappedType]:
139
101
  """
140
102
  Shorthand for using query_models to search for records given values on a specific field and then using
141
103
  map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
@@ -144,19 +106,15 @@ class RecordHandler:
144
106
  :param wrapper_type: The record model wrapper to use.
145
107
  :param field: The field to query and map on.
146
108
  :param value_list: The values of the field to query on.
147
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
148
- only functions if you set a page size or the platform enforces a page size.
149
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
109
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
150
110
  :param mapping_field: If provided, use this field to map against instead of the field that was queried on.
151
111
  :return: The record models for the queried records mapped by field values to the record with that value.
152
112
  """
153
113
  if mapping_field is None:
154
114
  mapping_field = field
155
- return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
156
- mapping_field)
115
+ return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit), mapping_field)
157
116
 
158
- def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
159
- value_list: Iterable[FieldValue],
117
+ def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
160
118
  paging_criteria: DataRecordPojoPageCriteria | None = None,
161
119
  page_limit: int | None = None) \
162
120
  -> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
@@ -169,33 +127,26 @@ class RecordHandler:
169
127
  :param value_list: The values of the field to query on.
170
128
  :param paging_criteria: The paging criteria to start the query with.
171
129
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
172
- possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
173
- enforces a page size.
130
+ possible pages.
174
131
  :return: The record models for the queried records and the final paging criteria.
175
132
  """
176
133
  dt: str = wrapper_type.get_wrapper_data_type_name()
177
- field: str = AliasUtil.to_data_field_name(field)
178
134
  pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
179
135
  pager.max_page = page_limit
180
136
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
181
137
 
182
138
  def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
183
- page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
139
+ page_limit: int | None = None) -> list[WrappedType]:
184
140
  """
185
141
  Shorthand for using the data record manager to query for a list of data records by record ID
186
142
  and then converting the results into a list of record models.
187
143
 
188
144
  :param wrapper_type: The record model wrapper to use.
189
145
  :param ids: The list of record IDs to query.
190
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
191
- only functions if you set a page size or the platform enforces a page size.
192
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
146
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
193
147
  :return: The record models for the queried records.
194
148
  """
195
- criteria: DataRecordPojoPageCriteria | None = None
196
- if page_size is not None:
197
- criteria = DataRecordPojoPageCriteria(page_size=page_size)
198
- return self.query_models_by_id_with_criteria(wrapper_type, ids, criteria, page_limit)[0]
149
+ return self.query_models_by_id_with_criteria(wrapper_type, ids, None, page_limit)[0]
199
150
 
200
151
  def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
201
152
  paging_criteria: DataRecordPojoPageCriteria | None = None,
@@ -209,8 +160,7 @@ class RecordHandler:
209
160
  :param ids: The list of record IDs to query.
210
161
  :param paging_criteria: The paging criteria to start the query with.
211
162
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
212
- possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
213
- enforces a page size.
163
+ possible pages.
214
164
  :return: The record models for the queried records and the final paging criteria.
215
165
  """
216
166
  dt: str = wrapper_type.get_wrapper_data_type_name()
@@ -218,38 +168,16 @@ class RecordHandler:
218
168
  pager.max_page = page_limit
219
169
  return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
220
170
 
221
- def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
222
- page_limit: int | None = None, page_size: int | None = None) \
223
- -> dict[int, WrappedType]:
224
- """
225
- Shorthand for using the data record manager to query for a list of data records by record ID
226
- and then converting the results into a dictionary of record ID to the record model for that ID.
227
-
228
- :param wrapper_type: The record model wrapper to use.
229
- :param ids: The list of record IDs to query.
230
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
231
- only functions if you set a page size or the platform enforces a page size.
232
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
233
- :return: The record models for the queried records mapped in a dictionary by their record ID.
234
- """
235
- return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
236
-
237
- def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
238
- page_size: int | None = None) -> list[WrappedType]:
171
+ def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None) -> list[WrappedType]:
239
172
  """
240
173
  Shorthand for using the data record manager to query for all data records of a given type
241
174
  and then converting the results into a list of record models.
242
175
 
243
176
  :param wrapper_type: The record model wrapper to use.
244
- :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
245
- only functions if you set a page size or the platform enforces a page size.
246
- :param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
177
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
247
178
  :return: The record models for the queried records.
248
179
  """
249
- criteria: DataRecordPojoPageCriteria | None = None
250
- if page_size is not None:
251
- criteria = DataRecordPojoPageCriteria(page_size=page_size)
252
- return self.query_all_models_with_criteria(wrapper_type, criteria, page_limit)[0]
180
+ return self.query_all_models_with_criteria(wrapper_type, None, page_limit)[0]
253
181
 
254
182
  def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
255
183
  paging_criteria: DataRecordPojoPageCriteria | None = None,
@@ -262,8 +190,7 @@ class RecordHandler:
262
190
  :param wrapper_type: The record model wrapper to use.
263
191
  :param paging_criteria: The paging criteria to start the query with.
264
192
  :param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
265
- possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
266
- enforces a page size.
193
+ possible pages.
267
194
  :return: The record models for the queried records and the final paging criteria.
268
195
  """
269
196
  dt: str = wrapper_type.get_wrapper_data_type_name()
@@ -273,7 +200,7 @@ class RecordHandler:
273
200
 
274
201
  def query_models_by_report(self, wrapper_type: type[WrappedType],
275
202
  report_name: str | RawReportTerm | CustomReportCriteria,
276
- filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
203
+ filters: dict[str, Iterable[Any]] | None = None,
277
204
  page_limit: int | None = None,
278
205
  page_size: int | None = None,
279
206
  page_number: int | None = None) -> list[WrappedType]:
@@ -301,11 +228,11 @@ class RecordHandler:
301
228
  :return: The record models for the queried records that matched the given report.
302
229
  """
303
230
  if isinstance(report_name, str):
304
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
305
- page_limit, page_size, page_number)
231
+ results: list[dict[str, Any]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
232
+ page_limit, page_size, page_number)
306
233
  elif isinstance(report_name, RawReportTerm):
307
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
308
- page_limit, page_size, page_number)
234
+ results: list[dict[str, Any]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
235
+ page_limit, page_size, page_number)
309
236
  elif isinstance(report_name, CustomReportCriteria):
310
237
  dt: str = wrapper_type.get_wrapper_data_type_name()
311
238
  # Ensure that the root data type is the one we're looking for.
@@ -316,8 +243,8 @@ class RecordHandler:
316
243
  # Enforce that the given custom report has a record ID column.
317
244
  if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report_name.column_list]):
318
245
  report_name.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
319
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
320
- page_limit, page_size, page_number)
246
+ results: list[dict[str, Any]] = CustomReportUtil.run_custom_report(self.user, report_name, filters,
247
+ page_limit, page_size, page_number)
321
248
  else:
322
249
  raise SapioException("Unrecognized report object.")
323
250
 
@@ -346,8 +273,7 @@ class RecordHandler:
346
273
  """
347
274
  return self.inst_man.add_new_records_of_type(num, wrapper_type)
348
275
 
349
- def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
350
- -> list[WrappedType]:
276
+ def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldMap]) -> list[WrappedType]:
351
277
  """
352
278
  Shorthand for using the instance manager to add new models of the given type, and then initializing all those
353
279
  models with the given fields.
@@ -357,14 +283,13 @@ class RecordHandler:
357
283
  :return: The newly added record models with the provided fields set. The records will be in the same order as
358
284
  the fields in the fields list.
359
285
  """
360
- fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
361
286
  models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
362
287
  for model, field_list in zip(models, fields):
363
288
  model.set_field_values(field_list)
364
289
  return models
365
290
 
366
- def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
367
- id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
291
+ def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
292
+ secondary_identifiers: FieldMap | None = None) -> WrappedType:
368
293
  """
369
294
  Find a unique record that matches the given field values. If no such records exist, add a record model to the
370
295
  cache with the identifying fields set to the desired values. This record will be created in the system when
@@ -387,8 +312,6 @@ class RecordHandler:
387
312
  if secondary_identifiers is None:
388
313
  secondary_identifiers = {}
389
314
 
390
- primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
391
- secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
392
315
  unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
393
316
  secondary_identifiers)
394
317
  # If a unique record matched the identifiers, return it.
@@ -415,7 +338,7 @@ class RecordHandler:
415
338
  dt: str = wrapper_type.get_wrapper_data_type_name()
416
339
  return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
417
340
 
418
- def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
341
+ def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldMap]) \
419
342
  -> list[WrappedType]:
420
343
  """
421
344
  Shorthand for creating new records via the data record manager with field data to initialize the records with
@@ -429,12 +352,10 @@ class RecordHandler:
429
352
  :return: The newly created record models.
430
353
  """
431
354
  dt: str = wrapper_type.get_wrapper_data_type_name()
432
- fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
433
355
  return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
434
356
 
435
- def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
436
- id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
437
- -> WrappedType:
357
+ def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
358
+ secondary_identifiers: FieldMap | None = None) -> WrappedType:
438
359
  """
439
360
  Find a unique record that matches the given field values. If no such records exist, create one with the
440
361
  identifying fields set to the desired values. If more than one record with the identifying values exists,
@@ -458,8 +379,6 @@ class RecordHandler:
458
379
  if secondary_identifiers is None:
459
380
  secondary_identifiers = {}
460
381
 
461
- primary_identifier: str = AliasUtil.to_data_field_name(primary_identifier)
462
- secondary_identifiers: FieldMap = AliasUtil.to_data_field_names_dict(secondary_identifiers)
463
382
  unique_record: WrappedType | None = self.__find_model(wrapper_type, primary_identifier, id_value,
464
383
  secondary_identifiers)
465
384
  # If a unique record matched the identifiers, return it.
@@ -578,7 +497,7 @@ class RecordHandler:
578
497
 
579
498
  @staticmethod
580
499
  def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
581
- -> dict[WrappedType, RecordModel]:
500
+ -> dict[WrappedType, list[RecordModel]]:
582
501
  """
583
502
  Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
584
503
  If two records share the same child, an exception will be thrown. The children must already be loaded.
@@ -619,7 +538,7 @@ class RecordHandler:
619
538
  return by_children
620
539
 
621
540
  @staticmethod
622
- def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
541
+ def map_to_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
623
542
  side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
624
543
  """
625
544
  Map a list of record models to their forward side link. The forward side link must already be loaded.
@@ -630,14 +549,13 @@ class RecordHandler:
630
549
  :return: A dict[ModelType, SlideLink]. If an input model doesn't have a forward side link of the given type,
631
550
  then it will map to None.
632
551
  """
633
- field_name: str = AliasUtil.to_data_field_name(field_name)
634
552
  return_dict: dict[WrappedRecordModel, WrappedType] = {}
635
553
  for model in models:
636
554
  return_dict[model] = model.get_forward_side_link(field_name, side_link_type)
637
555
  return return_dict
638
556
 
639
557
  @staticmethod
640
- def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
558
+ def map_by_forward_side_links(models: Iterable[WrappedRecordModel], field_name: str,
641
559
  side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
642
560
  """
643
561
  Take a list of record models and map them by their forward side link. Essentially an inversion of
@@ -650,7 +568,6 @@ class RecordHandler:
650
568
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have a forward side link of the given type
651
569
  pointing to it, then it will not be in the resulting dictionary.
652
570
  """
653
- field_name: str = AliasUtil.to_data_field_name(field_name)
654
571
  to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
655
572
  .map_to_forward_side_link(models, field_name, side_link_type)
656
573
  by_side_link: dict[WrappedType, list[WrappedRecordModel]] = {}
@@ -661,7 +578,7 @@ class RecordHandler:
661
578
  return by_side_link
662
579
 
663
580
  @staticmethod
664
- def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
581
+ def map_by_forward_side_link(models: Iterable[WrappedRecordModel], field_name: str,
665
582
  side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
666
583
  """
667
584
  Take a list of record models and map them by their forward side link. Essentially an inversion of
@@ -674,7 +591,6 @@ class RecordHandler:
674
591
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a forward side link of the given type
675
592
  pointing to it, then it will not be in the resulting dictionary.
676
593
  """
677
- field_name: str = AliasUtil.to_data_field_name(field_name)
678
594
  to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
679
595
  .map_to_forward_side_link(models, field_name, side_link_type)
680
596
  by_side_link: dict[WrappedType, WrappedRecordModel] = {}
@@ -688,7 +604,7 @@ class RecordHandler:
688
604
  return by_side_link
689
605
 
690
606
  @staticmethod
691
- def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
607
+ def map_to_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
692
608
  side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, list[WrappedType]]:
693
609
  """
694
610
  Map a list of record models to a list reverse side links of a given type. The reverse side links must already
@@ -701,14 +617,13 @@ class RecordHandler:
701
617
  :return: A dict[ModelType, list[SideLink]]. If an input model doesn't have reverse side links of the given type,
702
618
  then it will map to an empty list.
703
619
  """
704
- field_name: str = AliasUtil.to_data_field_name(field_name)
705
620
  return_dict: dict[WrappedRecordModel, list[WrappedType]] = {}
706
621
  for model in models:
707
622
  return_dict[model] = model.get_reverse_side_link(field_name, side_link_type)
708
623
  return return_dict
709
624
 
710
625
  @staticmethod
711
- def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
626
+ def map_to_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: str,
712
627
  side_link_type: type[WrappedType]) -> dict[WrappedRecordModel, WrappedType]:
713
628
  """
714
629
  Map a list of record models to the reverse side link of a given type. If a given record has more than one
@@ -721,7 +636,6 @@ class RecordHandler:
721
636
  :return: A dict[ModelType, SideLink]. If an input model doesn't have reverse side links of the given type,
722
637
  then it will map to None.
723
638
  """
724
- field_name: str = AliasUtil.to_data_field_name(field_name)
725
639
  return_dict: dict[WrappedRecordModel, WrappedType] = {}
726
640
  for model in models:
727
641
  links: list[WrappedType] = model.get_reverse_side_link(field_name, side_link_type)
@@ -732,7 +646,7 @@ class RecordHandler:
732
646
  return return_dict
733
647
 
734
648
  @staticmethod
735
- def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
649
+ def map_by_reverse_side_links(models: Iterable[WrappedRecordModel], field_name: str,
736
650
  side_link_type: type[WrappedType]) -> dict[WrappedType, list[WrappedRecordModel]]:
737
651
  """
738
652
  Take a list of record models and map them by their reverse side links. Essentially an inversion of
@@ -746,7 +660,6 @@ class RecordHandler:
746
660
  :return: A dict[SideLink, list[ModelType]]. If an input model doesn't have reverse side links of the given type
747
661
  pointing to it, then it will not be in the resulting dictionary.
748
662
  """
749
- field_name: str = AliasUtil.to_data_field_name(field_name)
750
663
  to_side_links: dict[WrappedRecordModel, list[WrappedType]] = RecordHandler\
751
664
  .map_to_reverse_side_links(models, field_name, side_link_type)
752
665
  by_side_links: dict[WrappedType, list[WrappedRecordModel]] = {}
@@ -756,7 +669,7 @@ class RecordHandler:
756
669
  return by_side_links
757
670
 
758
671
  @staticmethod
759
- def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: FieldIdentifier,
672
+ def map_by_reverse_side_link(models: Iterable[WrappedRecordModel], field_name: str,
760
673
  side_link_type: type[WrappedType]) -> dict[WrappedType, WrappedRecordModel]:
761
674
  """
762
675
  Take a list of record models and map them by their reverse side link. Essentially an inversion of
@@ -770,7 +683,6 @@ class RecordHandler:
770
683
  :return: A dict[SideLink, ModelType]. If an input model doesn't have a reverse side link of the given type
771
684
  pointing to it, then it will not be in the resulting dictionary.
772
685
  """
773
- field_name: str = AliasUtil.to_data_field_name(field_name)
774
686
  to_side_link: dict[WrappedRecordModel, WrappedType] = RecordHandler\
775
687
  .map_to_reverse_side_link(models, field_name, side_link_type)
776
688
  by_side_link: dict[WrappedType, WrappedRecordModel] = {}
@@ -797,8 +709,7 @@ class RecordHandler:
797
709
  return ret_dict
798
710
 
799
711
  @staticmethod
800
- def map_by_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
801
- -> dict[FieldValue, list[SapioRecord]]:
712
+ def map_by_field(models: Iterable[SapioRecord], field_name: str) -> dict[Any, list[SapioRecord]]:
802
713
  """
803
714
  Map the given records by one of their fields. If any two records share the same field value, they'll appear in
804
715
  the same value list.
@@ -807,16 +718,14 @@ class RecordHandler:
807
718
  :param field_name: The field name to map against.
808
719
  :return: A dict mapping field values to the records with that value.
809
720
  """
810
- field_name: str = AliasUtil.to_data_field_name(field_name)
811
- ret_dict: dict[FieldValue, list[SapioRecord]] = {}
721
+ ret_dict: dict[Any, list[SapioRecord]] = {}
812
722
  for model in models:
813
- val: FieldValue = model.get_field_value(field_name)
723
+ val: Any = model.get_field_value(field_name)
814
724
  ret_dict.setdefault(val, []).append(model)
815
725
  return ret_dict
816
726
 
817
727
  @staticmethod
818
- def map_by_unique_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) \
819
- -> dict[FieldValue, SapioRecord]:
728
+ def map_by_unique_field(models: Iterable[SapioRecord], field_name: str) -> dict[Any, SapioRecord]:
820
729
  """
821
730
  Uniquely map the given records by one of their fields. If any two records share the same field value, throws
822
731
  an exception.
@@ -825,17 +734,16 @@ class RecordHandler:
825
734
  :param field_name: The field name to map against.
826
735
  :return: A dict mapping field values to the record with that value.
827
736
  """
828
- field_name: str = AliasUtil.to_data_field_name(field_name)
829
- ret_dict: dict[FieldValue, SapioRecord] = {}
737
+ ret_dict: dict[Any, SapioRecord] = {}
830
738
  for model in models:
831
- val: FieldValue = model.get_field_value(field_name)
739
+ val: Any = model.get_field_value(field_name)
832
740
  if val in ret_dict:
833
741
  raise SapioException(f"Value {val} encountered more than once in models list.")
834
742
  ret_dict.update({val: model})
835
743
  return ret_dict
836
744
 
837
745
  @staticmethod
838
- def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
746
+ def sum_of_field(models: Iterable[SapioRecord], field_name: str) -> float:
839
747
  """
840
748
  Sum up the numeric value of a given field across all input models. Excepts that all given models have a value.
841
749
  If the field is an integer field, the value will be converted to a float.
@@ -844,14 +752,13 @@ class RecordHandler:
844
752
  :param field_name: The name of the numeric field to sum.
845
753
  :return: The sum of the field values for the collection of models.
846
754
  """
847
- field_name: str = AliasUtil.to_data_field_name(field_name)
848
755
  field_sum: float = 0
849
756
  for model in models:
850
757
  field_sum += float(model.get_field_value(field_name))
851
758
  return field_sum
852
759
 
853
760
  @staticmethod
854
- def mean_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
761
+ def mean_of_field(models: Iterable[SapioRecord], field_name: str) -> float:
855
762
  """
856
763
  Calculate the mean of the numeric value of a given field across all input models. Excepts that all given models
857
764
  have a value. If the field is an integer field, the value will be converted to a float.
@@ -892,8 +799,8 @@ class RecordHandler:
892
799
  return oldest
893
800
 
894
801
  @staticmethod
895
- def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
896
- existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
802
+ def values_to_field_maps(field_name: str, values: Iterable[Any], existing_fields: list[FieldMap] | None = None) \
803
+ -> list[FieldMap]:
897
804
  """
898
805
  Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
899
806
 
@@ -904,8 +811,6 @@ class RecordHandler:
904
811
  :return: A fields map list that contains the given values mapped by the given field name.
905
812
  """
906
813
  # Update the existing fields map list if one is given.
907
- field_name: str = AliasUtil.to_data_field_name(field_name)
908
- existing_fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(existing_fields)
909
814
  if existing_fields:
910
815
  values = list(values)
911
816
  # The number of new values must match the length of the existing fields list.
@@ -926,6 +831,8 @@ class RecordHandler:
926
831
  path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
927
832
  relationship path must already be loaded.
928
833
 
834
+ Currently, the relationship path may only contain parent/child nodes.
835
+
929
836
  :param models: A list of record models.
930
837
  :param path: The relationship path to follow.
931
838
  :param wrapper_type: The record model wrapper to use.
@@ -936,44 +843,15 @@ class RecordHandler:
936
843
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
937
844
  path: list[RelationshipNode] = path.path
938
845
  for model in models:
939
- current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
846
+ current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
940
847
  for node in path:
941
- data_type: str = node.data_type_name
942
- direction: RelationshipNodeType = node.direction
848
+ direction = node.direction
943
849
  if current is None:
944
850
  break
945
851
  if direction == RelationshipNodeType.CHILD:
946
- current = current.get_child_of_type(data_type)
852
+ current = current.get_child_of_type(node.data_type_name)
947
853
  elif direction == RelationshipNodeType.PARENT:
948
- current = current.get_parent_of_type(data_type)
949
- elif direction == RelationshipNodeType.ANCESTOR:
950
- ancestors: list[PyRecordModel] = list(self.an_man.get_ancestors_of_type(current, data_type))
951
- if not ancestors:
952
- current = None
953
- elif len(ancestors) > 1:
954
- raise SapioException(f"Hierarchy contains multiple ancestors of type {data_type}.")
955
- else:
956
- current = ancestors[0]
957
- elif direction == RelationshipNodeType.DESCENDANT:
958
- descendants: list[PyRecordModel] = list(self.an_man.get_descendant_of_type(current, data_type))
959
- if not descendants:
960
- current = None
961
- elif len(descendants) > 1:
962
- raise SapioException(f"Hierarchy contains multiple descendants of type {data_type}.")
963
- else:
964
- current = descendants[0]
965
- elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
966
- current = current.get_forward_side_link(node.data_field_name)
967
- elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
968
- field_name: str = node.data_field_name
969
- reverse_links: list[PyRecordModel] = current.get_reverse_side_link(field_name, data_type)
970
- if not reverse_links:
971
- current = None
972
- elif len(reverse_links) > 1:
973
- raise SapioException(f"Hierarchy contains multiple reverse links of type {data_type} on field "
974
- f"{field_name}.")
975
- else:
976
- current = reverse_links[0]
854
+ current = current.get_parent_of_type(node.data_type_name)
977
855
  else:
978
856
  raise SapioException("Unsupported path direction.")
979
857
  ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
@@ -986,6 +864,8 @@ class RecordHandler:
986
864
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
987
865
  relationship path must already be loaded.
988
866
 
867
+ Currently, the relationship path may only contain parent/child nodes.
868
+
989
869
  :param models: A list of record models.
990
870
  :param path: The relationship path to follow.
991
871
  :param wrapper_type: The record model wrapper to use.
@@ -1000,23 +880,14 @@ class RecordHandler:
1000
880
  next_search: set[PyRecordModel] = set()
1001
881
  # Exhaust the records at each step in the path, then use those records for the next step.
1002
882
  for node in path:
1003
- data_type: str = node.data_type_name
1004
- direction: RelationshipNodeType = node.direction
883
+ direction = node.direction
1005
884
  if len(current_search) == 0:
1006
885
  break
1007
886
  for search in current_search:
1008
887
  if direction == RelationshipNodeType.CHILD:
1009
- next_search.update(search.get_children_of_type(data_type))
888
+ next_search.update(search.get_children_of_type(node.data_type_name))
1010
889
  elif direction == RelationshipNodeType.PARENT:
1011
- next_search.update(search.get_parents_of_type(data_type))
1012
- elif direction == RelationshipNodeType.ANCESTOR:
1013
- next_search.update(self.an_man.get_ancestors_of_type(search, data_type))
1014
- elif direction == RelationshipNodeType.DESCENDANT:
1015
- next_search.update(self.an_man.get_descendant_of_type(search, data_type))
1016
- elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
1017
- next_search.add(search.get_forward_side_link(node.data_field_name))
1018
- elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
1019
- next_search.update(search.get_reverse_side_link(node.data_field_name, data_type))
890
+ next_search.update(search.get_parents_of_type(node.data_type_name))
1020
891
  else:
1021
892
  raise SapioException("Unsupported path direction.")
1022
893
  current_search = next_search
@@ -1037,6 +908,8 @@ class RecordHandler:
1037
908
  relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
1038
909
  together into a single sample).
1039
910
 
911
+ Currently, the relationship path may only contain parent/child nodes.
912
+
1040
913
  :param models: A list of record models.
1041
914
  :param path: The relationship path to follow.
1042
915
  :param wrapper_type: The record model wrapper to use.
@@ -1049,29 +922,20 @@ class RecordHandler:
1049
922
  for model in models:
1050
923
  current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
1051
924
  for node in path:
1052
- data_type: str = node.data_type_name
1053
- direction: RelationshipNodeType = node.direction
925
+ direction = node.direction
1054
926
  if len(current) == 0:
1055
927
  break
1056
928
  if direction == RelationshipNodeType.CHILD:
1057
- current = current[0].get_children_of_type(data_type)
929
+ current = current[0].get_children_of_type(node.data_type_name)
1058
930
  elif direction == RelationshipNodeType.PARENT:
1059
- current = current[0].get_parents_of_type(data_type)
1060
- elif direction == RelationshipNodeType.ANCESTOR:
1061
- current = list(self.an_man.get_ancestors_of_type(current[0], data_type))
1062
- elif direction == RelationshipNodeType.DESCENDANT:
1063
- current = list(self.an_man.get_descendant_of_type(current[0], data_type))
1064
- elif direction == RelationshipNodeType.FORWARD_SIDE_LINK:
1065
- current = [current[0].get_forward_side_link(node.data_field_name)]
1066
- elif direction == RelationshipNodeType.REVERSE_SIDE_LINK:
1067
- current = current[0].get_reverse_side_link(node.data_field_name, data_type)
931
+ current = current[0].get_parents_of_type(node.data_type_name)
1068
932
  else:
1069
933
  raise SapioException("Unsupported path direction.")
1070
934
  ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
1071
935
  return ret_dict
1072
936
 
1073
- def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
1074
- secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
937
+ def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: Any,
938
+ secondary_identifiers: FieldMap | None = None) -> WrappedType | None:
1075
939
  """
1076
940
  Find a record from the system that matches the given field values. The primary identifier and value is used
1077
941
  to query for the record, then the secondary identifiers may be optionally provided to further filter the
@@ -1095,18 +959,3 @@ class RecordHandler:
1095
959
  f"encountered in system that matches all provided identifiers.")
1096
960
  unique_record = result
1097
961
  return unique_record
1098
-
1099
- @staticmethod
1100
- def __verify_data_type(records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> None:
1101
- """
1102
- Throw an exception if the data type of the given records and wrapper don't match.
1103
- """
1104
- model_type: str = wrapper_type.get_wrapper_data_type_name()
1105
- for record in records:
1106
- record_type: str = record.data_type_name
1107
- # Account for ELN data type records.
1108
- if ElnBaseDataType.is_eln_type(record_type):
1109
- record_type = ElnBaseDataType.get_base_type(record_type).data_type_name
1110
- if record_type != model_type:
1111
- raise SapioException(f"Data record of type {record_type} cannot be wrapped by the record model wrapper "
1112
- f"of type {model_type}")