sapiopycommons 2024.8.29a317__py3-none-any.whl → 2024.8.30a320__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

Files changed (33) hide show
  1. sapiopycommons/callbacks/callback_util.py +133 -37
  2. sapiopycommons/customreport/__init__.py +0 -0
  3. sapiopycommons/customreport/column_builder.py +60 -0
  4. sapiopycommons/customreport/custom_report_builder.py +125 -0
  5. sapiopycommons/customreport/term_builder.py +299 -0
  6. sapiopycommons/datatype/attachment_util.py +11 -10
  7. sapiopycommons/eln/experiment_handler.py +209 -48
  8. sapiopycommons/eln/experiment_report_util.py +33 -129
  9. sapiopycommons/files/complex_data_loader.py +5 -4
  10. sapiopycommons/files/file_bridge.py +15 -14
  11. sapiopycommons/files/file_bridge_handler.py +27 -5
  12. sapiopycommons/files/file_data_handler.py +2 -5
  13. sapiopycommons/files/file_util.py +38 -5
  14. sapiopycommons/files/file_validator.py +26 -11
  15. sapiopycommons/files/file_writer.py +44 -15
  16. sapiopycommons/general/aliases.py +147 -3
  17. sapiopycommons/general/audit_log.py +196 -0
  18. sapiopycommons/general/custom_report_util.py +34 -32
  19. sapiopycommons/general/popup_util.py +17 -0
  20. sapiopycommons/general/sapio_links.py +50 -0
  21. sapiopycommons/general/time_util.py +40 -0
  22. sapiopycommons/multimodal/multimodal_data.py +0 -1
  23. sapiopycommons/processtracking/endpoints.py +22 -22
  24. sapiopycommons/recordmodel/record_handler.py +228 -77
  25. sapiopycommons/rules/eln_rule_handler.py +34 -25
  26. sapiopycommons/rules/on_save_rule_handler.py +34 -31
  27. sapiopycommons/webhook/webhook_handlers.py +90 -26
  28. sapiopycommons/webhook/webservice_handlers.py +67 -0
  29. {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/METADATA +1 -1
  30. sapiopycommons-2024.8.30a320.dist-info/RECORD +50 -0
  31. sapiopycommons-2024.8.29a317.dist-info/RECORD +0 -43
  32. {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/WHEEL +0 -0
  33. {sapiopycommons-2024.8.29a317.dist-info → sapiopycommons-2024.8.30a320.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  from collections.abc import Mapping, Iterable
5
+ from weakref import WeakValueDictionary
3
6
 
7
+ from sapiopylib.rest.DataMgmtService import DataMgmtServer
4
8
  from sapiopylib.rest.ELNService import ElnManager
9
+ from sapiopylib.rest.User import SapioUser
5
10
  from sapiopylib.rest.pojo.DataRecord import DataRecord
6
11
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
7
12
  InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
@@ -16,8 +21,10 @@ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
16
21
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
17
22
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
18
23
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
24
+ from sapiopylib.rest.utils.recordmodel.properties import Child
19
25
 
20
- from sapiopycommons.general.aliases import AliasUtil, SapioRecord
26
+ from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
27
+ DataTypeIdentifier, RecordModel
21
28
  from sapiopycommons.general.exceptions import SapioException
22
29
 
23
30
  Step = str | ElnEntryStep
@@ -27,7 +34,8 @@ itself."""
27
34
 
28
35
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
29
36
  class ExperimentHandler:
30
- context: SapioWebhookContext
37
+ user: SapioUser
38
+ context: SapioWebhookContext | None
31
39
  """The context that this handler is working from."""
32
40
 
33
41
  # Basic experiment info from the context.
@@ -77,45 +85,104 @@ class ExperimentHandler:
77
85
  ElnExperimentStatus.Canceled]
78
86
  """The set of statuses that an ELN experiment could have and be considered locked."""
79
87
 
80
- def __init__(self, context: SapioWebhookContext, experiment: ElnExperiment | None = None):
88
+ __instances: WeakValueDictionary[str, ExperimentHandler] = WeakValueDictionary()
89
+ __initialized: bool
90
+
91
+ def __new__(cls, context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None):
92
+ """
93
+ :param context: The current webhook context or a user object to send requests from.
94
+ :param experiment: If an experiment is provided that is separate from the experiment that is in the context,
95
+ that experiment will be used by this ExperimentHandler instead. An experiment can be provided in various
96
+ forms, including an ElnExperiment, ElnExperimentProtocol, an experiment record, or a notebook experiment ID.
97
+ """
98
+ param_results = cls.__parse_params(context, experiment)
99
+ user = param_results[0]
100
+ experiment = param_results[2]
101
+ key = f"{user.__hash__()}:{experiment.notebook_experiment_id}"
102
+ obj = cls.__instances.get(key)
103
+ if not obj:
104
+ obj = object.__new__(cls)
105
+ obj.__initialized = False
106
+ cls.__instances[key] = obj
107
+ return obj
108
+
109
+ def __init__(self, context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None):
81
110
  """
82
111
  Initialization will throw an exception if there is no ELN Experiment in the provided context and no experiment
83
112
  is provided.
84
113
 
85
- :param context: The current webhook context.
114
+ :param context: The current webhook context or a user object to send requests from.
86
115
  :param experiment: If an experiment is provided that is separate from the experiment that is in the context,
87
- that experiment will be used by this ExperimentHandler instead.
116
+ that experiment will be used by this ExperimentHandler instead. An experiment can be provided in various
117
+ forms, including an ElnExperiment, ElnExperimentProtocol, an experiment record, or a notebook experiment ID.
88
118
  """
89
- # FR-46495 - Allow the init function of ExperimentHandler to take in an ElnExperiment that is separate from the
90
- # context.
91
- if context.eln_experiment is None and experiment is None:
92
- raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment in the context.")
93
- if context.eln_experiment == experiment:
94
- experiment = None
95
- self.context = context
119
+ param_results = self.__parse_params(context, experiment)
120
+ self.user = param_results[0]
121
+ self.context = param_results[1]
122
+ experiment = param_results[2]
96
123
 
97
124
  # Get the basic information about this experiment that already exists in the context and is often used.
98
- self.__eln_exp = experiment if experiment else context.eln_experiment
99
- self.__protocol = ElnExperimentProtocol(experiment, context.user) if experiment else context.active_protocol
125
+ self.__eln_exp = experiment
126
+ self.__protocol = ElnExperimentProtocol(experiment, self.user)
100
127
  self.__exp_id = self.__protocol.get_id()
101
128
 
102
129
  # Grab various managers that may be used.
103
- self.__eln_man = context.eln_manager
104
- self.__inst_man = RecordModelManager(context.user).instance_manager
130
+ self.__eln_man = DataMgmtServer.get_eln_manager(self.user)
131
+ self.__inst_man = RecordModelManager(self.user).instance_manager
105
132
 
106
133
  # Create empty caches to fill when necessary.
107
134
  self.__steps = {}
108
135
  self.__step_options = {}
109
136
  # CR-46330: Cache any experiment entry information that might already exist in the context.
110
137
  self.__queried_all_steps = False
111
- # We can only trust the entries in the context if no experiment was given as an input parameter.
112
- if experiment is None:
113
- if context.experiment_entry is not None:
114
- self.__steps.update({context.active_step.get_name(): context.active_step})
115
- if context.experiment_entry_list is not None:
116
- for entry in context.experiment_entry_list:
138
+ # We can only trust the entries in the context if the experiment that this handler is for is the same as the
139
+ # one from the context.
140
+ if self.context is not None and self.context.eln_experiment == experiment:
141
+ if self.context.experiment_entry is not None:
142
+ self.__steps.update({self.context.active_step.get_name(): self.context.active_step})
143
+ if self.context.experiment_entry_list is not None:
144
+ for entry in self.context.experiment_entry_list:
117
145
  self.__steps.update({entry.entry_name: ElnEntryStep(self.__protocol, entry)})
118
146
 
147
+ @staticmethod
148
+ def __parse_params(context: UserIdentifier, experiment: ExperimentIdentifier | SapioRecord | None = None) \
149
+ -> tuple[SapioUser, SapioWebhookContext | None, ElnExperiment]:
150
+ if isinstance(context, SapioWebhookContext):
151
+ user = context.user
152
+ context = context
153
+ else:
154
+ user = context
155
+ context = None
156
+ if context is not None and context.eln_experiment is not None and experiment is None:
157
+ experiment = context.eln_experiment
158
+ # FR-46495 - Allow the init function of ExperimentHandler to take in an ElnExperiment that is separate from the
159
+ # context.
160
+ # CR-37038 - Allow other experiment object types to be provided. Convert them all down to ElnExperiment.
161
+ if (context is None or context.eln_experiment is None) and experiment is not None:
162
+ eln_manager = DataMgmtServer.get_eln_manager(user)
163
+ # If this object is already an ElnExperiment, do nothing.
164
+ if isinstance(experiment, ElnExperiment):
165
+ pass
166
+ # If this object is an ElnExperimentProtocol, then we can get the ElnExperiment from the object.
167
+ elif isinstance(experiment, ElnExperimentProtocol):
168
+ experiment: ElnExperiment = experiment.eln_experiment
169
+ # If this object is an integer, assume it is a notebook ID that we can query the system with.
170
+ elif isinstance(experiment, int):
171
+ notebook_id: int = experiment
172
+ experiment: ElnExperiment = eln_manager.get_eln_experiment_by_id(notebook_id)
173
+ if not experiment:
174
+ raise SapioException(f"No experiment with notebook ID {notebook_id} located in the system.")
175
+ # If this object is a record, assume it is an experiment record that we can query the system with.
176
+ else:
177
+ record_id: int = AliasUtil.to_record_ids([experiment])[0]
178
+ experiment: ElnExperiment = eln_manager.get_eln_experiment_by_record_id(record_id)
179
+ if not experiment:
180
+ raise SapioException(f"No experiment with record ID {record_id} located in the system.")
181
+ if experiment is None:
182
+ raise SapioException("Cannot initialize ExperimentHandler. No ELN Experiment found in the provided parameters.")
183
+
184
+ return user, context, experiment
185
+
119
186
  # FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
120
187
  @staticmethod
121
188
  def create_experiment(context: SapioWebhookContext,
@@ -263,11 +330,7 @@ class ExperimentHandler:
263
330
  :return: The data record for this experiment. None if it has no record.
264
331
  """
265
332
  if not hasattr(self, "_ExperimentHandler__exp_record"):
266
- drm = self.context.data_record_manager
267
- dt = self.__eln_exp.experiment_data_type_name
268
- results = drm.query_data_records_by_id(dt, [self.__eln_exp.experiment_record_id]).result_list
269
- # PR-46504: Set the exp_record to None if there are no results.
270
- self.__exp_record = results[0] if results else None
333
+ self.__exp_record = self.__protocol.get_record()
271
334
  if self.__exp_record is None and exception_on_none:
272
335
  raise SapioException(f"Experiment record not found for experiment with ID {self.__exp_id}.")
273
336
  return self.__exp_record
@@ -467,7 +530,7 @@ class ExperimentHandler:
467
530
  ret_list.append(step)
468
531
  return ret_list
469
532
 
470
- 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]:
471
534
  """
472
535
  Get a list of every entry in the experiment. Optionally filter the returned entries by a data type.
473
536
 
@@ -483,8 +546,7 @@ class ExperimentHandler:
483
546
  all_steps: list[ElnEntryStep] = self.__protocol.get_sorted_step_list()
484
547
  if data_type is None:
485
548
  return all_steps
486
- if not isinstance(data_type, str):
487
- data_type: str = data_type.get_wrapper_data_type_name()
549
+ data_type: str = AliasUtil.to_data_type_name(data_type)
488
550
  return [x for x in all_steps if data_type in x.get_data_type_names()]
489
551
 
490
552
  def get_step_records(self, step: Step) -> list[DataRecord]:
@@ -536,6 +598,10 @@ class ExperimentHandler:
536
598
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
537
599
  """
538
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]}.")
539
605
  step.add_records(AliasUtil.to_data_records(records))
