sapiopycommons 2024.8.19a305__tar.gz → 2024.8.26a307__tar.gz

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 (60) hide show
  1. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/PKG-INFO +1 -1
  2. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/pyproject.toml +1 -1
  3. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/callbacks/callback_util.py +14 -15
  4. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/customreport/term_builder.py +5 -2
  5. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/datatype/attachment_util.py +6 -14
  6. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/eln/experiment_handler.py +24 -17
  7. sapiopycommons-2024.8.26a307/src/sapiopycommons/eln/experiment_report_util.py +118 -0
  8. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/complex_data_loader.py +4 -3
  9. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_bridge.py +14 -13
  10. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_bridge_handler.py +8 -7
  11. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_data_handler.py +2 -5
  12. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_validator.py +7 -7
  13. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/aliases.py +54 -1
  14. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/audit_log.py +19 -23
  15. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/custom_report_util.py +34 -32
  16. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/sapio_links.py +4 -2
  17. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/multimodal/multimodal_data.py +0 -1
  18. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/processtracking/endpoints.py +22 -22
  19. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/recordmodel/record_handler.py +119 -65
  20. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/rules/eln_rule_handler.py +5 -3
  21. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/rules/on_save_rule_handler.py +5 -3
  22. sapiopycommons-2024.8.19a305/src/sapiopycommons/eln/experiment_report_util.py +0 -214
  23. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/.gitignore +0 -0
  24. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/LICENSE +0 -0
  25. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/README.md +0 -0
  26. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/__init__.py +0 -0
  27. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/callbacks/__init__.py +0 -0
  28. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  29. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/chem/Molecules.py +0 -0
  30. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/chem/__init__.py +0 -0
  31. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/customreport/__init__.py +0 -0
  32. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/customreport/column_builder.py +0 -0
  33. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/customreport/custom_report_builder.py +0 -0
  34. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/datatype/__init__.py +0 -0
  35. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/eln/__init__.py +0 -0
  36. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/eln/plate_designer.py +0 -0
  37. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/__init__.py +0 -0
  38. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_util.py +0 -0
  39. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/files/file_writer.py +0 -0
  40. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/__init__.py +0 -0
  41. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/accession_service.py +0 -0
  42. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/exceptions.py +0 -0
  43. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/popup_util.py +0 -0
  44. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/storage_util.py +0 -0
  45. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/general/time_util.py +0 -0
  46. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  47. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/processtracking/__init__.py +0 -0
  48. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/recordmodel/__init__.py +0 -0
  49. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/rules/__init__.py +0 -0
  50. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/webhook/__init__.py +0 -0
  51. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/webhook/webhook_handlers.py +0 -0
  52. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/src/sapiopycommons/webhook/webservice_handlers.py +0 -0
  53. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/_do_not_add_init_py_here +0 -0
  54. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/accession_test.py +0 -0
  55. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/bio_reg_test.py +0 -0
  56. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/chem_test.py +0 -0
  57. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/data_type_models.py +0 -0
  58. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/kappa.chains.fasta +0 -0
  59. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/mafft_test.py +0 -0
  60. {sapiopycommons-2024.8.19a305 → sapiopycommons-2024.8.26a307}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.19a305
3
+ Version: 2024.8.26a307
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>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sapiopycommons"
7
- version='2024.08.19a305'
7
+ version='2024.08.26a307'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
- from typing import Any
5
4
  from weakref import WeakValueDictionary
6
5
 
7
6
  from sapiopylib.rest.ClientCallbackService import ClientCallback
@@ -18,7 +17,6 @@ from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogReque
18
17
  DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
19
18
  TempTableSelectionRequest, DisplayPopupRequest, PopupType
20
19
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
21
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
22
20
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
23
21
  from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
24
22
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
@@ -26,7 +24,8 @@ from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
26
24
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
27
25
 
28
26
  from sapiopycommons.files.file_util import FileUtil
29
- from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
27
+ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
28
+ UserIdentifier
30
29
  from sapiopycommons.general.custom_report_util import CustomReportUtil
