sapiopycommons 2024.3.19a157__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 +46 -1
  5. sapiopycommons/chem/Molecules.py +100 -21
  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 +14 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +355 -91
  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 +149 -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 +263 -163
  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 +250 -15
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +251 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +59 -7
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +91 -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 +621 -148
  41. sapiopycommons/rules/eln_rule_handler.py +87 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +87 -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 +614 -71
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.19a157.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.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.19a157.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -1,26 +1,88 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageDisplayType(Enum):
5
+ """
6
+ An enum representing the different ways in which a message can be displayed to the user.
7
+ """
8
+ TOASTER_SUCCESS = 0
9
+ TOASTER_INFO = 1
10
+ TOASTER_WARNING = 2
11
+ TOASTER_ERROR = 3
12
+ OK_DIALOG = 4
13
+ DISPLAY_INFO = 5
14
+ DISPLAY_WARNING = 6
15
+ DISPLAY_ERROR = 7
16
+
17
+
1
18
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
2
19
  class SapioException(Exception):
3
20
  """
4
21
  A generic exception thrown by sapiopycommons methods. Typically caused by programmer error, but may also be from
5
22
  extremely edge case user errors. For expected user errors, use SapioUserErrorException.
23
+
24
+ CommonsWebhookHandler's default behavior for this and any other exception that doesn't extend SapioException is
25
+ to return a generic toaster message saying that an unexpected error has occurred.
26
+ """
27
+ pass
28
+
29
+
30
+ class SapioUserCancelledException(SapioException):
31
+ """
32
+ An exception thrown when the user cancels a client callback.
33
+
34
+ CommonsWebhookHandler's default behavior is to simply end the webhook session with a true result without logging
35
+ the exception.
6
36
  """
7
37
  pass
8
38
 
9
39
 
10
- class SapioUserErrorException(SapioException):
40
+ class SapioDialogTimeoutException(SapioException):
41
+ """
42
+ An exception thrown when the user leaves a client callback open for too long.
43
+
44
+ CommonsWebhookHandler's default behavior is to display an OK popup notifying the user that the dialog has timed out.
45
+ """
46
+ pass
47
+
48
+
49
+ class DisplayableException(SapioException):
50
+ """
51
+ A generic exception that promises to return a user-friendly message explaining the error that should be displayed to
52
+ the user. Note that it is up to whichever class that catches this exception to actually display the message.
53
+ """
54
+ msg: str
55
+ display_type: MessageDisplayType | None
56
+ title: str | None
57
+
58
+ def __init__(self, msg: str, display_type: MessageDisplayType | None = None, title: str | None = None):
59
+ """
60
+ :param msg: The message that should be displayed to the user.
61
+ :param display_type: The manner in which the message should be displayed. If None, then the display type should
62
+ be controlled by the class that catches this exception.
63
+ :param title: If the display type is able to have a title, this is the title that will be displayed. If None,
64
+ then the title should be controlled by the class that catches this exception.
65
+ """
66
+ self.msg = msg
67
+ self.display_type = display_type
68
+ self.title = title
69
+
70
+
71
+ class SapioUserErrorException(DisplayableException):
11
72
  """
12
73
  An exception caused by user error (e.g. user provided a CSV when an XLSX was expected), which promises to return a
13
- user-friendly message explaining the error that should be displayed to the user. It is the responsibility of the
14
- programmer to catch any such exceptions and return the value in e.args[0] as text for the user to see (such as
15
- through the display text of a webhook result).
74
+ user-friendly message explaining the error that should be displayed to the user.
75
+
76
+ CommonsWebhookHandler's default behavior is to return the error message in a toaster popup.
16
77
  """
17
78
  pass
18
79
 
19
80
 
20
- class SapioCriticalErrorException(SapioException):
81
+ class SapioCriticalErrorException(DisplayableException):
21
82
  """
22
83
  A critical exception caused by user error, which promises to return a user-friendly message explaining the error
23
- that should be displayed to the user. It is the responsibility of the programmer to catch any such exceptions and
24
- return the value in e.args[0] as text for the user to see (such as through a dialog form client callback request).
84
+ that should be displayed to the user.
85
+
86
+ CommonsWebhookHandler's default behavior is to return the error message in a display_error callback.
25
87
  """
