sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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 (52) hide show
  1. sapiopycommons/callbacks/__init__.py +0 -0
  2. sapiopycommons/callbacks/callback_util.py +2041 -0
  3. sapiopycommons/callbacks/field_builder.py +545 -0
  4. sapiopycommons/chem/IndigoMolecules.py +52 -5
  5. sapiopycommons/chem/Molecules.py +114 -30
  6. sapiopycommons/customreport/__init__.py +0 -0
  7. sapiopycommons/customreport/column_builder.py +60 -0
  8. sapiopycommons/customreport/custom_report_builder.py +137 -0
  9. sapiopycommons/customreport/term_builder.py +315 -0
  10. sapiopycommons/datatype/attachment_util.py +17 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +390 -90
  14. sapiopycommons/eln/experiment_report_util.py +649 -0
  15. sapiopycommons/eln/plate_designer.py +152 -0
  16. sapiopycommons/files/complex_data_loader.py +31 -0
  17. sapiopycommons/files/file_bridge.py +153 -25
  18. sapiopycommons/files/file_bridge_handler.py +555 -0
  19. sapiopycommons/files/file_data_handler.py +633 -0
  20. sapiopycommons/files/file_util.py +270 -158
  21. sapiopycommons/files/file_validator.py +569 -0
  22. sapiopycommons/files/file_writer.py +377 -0
  23. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  24. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  25. sapiopycommons/general/accession_service.py +375 -0
  26. sapiopycommons/general/aliases.py +259 -18
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +252 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +85 -18
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +97 -7
  35. sapiopycommons/multimodal/multimodal.py +146 -0
  36. sapiopycommons/multimodal/multimodal_data.py +490 -0
  37. sapiopycommons/processtracking/__init__.py +0 -0
  38. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  39. sapiopycommons/processtracking/endpoints.py +192 -0
  40. sapiopycommons/recordmodel/record_handler.py +653 -149
  41. sapiopycommons/rules/eln_rule_handler.py +89 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +89 -12
  43. sapiopycommons/sftpconnect/__init__.py +0 -0
  44. sapiopycommons/sftpconnect/sftp_builder.py +70 -0
  45. sapiopycommons/webhook/webhook_context.py +39 -0
  46. sapiopycommons/webhook/webhook_handlers.py +617 -69
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
  49. sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
  50. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+ from weakref import WeakValueDictionary