31
30
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
32
31
  from sapiopycommons.recordmodel.record_handler import RecordHandler
@@ -42,11 +41,11 @@ class CallbackUtil:
42
41
  __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
43
42
  __initialized: bool
44
43
 
45
- def __new__(cls, context: SapioWebhookContext | SapioUser):
44
+ def __new__(cls, context: UserIdentifier):
46
45
  """
47
46
  :param context: The current webhook context or a user object to send requests from.
48
47
  """
49
- user = context if isinstance(context, SapioUser) else context.user
48
+ user = AliasUtil.to_sapio_user(context)
50
49
  obj = cls.__instances.get(user)
51
50
  if not obj:
52
51
  obj = object.__new__(cls)
@@ -54,7 +53,7 @@ class CallbackUtil:
54
53
  cls.__instances[user] = obj
55
54
  return obj
56
55
 
57
- def __init__(self, context: SapioWebhookContext | SapioUser):
56
+ def __init__(self, context: UserIdentifier):
58
57
  """
59
58
  :param context: The current webhook context or a user object to send requests from.
60
59
  """
@@ -62,7 +61,7 @@ class CallbackUtil:
62
61
  return
63
62
  self.__initialized = True
64
63
 
65
- self.user = context if isinstance(context, SapioUser) else context.user
64
+ self.user = AliasUtil.to_sapio_user(context)
66
65
  self.callback = DataMgmtServer.get_client_callback(self.user)
67
66
  self.dt_cache = DataTypeCacheManager(self.user)
68
67
  self.width_pixels = None
@@ -281,7 +280,7 @@ class CallbackUtil:
281
280
  modifier = FieldModifier(visible=True, editable=editable)
282
281
 
283
282
  # Build the form using only those fields that are desired.
284
- values: dict[str, Any] = {}
283
+ values: dict[str, FieldValue] = {}
285
284
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
286
285
  for field_name in fields:
287
286
  field_def = field_defs.get(field_name)
@@ -303,7 +302,7 @@ class CallbackUtil:
303
302
  raise SapioUserCancelledException()
304
303
  return response
305
304
 
306
- def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> Any:
305
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
307
306
  """
308
307
  Create an input dialog where the user must input data for a singular field.
309
308
 
@@ -314,7 +313,7 @@ class CallbackUtil:
314
313
  """
315
314
  request = InputDialogCriteria(title, msg, field,
316
315
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
317
- response: Any | None = self.callback.show_input_dialog(request)
316
+ response: FieldValue | None = self.callback.show_input_dialog(request)
318
317
  if response is None:
319
318
  raise SapioUserCancelledException()
320
319
  return response
@@ -606,10 +605,10 @@ class CallbackUtil:
606
605
  field_names.append(name)
607
606
 
608
607
  # Get the values for each row.
609
- values: list[dict[str, Any]] = []
608
+ values: list[dict[str, FieldValue]] = []
610
609
  for row in row_contents:
611
610
  # The final values for this row:
612
- row_values: dict[str, Any] = {}
611
+ row_values: dict[str, FieldValue] = {}
613
612
 
614
613
  # Map the records for this row by their data type. If a field map is provided, its data type is Default.
615
614
  row_records: dict[str, SapioRecord | FieldMap] = {}
@@ -901,7 +900,7 @@ class CallbackUtil:
901
900
  return response
902
901
 
903
902
  def request_file(self, title: str, exts: list[str] | None = None,
904
- show_image_editor: bool = False, show_camera_button: bool = False) -> (str, bytes):
903
+ show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
905
904
  """
906
905
  Request a single file from the user.
907
906
 
@@ -934,7 +933,7 @@ class CallbackUtil:
934
933
  return file_path, sink.data
935
934
 
936
935
  def request_files(self, title: str, exts: list[str] | None = None,
937
- show_image_editor: bool = False, show_camera_button: bool = False):
936
+ show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
938
937
  """
939
938
  Request multiple files from the user.
940
939
 
@@ -965,7 +964,7 @@ class CallbackUtil:
965
964
  return ret_dict
966
965
 
967
966
  @staticmethod
968
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]):
967
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
969
968
  """
970
969
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
971
970
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -18,7 +18,7 @@ AND = CompositeTermOperation.AND_OPERATOR
18
18
  OR = CompositeTermOperation.OR_OPERATOR
19
19
 
20
20
  # Forms that field term values can take.
21
- TermValue = str | int | float | bool | Iterable
21
+ TermValue = str | int | float | bool | Iterable | None
22
22
 
23
23
 
24
24
  class TermBuilder:
@@ -281,9 +281,12 @@ class TermBuilder:
281
281
  """
