sapiopycommons 2024.7.23a296__py3-none-any.whl → 2024.7.23a297__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.

@@ -15,7 +15,7 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefi
15
15
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
16
16
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
17
17
  DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
18
- TempTableSelectionRequest, DisplayPopupRequest, PopupType
18
+ TempTableSelectionRequest
19
19
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
20
20
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
21
21
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
@@ -52,23 +52,11 @@ class CallbackUtil:
52
52
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
53
53
 
54
54
  :param width_pixels: The number of pixels wide that dialogs will appear as.
55
- :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
56
- will appear as.
55
+ :param width_percent: The percentage of the client's screen width that dialogs will appear as.
57
56
  """
58
57
  self.width_pixels = width_pixels
59
58
  self.width_percent = width_percent
60
-
61
- def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
62
- """
63
- Display a toaster popup in the bottom right corner of the user's screen.
64
-
65
- :param message: The message to display in the toaster.
66
- :param title: The title to display at the top of the toaster.
67
- :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
68
- Info is blue, Success is green, Warning is yellow, and Error is red
69
- """
70
- self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
71
-
59
+
72
60
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
73
61
  user_can_cancel: bool = False) -> str:
74
62
  """
@@ -83,8 +71,7 @@ class CallbackUtil:
83
71
  SapioUserCancelledException is thrown.
84
72
  :return: The name of the button that the user selected.
85
73
  """
86
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
87
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
74
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
88
75
  response: int | None = self.callback.show_option_dialog(request)
89
76
  if response is None:
90
77
  raise SapioUserCancelledException()
@@ -124,20 +111,16 @@ class CallbackUtil:
124
111
  """
125
112
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
126
113
 
127
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
128
- preselected_values: list[str] | None = None) -> list[str]:
114
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
129
115
  """
130
116
  Create a list dialog with the given options for the user to choose from.
131
117
 
132
118
  :param title: The title of the dialog.
133
119
  :param options: The list options that the user has to choose from.
134
120
  :param multi_select: Whether the user is able to select multiple options from the list.
135
- :param preselected_values: A list of values that will already be selected when the list dialog is created. The
136
- user can unselect these values if they want to.
137
121
  :return: The list of options that the user selected.
138
122
  """