540
606
 
541
607
  def remove_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
@@ -554,6 +620,10 @@ class ExperimentHandler:
554
620
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
555
621
  """
556
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]}.")
557
627
  step.remove_records(AliasUtil.to_data_records(records))
558
628
 
559
629
  def set_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
@@ -577,11 +647,13 @@ class ExperimentHandler:
577
647
  The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
578
648
  """
579
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]}.")
580
654
  step.set_records(AliasUtil.to_data_records(records))
581
655
 
582
656
  # 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.
585
657
  def set_form_record(self, step: Step, record: SapioRecord) -> None:
586
658
  """
587
659
  Sets the record for a form entry.
@@ -599,7 +671,8 @@ class ExperimentHandler:
599
671
  self.set_step_records(step, [record])
600
672
 
601
673
  # FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
602
- def add_eln_rows(self, step: Step, count: int) -> list[PyRecordModel]:
674
+ def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
675
+ -> list[PyRecordModel | WrappedType]:
603
676
  """
604
677
  Add rows to an ELNExperimentDetail or ELNSampleDetail table entry. The rows will not appear in the system
605
678
  until a record manager store and commit has occurred.
@@ -611,15 +684,37 @@ class ExperimentHandler:
611
684
  The step may be provided as either a string for the name of the step or an ElnEntryStep.