282
282
  # If the given value is already a string, then nothing needs to be done with it.
283
283
  if not isinstance(value, str):
284
+ # If the given value is None, then use an empty string for the search instead.
285
+ if value is None:
286
+ value = ""
284
287
  # If the given value is an iterable object, then the return value is the contents of that iterable
285
288
  # in a comma separated list surrounded by curly braces.
286
- if isinstance(value, Iterable):
289
+ elif isinstance(value, Iterable):
287
290
  # When converting a list of values to a string, values in the list which are already strings should be
288
291
  # put in quotation marks so that strings that contain commas do not get split up. All other value
289
292
  # types can be simply converted to a string, though.
@@ -1,11 +1,9 @@
1
1
  import io
2
2
 
3
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
4
- from sapiopylib.rest.User import SapioUser
5
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
6
4
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
7
5
 
8
- from sapiopycommons.general.aliases import AliasUtil, SapioRecord
6
+ from sapiopycommons.general.aliases import AliasUtil, SapioRecord, UserIdentifier
9
7
  from sapiopycommons.general.exceptions import SapioException
10
8
  from sapiopycommons.recordmodel.record_handler import RecordHandler
11
9
 
@@ -13,7 +11,7 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
13
11
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
14
12
  class AttachmentUtil:
15
13
  @staticmethod
16
- def get_attachment_bytes(context: SapioWebhookContext | SapioUser, attachment: SapioRecord) -> bytes:
14
+ def get_attachment_bytes(context: UserIdentifier, attachment: SapioRecord) -> bytes:
17
15
  """
18
16
  Get the data bytes for the given attachment record. Makes a webservice call to retrieve the data.
19
17
 
@@ -22,10 +20,7 @@ class AttachmentUtil:
22
20
  :return: The bytes for the attachment's file data.
23
21
  """
24
22
  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)
23
+ dr_man = DataMgmtServer.get_data_record_manager(AliasUtil.to_sapio_user(context))
29
24
  with io.BytesIO() as data_sink:
30
25
  def consume_data(chunk: bytes):
31
26
  data_sink.write(chunk)
@@ -36,7 +31,7 @@ class AttachmentUtil:
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: UserIdentifier, 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.
@@ -50,15 +45,12 @@ 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)
48
+ dr_man = DataMgmtServer.get_data_record_manager(AliasUtil.to_sapio_user(context))
57
49
  with io.BytesIO(file_bytes) as stream:
58
50
  dr_man.set_attachment_data(attachment, file_name, stream)
59
51
 
60
52
  @staticmethod