139
- request = ListDialogRequest(title, multi_select, options, preselected_values,
140
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
123
+ request = ListDialogRequest(title, multi_select, options)
141
124
  response: list[str] | None = self.callback.show_list_dialog(request)
142
125
  if response is None:
143
126
  raise SapioUserCancelledException()
@@ -180,6 +163,8 @@ class CallbackUtil:
180
163
  builder = FormBuilder(data_type, display_name, plural_display_name)
181
164
  for field_def in fields:
182
165
  field_name = field_def.data_field_name
166
+ if values and hasattr(field_def, "default_value"):
167
+ field_def.default_value = values.get(field_name)
183
168
  column: int = 0
184
169
  span: int = 4
185
170
  if column_positions and field_name in column_positions:
@@ -188,8 +173,7 @@ class CallbackUtil:
188
173
  span = position[1]
189
174
  builder.add_field(field_def, column, span)
190
175
 
191
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
192
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
176
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
193
177
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
194
178
  if response is None:
195
179
  raise SapioUserCancelledException()
@@ -231,13 +215,13 @@ class CallbackUtil:
231
215
  modifier = FieldModifier(visible=True, editable=editable)
232
216
 
233
217
  # Build the form using only those fields that are desired.
234
- values: dict[str, Any] = {}
235
218
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
236
219
  for field_name in fields:
237
220
  field_def = field_defs.get(field_name)
238
221
  if field_def is None:
239
222
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
240
- values[field_name] = record.get_field_value(field_name)
223
+ if hasattr(field_def, "default_value"):
224
+ field_def.default_value = record.get_field_value(field_name)
241
225
  column: int = 0
242
226
  span: int = 4
243
227
  if column_positions and field_name in column_positions:
@@ -246,8 +230,7 @@ class CallbackUtil:
246
230
  span = position[1]
247
231
  builder.add_field(modifier.modify_field(field_def), column, span)
248
232
 
249
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
250
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
233
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
251
234
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
252
235
  if response is None:
253
236
  raise SapioUserCancelledException()
@@ -262,8 +245,7 @@ class CallbackUtil:
262
245
  :param field: The definition for a field that the user must provide input to.
263
246
  :return: The response value from the user for the given field.
264
247
  """
265
- request = InputDialogCriteria(title, msg, field,
266
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
248
+ request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
267
249
  response: Any | None = self.callback.show_input_dialog(request)
268
250
  if response is None:
269
251
  raise SapioUserCancelledException()
@@ -340,8 +322,6 @@ class CallbackUtil:
340
322
  msg: str,
341
323
  fields: list[AbstractVeloxFieldDefinition],
342
324
  values: list[FieldMap],
343
- group_by: str | None = None,
344
- image_data: list[bytes] | None = None,
345
325
  *,
346
326
  data_type: str = "Default",
347
327
  display_name: str | None = None,
@@ -355,10 +335,6 @@ class CallbackUtil:
355
335
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
356
336
  they are provided in this list.
357
337
  :param values: The values to set for each row of the table.
358
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
359
- The user may remove this grouping if they want to.
360
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
361
- the image data list corresponds to the element at the same index in the values list.
362
338
  :param data_type: The data type name for the temporary data type that will be created for this table.
363
339
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
364
340
  name.
@@ -380,9 +356,7 @@ class CallbackUtil:
380
356
  for field in fields:
381
357
  builder.add_field(modifier.modify_field(field))
382
358
 
383
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
384
- record_image_data_list=image_data, group_by_field=group_by,
385
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
359
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
386
360
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
387
361
  if response is None:
388
362
  raise SapioUserCancelledException()
@@ -393,9 +367,7 @@ class CallbackUtil:
393
367
  msg: str,
394
368
  fields: list[str],
395
369
  records: list[SapioRecord],
396
- editable: bool | None = True,
397
- group_by: str | None = None,
398
- image_data: list[bytes] | None = None) -> list[FieldMap]:
370
+ editable: bool | None = True) -> list[FieldMap]:
399
371
  """
400
372
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
401
373
  a given list of records of a singular type. Provided field names must match fields on the definition of the data
@@ -412,10 +384,6 @@ class CallbackUtil:
412
384
  they are provided in this list.
413
385
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
414
386
  uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
415
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
416
- The user may remove this grouping if they want to.
417
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
418
- the image data list corresponds to the element at the same index in the records list.
419
387
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
420
388
  value from the user for that field for each row.
421
389
  """
@@ -442,9 +410,7 @@ class CallbackUtil:
442
410
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
443
411
  builder.add_field(modifier.modify_field(field_def))
444
412
 
445
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
446
- record_image_data_list=image_data, group_by_field=group_by,
447
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
413
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
448
414
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
449
415
  if response is None:
450
416
  raise SapioUserCancelledException()
@@ -601,8 +567,7 @@ class CallbackUtil:
601
567
  for field in final_fields:
602
568
  builder.add_field(field)
603
569
 
604
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
605
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
570
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
606
571
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
607
572
  if response is None:
608
573
  raise SapioUserCancelledException()
@@ -651,8 +616,7 @@ class CallbackUtil:
651
616
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
652
617
  f"\"{layout_name}\" in the system.")
653
618
 
654
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
655
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
619
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
656
620
  response: bool = self.callback.data_record_form_view_dialog(request)
657
621
  if not response:
658
622
  raise SapioUserCancelledException()
@@ -699,7 +663,7 @@ class CallbackUtil:
699
663
  return response
700
664
 
701
665
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
702
- multi_select: bool = True) -> list[SapioRecord]:
666
+ multi_select: bool = True) -> list[FieldMap]:
703
667
  """
704
668
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
705
669
  match fields on the definition of the data type of the given records.
@@ -712,7 +676,7 @@ class CallbackUtil:
712
676
  they are provided in this list.
713
677
  :param records: The records to display as rows in the table.
714
678
  :param multi_select: Whether the user is able to select multiple records from the list.
715
- :return: A list of the selected records.
679
+ :return: A list of field maps corresponding to the chosen input records.
716
680
  """
717
681
  data_types: set[str] = {x.data_type_name for x in records}
718
682
  if len(data_types) > 1:
@@ -843,8 +807,7 @@ class CallbackUtil:
843
807
  for field in additional_fields:
844
808
  builder.add_field(field)
845
809
  temp_dt = builder.get_temporary_data_type()
846
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
847
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
810
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
848
811
  response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
849
812
  if response is None:
850
813
  raise SapioUserCancelledException()
@@ -943,7 +906,7 @@ class CallbackUtil:
943
906
  :param file_name: The name of the file.
944
907
  :param file_data: The data of the file, provided as either a string or as a bytes array.
945
908
  """
946
- data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
909
+ data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
947
910
  self.callback.send_file(file_name, False, data)
948
911
 
949
912
 
@@ -1,6 +1,5 @@
1
1
  import io
2
2
 
3
- from sapiopylib.rest.DataMgmtService import DataMgmtServer
4
3
  from sapiopylib.rest.User import SapioUser
5
4
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
6
5
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
@@ -13,35 +12,31 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
13
12
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
14
13
  class AttachmentUtil:
15
14
  @staticmethod
16
- def get_attachment_bytes(context: SapioWebhookContext | SapioUser, attachment: SapioRecord) -> bytes:
15
+ def get_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord) -> bytes:
17
16
  """
18
17
  Get the data bytes for the given attachment record. Makes a webservice call to retrieve the data.
19
18
 
20
- :param context: The current webhook context or a user object to send requests from.
19
+ :param context: The current webhook context.
21
20
  :param attachment: The attachment record.
22
21
  :return: The bytes for the attachment's file data.
23
22
  """
24
23
  attachment = AliasUtil.to_data_record(attachment)
25
- if isinstance(context, SapioWebhookContext):
26
- dr_man = context.data_record_manager
27
- else:
28
- dr_man = DataMgmtServer.get_data_record_manager(context)
29
24
  with io.BytesIO() as data_sink:
30
25
  def consume_data(chunk: bytes):
31
26
  data_sink.write(chunk)
32
- dr_man.get_attachment_data(attachment, consume_data)
27
+ context.data_record_manager.get_attachment_data(attachment, consume_data)
33
28
  data_sink.flush()
34
29
  data_sink.seek(0)
35
30
  file_bytes = data_sink.read()
36
31
  return file_bytes
37
32
 
38
33
  @staticmethod
39
- def set_attachment_bytes(context: SapioWebhookContext | SapioUser, attachment: SapioRecord,
34
+ def set_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord,
40
35
  file_name: str, file_bytes: bytes) -> None:
41
36
  """
42
37
  Set the attachment data for a given attachment record. Makes a webservice call to set the data.
43
38
 
44
- :param context: The current webhook context or a user object to send requests from.
39
+ :param context: The current webhook context.
45
40
  :param attachment: The attachment record. Must be an existing data record that is an attachment type.
46
41
  :param file_name: The name of the attachment.
47
42
  :param file_bytes: The bytes of the attachment data.
@@ -50,12 +45,8 @@ class AttachmentUtil:
50
45
  raise SapioException("Provided record cannot have its attachment data set, as it does not exist in the "
51
46
  "system yet.")
52
47
  attachment = AliasUtil.to_data_record(attachment)
53
- if isinstance(context, SapioWebhookContext):
54
- dr_man = context.data_record_manager
55
- else:
56
- dr_man = DataMgmtServer.get_data_record_manager(context)
57
48
  with io.BytesIO(file_bytes) as stream:
58
- dr_man.set_attachment_data(attachment, file_name, stream)
49
+ context.data_record_manager.set_attachment_data(attachment, file_name, stream)
59
50
 
60
51
  @staticmethod
61
52
  def create_attachment(context: SapioWebhookContext | SapioUser, file_name: str, file_bytes: bytes,
@@ -1,9 +1,7 @@
1
1
  import time
2
2
  from collections.abc import Mapping, Iterable
3
3
 
4
- from sapiopylib.rest.DataMgmtService import DataMgmtServer
5
4
  from sapiopylib.rest.ELNService import ElnManager
6
- from sapiopylib.rest.User import SapioUser
7
5
  from sapiopylib.rest.pojo.DataRecord import DataRecord
8
6
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
9
7
  InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
@@ -19,7 +17,7 @@ from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
19
17
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
20
18
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
21
19
 
22
- from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier
20
+ from sapiopycommons.general.aliases import AliasUtil, SapioRecord
23
21
  from sapiopycommons.general.exceptions import SapioException
24
22
 
25
23
  Step = str | ElnEntryStep
@@ -29,8 +27,7 @@ itself."""
29
27
 
30
28
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
31
29
  class ExperimentHandler:
32
- user: SapioUser
33
- context: SapioWebhookContext | None
30
+ context: SapioWebhookContext
34
31
  """The context that this handler is working from."""
35
32
 
36
33
  # Basic experiment info from the context.
@@ -80,57 +77,31 @@ class ExperimentHandler:
80
77
  ElnExperimentStatus.Canceled]
81
78
  """The set of statuses that an ELN experiment could have and be considered locked."""
82
79
 
83
- def __init__(self, context: SapioWebhookContext | SapioUser, experiment: ExperimentIdentifier | SapioRecord | None = None):
80
+ def __init__(self, context: SapioWebhookContext, experiment: ElnExperiment | None = None):
84
81
  """
85
82
  Initialization will throw an exception if there is no ELN Experiment in the provided context and no experiment
86
83
  is provided.
87
84
 
88
- :param context: The current webhook context or a user object to send requests from.
85
+ :param context: The current webhook context.
89
86
  :param experiment: If an experiment is provided that is separate from the experiment that is in the context,
90
- that experiment will be used by this ExperimentHandler instead. An experiment can be provided in various
91
- forms, including an ElnExperiment, ElnExperimentProtocol, an experiment record, or a notebook experiment ID.
92
- """
93
- if isinstance(context, SapioWebhookContext):
94
- self.user = context.user
95
- self.context = context
96
- else:
97
- self.user = context
98
- self.context = None
87
+ that experiment will be used by this ExperimentHandler instead.
88
+ """
99
89
  # FR-46495 - Allow the init function of ExperimentHandler to take in an ElnExperiment that is separate from the
100
90
  # context.
101
- # CR-37038 - Allow other experiment object types to be provided. Convert them all down to ElnExperiment.
102
- if experiment is not None:
103
- # If this object is already an ElnExperiment, do nothing.
104
- if isinstance(experiment, ElnExperiment):
105
- pass
106
- # If this object is an ElnExperimentProtocol, then we can get the ElnExperiment from the object.
107
- elif isinstance(experiment, ElnExperimentProtocol):
108
- experiment: ElnExperiment = experiment.eln_experiment
109
- # If this object is an integer, assume it is a notebook ID that we can query the system with.
110
- elif isinstance(experiment, int):
111
- notebook_id: int = experiment
112
- experiment: ElnExperiment = context.eln_manager.get_eln_experiment_by_id(notebook_id)
113
- if not experiment:
114
- raise SapioException(f"No experiment with notebook ID {notebook_id} located in the system.")
115
- # If this object is a record, assume it is an experiment record that we can query the system with.
116
- else:
117
- record_id: int = AliasUtil.to_record_ids([experiment])[0]
118
- experiment: ElnExperiment = context.eln_manager.get_eln_experiment_by_record_id(record_id)
119
- if not experiment:
120
- raise SapioException(f"No experiment with record ID {record_id} located in the system.")
121
- if (context is None or context.eln_experiment is None) and experiment is None:
91
+ if context.eln_experiment is None and experiment is None:
122
92
  raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment in the context.")
123
- if context and context.eln_experiment == experiment:
124
- experiment: ElnExperiment | None = None
93
+ if context.eln_experiment == experiment:
94
+ experiment = None
95
+ self.context = context
125
96
 
126
97
  # Get the basic information about this experiment that already exists in the context and is often used.
127
98
  self.__eln_exp = experiment if experiment else context.eln_experiment
128
- self.__protocol = ElnExperimentProtocol(experiment, self.user) if experiment else context.active_protocol
99
+ self.__protocol = ElnExperimentProtocol(experiment, context.user) if experiment else context.active_protocol
129
100
  self.__exp_id = self.__protocol.get_id()
130
101
 
131
102
  # Grab various managers that may be used.
132
- self.__eln_man = DataMgmtServer.get_eln_manager(self.user)
133
- self.__inst_man = RecordModelManager(self.user).instance_manager
103
+ self.__eln_man = context.eln_manager
104
+ self.__inst_man = RecordModelManager(context.user).instance_manager
134
105
 
135
106
  # Create empty caches to fill when necessary.
136
107
  self.__steps = {}
@@ -292,7 +263,7 @@ class ExperimentHandler:
292
263
  :return: The data record for this experiment. None if it has no record.
293
264
  """