6
+
7
+ from sapiopylib.rest.User import SapioUser
8
+
9
+ _STR_JAVA_TYPE = "java.lang.String"
10
+ _INT_JAVA_TYPE = "java.lang.Integer"
11
+ _BOOL_JAVA_TYPE = "java.lang.Boolean"
12
+
13
+
14
+ class AbstractAccessionServiceOperator(ABC):
15
+ """
16
+ Abstract class to define an accession service operator.
17
+ The default one in sapiopycommon only includes the out of box operators.
18
+ More can be added via java plugins for global operators.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def op_class_name(self) -> str:
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def op_param_value_list(self) -> list[Any] | None:
29
+ pass
30
+
31
+ @property
32
+ @abstractmethod
33
+ def op_param_class_name_list(self) -> list[str] | None:
34
+ pass
35
+
36
+ @property
37
+ @abstractmethod
38
+ def default_accessor_name(self) -> str:
39
+ pass
40
+
41
+
42
+ class AccessionWithPrefixSuffix(AbstractAccessionServiceOperator):
43
+ """
44
+ Local operator for accessioning prefix and suffix format.
45
+ """
46
+ _prefix: str | None
47
+ _suffix: str | None
48
+ _num_of_digits: int | None
49
+ _start_num: int
50
+ _strict_mode: bool
51
+
52
+ @property
53
+ def prefix(self):
54
+ return self._prefix
55
+
56
+ @property
57
+ def suffix(self):
58
+ return self._suffix
59
+
60
+ @property
61
+ def num_of_digits(self):
62
+ return self._num_of_digits
63
+
64
+ @property
65
+ def start_num(self):
66
+ return self._start_num
67
+
68
+ @property
69
+ def strict_mode(self):
70
+ return self._strict_mode
71
+
72
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
73
+ start_num: int = 1, strict_mode: bool = False):
74
+ if prefix is None:
75
+ prefix = ""
76
+ if suffix is None:
77
+ suffix = ""
78
+ self._prefix = prefix
79
+ self._suffix = suffix
80
+ self._num_of_digits = num_of_digits
81
+ self._start_num = start_num
82
+ self._strict_mode = strict_mode
83
+
84
+ @property
85
+ def op_param_value_list(self):
86
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
87
+
88
+ @property
89
+ def op_param_class_name_list(self):
90
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
91
+
92
+ @property
93
+ def op_class_name(self):
94
+ return "com.velox.accessionservice.operators.AccessionWithPrefixSuffix"
95
+
96
+ @property
97
+ def default_accessor_name(self):
98
+ return "PREFIX_AND_SUFFIX" + "(" + self.prefix + "," + self.suffix + ")";
99
+
100
+
101
+ class AccessionGlobalPrefixSuffix(AbstractAccessionServiceOperator):
102
+ """
103
+ Global operator for accessioning prefix and suffix format.
104
+ """
105
+ _prefix: str | None
106
+ _suffix: str | None
107
+ _num_of_digits: int | None
108
+ _start_num: int
109
+ _strict_mode: bool
110
+
111
+ @property
112
+ def prefix(self):
113
+ return self._prefix
114
+
115
+ @property
116
+ def suffix(self):
117
+ return self._suffix
118
+
119
+ @property
120
+ def num_of_digits(self):
121
+ return self._num_of_digits
122
+
123
+ @property
124
+ def start_num(self):
125
+ return self._start_num
126
+
127
+ @property
128
+ def strict_mode(self):
129
+ return self._strict_mode
130
+
131
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
132
+ start_num: int = 1, strict_mode: bool = False):
133
+ if prefix is None:
134
+ prefix = ""
135
+ if suffix is None:
136
+ suffix = ""
137
+ self._prefix = prefix
138
+ self._suffix = suffix
139
+ self._num_of_digits = num_of_digits
140
+ self._start_num = start_num
141
+ self._strict_mode = strict_mode
142
+
143
+ @property
144
+ def op_param_value_list(self):
145
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
146
+
147
+ @property
148
+ def op_param_class_name_list(self):
149
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
150
+
151
+ @property
152
+ def op_class_name(self):
153
+ return "com.velox.accessionservice.operators.sapio.AccessionGlobalPrefixSuffix"
154
+
155
+ @property
156
+ def default_accessor_name(self):
157
+ return "PREFIX_AND_SUFFIX" + "(" + self._prefix + "," + self._suffix + ")"
158
+
159
+
160
+ class AccessionNextBarcode(AbstractAccessionServiceOperator):
161
+ """
162
+ From Java description:
163
+ This will start accessioning at the getNextBarcode() when there's no system preference to be backward compatible.
164
+ However, once it completes setting the first ID, it will start increment by its own preference and disregards getNextBarcode().
165
+
166
+ Recommend using AccessionServiceBasicManager to accession next barcode.
167
+ To avoid ambiguity in preference cache.
168
+
169
+ This should not be used unless we are using something legacy such as plate mapping template record creation
170
+ (Note: not 3D plating, I'm talking about the older aliquoter).
171
+ """
172
+
173
+ @property
174
+ def op_param_value_list(self):
175
+ return []
176
+
177
+ @property
178
+ def op_param_class_name_list(self):
179
+ return []
180
+
181
+ @property
182
+ def op_class_name(self):
183
+ return "com.velox.accessionservice.operators.sapio.AccessionNextBarcode"
184
+
185
+ @property
186
+ def default_accessor_name(self):
187
+ return "Barcode"
188
+
189
+
190
+ class AccessionRequestId(AbstractAccessionServiceOperator):
191
+ """
192
+ This class implements the accessioning operator for com.velox.sapioutils.shared.managers.DataRecordUtilManager.getNextRequestId()
193
+ and getNextRequestId(int numberOfCharacters).
194
+
195
+ Operation: For 4 characters start with A001, increment by 1 until A999. Then We use B001.
196
+ After Z999 we start with AA01 until we get to AA99, etc.
197
+
198
+ Exception: Skips I and O to prevent confusions with 1 and 0 when incrementing letters.
199
+
200
+ Properties:
201
+ numberOfCharacters: Number of characters maximum in the request ID.
202
+ accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
203
+ """
204
+ _num_of_characters: int
205
+ _accessor_name: str
206
+
207
+ @property
208
+ def num_of_characters(self):
209
+ return self._num_of_characters
210
+
211
+ @property
212
+ def accessor_name(self):
213
+ return self._accessor_name
214
+
215
+ def __init__(self, num_of_characters: int = 4, accessor_name: str = None):
216
+ self._num_of_characters = num_of_characters
217
+ if not accessor_name:
218
+ accessor_name = self.default_accessor_name
219
+ self._accessor_name = accessor_name
220
+
221
+ @property
222
+ def op_class_name(self):
223
+ return "com.velox.accessionservice.operators.sapio.AccessionRequestId"
224
+
225
+ @property
226
+ def op_param_value_list(self):
227
+ return [self._num_of_characters, self._accessor_name]
228
+
229
+ @property
230
+ def op_param_class_name_list(self):
231
+ return [_INT_JAVA_TYPE, _STR_JAVA_TYPE]
232
+
233
+ @property
234
+ def default_accessor_name(self):
235
+ return "SapioNextRequestIdMap"
236
+
237
+
238
+ class AccessionServiceDescriptor:
239
+ """
240
+ Describes a single accession service's accessioning request
241
+
242
+ Attributes:
243
+ opClassName: The accession service operator class name as in Java
244
+ opParamValueList: Ordered list of parameter values to construct the accession service operator.
245
+ opParamClassNameList: Ordered list of FQCN of java classes in order of parameter value list.
246
+ dataTypeName: The data type to accession. Should be blank if opClassName resolves to a global operator.
247
+ dataFieldName: The data field to accession. Should be blank if opClassName resolves to a global operator.
248
+ accessorName: The accessor cache name to be used for accessioning.
249
+ numIds: The number of IDs to accession.
250
+ """
251
+ op: AbstractAccessionServiceOperator
252
+ dataTypeName: str | None
253
+ dataFieldName: str | None
254
+ accessorName: str
255
+ numIds: int
256
+
257
+ def __init__(self, accessor_name: str, op: AbstractAccessionServiceOperator, num_ids: int,
258
+ data_type_name: str | None, data_field_name: str | None):
259
+ self.accessorName = accessor_name
260
+ self.op = op
261
+ self.dataTypeName = data_type_name
262
+ self.dataFieldName = data_field_name
263
+ self.numIds = num_ids
264
+
265
+ def to_json(self):
266
+ return {
267
+ "opClassName": self.op.op_class_name,
268
+ "opParamValueList": self.op.op_param_value_list,
269
+ "opParamClassNameList": self.op.op_param_class_name_list,
270
+ "accessorName": self.accessorName,
271
+ "numIds": self.numIds,
272
+ "dataTypeName": self.dataTypeName,
273
+ "dataFieldName": self.dataFieldName
274
+ }
275
+
276
+
277
+ class AccessionService:
278
+ """
279
+ Provides Sapio Foundations Accession Service functionalities.
280
+ """
281
+ _user: SapioUser
282
+
283
+ __instances: WeakValueDictionary[SapioUser, AccessionService] = WeakValueDictionary()
284
+ __initialized: bool
285
+
286
+ @property
287
+ def user(self) -> SapioUser:
288
+ return self._user
289
+
290
+ def __new__(cls, user: SapioUser):
291
+ """
292
+ Observes singleton pattern per record model manager object.
293
+
294
+ :param user: The user that will make the webservice request to the application.
295
+ """
296
+ obj = cls.__instances.get(user)
297
+ if not obj:
298
+ obj = object.__new__(cls)
299
+ obj.__initialized = False
300
+ cls.__instances[user] = obj
301
+ return obj
302
+
303
+ def __init__(self, user: SapioUser):
304
+ if self.__initialized:
305
+ return
306
+ self._user = user
307
+ self.__initialized = True
308
+
309
+ def accession_with_config(self, data_type_name: str, data_field_name: str, num_ids: int) -> list[str]:
310
+ """
311
+ Accession with Configuration Manager => Accession Service configuration (This is not visible to regular users in SaaS)
312
+ """
313
+ payload = {
314
+ "dataTypeName": data_type_name,
315
+ "dataFieldName": data_field_name,
316
+ "numIds": num_ids
317
+ }
318
+ response = self.user.plugin_post("accessionservice/accession_with_config", payload=payload)
319
+ self.user.raise_for_status(response)
320
+ return list(response.json())
321
+
322
+ def accession_in_batch(self, descriptor: AccessionServiceDescriptor) -> list[str]:
323
+ """
324
+ This is the most flexible way to make use of accession service: directly via a descriptor object.
325
+ """
326
+ payload = descriptor.to_json()
327
+ response = self.user.plugin_post("accessionservice/accession", payload=payload)
328
+ self.user.raise_for_status(response)
329
+ return list(response.json())
330
+
331
+ def accession_next_request_id_list(self, num_of_characters: int, num_ids: int) -> list[str]:
332
+ """
333
+ Accession Request ID by old LIMS format. This is usually deprecated today.
334
+ :param num_of_characters: Number of characters minimum in request ID.
335
+ :param num_ids: Number of request IDs to accession.
336
+ """
337
+ op = AccessionRequestId(num_of_characters)
338
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
339
+ return self.accession_in_batch(descriptor)
340
+
341
+ def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
342
+ suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
343
+ """
344
+ Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
345
+ :param data_type_name: The datatype name to look for max ID
346
+ :param data_field_name: The datafield name to look for max ID
347
+ :param num_ids: The number of IDs to accession.
348
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
349
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
350
+ :param num_digits: None if unlimited with no leading zeros.
351
+ :param start_num The number to begin accessioning if this is the first time.
352
+ :return:
353
+ """
354
+ op = AccessionWithPrefixSuffix(prefix, suffix, num_digits, start_num)
355
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, data_type_name, data_field_name)
356
+ return self.accession_in_batch(descriptor)
357
+
358
+ def get_global_affixed_id_in_batch(
359
+ self, num_ids: int, prefix: str | None, suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
360
+ """
361
+ Get the next numOfIds affixed IDs using system preference cache that's maximum across all datatype and datafields and maximal for the format.
362
+ This method allows users to customize a start number instead of always starting at 1.
363
+ :param num_ids: The number of IDs to accession.
364
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
365
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
366
+ :param num_digits: None if unlimited with no leading zeros.
367
+ :param start_num The number to begin accessioning if this is the first time.
368
+ """
369
+ op: AbstractAccessionServiceOperator
370
+ if not prefix and not suffix:
371
+ op = AccessionNextBarcode()
372
+ else:
373
+ op = AccessionGlobalPrefixSuffix(prefix, suffix, num_digits, start_num)
374
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
375
+ return self.accession_in_batch(descriptor)
@@ -1,14 +1,51 @@
1
1
  from collections.abc import Iterable
2
- from typing import Any
2
+ from typing import Any, TypeAlias
3
3
 
4
+ from sapiopylib.rest.User import SapioUser
4
5
  from sapiopylib.rest.pojo.DataRecord import DataRecord
5
- from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
6
- from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
6
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType, AbstractVeloxFieldDefinition
7
+ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
8
+ from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
9
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
10
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
11
+ from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
12
+ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModel
13
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
7
14
 
8
- # Different forms that a record model could take.
9
- RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
10
- # A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType).
11
- SapioRecord = DataRecord | RecordModel
15
+ from sapiopycommons.general.exceptions import SapioException
16
+
17
+ FieldValue: TypeAlias = int | float | str | bool | None
18
+ """Allowable values for fields in the system."""
19
+ RecordModel: TypeAlias = PyRecordModel | AbstractRecordModel | WrappedRecordModel
20
+ """Different forms that a record model could take."""
21
+ SapioRecord: TypeAlias = DataRecord | RecordModel
22
+ """A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
23
+ RecordIdentifier: TypeAlias = SapioRecord | int
24
+ """A RecordIdentifier is either a record type or an integer for the record's record ID."""
25
+ DataTypeIdentifier: TypeAlias = SapioRecord | type[WrappedType] | str
26
+ """A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
27
+ FieldIdentifier: TypeAlias = AbstractVeloxFieldDefinition | WrapperField | str | tuple[str, FieldType]
28
+ """A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
29
+ and field type."""
30
+ FieldIdentifierKey: TypeAlias = WrapperField | str
31
+ """A FieldIdentifierKey is a FieldIdentifier, except it can't be a tuple, s tuples can't be used as keys in
32
+ dictionaries.."""
33
+ HasFieldWrappers: TypeAlias = type[WrappedType] | WrappedRecordModel
34
+ """An identifier for classes that have wrapper fields."""
35
+ ExperimentIdentifier: TypeAlias = ElnExperimentProtocol | ElnExperiment | int
36
+ """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for the experiment's notebook
37
+ ID."""
38
+ ExperimentEntryIdentifier: TypeAlias = ElnEntryStep | ExperimentEntry | int
39
+ """An ExperimentEntryIdentifier is either an ELN entry step, experiment entry, or an integer for the entry's ID."""
40
+ FieldMap: TypeAlias = dict[str, FieldValue]
41
+ """A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
42
+ any random dict in a webhook from one which is explicitly used for record fields."""
43
+ FieldIdentifierMap: TypeAlias = dict[FieldIdentifierKey, FieldValue]
44
+ """A field identifier map is the same thing as a field map, except the keys can be field identifiers instead
45
+ of just strings. Note that although one of the allowed field identifiers is a tuple, you can't use tuples as
46
+ keys in a dictionary."""
47
+ UserIdentifier: TypeAlias = SapioWebhookContext | SapioUser
48
+ """An identifier for classes from which a user object can be used for sending requests."""
12
49
 
13
50
 
14
51
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
@@ -16,7 +53,8 @@ class AliasUtil:
16
53
  @staticmethod
17
54
  def to_data_record(record: SapioRecord) -> DataRecord:
18
55
  """
19
- Convert a single DataRecord, PyRecordModel, or WrappedRecordModel to just a DataRecord
56
+ Convert a single DataRecord, PyRecordModel, or WrappedRecordModel to just a DataRecord.
57
+
20
58
  :return: The DataRecord of the input SapioRecord.
21
59
  """
22
60
  return record if isinstance(record, DataRecord) else record.get_data_record()
@@ -26,30 +64,233 @@ class AliasUtil:
26
64
  """
27
65
  Convert a list of variables that could either be DataRecords, PyRecordModels,
28
66
  or WrappedRecordModels to just DataRecords.
67
+
29
68
  :return: A list of DataRecords for the input records.
30
69
  """
31
70
  return [(x if isinstance(x, DataRecord) else x.get_data_record()) for x in records]
32
71
 
33
72
  @staticmethod
34
- def to_record_ids(records: Iterable[int | SapioRecord]) -> list[int]:
73
+ def to_record_ids(records: Iterable[RecordIdentifier]) -> list[int]:
35
74
  """
36
75
  Convert a list of variables that could either be integers, DataRecords, PyRecordModels,
37
76
  or WrappedRecordModels to just integers (taking the record ID from the records).
77
+
38
78
  :return: A list of record IDs for the input records.
39
79
  """
40
- return [(x if isinstance(x, int) else x.record_id) for x in records]
80
+ return [(AliasUtil.to_record_id(x)) for x in records]
41
81
 
42
82
  @staticmethod
43
- def to_field_map_lists(records: Iterable[SapioRecord]) -> list[dict[str, Any]]:
83
+ def to_record_id(record: RecordIdentifier):
44
84
  """
45
- Convert a list of variables that could either be DataRecords, PyRecordModels,
46
- or WrappedRecordModels to a list of their field maps.
85
+ Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
86
+ or WrappedRecordModel to just an integer (taking the record ID from the record).
87
+
88
+ :return: A record ID for the input record.
89
+ """
90
+ return record if isinstance(record, int) else record.record_id
91
+
92
+ @staticmethod
93
+ def to_data_type_name(value: DataTypeIdentifier, convert_eln_dts: bool = True) -> str:
94
+ """
95
+ Convert a given value to a data type name.
96
+
97
+ :param value: A value which is a string, record, or record model type.
98
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
99
+ :return: A string of the data type name of the input value.
100
+ """
101
+ if isinstance(value, SapioRecord):
102
+ value = value.data_type_name
103
+ elif not isinstance(value, str):
104
+ value = value.get_wrapper_data_type_name()
105
+ if convert_eln_dts and ElnBaseDataType.is_eln_type(value):
106
+ return ElnBaseDataType.get_base_type(value).data_type_name
107
+ return value
108
+
109
+ @staticmethod
110
+ def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False,
111
+ convert_eln_dts: bool = True) -> list[str] | set[str]:
112
+ """
113
+ Convert a given iterable of values to a list or set of data type names.
114
+
115
+ :param values: An iterable of values which are strings, records, or record model types.
116
+ :param return_set: If true, return a set instead of a list.
117
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
118
+ :return: A list or set of strings of the data type name of the input value.
119
+ """
120
+ values = [AliasUtil.to_data_type_name(x, convert_eln_dts) for x in values]
121
+ return set(values) if return_set else values
122
+
123
+ @staticmethod
124
+ def to_singular_data_type_name(values: Iterable[DataTypeIdentifier], convert_eln_dts: bool = True) -> str:
125
+ """
126
+ Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
127
+ than one data type name exists in the provided list of identifiers.
128
+
129
+ :param values: An iterable of values which are strings, records, or record model types.
130
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
131
+ :return: The single data type name that the input vales share. Returns an empty string if an empty iterable
132
+ was provided.
133
+ """
134
+ if not values:
135
+ return ""
136
+ data_types: set[str] = AliasUtil.to_data_type_names(values, True, convert_eln_dts)
137
+ if len(data_types) > 1:
138
+ raise SapioException(f"Provided values contain multiple data types: {data_types}. "
139
+ f"Only expecting a single data type.")
140
+ return data_types.pop()
141
+
142
+ @staticmethod
143
+ def to_data_field_name(value: FieldIdentifier) -> str:
144
+ """
145
+ Convert an object that can be used to identify a data field to a data field name string.
146
+
147
+ :param value: An object that can be used to identify a data field.
148
+ :return: A string of the data field name of the input value.
149
+ """
150
+ if isinstance(value, tuple):
151
+ return value[0]
152
+ if isinstance(value, WrapperField):
153
+ return value.field_name
154
+ if isinstance(value, AbstractVeloxFieldDefinition):
155
+ return value.data_field_name
156
+ return value
157
+
158
+ @staticmethod
159
+ def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
160
+ """
161
+ Convert an iterable of objects that can be used to identify data fields to a list of data field name strings.
162
+
163
+ :param values: An iterable of objects that can be used to identify a data field.
164
+ :return: A list of strings of the data field names of the input values.
165
+ """
166
+ return [AliasUtil.to_data_field_name(x) for x in values]
167
+
168
+ @staticmethod
169
+ def to_data_field_names_dict(values: dict[FieldIdentifierKey, Any]) -> dict[str, Any]:
170
+ """
171
+ Take a dictionary whose keys are field identifiers and convert them all to strings for the data field name.
172
+
173
+ :param values: A dictionary of field identifiers to field values.
174
+ :return: A dictionary of strings of the data field names to field values for the input values.
175
+ """
176
+ ret_dict: dict[str, FieldValue] = {}
177
+ for field, value in values.items():
178
+ ret_dict[AliasUtil.to_data_field_name(field)] = value
179
+ return ret_dict
180
+
181
+ @staticmethod
182
+ def to_data_field_names_list_dict(values: list[dict[FieldIdentifierKey, Any]]) -> list[dict[str, Any]]:
183
+ ret_list: list[dict[str, Any]] = []
184
+ for field_map in values:
185
+ ret_list.append(AliasUtil.to_data_field_names_dict(field_map))
186
+ return ret_list
187
+
188
+ @staticmethod
189
+ def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
190
+ """
191
+ Convert a given field identifier to the field type for that field.
192
+
193
+ :param field: A string or WrapperField.
194
+ :param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
195
+ must be provided to determine the field type.
196
+ :return: The field type of the given field.
197
+ """
198
+ if isinstance(field, tuple):
199
+ return field[1]
200
+ if isinstance(field, WrapperField):
201
+ return field.field_type
202
+ for var in dir(data_type):
203
+ attr = getattr(data_type, var)
204
+ if isinstance(attr, WrapperField) and attr.field_name == field:
205
+ return attr.field_type
206
+ raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
207
+ f"field with the name \"{field}\",")
208
+
209
+ @staticmethod
210
+ def to_field_map(record: SapioRecord) -> FieldMap:
211
+ """
212
+ Convert a given record value to a field map. This includes the given RecordId of the given record.
213
+
214
+ :return: The field map for the input record.
215
+ """
216
+ if isinstance(record, DataRecord):
217
+ # noinspection PyTypeChecker
218
+ fields: FieldMap = record.get_fields()
219
+ else:
220
+ fields: FieldMap = record.fields.copy_to_dict()
221
+ fields["RecordId"] = AliasUtil.to_record_id(record)
222
+ return fields
223
+
224
+ @staticmethod
225
+ def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
226
+ """
227
+ Convert a list of variables that could either be DataRecords, PyRecordModels, or WrappedRecordModels
228
+ to a list of their field maps. This includes the given RecordId of the given records.
229
+
47
230
  :return: A list of field maps for the input records.
48
231
  """