61
- def create_attachment(context: SapioWebhookContext | SapioUser, file_name: str, file_bytes: bytes,
53
+ def create_attachment(context: UserIdentifier, file_name: str, file_bytes: bytes,
62
54
  wrapper_type: type[WrappedType]) -> WrappedType:
63
55
  """
64
56
  Create an attachment data type and initialize its attachment bytes at the same time.
@@ -23,7 +23,8 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInst
23
23
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
24
24
  from sapiopylib.rest.utils.recordmodel.properties import Child
25
25
 
26
- from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, RecordModel
26
+ from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
27
+ DataTypeIdentifier, RecordModel
27
28
  from sapiopycommons.general.exceptions import SapioException
28
29
 
29
30
  Step = str | ElnEntryStep
@@ -87,8 +88,7 @@ class ExperimentHandler:
87
88
  __instances: WeakValueDictionary[str, ExperimentHandler] = WeakValueDictionary()
88
89
  __initialized: bool
89
90
 
90
- def __new__(cls, context: SapioWebhookContext | SapioUser,
91
- experiment: ExperimentIdentifier | SapioRecord | None = None):
91
+ def __new__(cls, context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None):
92
92
  """
93
93
  :param context: The current webhook context or a user object to send requests from.
94
94
  :param experiment: If an experiment is provided that is separate from the experiment that is in the context,
@@ -106,8 +106,7 @@ class ExperimentHandler:
106
106
  cls.__instances[key] = obj
107
107
  return obj
108
108
 
109
- def __init__(self, context: SapioWebhookContext | SapioUser,
110
- experiment: ExperimentIdentifier | SapioRecord | None = None):
109
+ def __init__(self, context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None):
111
110
  """
112
111
  Initialization will throw an exception if there is no ELN Experiment in the provided context and no experiment
113
112
  is provided.
@@ -146,8 +145,7 @@ class ExperimentHandler:
146
145
  self.__steps.update({entry.entry_name: ElnEntryStep(self.__protocol, entry)})
147
146
 
148
147
  @staticmethod
149
- def __parse_params(context: SapioWebhookContext | SapioUser,
150
- experiment: ExperimentIdentifier | SapioRecord | None = None) \
148
+ def __parse_params(context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None) \
151
149
  -> tuple[SapioUser, SapioWebhookContext | None, ElnExperiment]:
152
150
  if isinstance(context, SapioWebhookContext):
153
151
  user = context.user
@@ -332,11 +330,7 @@ class ExperimentHandler:
332
330
  :return: The data record for this experiment. None if it has no record.
333
331
  """
334
332
  if not hasattr(self, "_ExperimentHandler__exp_record"):
335
- drm = DataMgmtServer.get_data_record_manager(self.user)
336
- dt = self.__eln_exp.experiment_data_type_name
337
- results = drm.query_data_records_by_id(dt, [self.__eln_exp.experiment_record_id]).result_list
338
- # PR-46504: Set the exp_record to None if there are no results.
339
- self.__exp_record = results[0] if results else None
333
+ self.__exp_record = self.__protocol.get_record()
340
334
  if self.__exp_record is None and exception_on_none:
341
335
  raise SapioException(f"Experiment record not found for experiment with ID {self.__exp_id}.")
342
336
  return self.__exp_record
@@ -536,7 +530,7 @@ class ExperimentHandler:
536
530
  ret_list.append(step)
537
531
  return ret_list
538
532
 
539
- def get_all_steps(self, data_type: str | type[WrappedType] | None = None) -> list[ElnEntryStep]:
533
+ def get_all_steps(self, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
540
534
  """
541
535
  Get a list of every entry in the experiment. Optionally filter the returned entries by a data type.
542
536
 
@@ -552,8 +546,7 @@ class ExperimentHandler:
552
546
  all_steps: list[ElnEntryStep] = self.__protocol.get_sorted_step_list()
553
547
  if data_type is None:
554
548
  return all_steps
555
- if not isinstance(data_type, str):
556
- data_type: str = data_type.get_wrapper_data_type_name()
549
+ data_type: str = AliasUtil.to_data_type_name(data_type)
557
550
  return [x for x in all_steps if data_type in x.get_data_type_names()]
558
551
 
559
552
  def get_step_records(self, step: Step) -> list[DataRecord]:
@@ -605,6 +598,10 @@ class ExperimentHandler:
605
598
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
606
599
  """
607
600
  step = self.__to_eln_step(step)
601
+ dt: str = AliasUtil.to_singular_data_type_name(records)
602
+ if dt != step.get_data_type_names()[0]:
603
+ raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
604
+ f"{step.get_data_type_names()[0]}.")
608
605
  step.add_records(AliasUtil.to_data_records(records))
609
606
 
610
607
  def remove_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
@@ -623,6 +620,10 @@ class ExperimentHandler:
623
620
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
624
621
  """
625
622
  step = self.__to_eln_step(step)
623
+ dt: str = AliasUtil.to_singular_data_type_name(records)
624
+ if dt != step.get_data_type_names()[0]:
625
+ raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
626
+ f"{step.get_data_type_names()[0]}.")
626
627
  step.remove_records(AliasUtil.to_data_records(records))
627
628
 
628
629
  def set_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
@@ -646,6 +647,10 @@ class ExperimentHandler:
646
647
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
647
648
  """
648
649
  step = self.__to_eln_step(step)
650
+ dt: str = AliasUtil.to_singular_data_type_name(records)
651
+ if dt != step.get_data_type_names()[0]:
652
+ raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
653
+ f"{step.get_data_type_names()[0]}.")
649
654
  step.set_records(AliasUtil.to_data_records(records))
