sapiopycommons 2024.8.15a304__py3-none-any.whl → 2024.8.20a306__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.
- sapiopycommons/callbacks/callback_util.py +130 -34
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/eln/experiment_handler.py +209 -44
- sapiopycommons/eln/experiment_report_util.py +33 -129
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +15 -14
- sapiopycommons/files/file_bridge_handler.py +26 -4
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +38 -5
- sapiopycommons/files/file_validator.py +26 -11
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/general/aliases.py +147 -3
- sapiopycommons/general/audit_log.py +196 -0
- sapiopycommons/general/custom_report_util.py +34 -32
- sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +40 -0
- sapiopycommons/multimodal/multimodal_data.py +0 -1
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +183 -61
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.20a306.dist-info/RECORD +50 -0
- sapiopycommons-2024.8.15a304.dist-info/RECORD +0 -43
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.20a306.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
99
|
-
self.__protocol = ElnExperimentProtocol(experiment,
|
|
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 =
|
|
104
|
-
self.__inst_man = RecordModelManager(
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,7 +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.
|
|
333
|
+
drm = DataMgmtServer.get_data_record_manager(self.user)
|
|
267
334
|
dt = self.__eln_exp.experiment_data_type_name
|
|
268
335
|
results = drm.query_data_records_by_id(dt, [self.__eln_exp.experiment_record_id]).result_list
|
|
269
336
|
# PR-46504: Set the exp_record to None if there are no results.
|
|
@@ -467,7 +534,7 @@ class ExperimentHandler:
|
|
|
467
534
|
ret_list.append(step)
|
|
468
535
|
return ret_list
|
|
469
536
|
|
|
470
|
-
def get_all_steps(self, data_type:
|
|
537
|
+
def get_all_steps(self, data_type: DataTypeIdentifier | None = None) -> list[ElnEntryStep]:
|
|
471
538
|
"""
|
|
472
539
|
Get a list of every entry in the experiment. Optionally filter the returned entries by a data type.
|
|
473
540
|
|
|
@@ -483,8 +550,7 @@ class ExperimentHandler:
|
|
|
483
550
|
all_steps: list[ElnEntryStep] = self.__protocol.get_sorted_step_list()
|
|
484
551
|
if data_type is None:
|
|
485
552
|
return all_steps
|
|
486
|
-
|
|
487
|
-
data_type: str = data_type.get_wrapper_data_type_name()
|
|
553
|
+
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
488
554
|
return [x for x in all_steps if data_type in x.get_data_type_names()]
|
|
489
555
|
|
|
490
556
|
def get_step_records(self, step: Step) -> list[DataRecord]:
|
|
@@ -536,6 +602,10 @@ class ExperimentHandler:
|
|
|
536
602
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
537
603
|
"""
|
|
538
604
|
step = self.__to_eln_step(step)
|
|
605
|
+
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
606
|
+
if dt != step.get_data_type_names()[0]:
|
|
607
|
+
raise SapioException(f"Cannot add {dt} records to entry {step.get_name()} of type "
|
|
608
|
+
f"{step.get_data_type_names()[0]}.")
|
|
539
609
|
step.add_records(AliasUtil.to_data_records(records))
|
|
540
610
|
|
|
541
611
|
def remove_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
@@ -554,6 +624,10 @@ class ExperimentHandler:
|
|
|
554
624
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
555
625
|
"""
|
|
556
626
|
step = self.__to_eln_step(step)
|
|
627
|
+
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
628
|
+
if dt != step.get_data_type_names()[0]:
|
|
629
|
+
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
630
|
+
f"{step.get_data_type_names()[0]}.")
|
|
557
631
|
step.remove_records(AliasUtil.to_data_records(records))
|
|
558
632
|
|
|
559
633
|
def set_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
@@ -577,11 +651,13 @@ class ExperimentHandler:
|
|
|
577
651
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
578
652
|
"""
|
|
579
653
|
step = self.__to_eln_step(step)
|
|
654
|
+
dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
655
|
+
if dt != step.get_data_type_names()[0]:
|
|
656
|
+
raise SapioException(f"Cannot set {dt} records for entry {step.get_name()} of type "
|
|
657
|
+
f"{step.get_data_type_names()[0]}.")
|
|
580
658
|
step.set_records(AliasUtil.to_data_records(records))
|
|
581
659
|
|
|
582
660
|
# 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
661
|
def set_form_record(self, step: Step, record: SapioRecord) -> None:
|
|
586
662
|
"""
|
|
587
663
|
Sets the record for a form entry.
|
|
@@ -599,7 +675,8 @@ class ExperimentHandler:
|
|
|
599
675
|
self.set_step_records(step, [record])
|
|
600
676
|
|
|
601
677
|
# 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
|
|
678
|
+
def add_eln_rows(self, step: Step, count: int, wrapper_type: type[WrappedType] | None = None) \
|
|
679
|
+
-> list[PyRecordModel | WrappedType]:
|
|
603
680
|
"""
|
|
604
681
|
Add rows to an ELNExperimentDetail or ELNSampleDetail table entry. The rows will not appear in the system
|
|
605
682
|
until a record manager store and commit has occurred.
|
|
@@ -611,15 +688,37 @@ class ExperimentHandler:
|
|
|
611
688
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
612
689
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
613
690
|
:param count: The number of new rows to add to the entry.
|
|
691
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
692
|
+
an unwrapped PyRecordModel.
|
|
614
693
|
:return: A list of the newly created rows.
|
|
615
694
|
"""
|
|
616
695
|
step = self.__to_eln_step(step)
|
|
617
696
|
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
618
697
|
raise SapioException("The provided step is not a table entry.")
|
|
619
698
|
dt: str = step.get_data_type_names()[0]
|
|
620
|
-
if not
|
|
699
|
+
if not ElnBaseDataType.is_eln_type(dt):
|
|
621
700
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
622
|
-
|
|
701
|
+
records: list[PyRecordModel] = self.__inst_man.add_new_records(dt, count)
|
|
702
|
+
if wrapper_type:
|
|
703
|
+
return self.__inst_man.wrap_list(records, wrapper_type)
|
|
704
|
+
return records
|
|
705
|
+
|
|
706
|
+
def add_eln_row(self, step: Step, wrapper_type: type[WrappedType] | None = None) -> PyRecordModel | WrappedType:
|
|
707
|
+
"""
|
|
708
|
+
Add a row to an ELNExperimentDetail or ELNSampleDetail table entry. The row will not appear in the system
|
|
709
|
+
until a record manager store and commit has occurred.
|
|
710
|
+
|
|
711
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
712
|
+
list of steps in the experiment and caches them.
|
|
713
|
+
|
|
714
|
+
:param step:
|
|
715
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
716
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
717
|
+
:param wrapper_type: Optionally wrap the ELN data type in a record model wrapper. If not provided, returns
|
|
718
|
+
an unwrapped PyRecordModel.
|
|
719
|
+
:return: The newly created row.
|
|
720
|
+
"""
|
|
721
|
+
return self.add_eln_rows(step, 1, wrapper_type)[0]
|
|
623
722
|
|
|
624
723
|
def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
|
|
625
724
|
"""
|
|
@@ -641,10 +740,12 @@ class ExperimentHandler:
|
|
|
641
740
|
"""
|
|
642
741
|
step = self.__to_eln_step(step)
|
|
643
742
|
dt: str = step.get_data_type_names()[0]
|
|
644
|
-
if not
|
|
743
|
+
if not ElnBaseDataType.is_eln_type(dt):
|
|
645
744
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
646
|
-
|
|
647
|
-
|
|
745
|
+
record_dt: str = AliasUtil.to_singular_data_type_name(records)
|
|
746
|
+
if record_dt != dt:
|
|
747
|
+
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
748
|
+
f"{step.get_data_type_names()[0]}.")
|
|
648
749
|
# If any rows were provided as data records, turn them into record models before deleting them, as otherwise
|
|
649
750
|
# this function would need to make a webservice call to do the deletion.
|
|
650
751
|
data_records: list[DataRecord] = []
|
|
@@ -658,16 +759,59 @@ class ExperimentHandler:
|
|
|
658
759
|
for record in record_models:
|
|
659
760
|
record.delete()
|
|
660
761
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
762
|
+
def remove_eln_row(self, step: Step, record: SapioRecord) -> None:
|
|
763
|
+
"""
|
|
764
|
+
Remove a row from an ELNExperimentDetail or ELNSampleDetail table entry. ELN data type table entries display all
|
|
765
|
+
records in the system that match the entry's data type. This means that removing rows from an ELN data type
|
|
766
|
+
table entry is equivalent to deleting the records for the rows.
|
|
767
|
+
|
|
768
|
+
The row will not be deleted in the system until a record manager store and commit has occurred.
|
|
769
|
+
|
|
770
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
771
|
+
list of steps in the experiment and caches them.
|
|
772
|
+
|
|
773
|
+
:param step:
|
|
774
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
775
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
776
|
+
:param record:
|
|
777
|
+
The record to remove from the given step.
|
|
778
|
+
The record may be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
779
|
+
"""
|
|
780
|
+
self.remove_eln_row(step, [record])
|
|
781
|
+
|
|
782
|
+
def add_sample_details(self, step: Step, samples: list[RecordModel], wrapper_type: type[WrappedType]) \
|
|
783
|
+
-> list[PyRecordModel | WrappedType]:
|
|
784
|
+
"""
|
|
785
|
+
Add sample details to a sample details entry while relating them to the input sample records.
|
|
786
|
+
|
|
787
|
+
:param step:
|
|
788
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
789
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
790
|
+
:param samples: The sample records to add the sample details to.
|
|
791
|
+
:param wrapper_type: Optionally wrap the sample details in a record model wrapper. If not provided, returns
|
|
792
|
+
an unwrapped PyRecordModel.
|
|
793
|
+
:return: The newly created sample details. The indices of the samples in the input list match the index of the
|
|
794
|
+
sample details in this list that they are related to.
|
|
795
|
+
"""
|
|
796
|
+
step = self.__to_eln_step(step)
|
|
797
|
+
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
798
|
+
raise SapioException("The provided step is not a table entry.")
|
|
799
|
+
dt: str = step.get_data_type_names()[0]
|
|
800
|
+
if not ElnBaseDataType.is_eln_type(dt) or ElnBaseDataType.get_base_type(dt) != ElnBaseDataType.SAMPLE_DETAIL:
|
|
801
|
+
raise SapioException("The provided step is not an ELNSampleDetail entry.")
|
|
802
|
+
records: list[PyRecordModel] = []
|
|
803
|
+
for sample in samples:
|
|
804
|
+
if sample.data_type_name != "Sample":
|
|
805
|
+
raise SapioException(f"Received a {sample.data_type_name} record when Sample records were expected.")
|
|
806
|
+
detail: PyRecordModel = sample.add(Child.of_type_name(dt))
|
|
807
|
+
detail.set_field_values({
|
|
808
|
+
"SampleId": sample.get_field_value("SampleId"),
|
|
809
|
+
"OtherSampleId": sample.get_field_value("OtherSampleId")
|
|
810
|
+
})
|
|
811
|
+
records.append(detail)
|
|
812
|
+
if wrapper_type:
|
|
813
|
+
return self.__inst_man.wrap_list(records, wrapper_type)
|
|
814
|
+
return records
|
|
671
815
|
|
|
672
816
|
def update_step(self, step: Step,
|
|
673
817
|
entry_name: str | None = None,
|
|
@@ -956,6 +1100,27 @@ class ExperimentHandler:
|
|
|
956
1100
|
step.unlock_step()
|
|
957
1101
|
step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
|
|
958
1102
|
|
|
1103
|
+
def disable_step(self, step: Step) -> None:
|
|
1104
|
+
"""
|
|
1105
|
+
Set the status of the input step to Disabled. This is the state that entries are in when they are waiting for
|
|
1106
|
+
entries that they are dependent upon to be submitted before they can be enabled. If you have unsubmitted an
|
|
1107
|
+
entry and want its dependent entries to be locked again, then you would use this to set their status to
|
|
1108
|
+
disabled.
|
|
1109
|
+
|
|
1110
|
+
Makes a webservice call to update the step. Checks if the step is already unlocked, and does nothing if so.
|
|
1111
|
+
|
|
1112
|
+
If no step functions have been called before and a step is being searched for by name, queries for the
|
|
1113
|
+
list of steps in the experiment and caches them.
|
|
1114
|
+
|
|
1115
|
+
:param step:
|
|
1116
|
+
The step to disable.
|
|
1117
|
+
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
1118
|
+
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
1119
|
+
"""
|
|
1120
|
+
step = self.__to_eln_step(step)
|
|
1121
|
+
if step.eln_entry.entry_status in self.__ENTRY_LOCKED_STATUSES:
|
|
1122
|
+
self.update_step(step, entry_status=ExperimentEntryStatus.Disabled)
|
|
1123
|
+
|
|
959
1124
|
def step_is_submitted(self, step: Step) -> bool:
|
|
960
1125
|
"""
|
|
961
1126
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
26
|
-
with io.
|
|
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.
|