49
- field_map_list: list[dict[str, Any]] = []
232
+ field_map_list: list[FieldMap] = []
50
233
  for record in records:
51
- if isinstance(record, DataRecord):
52
- field_map_list.append(record.get_fields())
53
- else:
54
- field_map_list.append(record.fields.copy_to_dict())
234
+ field_map_list.append(AliasUtil.to_field_map(record))
55
235
  return field_map_list
236
+
237
+ @staticmethod
238
+ def to_notebook_id(experiment: ExperimentIdentifier) -> int:
239
+ """
240
+ Convert an object that identifies an ELN experiment to its notebook ID.
241
+
242
+ :return: The notebook ID for the experiment identifier.
243
+ """
244
+ if isinstance(experiment, int):
245
+ return experiment
246
+ if isinstance(experiment, ElnExperiment):
247
+ return experiment.notebook_experiment_id
248
+ return experiment.get_id()
249
+
250
+ @staticmethod
251
+ def to_notebook_ids(experiments: list[ExperimentIdentifier]) -> list[int]:
252
+ """
253
+ Convert a list of objects that identify ELN experiments to their notebook IDs.
254
+
255
+ :return: The list of notebook IDs for the experiment identifiers.
256
+ """
257
+ notebook_ids: list[int] = []
258
+ for experiment in experiments:
259
+ notebook_ids.append(AliasUtil.to_notebook_id(experiment))
260
+ return notebook_ids
261
+
262
+ @staticmethod
263
+ def to_entry_id(entry: ExperimentEntryIdentifier) -> int:
264
+ """
265
+ Convert an object that identifies an experiment entry to its entry ID.
266
+
267
+ :return: The entry ID for the entry identifier.
268
+ """
269
+ if isinstance(entry, int):
270
+ return entry
271
+ elif isinstance(entry, ExperimentEntry):
272
+ return entry.entry_id
273
+ elif isinstance(entry, ElnEntryStep):
274
+ return entry.get_id()
275
+ raise SapioException(f"Unrecognized entry identifier of type {type(entry)}")
276
+
277
+ @staticmethod
278
+ def to_entry_ids(entries: list[ExperimentEntryIdentifier]) -> list[int]:
279
+ """
280
+ Convert a list of objects that identify experiment entries to their entry IDs.
281
+
282
+ :return: The list of entry IDs for the entry identifiers.
283
+ """
284
+ entry_ids: list[int] = []
285
+ for entry in entries:
286
+ entry_ids.append(AliasUtil.to_entry_id(entry))
287
+ return entry_ids
288
+
289
+ @staticmethod
290
+ def to_sapio_user(context: UserIdentifier) -> SapioUser:
291
+ """
292
+ Convert an object that could be either a SapioUser or SapioWebhookContext to just a SapioUser.
293
+
294
+ :return: A SapioUser object.
295
+ """
296
+ return context if isinstance(context, SapioUser) else context.user