650
655
 
651
656
  # FR-46496 - Provide alias of set_step_records for use with form entries.
@@ -733,8 +738,10 @@ class ExperimentHandler:
733
738
  dt: str = step.get_data_type_names()[0]
734
739
  if not ElnBaseDataType.is_eln_type(dt):
735
740
  raise SapioException("The provided step is not an ELN data type entry.")
736
- if any([x.data_type_name != dt for x in records]):
737
- raise SapioException("Not all of the provided records match the data type of the step.")
741
+ record_dt: str = AliasUtil.to_singular_data_type_name(records)
742
+ if record_dt != dt:
743
+ raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
744
+ f"{step.get_data_type_names()[0]}.")
738
745
  # If any rows were provided as data records, turn them into record models before deleting them, as otherwise
739
746
  # this function would need to make a webservice call to do the deletion.
740
747
  data_records: list[DataRecord] = []
@@ -0,0 +1,118 @@
1
+ from sapiopylib.rest.User import SapioUser
2
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
3
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
4
+
5
+ from sapiopycommons.customreport.custom_report_builder import CustomReportBuilder
6
+ from sapiopycommons.customreport.term_builder import TermBuilder
7
+ from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil
8
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
9
+ from sapiopycommons.recordmodel.record_handler import RecordHandler
10
+
11
+ _NOTEBOOK_ID = "EXPERIMENTID"
12
+ _RECORD_ID = "RECORDID"
13
+
14
+
15
+ # FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
16
+ # that given records were used in or getting all records of a datatype used in given experiments.
17
+ class ExperimentReportUtil:
18
+ @staticmethod
19
+ def map_records_to_experiment_ids(context: UserIdentifier, records: list[SapioRecord]) \
20
+ -> dict[SapioRecord, list[int]]:
21
+ """
22
+ Return a dictionary mapping each record to a list of ids of experiments that they were used in.
23
+ If a record wasn't used in any experiments then it will be mapped to an empty list.
24
+
25
+ :param context: The current webhook context or a user object to send requests from.
26
+ :param records: a list of records of the same data type.
27
+ :return: a dictionary mapping each record to a list of ids of each experiment it was used in.
28
+ """
29
+ if not records:
30
+ return {}
31
+
32
+ user: SapioUser = AliasUtil.to_sapio_user(context)
33
+ data_type_name: str = AliasUtil.to_singular_data_type_name(records)
34
+
35
+ record_ids = [record.record_id for record in records]
36
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, record_ids=record_ids)
37
+
38
+ id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
39
+ record_to_exps: dict[SapioRecord, set[int]] = {record: set() for record in records}
40
+ for row in rows:
41
+ record_id: int = row[_RECORD_ID]
42
+ exp_id: int = row[_NOTEBOOK_ID]
43
+ record = id_to_record[record_id]
44
+ record_to_exps[record].add(exp_id)
45
+
46
+ return {record: list(exps) for record, exps in record_to_exps.items()}
47
+
48
+ @staticmethod
49
+ def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[int],
50
+ wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
51
+ """
52
+ Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
53
+ If an experiment didn't use any records of the given type then it will be mapped to an empty list.
54
+
55
+ :param context: The current webhook context or a user object to send requests from.
56
+ :param exp_ids: a list of experiment ids. These are specifically the Notebook Experiment ids which can be found in the title of the experiment.
57
+ :param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
58
+ :return: a dictionary mapping each experiment id to a list of records of the given type that were used in that experiment.
59
+ """
60
+ if not exp_ids:
61
+ return {}
62
+
63
+ user = AliasUtil.to_sapio_user(context)
64
+ record_handler = RecordHandler(user)
65
+ data_type_name: str = wrapper_type.get_wrapper_data_type_name()
66
+
67
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, exp_ids=exp_ids)
68
+ record_ids: set[int] = {row[_RECORD_ID] for row in rows}
69
+ records = record_handler.query_models_by_id(wrapper_type, record_ids)
70
+
71
+ id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
72
+ exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
73
+ for row in rows:
74
+ record_id: int = row[_RECORD_ID]
75
+ exp_id: int = row[_NOTEBOOK_ID]
76
+ record = id_to_record[record_id]
77
+ exp_to_records[exp_id].add(record)
78
+
79
+ return {exp: list(records) for exp, records in exp_to_records.items()}
80
+
81
+ @staticmethod
82
+ def __get_record_experiment_relation_rows(user: SapioUser, data_type_name: str, record_ids: list[int] | None = None,
83
+ exp_ids: list[int] | None = None) -> list[dict[str, int]]:
84
+ """
85
+ Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
86
+ At least one of record_ids and exp_ids should be provided.
87
+ """
88
+ assert (record_ids or exp_ids)
89
+
90
+ if record_ids:
91
+ records_term = TermBuilder.is_term(data_type_name, "RECORDID", record_ids)
92
+ else:
93
+ # Get all records of the given type
94
+ records_term = TermBuilder.all_records_term(data_type_name)
95
+
96
+ if exp_ids:
97
+ exp_term = TermBuilder.is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", exp_ids)
98
+ else:
99
+ # Get all experiments
100
+ exp_term = TermBuilder.gte_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "0")
101
+
102
+ root_term = TermBuilder.and_terms(records_term, exp_term)
103
+
104
+ # Join records on the experiment entry records that correspond to them.
105
+ records_entry_join = TermBuilder.compare_is_term("EXPERIMENTENTRYRECORD", "RECORDID", data_type_name, "RECORDID")
106
+ # Join entry records on the experiment entries they are in.
107
+ experiment_entry_enb_entry_join = TermBuilder.compare_is_term("ENBENTRY", "ENTRYID", "EXPERIMENTENTRYRECORD", "ENTRYID")
108
+ # Join entries on the experiments they are in.
109
+ enb_entry_experiment_join = TermBuilder.compare_is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "ENBENTRY", "EXPERIMENTID")
110
+
111
+ report_builder = CustomReportBuilder(data_type_name)
112
+ report_builder.set_root_term(root_term)
113
+ report_builder.add_column("RECORDID", FieldType.LONG, data_type=data_type_name)
114
+ report_builder.add_column("EXPERIMENTID", FieldType.LONG, data_type="NOTEBOOKEXPERIMENT")
115
+ report_builder.add_join(records_entry_join)
116
+ report_builder.add_join(experiment_entry_enb_entry_join)
117
+ report_builder.add_join(enb_entry_experiment_join)
118
+ return CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria())
@@ -1,12 +1,13 @@
1
1
  import io