294
265
  if not hasattr(self, "_ExperimentHandler__exp_record"):
295
- drm = DataMgmtServer.get_data_record_manager(self.user)
266
+ drm = self.context.data_record_manager
296
267
  dt = self.__eln_exp.experiment_data_type_name
297
268
  results = drm.query_data_records_by_id(dt, [self.__eln_exp.experiment_record_id]).result_list
298
269
  # PR-46504: Set the exp_record to None if there are no results.
@@ -609,6 +580,8 @@ class ExperimentHandler:
609
580
  step.set_records(AliasUtil.to_data_records(records))
610
581
 
611
582
  # FR-46496 - Provide alias of set_step_records for use with form entries.
583
+ # TODO: Provide a similar aliased function for attachment entries once sapiopylib allows setting multiple
584
+ # attachments to an attachment step.
612
585
  def set_form_record(self, step: Step, record: SapioRecord) -> None:
613
586
  """
614
587
  Sets the record for a form entry.
@@ -644,7 +617,7 @@ class ExperimentHandler:
644
617
  if step.eln_entry.entry_type != ElnEntryType.Table:
645
618
  raise SapioException("The provided step is not a table entry.")
646
619
  dt: str = step.get_data_type_names()[0]
647
- if not ElnBaseDataType.is_eln_type(dt):
620
+ if not self.__is_eln_type(dt):
648
621
  raise SapioException("The provided step is not an ELN data type entry.")
649
622
  return self.__inst_man.add_new_records(dt, count)
650
623
 
@@ -668,7 +641,7 @@ class ExperimentHandler:
668
641
  """
669
642
  step = self.__to_eln_step(step)
670
643
  dt: str = step.get_data_type_names()[0]
671
- if not ElnBaseDataType.is_eln_type(dt):
644
+ if not self.__is_eln_type(dt):
672
645
  raise SapioException("The provided step is not an ELN data type entry.")
673
646
  if any([x.data_type_name != dt for x in records]):
674
647
  raise SapioException("Not all of the provided records match the data type of the step.")
@@ -685,6 +658,17 @@ class ExperimentHandler:
685
658
  for record in record_models:
686
659
  record.delete()
687
660
 
661
+ # TODO: Remove and use the function of the same name in ElnBaseDataType in the future. Currently this function is
662
+ # bugged in sapiopylib and is comparing against base_type.name instead of base_type.value.
663
+ @staticmethod
664
+ def __is_eln_type(data_type: str):
665
+ if data_type is None or not data_type:
666
+ return False
667
+ for base_type in ElnBaseDataType:
668
+ if data_type.lower().startswith(base_type.value.lower()):
669
+ return True
670
+ return False
671
+
688
672
  def update_step(self, step: Step,
689
673
  entry_name: str | None = None,
690
674
  related_entry_set: Iterable[int] | None = None,
@@ -972,27 +956,6 @@ class ExperimentHandler:
972
956
  step.unlock_step()
973
957
  step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
974
958
 
975
- def disable_step(self, step: Step) -> None:
976
- """
977
- Set the status of the input step to Disabled. This is the state that entries are in when they are waiting for
978
- entries that they are dependent upon to be submitted before they can be enabled. If you have unsubmitted an
979
- entry and want its dependent entries to be locked again, then you would use this to set their status to
980
- disabled.
981
-
982
- Makes a webservice call to update the step. Checks if the step is already unlocked, and does nothing if so.
983
-
984
- If no step functions have been called before and a step is being searched for by name, queries for the
985
- list of steps in the experiment and caches them.
986
-
987
- :param step:
988
- The step to disable.
989
- The step may be provided as either a string for the name of the step or an ElnEntryStep.
990
- If given a name, throws an exception if no step of the given name exists in the experiment.
991
- """
992
- step = self.__to_eln_step(step)
993
- if step.eln_entry.entry_status in self.__ENTRY_LOCKED_STATUSES:
994
- self.update_step(step, entry_status=ExperimentEntryStatus.Disabled)
995
-
996
959
  def step_is_submitted(self, step: Step) -> bool:
997
960
  """
998
961
  Determine if the input step has already been submitted.
@@ -23,7 +23,7 @@ class CDL:
23
23
  "fileName": file_name
24
24
  }
25
25
  user: SapioUser = context if isinstance(context, SapioUser) else context.user
26
- with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
26
+ with io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data) as data_stream:
27
27
  response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
28
28
  user.raise_for_status(response)
29
29
  # The response content is returned as bytes for a comma separated string of record IDs.
@@ -54,7 +54,7 @@ class FileBridge:
54
54
  'Filepath': f"bridge://{bridge_name}/{file_path}"
55
55
  }
56
56
  user: SapioUser = context if isinstance(context, SapioUser) else context.user
57
- with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
57
+ with io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data) as data_stream:
58
58
  response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
59
59
  user.raise_for_status(response)
60
60
 
@@ -21,8 +21,7 @@ class FileUtil:
21
21
  """
22
22
  @staticmethod
23
23
  def tokenize_csv(file_bytes: bytes, required_headers: list[str] | None = None, header_row_index: int | None = 0,
24
- seperator: str = ",", *, encoding: str | None = None, exception_on_empty: bool = True) \
25
- -> tuple[list[dict[str, str]], list[list[str]]]:
24
+ seperator: str = ",", *, encoding: str | None = None) -> tuple[list[dict[str, str]], list[list[str]]]:
26
25
  """
27
26
  Tokenize a CSV file. The provided file must be uniform. That is, if row 1 has 10 cells, all the rows in the file
28
27
  must have 10 cells. Otherwise, the Pandas parser throws a tokenizer exception.
@@ -38,8 +37,6 @@ class FileUtil:
38
37
  :param encoding: The encoding used to read the given file bytes. If not provided, uses utf-8. If your file
39
38
  contains a non-utf-8 character, then a UnicodeDecodeError will be thrown. If this happens, consider using
40
39
  ISO-8859-1 as the encoding.
41
- :param exception_on_empty: Throw a user error exception if the provided file bytes result in an empty list in
42
- the first element of the returned tuple.
43
40
  :return: The CSV parsed into a list of dicts where each dict is a row, mapping the headers to the cells for
44
41
  that row. Also returns a list of each row above the headers (the metadata), parsed into a list of each cell.
45
42
  If the header row index is 0 or None, this list will be empty.
@@ -52,13 +49,11 @@ class FileUtil:
52
49
  metadata: list[list[str]] = FileUtil.data_frame_to_lists(file_metadata)
53
50
  # Parse the data from the file body into a list of dicts.
54
51
  rows: list[dict[str, str]] = FileUtil.data_frame_to_dicts(file_body, required_headers, header_row_index)
55
- if exception_on_empty and not rows:
56
- raise SapioUserErrorException("The provided file contains no rows of information below the headers.")
57
52
  return rows, metadata
58
53
 
59
54
  @staticmethod
60
- def tokenize_xlsx(file_bytes: bytes, required_headers: list[str] | None = None, header_row_index: int | None = 0,
61
- *, exception_on_empty: bool = True) -> tuple[list[dict[str, str]], list[list[str]]]:
55
+ def tokenize_xlsx(file_bytes: bytes, required_headers: list[str] | None = None, header_row_index: int | None = 0) \
56
+ -> tuple[list[dict[str, str]], list[list[str]]]:
62
57
  """