612
685
  If given a name, throws an exception if no step of the given name exists in the experiment.
613
686
  :param count: The number of new rows to add to the entry.
687
+ :param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
688
+ an unwrapped PyRecordModel.
614
689
  :return: A list of the newly created rows.
615
690
  """
616
691
  step = self.__to_eln_step(step)
617
692
  if step.eln_entry.entry_type != ElnEntryType.Table:
618
693
  raise SapioException("The provided step is not a table entry.")
619
694
  dt: str = step.get_data_type_names()[0]
620
- if not self.__is_eln_type(dt):
695
+ if not ElnBaseDataType.is_eln_type(dt):
621
696
  raise SapioException("The provided step is not an ELN data type entry.")
622
- return self.__inst_man.add_new_records(dt, count)
697
+ records: list[PyRecordModel] = self.__inst_man.add_new_records(dt, count)
698
+ if wrapper_type:
699
+ return self.__inst_man.wrap_list(records, wrapper_type)
700
+ return records
701
+
702
+ def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> PyRecordModel | WrappedType:
703
+ """
704
+ Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
705
+ until a record manager store and commit has occurred.
706
+
707
+ If no step functions have been called before and a step is being searched for by name, queries for the
708
+ list of steps in the experiment and caches them.
709
+
710
+ :param step:
711
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
712
+ If given a name, throws an exception if no step of the given name exists in the experiment.
713
+ :param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
714
+ an unwrapped PyRecordModel.
715
+ :return: The newly created row.
716
+ """
717
+ return self.add_eln_rows(step, 1, wrapper_type)[0]
623
718
 
624
719
  def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
625
720
  """