2
2
 
3
3
  from sapiopylib.rest.User import SapioUser
4
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
4
+
5
+ from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
5
6
 
6
7
 
7
8
  class CDL:
8
9
  @staticmethod
9
- def load_cdl(context: SapioWebhookContext | SapioUser, config_name: str, file_name: str, file_data: bytes | str) \
10
+ def load_cdl(context: UserIdentifier, config_name: str, file_name: str, file_data: bytes | str) \
10
11
  -> list[int]:
11
12
  """
12
13
  Create data records from a file using one of the complex data loader (CDL) configurations in the system.
@@ -22,7 +23,7 @@ class CDL:
22
23
  "configName": config_name,
23
24
  "fileName": file_name
24
25
  }
25
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
26
+ user: SapioUser = AliasUtil.to_sapio_user(context)
26
27
  with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
27
28
  response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
28
29
  user.raise_for_status(response)
@@ -4,13 +4,14 @@ import urllib.parse
4
4
 
5
5
  from requests import Response
6
6
  from sapiopylib.rest.User import SapioUser
7
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
7
+
8
+ from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
8
9
 
9
10
 
10
11
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
11
12
  class FileBridge:
12
13
  @staticmethod
13
- def read_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str,
14
+ def read_file(context: UserIdentifier, bridge_name: str, file_path: str,
14
15
  base64_decode: bool = True) -> bytes:
15
16
  """