63
58
  Tokenize an XLSX file row by row.
64
59
 
@@ -69,8 +64,6 @@ class FileUtil:
69
64
  row is returned in the metadata list. If input is None, then no row is considered to be the header row,
70
65
  meaning that required headers are also ignored if any are provided. By default, the first row (0th index)
71
66
  is assumed to be the header row.
72
- :param exception_on_empty: Throw a user error exception if the provided file bytes result in an empty list in
73
- the first element of the returned tuple.
74
67
  :return: The XLSX parsed into a list of dicts where each dict is a row, mapping the headers to the cells for
75
68
  that row. Also returns a list of each row above the headers (the metadata), parsed into a list of each cell.
76
69
  If the header row index is 0 or None, this list will be empty.
@@ -82,8 +75,6 @@ class FileUtil:
82
75
  metadata: list[list[str]] = FileUtil.data_frame_to_lists(file_metadata)
83
76
  # Parse the data from the file body into a list of dicts.
84
77
  rows: list[dict[str, str]] = FileUtil.data_frame_to_dicts(file_body, required_headers, header_row_index)
85
- if exception_on_empty and not rows:
86
- raise SapioUserErrorException("The provided file contains no rows of information below the headers.")
87
78
  return rows, metadata
88
79
 
89
80
  @staticmethod
@@ -238,7 +229,7 @@ class FileUtil:
238
229
  :param file_data: The CSV file to be converted.
239
230
  :return: The bytes of the CSV file converted to an XLSX file.
240
231
  """
241
- with (io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)) as csv:
232
+ with (io.BytesIO(file_data) if isinstance(file_data, bytes) else io.StringIO(file_data)) as csv:
242
233
  # Setting header to false makes pandas read the CSV as-is.
243
234
  data_frame = pandas.read_csv(csv, sep=",", header=None)
244
235
 
@@ -8,6 +8,7 @@ from sapiopylib.rest.pojo.CustomReport import RawReportTerm, RawTermOperation
8
8
  from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxIntegerFieldDefinition, VeloxStringFieldDefinition, \
9
9
  AbstractVeloxFieldDefinition
10
10
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
11
+ from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
11
12
 
12
13
  from sapiopycommons.callbacks.callback_util import CallbackUtil
13
14
  from sapiopycommons.files.file_data_handler import FileDataHandler, FilterList
@@ -79,7 +80,7 @@ class FileValidator:
79
80
 
80
81
  return failed_rows
81
82
 
82
- def build_violation_report(self, context: SapioWebhookContext | SapioUser,
83
+ def build_violation_report(self, context: SapioWebhookResult | SapioUser,
83
84
  rule_violations: dict[int, list[ValidationRule]]) -> None:
84
85
  """
85
86
  Build a simple report of any rule violations in the file to display to the user as a table dialog.
@@ -7,7 +7,6 @@ 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
11
10
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
12
11
  from sapiopylib.rest.utils.autopaging import QueryDataRecordsAutoPager, QueryDataRecordByIdListAutoPager, \
13
12
  QueryAllRecordsOfTypeAutoPager
@@ -17,7 +16,6 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelMana
17
16
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType, WrappedRecordModel
18
17
  from sapiopylib.rest.utils.recordmodel.RelationshipPath import RelationshipPath, RelationshipNode, \
19
18
  RelationshipNodeType