@@ -641,10 +736,12 @@ class ExperimentHandler:
641
736
  """
642
737
  step = self.__to_eln_step(step)
643
738
  dt: str = step.get_data_type_names()[0]
644
- if not self.__is_eln_type(dt):
739
+ if not ElnBaseDataType.is_eln_type(dt):
645
740
  raise SapioException("The provided step is not an ELN data type entry.")
646
- if any([x.data_type_name != dt for x in records]):
647
- 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]}.")
648
745
  # If any rows were provided as data records, turn them into record models before deleting them, as otherwise
649
746
  # this function would need to make a webservice call to do the deletion.
650
747
  data_records: list[DataRecord] = []
@@ -658,16 +755,59 @@ class ExperimentHandler:
658
755
  for record in record_models:
659
756
  record.delete()
660
757
 
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
758
+ def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
759
+ """
760
+ Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
761
+ records in the system that match the entry's data type. This means that removing rows from an ELN data type
762
+ table entry is equivalent to deleting the records for the rows.
763
+
764
+ The row will not be deleted in the system until a record manager store and commit has occurred.
765
+
766
+ If no step functions have been called before and a step is being searched for by name, queries for the
767
+ list of steps in the experiment and caches them.
768
+
769
+ :param step:
770
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
771
+ If given a name, throws an exception if no step of the given name exists in the experiment.
772
+ :param record:
773
+ The record to remove from the given step.
774
+ The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
775
+ """
776
+ self.remove_eln_row(step, [record])
777
+
778
+ def add_sample_details(self, step: Step, samples: list[RecordModel], wrapper_type: type[WrappedType]) \
779
+ -> list[PyRecordModel | WrappedType]:
780
+ """
781
+ Add sample details to a sample details entry while relating them to the input sample records.
782
+
783
+ :param step:
784
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
785
+ If given a name, throws an exception if no step of the given name exists in the experiment.
786
+ :param samples: The sample records to add the sample details to.
787
+ :param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
788
+ an unwrapped PyRecordModel.
789
+ :return: The newly created sample details. The indices of the samples in the input list match the index of the
790
+ sample details in this list that they are related to.
791
+ """
792
+ step = self.__to_eln_step(step)
793
+ if step.eln_entry.entry_type != ElnEntryType.Table:
794
+ raise SapioException("The provided step is not a table entry.")
795
+ dt: str = step.get_data_type_names()[0]
796
+ if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
797
+ raise SapioException("The provided step is not an ELNSampleDetail entry.")
798
+ records: list[PyRecordModel] = []
799
+ for sample in samples:
800
+ if sample.data_type_name != "Sample":
801
+ raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
802
+ detail: PyRecordModel = sample.add(Child.of_type_name(dt))
803
+ detail.set_field_values({
804
+ "SampleId": sample.get_field_value("SampleId"),
805
+ "OtherSampleId": sample.get_field_value("OtherSampleId")
806
+ })
807
+ records.append(detail)
808
+ if wrapper_type:
809
+ return self.__inst_man.wrap_list(records, wrapper_type)
810
+ return records
671
811
 
672
812
  def update_step(self, step: Step,
673
813
  entry_name: str | None = None,
@@ -956,6 +1096,27 @@ class ExperimentHandler:
956
1096
  step.unlock_step()
957
1097
  step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
958
1098
 
1099
+ def disable_step(self, step: Step) -> None:
1100
+ """
1101
+ Set the status of the input step to Disabled. This is the state that entries are in when they are waiting for
1102
+ entries that they are dependent upon to be submitted before they can be enabled. If you have unsubmitted an
1103
+ entry and want its dependent entries to be locked again, then you would use this to set their status to
1104
+ disabled.
1105
+
1106
+ Makes a webservice call to update the step. Checks if the step is already unlocked, and does nothing if so.
1107
+
1108
+ If no step functions have been called before and a step is being searched for by name, queries for the
1109
+ list of steps in the experiment and caches them.
1110
+
1111
+ :param step:
1112
+ The step to disable.
1113
+ The step may be provided as either a string for the name of the step or an ElnEntryStep.
1114
+ If given a name, throws an exception if no step of the given name exists in the experiment.
1115
+ """
1116
+ step = self.__to_eln_step(step)
1117
+ if step.eln_entry.entry_status in self.__ENTRY_LOCKED_STATUSES:
1118
+ self.update_step(step, entry_status=ExperimentEntryStatus.Disabled)
1119
+
959
1120
  def step_is_submitted(self, step: Step) -> bool:
960
1121
  """