16
17
  Read a file from FileBridge.
@@ -27,7 +28,7 @@ class FileBridge:
27
28
  params = {
28
29
  'Filepath': f"bridge://{bridge_name}/{file_path}"
29
30
  }
30
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
31
+ user: SapioUser = AliasUtil.to_sapio_user(context)
31
32
  response = user.get(sub_path, params)
32
33
  user.raise_for_status(response)
33
34
 
@@ -37,7 +38,7 @@ class FileBridge:
37
38
  return ret_val
38
39
 
39
40
  @staticmethod
40
- def write_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str,
41
+ def write_file(context: UserIdentifier, bridge_name: str, file_path: str,
41
42
  file_data: bytes | str) -> None:
42
43
  """
43
44
  Write a file to FileBridge.
@@ -53,13 +54,13 @@ class FileBridge:
53
54
  params = {
54
55
  'Filepath': f"bridge://{bridge_name}/{file_path}"
55
56
  }
56
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
57
+ user: SapioUser = AliasUtil.to_sapio_user(context)
57
58
  with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
58
59
  response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
59
60
  user.raise_for_status(response)
60
61
 
61
62
  @staticmethod
62
- def list_directory(context: SapioWebhookContext | SapioUser, bridge_name: str,
63
+ def list_directory(context: UserIdentifier, bridge_name: str,
63
64
  file_path: str | None = "") -> list[str]:
64
65
  """
65
66
  List the contents of a FileBridge directory.
@@ -74,7 +75,7 @@ class FileBridge:
74
75
  params = {
75
76
  'Filepath': f"bridge://{bridge_name}/{file_path}"
76
77
  }
77
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
78
+ user: SapioUser = AliasUtil.to_sapio_user(context)
78
79
  response: Response = user.get(sub_path, params=params)
79
80
  user.raise_for_status(response)
80
81
 
@@ -83,7 +84,7 @@ class FileBridge:
83
84
  return [urllib.parse.unquote(value)[path_length:] for value in response_body]
84
85
 
85
86
  @staticmethod