26
88
  pass
@@ -1,4 +1,4 @@
1
- from typing import Any
1
+ import warnings
2
2
 
3
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
4
4
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
@@ -10,7 +10,7 @@ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
10
10
  from sapiopylib.rest.pojo.webhook.WebhookResult import SapioWebhookResult
11
11
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
12
12
 
13
- from sapiopycommons.general.aliases import SapioRecord, AliasUtil
13
+ from sapiopycommons.general.aliases import SapioRecord, AliasUtil, FieldMap
14
14
  from sapiopycommons.general.exceptions import SapioException
15
15
 
16
16
 
@@ -19,16 +19,21 @@ from sapiopycommons.general.exceptions import SapioException
19
19
  # and one FormEntryDialogRequest methods.)
20
20
  # CR-46332 - For any functions that use temporary data types, set the data type name, display name, and plural display
21
21
  # name in the form builder.
22
+ # FR-46716 - Add comments noting that this class is deprecated in favor of CallbackUtil.
22
23
  class PopupUtil:
23
24
  """
25
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
26
+
24
27
  Methods for creating boilerplate SapioWebhookResults with client callback requests to create popup dialogs.
25
28
  """
26
29
  @staticmethod
27
- def form_popup(title: str, msg: str, fields: list[AbstractVeloxFieldDefinition], values: dict[str, Any] = None,
30
+ def form_popup(title: str, msg: str, fields: list[AbstractVeloxFieldDefinition], values: FieldMap = None,
28
31
  column_positions: dict[str, tuple[int, int]] = None, data_type: str = "Default",
29
32
  *, display_name: str | None = None, plural_display_name: str | None = None,
30
33
  request_context: str | None = None) -> SapioWebhookResult:
31
34
  """
35
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
36
+
32
37
  Create a basic form entry dialog.
33
38
 
34
39
  The calling webhook must catch the FormEntryDialogResult that the client will send back.
@@ -48,6 +53,7 @@ class PopupUtil:
48
53
  :param request_context: Context that will be returned to the webhook server in the client callback result.
49
54
  :return: A SapioWebhookResult with the popup as its client callback request.
50
55
  """
56
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
51
57
  if display_name is None:
52
58
  display_name = data_type
53
59
  if plural_display_name is None:
@@ -74,12 +80,14 @@ class PopupUtil:
74
80
  column_positions: dict[str, tuple[int, int]] = None, editable: bool | None = True,
75
81
  *, request_context: str | None = None) -> SapioWebhookResult:
76
82
  """
83
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
84
+
77
85
  Create a basic form dialog for a record with displayed fields from the record data type.
78
86
 
79
87
  Makes webservice calls to get the data field and type definitions of the given data type.
80
88
  The calling webhook must catch the FormEntryDialogResult that the client will send back.
81
89
 
82
- :param context: The current webhook context
90
+ :param context: The current webhook context.
83
91
  :param title: The title of the dialog.
84
92
  :param msg: The message to display in the dialog.
85
93
  :param fields: The data field names of the fields from the record to display in the form. Fields will be
@@ -92,6 +100,7 @@ class PopupUtil:
92
100
  :param request_context: Context that will be returned to the webhook server in the client callback result.
93
101
  :return: A SapioWebhookResult with the popup as its client callback request.
94
102
  """
103
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
95
104
  # Get the field definitions of the data type.
96
105
  data_type: str = record.data_type_name
97
106
  type_man = DataMgmtServer.get_data_type_manager(context.user)
@@ -128,6 +137,8 @@ class PopupUtil:
128
137
  *, display_name: str | None = None, plural_display_name: str | None = None,
129
138
  request_context: str | None = None, **kwargs) -> SapioWebhookResult:
130
139
  """
140
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
141
+
131
142
  Create a form dialog with a single string field. May take additional parameters to be passed to the string field
132
143
  definition.
133
144
 
@@ -148,6 +159,7 @@ class PopupUtil:
148
159
  :param request_context: Context that will be returned to the webhook server in the client callback result.
149
160
  :return: A SapioWebhookResult with the popup as its client callback request.
150
161
  """