961
1122
  Determine if the input step has already been submitted.
@@ -1,19 +1,10 @@
1
1
  from sapiopylib.rest.User import SapioUser
2
- from sapiopylib.rest.pojo.CustomReport import (
3
- CompositeReportTerm,
4
- CompositeTermOperation,
5
- CustomReportCriteria,
6
- ExplicitJoinDefinition,
7
- FieldCompareReportTerm,
8
- RawReportTerm,
9
- RawTermOperation,
10
- ReportColumn,
11
- )
12
2
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
13
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
14
3
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
15
4
 
16
- from sapiopycommons.general.aliases import SapioRecord
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
17
8
  from sapiopycommons.general.custom_report_util import CustomReportUtil
18
9
  from sapiopycommons.recordmodel.record_handler import RecordHandler
19
10
 
@@ -25,10 +16,8 @@ _RECORD_ID = "RECORDID"
25
16
  # that given records were used in or getting all records of a datatype used in given experiments.
26
17
  class ExperimentReportUtil:
27
18
  @staticmethod
28
- def map_records_to_experiment_ids(
29
- context: SapioWebhookContext | SapioUser,
30
- records: list[SapioRecord],
31
- ) -> dict[SapioRecord, list[int]]:
19
+ def map_records_to_experiment_ids(context: UserIdentifier, records: list[SapioRecord]) \
20
+ -> dict[SapioRecord, list[int]]:
32
21
  """
33
22
  Return a dictionary mapping each record to a list of ids of experiments that they were used in.
34
23
  If a record wasn't used in any experiments then it will be mapped to an empty list.
@@ -40,38 +29,25 @@ class ExperimentReportUtil:
40
29
  if not records:
41
30
  return {}
42
31
 
43
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
44
-
45
- data_type_name = records[0].data_type_name
32
+ user: SapioUser = AliasUtil.to_sapio_user(context)
33
+ data_type_name: str = AliasUtil.to_singular_data_type_name(records)
46
34
 
47
35
  record_ids = [record.record_id for record in records]
48
-
49
- rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
50
- user, data_type_name, record_ids=record_ids
51
- )
36
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, record_ids=record_ids)
52
37
 
53
38
  id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
54
-
55
- record_to_exps: dict[SapioRecord, set[int]] = {
56
- record: set() for record in records
57
- }
58
-
39
+ record_to_exps: dict[SapioRecord, set[int]] = {record: set() for record in records}
59
40
  for row in rows:
60
41
  record_id: int = row[_RECORD_ID]
61
42
  exp_id: int = row[_NOTEBOOK_ID]
62
-
63
43
  record = id_to_record[record_id]
64
-
65
44
  record_to_exps[record].add(exp_id)
66
45
 
67
46
  return {record: list(exps) for record, exps in record_to_exps.items()}
68
47
 
69
48
  @staticmethod
70
- def map_experiments_to_records_of_type(
71
- context: SapioWebhookContext | SapioUser,
72
- exp_ids: list[int],
73
- wrapper_type: type[WrappedType],
74
- ) -> dict[int, list[WrappedType]]:
49
+ def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[int],
50
+ wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
75
51
  """