86
- def create_directory(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
87
+ def create_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
87
88
  """
88
89
  Create a new directory in FileBridge.
89
90
 
@@ -97,12 +98,12 @@ class FileBridge:
97
98
  params = {
98
99
  'Filepath': f"bridge://{bridge_name}/{file_path}"
99
100
  }
100
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
101
+ user: SapioUser = AliasUtil.to_sapio_user(context)
101
102
  response = user.post(sub_path, params=params)
102
103
  user.raise_for_status(response)
103
104
 
104
105
  @staticmethod
105
- def delete_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
106
+ def delete_file(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
106
107
  """
107
108
  Delete an existing file in FileBridge.
108
109
 
@@ -115,12 +116,12 @@ class FileBridge:
115
116
  params = {
116
117
  'Filepath': f"bridge://{bridge_name}/{file_path}"
117
118
  }
118
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
119
+ user: SapioUser = AliasUtil.to_sapio_user(context)
119
120
  response = user.delete(sub_path, params=params)
120
121
  user.raise_for_status(response)
121
122
 
122
123
  @staticmethod
123
- def delete_directory(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
124
+ def delete_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
124
125
  """
125
126
  Delete an existing directory in FileBridge.
126
127
 
@@ -133,6 +134,6 @@ class FileBridge:
133
134
  params = {
134
135
  'Filepath': f"bridge://{bridge_name}/{file_path}"
135
136
  }
136
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
137
+ user: SapioUser = AliasUtil.to_sapio_user(context)
137
138
  response = user.delete(sub_path, params=params)
138
139
  user.raise_for_status(response)
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
  from abc import abstractmethod, ABC
4
4
  from weakref import WeakValueDictionary
5
5
 
6
- from sapiopycommons.files.file_bridge import FileBridge
7
6
  from sapiopylib.rest.User import SapioUser
8
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
7
+
8
+ from sapiopycommons.files.file_bridge import FileBridge
9
+ from sapiopycommons.general.aliases import AliasUtil, UserIdentifier
9
10
 
10
11
 
11
12
  class FileBridgeHandler:
@@ -27,11 +28,11 @@ class FileBridgeHandler:
27
28
  __instances: WeakValueDictionary[str, FileBridgeHandler] = WeakValueDictionary()
28
29
  __initialized: bool
29
30
 
30
- def __new__(cls, context: SapioWebhookContext | SapioUser, bridge_name: str):
31
+ def __new__(cls, context: UserIdentifier, bridge_name: str):
31
32
  """
32
33
  :param context: The current webhook context or a user object to send requests from.
33
34
  """
34
- user = context if isinstance(context, SapioUser) else context.user
35
+ user = AliasUtil.to_sapio_user(context)
35
36
  key = f"{user.__hash__()}:{bridge_name}"
36
37
  obj = cls.__instances.get(key)
37
38
  if not obj:
@@ -40,7 +41,7 @@ class FileBridgeHandler:
40
41
  cls.__instances[key] = obj
41
42
  return obj
42
43
 
43
- def __init__(self, context: SapioWebhookContext | SapioUser, bridge_name: str):
44
+ def __init__(self, context: UserIdentifier, bridge_name: str):
44
45
  """
45
46
  :param context: The current webhook context or a user object to send requests from.
46
47
  :param bridge_name: The name of the bridge to communicate with. This is the "connection name" in the
@@ -50,7 +51,7 @@ class FileBridgeHandler:
50
51
  return
51
52
  self.__initialized = True
52
53
 
53
- self.user = context if isinstance(context, SapioUser) else context.user
54
+ self.user = AliasUtil.to_sapio_user(context)
54
55
  self.__bridge = bridge_name
55
56
  self.__file_cache = {}
56
57
  self.__files = {}
@@ -327,7 +328,7 @@ class Directory(FileBridgeObject):
327
328
  return {x: y for x, y in self.contents.items() if not y.is_file()}
328
329
 
329
330
 
330
- def split_path(file_path: str) -> (str, str):
331
+ def split_path(file_path: str) -> tuple[str, str]:
331
332
  """
332
333
  :param file_path: A file path where directories are separated the "/" characters.
333
334
  :return: A tuple of two strings that splits the path on its last slash. The first string is the name of the
@@ -1,13 +1,10 @@
1
1
  import re
2
2
  from typing import Any, Callable, Iterable
3
3
 
4
- from sapiopycommons.general.exceptions import SapioException
5
-
6
- from sapiopycommons.recordmodel.record_handler import RecordHandler
7
-
8
4
  from sapiopycommons.general.aliases import SapioRecord
9
-
5
+ from sapiopycommons.general.exceptions import SapioException
10
6
  from sapiopycommons.general.time_util import TimeUtil
7
+ from sapiopycommons.recordmodel.record_handler import RecordHandler
11
8
 
12
9
  FilterList = Iterable[int] | range | Callable[[int, dict[str, Any]], bool] | None
13
10
  """A FilterList is an object used to determine if a row in the file data should be skipped over. This can take the