20
- from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManager
21
19
 
22
20
  from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap
23
21
  from sapiopycommons.general.custom_report_util import CustomReportUtil
@@ -34,7 +32,6 @@ class RecordHandler:
34
32
  rec_man: RecordModelManager
35
33
  inst_man: RecordModelInstanceManager
36
34
  rel_man: RecordModelRelationshipManager
37
- an_man: RecordModelAncestorManager
38
35
 
39
36
  def __init__(self, context: SapioWebhookContext | SapioUser):
40
37
  """
@@ -45,7 +42,6 @@ class RecordHandler:
45
42
  self.rec_man = RecordModelManager(self.user)
46
43
  self.inst_man = self.rec_man.instance_manager
47
44
  self.rel_man = self.rec_man.relationship_manager
48
- self.an_man = RecordModelAncestorManager(self.rec_man)
49
45
 
50
46
  def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
51
47
  """
@@ -55,7 +51,6 @@ class RecordHandler:
55
51
  :param wrapper_type: The record model wrapper to use.
56
52
  :return: The record model for the input.
57
53
  """
58
- self.__verify_data_type([record], wrapper_type)
59
54
  return self.inst_man.add_existing_record_of_type(record, wrapper_type)
60
55
 
61
56
  def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
@@ -66,7 +61,6 @@ class RecordHandler:
66
61
  :param wrapper_type: The record model wrapper to use.
67
62
  :return: The record models for the input.
68
63
  """
69
- self.__verify_data_type(records, wrapper_type)
70
64
  return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
71
65
 
72
66
  def query_models(self, wrapper_type: type[WrappedType], field: str, value_list: Iterable[Any],
@@ -837,6 +831,8 @@ class RecordHandler:
837
831
  path, if any. The hierarchy must be linear (1:1 relationship between data types at every step) and the
838
832
  relationship path must already be loaded.
839
833
 
834
+ Currently, the relationship path may only contain parent/child nodes.
835
+
840
836
  :param models: A list of record models.
841
837
  :param path: The relationship path to follow.
842
838
  :param wrapper_type: The record model wrapper to use.
@@ -847,44 +843,15 @@ class RecordHandler:
847
843
  # PR-46832: Update path traversal to account for changes to RelationshipPath in Sapiopylib.
848
844
  path: list[RelationshipNode] = path.path
849
845
  for model in models:
850
- current: PyRecordModel | None = model if isinstance(model, PyRecordModel) else model.backing_model
846
+ current: PyRecordModel = model if isinstance(model, PyRecordModel) else model.backing_model
851
847
  for node in path:
852
- data_type: str = node.data_type_name
853
- direction: RelationshipNodeType = node.direction
848
+ direction = node.direction
854
849
  if current is None:
855
850
  break
856
851
  if direction == RelationshipNodeType.CHILD:
857
- current = current.get_child_of_type(data_type)
852
+ current = current.get_child_of_type(node.data_type_name)
858
853
  elif direction == RelationshipNodeType.PARENT:
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]
854
+ current = current.get_parent_of_type(node.data_type_name)
888
855
  else:
889
856
  raise SapioException("Unsupported path direction.")
890
857
  ret_dict.update({model: self.inst_man.wrap(current, wrapper_type) if current else None})
@@ -897,6 +864,8 @@ class RecordHandler:
897
864
  path, if any. The hierarchy may be non-linear (1:Many relationships between data types are allowed) and the
898
865
  relationship path must already be loaded.
899
866
 
867
+ Currently, the relationship path may only contain parent/child nodes.
868
+
900
869
  :param models: A list of record models.
901
870
  :param path: The relationship path to follow.
902
871
  :param wrapper_type: The record model wrapper to use.
@@ -911,23 +880,14 @@ class RecordHandler:
911
880
  next_search: set[PyRecordModel] = set()
912
881
  # Exhaust the records at each step in the path, then use those records for the next step.
913
882
  for node in path:
914
- data_type: str = node.data_type_name
915
- direction: RelationshipNodeType = node.direction
883
+ direction = node.direction
916
884
  if len(current_search) == 0:
917
885
  break
918
886
  for search in current_search:
919
887
  if direction == RelationshipNodeType.CHILD:
920
- next_search.update(search.get_children_of_type(data_type))
888
+ next_search.update(search.get_children_of_type(node.data_type_name))
921
889
  elif direction == RelationshipNodeType.PARENT:
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))
890
+ next_search.update(search.get_parents_of_type(node.data_type_name))
931
891
  else:
932
892
  raise SapioException("Unsupported path direction.")
933
893
  current_search = next_search
@@ -948,6 +908,8 @@ class RecordHandler:
948
908
  relationships (e.g. a sample which is aliquoted to a number of samples, then those aliquots are pooled back
949
909
  together into a single sample).
950
910
 
911
+ Currently, the relationship path may only contain parent/child nodes.
912
+
951
913
  :param models: A list of record models.
952
914
  :param path: The relationship path to follow.
953
915
  :param wrapper_type: The record model wrapper to use.
@@ -960,22 +922,13 @@ class RecordHandler:
960
922
  for model in models:
961
923
  current: list[PyRecordModel] = [model if isinstance(model, PyRecordModel) else model.backing_model]
962
924
  for node in path:
963
- data_type: str = node.data_type_name
964
- direction: RelationshipNodeType = node.direction
925
+ direction = node.direction
965
926
  if len(current) == 0:
966
927
  break
967
928
  if direction == RelationshipNodeType.CHILD:
968
- current = current[0].get_children_of_type(data_type)
929
+ current = current[0].get_children_of_type(node.data_type_name)
969
930
  elif direction == RelationshipNodeType.PARENT:
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)
931
+ current = current[0].get_parents_of_type(node.data_type_name)
979
932
  else:
980
933
  raise SapioException("Unsupported path direction.")
981
934
  ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
@@ -1006,18 +959,3 @@ class RecordHandler:
1006
959
  f"encountered in system that matches all provided identifiers.")
1007
960
  unique_record = result
1008
961
  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.eln.SapioELNEnums import ElnBaseDataType
2
+ from sapiopylib.rest.pojo.webhook.VeloxRules import VeloxRuleType, VeloxRuleParser
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,6 +12,7 @@ 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.
15
16
  """
16
17
  __context: SapioWebhookContext
17
18
  """The context that this handler is working from."""
@@ -63,8 +64,13 @@ class ElnRuleHandler:
63
64
  # Get the data type of this record. If this is an ELN type, ignore the digits.
64
65
  data_type: str = record.data_type_name
65
66
  # PR-46331: Ensure that all ELN types are converted to their base data type name.
66
- if ElnBaseDataType.is_eln_type(data_type):
67
- data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
67
+ # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
68
+ if data_type.startswith("ELNExperiment_"):
69
+ data_type = "ELNExperiment"
70
+ elif data_type.startswith("ELNExperimentDetail_"):
71
+ data_type = "ELNExperimentDetail"
72
+ elif data_type.startswith("ELNSampleDetail_"):
73
+ data_type = "ELNSampleDetail"
68
74
  # Update the list of records of this type that exist so far globally.
69
75
  self.__records.setdefault(data_type, set()).add(record)
70
76
  # Do the same for the list of records of this type for this specific entry.
@@ -79,9 +85,19 @@ class ElnRuleHandler:
79
85
  entry_dict: dict[str, dict[int, FieldMap]] = {}
80
86
  for record_result in entry_results:
81
87
  for result in record_result.velox_type_rule_field_map_result_list:
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
88
+ # TODO: sapiopylib currently has a bug where this velox_type_pojo variable is stored as a dict instead
89
+ # of as the intended VeloxRuleType object. Parse that dict as a VeloxRuleType before use.
90
+ velox_type: VeloxRuleType | dict = result.velox_type_pojo
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"
85
101
  for field_map in result.field_map_list:
86
102
  rec_id: int = field_map.get("RecordId")
87
103
  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.eln.SapioELNEnums import ElnBaseDataType
2
+ from sapiopylib.rest.pojo.webhook.VeloxRules import VeloxRuleType, VeloxRuleParser
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,6 +12,7 @@ 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.
15
16
  """
16
17
  __context: SapioWebhookContext
17
18
  """The context that this handler is working from."""
@@ -50,6 +51,9 @@ class OnSaveRuleHandler:
50
51
  self.__base_id_to_records = {}
51
52
  # Each record ID in the context has a list of results for that record.
52
53
  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)
53
57
  # Keep track of the records for this specific record ID.
54
58
  id_dict: dict[str, set[DataRecord]] = {}
55
59
  # The list of results for a record consist of a list of data records and a VeloxType that specifies
@@ -60,8 +64,13 @@ class OnSaveRuleHandler:
60
64
  # Get the data type of this record. If this is an ELN type, ignore the digits.
61
65
  data_type: str = record.data_type_name
62
66
  # PR-46331: Ensure that all ELN types are converted to their base data type name.
63
- if ElnBaseDataType.is_eln_type(data_type):
64
- data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
67
+ # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
68
+ if data_type.startswith("ELNExperiment_"):
69
+ data_type = "ELNExperiment"
70
+ elif data_type.startswith("ELNExperimentDetail_"):
71
+ data_type = "ELNExperimentDetail"
72
+ elif data_type.startswith("ELNSampleDetail_"):
73
+ data_type = "ELNSampleDetail"
65
74
  # Update the list of records of this type that exist so far globally.
66
75
  self.__records.setdefault(data_type, set()).add(record)
67
76
  # Do the same for the list of records of this type that relate to this record ID.
@@ -73,11 +82,24 @@ class OnSaveRuleHandler:
73
82
  self.__base_id_to_field_maps = {}
74
83
  # Repeat the same thing for the field map results.
75
84
  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)
76
88
  id_dict: dict[str, dict[int, FieldMap]] = {}
77
89
  for record_result in rule_results:
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
90
+ # TODO: sapiopylib currently has a bug where this velox_type_pojo variable is stored as a dict instead
91
+ # of as the intended VeloxRuleType object. Parse that dict as a VeloxRuleType before use.
92
+ velox_type: VeloxRuleType | dict = record_result.velox_type_pojo
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"
81
103
  for field_map in record_result.field_map_list:
82
104
  rec_id: int = field_map.get("RecordId")
83
105
  self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.7.23a296
3
+ Version: 2024.7.23a297
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -1,22 +1,22 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sapiopycommons/callbacks/callback_util.py,sha256=YdaN1iNnJ51EauvKfEOAUwahu1dt_YVvr4Zia29g9qc,61123
3
+ sapiopycommons/callbacks/callback_util.py,sha256=caeIWCHvK33jDs3TRskpJv0kDe7W8NPK4MyJPjgztwo,58012
4
4
  sapiopycommons/chem/IndigoMolecules.py,sha256=QqFDi9CKERj6sn_ZwVcS2xZq4imlkaTeCrpq1iNcEJA,1992
5
5
  sapiopycommons/chem/Molecules.py,sha256=t80IsQBPJ9mwE8ZxnWomAGrZDhdsOuPvLaTPb_N6jGU,8639