76
52
  Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
77
53
  If an experiment didn't use any records of the given type then it will be mapped to an empty list.
@@ -84,41 +60,27 @@ class ExperimentReportUtil:
84
60
  if not exp_ids:
85
61
  return {}
86
62
 
87
- user = context if isinstance(context, SapioUser) else context.user
88
-
63
+ user = AliasUtil.to_sapio_user(context)
89
64
  record_handler = RecordHandler(user)
90
-
91
65
  data_type_name: str = wrapper_type.get_wrapper_data_type_name()
92
66
 
93
- rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
94
- user, data_type_name, exp_ids=exp_ids
95
- )
96
-
67
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, exp_ids=exp_ids)
97
68
  record_ids: set[int] = {row[_RECORD_ID] for row in rows}
98
-
99
69
  records = record_handler.query_models_by_id(wrapper_type, record_ids)
100
70
 
101
71
  id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
102
-
103
72
  exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
104
-
105
73
  for row in rows:
106
74
  record_id: int = row[_RECORD_ID]
107
75
  exp_id: int = row[_NOTEBOOK_ID]
108
-
109
76
  record = id_to_record[record_id]
110
-
111
77
  exp_to_records[exp_id].add(record)
112
78
 
113
79
  return {exp: list(records) for exp, records in exp_to_records.items()}
114
80
 
115
81
  @staticmethod