162
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
151
163
  if max_length is None:
152
164
  max_length = len(default_value) if default_value else 100
153
165
  string_field = VeloxStringFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -161,6 +173,8 @@ class PopupUtil:
161
173
  *, display_name: str | None = None, plural_display_name: str | None = None,
162
174
  request_context: str | None = None, **kwargs) -> SapioWebhookResult:
163
175
  """
176
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
177
+
164
178
  Create a form dialog with a single integer field. May take additional parameters to be passed to the integer
165
179
  field definition.
166
180
 
@@ -182,6 +196,7 @@ class PopupUtil:
182
196
  :param request_context: Context that will be returned to the webhook server in the client callback result.
183
197
  :return: A SapioWebhookResult with the popup as its client callback request.
184
198
  """
199
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
185
200
  if default_value is None:
186
201
  default_value = max(0, min_value)
187
202
  integer_field = VeloxIntegerFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -197,6 +212,8 @@ class PopupUtil:
197
212
  plural_display_name: str | None = None, request_context: str | None = None, **kwargs) \
198
213
  -> SapioWebhookResult:
199
214
  """
215
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
216
+
200
217
  Create a form dialog with a single double field. May take additional parameters to be passed to the double
201
218
  field definition.
202
219
 
@@ -218,6 +235,7 @@ class PopupUtil:
218
235
  :param request_context: Context that will be returned to the webhook server in the client callback result.
219
236
  :return: A SapioWebhookResult with the popup as its client callback request.
220
237
  """
238
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
221
239
  if default_value is None:
222
240
  default_value = min_value
223
241
  double_field = VeloxDoubleFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -226,10 +244,12 @@ class PopupUtil:
226
244
  plural_display_name=plural_display_name, request_context=request_context)
227
245
 
228
246
  @staticmethod
229
- def table_popup(title: str, msg: str, fields: list[AbstractVeloxFieldDefinition], values: list[dict[str, Any]],
247
+ def table_popup(title: str, msg: str, fields: list[AbstractVeloxFieldDefinition], values: list[FieldMap],
230
248
  *, data_type: str = "Default", display_name: str | None = None,
231
249
  plural_display_name: str | None = None, request_context: str | None = None) -> SapioWebhookResult:
232
250
  """
251
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
252
+
233
253
  Create a basic table entry dialog.
234
254
 
235
255
  The calling webhook must catch the TableEntryDialogResult that the client will send back.
@@ -247,6 +267,7 @@ class PopupUtil:
247
267
  :param request_context: Context that will be returned to the webhook server in the client callback result.
248
268
  :return: A SapioWebhookResult with the popup as its client callback request.
249
269
  """
270
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
250
271
  if display_name is None:
251
272
  display_name = data_type
252
273
  if plural_display_name is None:
@@ -264,6 +285,8 @@ class PopupUtil:
264
285
  title: str, msg: str, fields: list[str], records: list[SapioRecord],
265
286
  editable: bool | None = True, *, request_context: str | None = None) -> SapioWebhookResult:
266
287
  """
288
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
289
+
267
290
  Create a table dialog for a list of record where the columns are specific fields from the record data type.
268
291
 
269
292
  Makes webservice calls to get the data field and type definitions of the given data type.
@@ -280,12 +303,15 @@ class PopupUtil:
280
303
  :param request_context: Context that will be returned to the webhook server in the client callback result.
281
304
  :return: A SapioWebhookResult with the popup as its client callback request.
282
305
  """
306
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
307
+ if not records:
308
+ raise SapioException("No records provided.")
283
309
  data_types: set[str] = {x.data_type_name for x in records}
284
310
  if len(data_types) > 1:
285
311
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
286
312
  data_type: str = data_types.pop()
287
313
  # Get the field maps from the records.
288
- field_map_list: list[dict[str, Any]] = AliasUtil.to_field_map_lists(records)
314
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
289
315
  # Get the field definitions of the data type.
290
316
  type_man = DataMgmtServer.get_data_type_manager(context.user)