6
6
  sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sapiopycommons/datatype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sapiopycommons/datatype/attachment_util.py,sha256=23JQ4avSmBBJdCv95LVj31x8rUCclzB_DYFBijH0NII,3708
8
+ sapiopycommons/datatype/attachment_util.py,sha256=YlnMprj5IGBbAZDLG2khS1P7JIYTw_NYfpJAfRZfP3M,3219
9
9
  sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- sapiopycommons/eln/experiment_handler.py,sha256=RspUDmPmBQIG7l7q_luFP8yeRljdU3E64066NH5CtdI,59817
10
+ sapiopycommons/eln/experiment_handler.py,sha256=v1pG4qtZb8OSNWfKtFo6NjnEkReqnu5R9i_hqWh_xxg,57198
11
11
  sapiopycommons/eln/experiment_report_util.py,sha256=FTLw-6SLAMeoWTOO-qhGROE9g54pZdyoQJIhiIzlwGw,7848
12
12
  sapiopycommons/eln/plate_designer.py,sha256=FYJfhhNq8hdfuXgDYOYHy6g0m2zNwQXZWF_MTPzElDg,7184
13
13
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- sapiopycommons/files/complex_data_loader.py,sha256=8jgYF5iGDD6Abw8JRWLYxUWVwj4s4pH5HylyxEGFZSU,1471
15
- sapiopycommons/files/file_bridge.py,sha256=njx_5Z3tvQUNW4mPazQerL8lopLAFStIByHWHJ7m5ug,6220
14
+ sapiopycommons/files/complex_data_loader.py,sha256=XSJOl676mIklJo78v07-70u1b015a5DI4sqZPI3C-Tw,1475
15
+ sapiopycommons/files/file_bridge.py,sha256=GI3-gWFzcL0q0c8jKOxTevbzJqtUpiElmkXfTnMsaOo,6224
16
16
  sapiopycommons/files/file_bridge_handler.py,sha256=MU2wZR4VY606yx6Bnv8-LzG3mGCeuXeRBn914WNRFCo,13601
17
17
  sapiopycommons/files/file_data_handler.py,sha256=3-guAdhJdeJWAFq1a27ijspkO7uMMZ6CapMCD_6o4jA,36746
18
- sapiopycommons/files/file_util.py,sha256=ronTlJimQ6ttJN6Y9qfAzEWoeWnDoUbohkdObNK5mJQ,27042
19
- sapiopycommons/files/file_validator.py,sha256=EqPCXfVCiilgnOb2G-yZg8XWeDqYp9iVCg8AaWznvf4,28040
18
+ sapiopycommons/files/file_util.py,sha256=44mzhn3M_QltoncBB-ooX7_yO6u5k-XU_bzUXHGxUiw,26299
19
+ sapiopycommons/files/file_validator.py,sha256=BhXB2XnoNEzdBXuwul1s2RNoj-3ZoiMmephUCU_0o3Y,28113
20
20
  sapiopycommons/files/file_writer.py,sha256=5u_iZXTQvuUU7ceHZr8Q001_tvgJhOqBwAnB_pxcAbQ,16027
21
21
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
@@ -31,13 +31,13 @@ sapiopycommons/multimodal/multimodal_data.py,sha256=zqgYHO-ULaPKV0POFWZVY9N-Sfm1
31
31
  sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  sapiopycommons/processtracking/endpoints.py,sha256=g5h_uCVByqacYm9zWAz8TyAdRsGfaO2o0b5RSJdOaSA,10926
33
33
  sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- sapiopycommons/recordmodel/record_handler.py,sha256=qqsHeGNyqQeFKuZ4U2v2MD278AlH4n1HMvSTOY0hbyo,58636
34
+ sapiopycommons/recordmodel/record_handler.py,sha256=AyK1H3x-g1eu1Mt9XD1h57yRrZp_TJjZlEaQ2kPP4Dc,54432
35
35
  sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- sapiopycommons/rules/eln_rule_handler.py,sha256=rz9E1PQLShCZM09JafMl_ylUqBbMs-et0FsOG41tQL0,9480
37
- sapiopycommons/rules/on_save_rule_handler.py,sha256=I06HwsNvulyU0avSXOU0itBDAmiOxsyW8KGk_gH0FHc,9238
36
+ sapiopycommons/rules/eln_rule_handler.py,sha256=qfkBZtck0KK1i9s9Xe2UZqkzQOgPCzDxRkhxE8Si1xk,10671
37
+ sapiopycommons/rules/on_save_rule_handler.py,sha256=JY9F30IcHwFVdgPAMQtTYuRastV1jeezhVktyrzNASU,10763
38
38
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  sapiopycommons/webhook/webhook_handlers.py,sha256=ibpBY3Sk3Eij919bIdW0awzlogYoQSWYDDOg--NwsQE,13431
40
- sapiopycommons-2024.7.23a296.dist-info/METADATA,sha256=dVBc2Wb8p-OLqvOAbKTFB71_otlN0RN3KLFDFidY9oY,3176
41
- sapiopycommons-2024.7.23a296.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
42
- sapiopycommons-2024.7.23a296.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
43
- sapiopycommons-2024.7.23a296.dist-info/RECORD,,
40
+ sapiopycommons-2024.7.23a297.dist-info/METADATA,sha256=ag5zhDGf1ubPLcNtPVmO32ehPESt0gSe-hkoAPShiwc,3176
41
+ sapiopycommons-2024.7.23a297.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
42
+ sapiopycommons-2024.7.23a297.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
43
+ sapiopycommons-2024.7.23a297.dist-info/RECORD,,