116
- def __get_record_experiment_relation_rows(
117
- user: SapioUser,
118
- data_type_name: str,
119
- record_ids: list[int] | None = None,
120
- exp_ids: list[int] | None = None,
121
- ) -> list[dict[str, int]]:
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]]:
122
84
  """
123
85
  Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
124
86
  At least one of record_ids and exp_ids should be provided.
@@ -126,89 +88,31 @@ class ExperimentReportUtil:
126
88
  assert (record_ids or exp_ids)
127
89
 
128
90
  if record_ids:
129
- rec_ids = [str(record_id) for record_id in record_ids]
130
-
131
- ids_str = "{" + ", ".join(rec_ids) + "}"
132
-
133
- records_term = RawReportTerm(
134
- data_type_name, "RECORDID", RawTermOperation.EQUAL_TO_OPERATOR, ids_str
135
- )
136
-
91
+ records_term = TermBuilder.is_term(data_type_name, "RECORDID", record_ids)
137
92
  else:
138
93
  # Get all records of the given type
139
- records_term = RawReportTerm(
140
- data_type_name,
141
- "RECORDID",
142
- RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
143
- "0",
144
- )
94
+ records_term = TermBuilder.all_records_term(data_type_name)
145
95
 
146
96
  if exp_ids:
147
- exp_ids = [str(exp_id) for exp_id in exp_ids]
148
-
149
- ids_str = "{" + ", ".join(exp_ids) + "}"
150
-
151
- exp_term = RawReportTerm(
152
- "NOTEBOOKEXPERIMENT",
153
- "EXPERIMENTID",
154
- RawTermOperation.EQUAL_TO_OPERATOR,
155
- ids_str,
156
- )
157
-
97
+ exp_term = TermBuilder.is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", exp_ids)
158
98
  else:
159
99
  # Get all experiments
160
- exp_term = RawReportTerm(
161
- "NOTEBOOKEXPERIMENT",
162
- "EXPERIMENTID",
163
- RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
164
- "0",
165
- )
166
-
167
- root_term = CompositeReportTerm(
168
- records_term, CompositeTermOperation.AND_OPERATOR, exp_term
169
- )
170
-
171
- # The columns the resulting dataframe will have
172
- column_list = [
173
- ReportColumn(data_type_name, "RECORDID", FieldType.LONG),
174
- ReportColumn("NOTEBOOKEXPERIMENT", "EXPERIMENTID", FieldType.LONG),
175
- ]
100
+ exp_term = TermBuilder.gte_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "0")
176
101
 
177
- # Join records on the experiment entry records that correspond to them.
178
- records_entry_join = FieldCompareReportTerm(
179
- data_type_name,
180
- "RECORDID",
181
- RawTermOperation.EQUAL_TO_OPERATOR,
182
- "EXPERIMENTENTRYRECORD",
183
- "RECORDID",
184
- )
102
+ root_term = TermBuilder.and_terms(records_term, exp_term)
185
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")
186
106
  # Join entry records on the experiment entries they are in.
187
- experiment_entry_enb_entry_join = FieldCompareReportTerm(
188
- "EXPERIMENTENTRYRECORD",
189
- "ENTRYID",
190
- RawTermOperation.EQUAL_TO_OPERATOR,
191
- "ENBENTRY",
192
- "ENTRYID",
193
- )
194
-
107
+ experiment_entry_enb_entry_join = TermBuilder.compare_is_term("ENBENTRY", "ENTRYID", "EXPERIMENTENTRYRECORD", "ENTRYID")
195
108
  # Join entries on the experiments they are in.
196
- enb_entry_experiment_join = FieldCompareReportTerm(
197
- "ENBENTRY",
198
- "EXPERIMENTID",
199
- RawTermOperation.EQUAL_TO_OPERATOR,
200
- "NOTEBOOKEXPERIMENT",
201
- "EXPERIMENTID",
202
- )
203
-
204
- report_criteria = CustomReportCriteria(
205
- column_list,
206
- root_term,
207
- join_list=[
208
- ExplicitJoinDefinition("EXPERIMENTENTRYRECORD", records_entry_join),
209
- ExplicitJoinDefinition("ENBENTRY", experiment_entry_enb_entry_join),
210
- ExplicitJoinDefinition("NOTEBOOKEXPERIMENT", enb_entry_experiment_join),
211
- ],
212
- )
213
-
214
- return CustomReportUtil.run_custom_report(user, report_criteria)
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,8 +23,8 @@ 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
- with io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data) as data_stream:
26
+ user: SapioUser = AliasUtil.to_sapio_user(context)
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)
29
30
  # The response content is returned as bytes for a comma separated string of record IDs.