291
317
  type_def: DataTypeDefinition = type_man.get_data_type_definition(data_type)
@@ -315,6 +341,8 @@ class PopupUtil:
315
341
  msg: str, fields: list[str], records: list[SapioRecord], multi_select: bool = True,
316
342
  *, request_context: str | None = None) -> SapioWebhookResult:
317
343
  """
344
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
345
+
318
346
  Create a record selection dialog for a list of record where the columns are specific fields from the record data
319
347
  type.
320
348
 
@@ -330,12 +358,15 @@ class PopupUtil:
330
358
  :param request_context: Context that will be returned to the webhook server in the client callback result.
331
359
  :return: A SapioWebhookResult with the popup as its client callback request.
332
360
  """
361
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
362
+ if not records:
363
+ raise SapioException("No records provided.")
333
364
  data_types: set[str] = {x.data_type_name for x in records}
334
365
  if len(data_types) > 1:
335
366
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
336
367
  data_type: str = data_types.pop()
337
368
  # Get the field maps from the records.
338
- field_map_list: list[dict[str, Any]] = AliasUtil.to_field_map_lists(records)
369
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
339
370
  # Get the field definitions of the data type.
340
371
  type_man = DataMgmtServer.get_data_type_manager(context.user)
341
372
  type_def: DataTypeDefinition = type_man.get_data_type_definition(data_type)
@@ -362,6 +393,8 @@ class PopupUtil:
362
393
  def list_popup(title: str, options: list[str], multi_select: bool = False,
363
394
  *, request_context: str | None = None) -> SapioWebhookResult:
364
395
  """
396
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
397
+
365
398
  Create a list dialog with the given options for the user to choose from.
366
399
 
367
400
  The calling webhook must catch the ListDialogResult that the client will send back.
@@ -372,6 +405,7 @@ class PopupUtil:
372
405
  :param request_context: Context that will be returned to the webhook server in the client callback result.
373
406
  :return: A SapioWebhookResult with the popup as its client callback request.
374
407
  """
408
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
375
409
  callback = ListDialogRequest(title, multi_select, options,
376
410
  callback_context_data=request_context)
377
411
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -380,6 +414,8 @@ class PopupUtil:
380
414
  def option_popup(title: str, msg: str, options: list[str], default_option: int = 0, user_can_cancel: bool = False,
381
415
  *, request_context: str | None = None) -> SapioWebhookResult:
382
416
  """
417
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
418
+
383
419
  Create an option dialog with the given options for the user to choose from.
384
420
 
385
421
  The calling webhook must catch the OptionDialogResult that the client will send back.
@@ -394,6 +430,7 @@ class PopupUtil:
394
430
  :param request_context: Context that will be returned to the webhook server in the client callback result.
395
431
  :return: A SapioWebhookResult with the popup as its client callback request.
396
432
  """
433
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
397
434
  callback = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
398
435
  callback_context_data=request_context)
399
436
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -402,6 +439,8 @@ class PopupUtil:
402
439
  def ok_popup(title: str, msg: str, user_can_cancel: bool = False, *, request_context: str | None = None) \
403
440
  -> SapioWebhookResult:
404
441
  """
442
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
443
+
405
444
  Create an option dialog where the only option is "OK".
406
445
 
407
446
  The calling webhook must catch the OptionDialogResult that the client will send back.
@@ -414,12 +453,15 @@ class PopupUtil:
414
453
  :param request_context: Context that will be returned to the webhook server in the client callback result.
415
454
  :return: A SapioWebhookResult with the popup as its client callback request.
416
455
  """
456
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
417
457
  return PopupUtil.option_popup(title, msg, ["OK"], 0, user_can_cancel, request_context=request_context)
418
458
 
419
459
  @staticmethod
420
460
  def yes_no_popup(title: str, msg: str, default_yes: bool = True, user_can_cancel: bool = False,
421
461
  *, request_context: str | None = None) -> SapioWebhookResult:
422
462
  """
463
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
464
+
423
465
  Create an option dialog where the only options are "Yes" and "No".
424
466
 
425
467
  The calling webhook must catch the OptionDialogResult that the client will send back.
@@ -433,6 +475,7 @@ class PopupUtil:
433
475
  :param request_context: Context that will be returned to the webhook server in the client callback result.
434
476
  :return: A SapioWebhookResult with the popup as its client callback request.
435
477
  """
478
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
436
479
  return PopupUtil.option_popup(title, msg, ["Yes", "No"], 0 if default_yes else 1, user_can_cancel,
437
480
  request_context=request_context)
438
481
 
@@ -441,8 +484,11 @@ class PopupUtil:
441
484
  def display_form_popup(title: str, field_name: str, msg: str, data_type: str = "Popup",
442
485
  request_context: str | None = None) -> SapioWebhookResult:
443
486
  """
487
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
488
+
444
489
  Deprecated for PopupUtil.text_field_popup.
445
490
  """
491
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
446
492
  return PopupUtil.string_field_popup(title, "", field_name, msg, len(msg), False, data_type,
447
493
  request_context=request_context, auto_size=True)
448
494
 
@@ -450,13 +496,19 @@ class PopupUtil:
450
496
  def display_option_popup(title: str, msg: str, options: list[str], user_can_cancel: bool = False,
451
497
  request_context: str | None = None) -> SapioWebhookResult:
452
498
  """
499
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
500
+
453
501
  Deprecated for PopupUtil.option_popup.
454
502
  """
503
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
455
504
  return PopupUtil.option_popup(title, msg, options, 0, user_can_cancel, request_context=request_context)
456
505
 
457
506
  @staticmethod
458
507
  def display_ok_popup(title: str, msg: str, request_context: str | None = None) -> SapioWebhookResult:
459
508
  """
509
+ DEPRECATED: Make use of CallbackUtil as of 24.5.
510
+
460
511
  Deprecated for PopupUtil.ok_popup.
461
512
  """
513
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
462
514
  return PopupUtil.ok_popup(title, msg, False, request_context=request_context)
@@ -0,0 +1,50 @@
1
+ from sapiopylib.rest.User import SapioUser
2
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
3
+
4
+ from sapiopycommons.general.aliases import RecordIdentifier, ExperimentIdentifier, AliasUtil, DataTypeIdentifier
5
+ from sapiopycommons.general.exceptions import SapioException
6
+
7
+
8
+ class SapioNavigationLinker:
9
+ """
10
+ Given a URL to a system's webservice API (example: https://company.exemplareln.com/webservice/api), construct
11
+ URLs for navigation links to various locations in the system.
12
+ """
13
+ base_url: str
14
+
15
+ def __init__(self, url: str | SapioUser | SapioWebhookContext):
16
+ """
17
+ :param url: A user or context object that is being used to send requests to a Sapio system, or a URL to a
18
+ system's webservice API.
19
+ """
20
+ if isinstance(url, SapioWebhookContext):
21
+ url = url.user.url
22
+ elif isinstance(url, SapioUser):
23
+ url = url.url
24
+ self.base_url = url.rstrip("/").replace('webservice/api', 'veloxClient')
25
+
26
+ def data_record(self, record_identifier: RecordIdentifier, data_type_name: DataTypeIdentifier | None = None) -> str:
27
+ """
28
+ :param record_identifier: An object that can be used to identify a record in the system, be that a record ID,
29
+ a data record, or a record model.
30
+ :param data_type_name: If the provided record identifier is a record ID, then the data type name of the record
31
+ must be provided in this parameter. Otherwise, this parameter is ignored.
32
+ :return: A URL for navigating to the input record.
33
+ """
34
+ record_id: int = AliasUtil.to_record_id(record_identifier)
35
+ if data_type_name:
36
+ data_type_name = AliasUtil.to_data_type_name(data_type_name)
37
+ if not isinstance(record_identifier, int):
38
+ data_type_name = AliasUtil.to_data_type_name(record_identifier)
39
+ if not data_type_name:
40
+ raise SapioException("Unable to create a data record link without a data type name. "
41
+ "Only a record ID was provided.")
42
+ return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
43
+
44
+ def experiment(self, experiment: ExperimentIdentifier) -> str:
45
+ """
46
+ :param experiment: An object that can be used to identify an experiment in the system, be that an experiment
47
+ object, experiment protocol, or a notebook ID.
48
+ :return: A URL for navigating to the input experiment.
49
+ """
50
+ return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
@@ -0,0 +1,148 @@
1
+ class StorageUtil:
2
+ """
3
+ A collection of utilities intended for converting to and from various forms of representing positions on a storage
4
+ unit, such as character + integer (e.g. A1), two integers (e.g. (0, 0)), or a single integer index (e.g. 1).
5
+ Integers are also sometimes zero-indexed and sometimes one-indexed, so both are supported.
6
+ """
7
+ @staticmethod
8
+ def map_index_to_coordinate(index: int, size: int, fill_by_row: bool = True, zero_indexed_input: bool = False,
9
+ zero_indexed_output: bool = False, char_row_output: bool = True) \
10
+ -> tuple[int | str, int]:
11
+ """
12
+ Convert an index representing a position on a plate/storage unit to a coordinate pair on that plate/storage
13
+ unit, able to be used in row/column storage fields on records. This will output a value for any input index;
14
+ it is up to the caller to determine if the output will actually fit on the plate/storage unit.
15
+ (The index should be within the range [1 < index < rows * columns] for one-indexed values.)
16
+
17
+ By default, expects the input to be a one-indexed integer filled row-by-row with the output being a
18
+ character, integer pair where the output integer is one-indexed.
19
+
20
+ :param index: The index to map to a coordinate position.
21
+ :param size: The number of columns or rows in the plate/storage unit, depending on whether you are filling by
22
+ row or by column.
23
+ :param fill_by_row: If true, map positions row-by-row (A1, A2, A3... B1, B2...) and use the above size as the
24
+ number of columns in the plate/storage unit. If false, map positions column-by-column (A1, B1, C1... A2,
25
+ B2...) and use the above size as the number of rows.
26
+ :param zero_indexed_input: If true, the input index is zero-indexed. If false, then they are one-indexed.
27
+ This does not influence the output, only the function's understanding of the input.
28
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
29
+ Has no effect on the column output if the column is set to output as a character.
30
+ :param char_row_output: If true, the output row value is converted to a character where 0 = A, 1 = B, 25 = Z,
31
+ 26 = AA, 27 = AB, etc. If false, then it is returned as an integer.
32
+ :return: A tuple representing a coordinate pair (row value, column value). The row value may be either an
33
+ integer or a string, while the column value is always an integer, influenced by the input parameters.
34
+ """
35
+ # If the given index isn't zero-indexed, then make it zero-indexed by subtracting one.
36
+ if not zero_indexed_input:
37
+ index -= 1
38
+
39
+ row: int = index // size
40
+ col: int = index % size
41
+
42
+ # If fill by row is false, then the above calculations are flipped,
43
+ # meaning the row is actually the column and vice versa.
44
+ if not fill_by_row:
45
+ temp = row
46
+ row = col
47
+ col = temp
48
+ # The column and row are zero-indexed by default. If it should be one-indexed, add one.
49
+ if not zero_indexed_output:
50
+ col += 1
51
+ # Only add one to the row if it won't be converted to a character.
52
+ if not char_row_output:
53
+ row += 1
54
+ return StorageUtil.map_index_to_char(row, True) if char_row_output else row, col
55
+
56
+ @staticmethod
57
+ def map_coordinate_to_index(row: int | str, col: int | str, size: int, fill_by_row: bool = True,
58
+ zero_indexed_input: bool = False, zero_indexed_output: bool = False) -> int:
59
+ """
60
+ Map row and column coordinates on a plate/storage unit to the index of that position.
61
+
62
+ By default, expects the input to be provided as a character, integer pair and outputs a single row-by-row
63
+ one-indexed integer.
64
+
65
+ :param row: The row coordinate of the position as a string of characters from A-Z or a zero-indexed integer.
66
+ :param col: The column coordinate of the position as an integer which may be zero-indexed or one-indexed.
67
+ (This integer may be in string form, such as is the case with column fields on storable records.)
68
+ :param size: The number of columns or rows in the plate/storage unit, depending on whether you are filling by
69
+ row or by column.
70
+ :param fill_by_row: If true, map positions row-by-row (A1, A2, A3... B1, B2...) and use the above size as the
71
+ number of columns in the plate/storage unit. If false, map positions column-by-column (A1, B1, C1... A2,
72
+ B2...) and use the above size as the number of rows.
73
+ :param zero_indexed_input: If true, the input coordinates for the row and column is zero-indexed. If false,
74
+ then they are one-indexed. This does not influence the output, only the function's understanding of the
75
+ input. This also has no effect if the input row is a string.
76
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
77
+ :return: The index of the storage position at the input row and column.
78
+ """
79
+ # If the column was provided as a string, cast it to an int.
80
+ if isinstance(col, str):
81
+ col: int = int(col)
82
+ # If the input isn't zero-indexed, then make it zero-indexed.
83
+ if not zero_indexed_input:
84
+ col -= 1
85
+ # Only subtract from the row if it's already in integer form.
86
+ # If it's a string, it'll be converted to a zero-indexed integer.
87
+ if isinstance(row, int):
88
+ row -= 1
89
+ # If the input row is a string, convert it to a zero-indexed integer.
90
+ if isinstance(row, str):
91
+ row: int = StorageUtil.map_char_to_index(row, True)
92
+
93
+ # Convert the row and column indices to a singular index across the entire storage unit.
94
+ index: int = row * size + col if fill_by_row else col * size + row
95
+
96
+ # The index is zero-indexed by default. If it should be one-indexed, add one.
97
+ if not zero_indexed_output:
98
+ index += 1
99
+ return index
100
+
101
+ @staticmethod
102
+ def map_index_to_char(index: int, zero_indexed_input: bool = False) -> str:
103
+ """
104
+ Map a given base-10 integer to a base-26 value where 0 = A, 1 = B, 25 = Z, 26 = AA, 27 = AB, etc.
105
+ Useful for mapping the index of a row to the character(s) representing that row in a storage unit.
106
+ May also be used for mapping the index to an Excel sheet's columns.
107
+
108
+ By default, expects the input as a one-indexed value.
109
+
110
+ :param index: The index to map to a character.
111
+ :param zero_indexed_input: If true, the input index is zero-indexed. If false, then they are one-indexed.
112
+ This does not influence the output, only the function's understanding of the input.
113
+ :return: The input integer mapped to a string representing that integer's position.
114
+ """
115
+ # If the given index isn't zero-indexed, then make it zero-indexed by subtracting one.
116
+ if not zero_indexed_input:
117
+ index -= 1
118
+ chars: str = ""
119
+ while index >= 0:
120
+ # Add new characters to the front of the string.
121
+ chars = chr(ord("A") + index % 26) + chars
122
+ # Reduce the index by the amount accounted for by the character that was just added.
123
+ index = index // 26 - 1
124
+ return chars
125
+
126
+ @staticmethod
127
+ def map_char_to_index(chars: str, zero_indexed_output: bool = False) -> int:
128
+ """
129
+ Map a given base-26 value of characters to a base-10 integer where A = 0, B = 1, Z = 25, AA = 26, AB = 27, etc.
130
+ Useful for mapping the character(s) representing a row in a storage unit to that row's index.
131
+ May also be used for mapping the index of an Excel sheet's columns.
132
+
133
+ By default, provides the output as a one-indexed value.
134
+
135
+ :param chars: A string of characters to be converted to an index. Characters are expected to be uppercase
136
+ characters in the range A to Z.
137
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
138
+ :return: The input character(s) converted to an index.
139
+ """
140
+ # Reverse iterate over the characters of the string and determine the value of each individual character.
141
+ # The value is multiplied by the base of the character given its digit position (26^0, 26^1, etc.)
142
+ value: int = 0
143
+ for i, c in enumerate(reversed(chars)):
144
+ value += (ord(c) - ord("A") + 1) * (26 ** i)
145
+ # The character value is one-indexed by default. If it should be zero-indexed, subtract one.
146
+ if zero_indexed_output:
147
+ value -= 1
148
+ return value