sapiopycommons 2024.8.27a310__py3-none-any.whl → 2024.8.28a313__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 +69 -407
- sapiopycommons/chem/IndigoMolecules.py +0 -1
- sapiopycommons/chem/Molecules.py +0 -1
- sapiopycommons/datatype/attachment_util.py +10 -11
- sapiopycommons/eln/experiment_handler.py +48 -209
- sapiopycommons/files/complex_data_loader.py +4 -5
- sapiopycommons/files/file_bridge.py +24 -31
- sapiopycommons/files/file_data_handler.py +5 -2
- sapiopycommons/files/file_util.py +10 -50
- sapiopycommons/files/file_validator.py +6 -92
- sapiopycommons/files/file_writer.py +15 -44
- sapiopycommons/general/aliases.py +3 -147
- sapiopycommons/general/custom_report_util.py +37 -211
- sapiopycommons/general/popup_util.py +0 -17
- sapiopycommons/general/time_util.py +0 -40
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +97 -481
- sapiopycommons/rules/eln_rule_handler.py +25 -34
- sapiopycommons/rules/on_save_rule_handler.py +31 -34
- sapiopycommons/webhook/webhook_handlers.py +26 -147
- {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/METADATA +2 -4
- sapiopycommons-2024.8.28a313.dist-info/RECORD +38 -0
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +0 -60
- sapiopycommons/customreport/custom_report_builder.py +0 -125
- sapiopycommons/customreport/term_builder.py +0 -299
- sapiopycommons/eln/experiment_report_util.py +0 -118
- sapiopycommons/files/file_bridge_handler.py +0 -340
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/general/audit_log.py +0 -196
- sapiopycommons/general/sapio_links.py +0 -50
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -486
- sapiopycommons/webhook/webservice_handlers.py +0 -67
- sapiopycommons-2024.8.27a310.dist-info/RECORD +0 -50
- {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.27a310.dist-info → sapiopycommons-2024.8.28a313.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,6 @@ indigo = Indigo()
|
|
|
6
6
|
renderer = IndigoRenderer(indigo)
|
|
7
7
|
indigo.setOption("render-output-format", "svg")
|
|
8
8
|
indigo.setOption("ignore-stereochemistry-errors", True)
|
|
9
|
-
indigo.setOption("render-stereo-style", "ext")
|
|
10
9
|
indigo.setOption("aromaticity-model", "generic")
|
|
11
10
|
indigo.setOption("render-coloring", True)
|
|
12
11
|
indigo_inchi = IndigoInchi(indigo);
|
sapiopycommons/chem/Molecules.py
CHANGED
|
@@ -181,7 +181,6 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
|
|
|
181
181
|
# We need to test the INCHI can be loaded back to indigo.
|
|
182
182
|
indigo_mol = indigo.loadMolecule(molBlock)
|
|
183
183
|
indigo_mol.aromatize()
|
|
184
|
-
indigo_inchi.resetOptions()
|
|
185
184
|
indigo_inchi_str = indigo_inchi.getInchi(indigo_mol)
|
|
186
185
|
molecule["inchi"] = indigo_inchi_str
|
|
187
186
|
indigo_inchi_key_str = indigo_inchi.getInchiKey(indigo_inchi_str)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import io
|
|
2
2
|
|
|
3
|
-
from sapiopylib.rest.
|
|
3
|
+
from sapiopylib.rest.User import SapioUser
|
|
4
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
4
5
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
5
6
|
|
|
6
|
-
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
7
|
+
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
7
8
|
from sapiopycommons.general.exceptions import SapioException
|
|
8
9
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
9
10
|
|
|
@@ -11,32 +12,31 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
|
11
12
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
12
13
|
class AttachmentUtil:
|
|
13
14
|
@staticmethod
|
|
14
|
-
def get_attachment_bytes(context:
|
|
15
|
+
def get_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord) -> bytes:
|
|
15
16
|
"""
|
|
16
17
|
Get the data bytes for the given attachment record. Makes a webservice call to retrieve the data.
|
|
17
18
|
|
|
18
|
-
:param context: The current webhook context
|
|
19
|
+
:param context: The current webhook context.
|
|
19
20
|
:param attachment: The attachment record.
|
|
20
21
|
:return: The bytes for the attachment's file data.
|
|
21
22
|
"""
|
|
22
23
|
attachment = AliasUtil.to_data_record(attachment)
|
|
23
|
-
dr_man = DataMgmtServer.get_data_record_manager(AliasUtil.to_sapio_user(context))
|
|
24
24
|
with io.BytesIO() as data_sink:
|
|
25
25
|
def consume_data(chunk: bytes):
|
|
26
26
|
data_sink.write(chunk)
|
|
27
|
-
|
|
27
|
+
context.data_record_manager.get_attachment_data(attachment, consume_data)
|
|
28
28
|
data_sink.flush()
|
|
29
29
|
data_sink.seek(0)
|
|
30
30
|
file_bytes = data_sink.read()
|
|
31
31
|
return file_bytes
|
|
32
32
|
|
|
33
33
|
@staticmethod
|
|
34
|
-
def set_attachment_bytes(context:
|
|
34
|
+
def set_attachment_bytes(context: SapioWebhookContext, attachment: SapioRecord,
|
|
35
35
|
file_name: str, file_bytes: bytes) -> None:
|
|
36
36
|
"""
|
|
37
37
|
Set the attachment data for a given attachment record. Makes a webservice call to set the data.
|
|
38
38
|
|
|
39
|
-
:param context: The current webhook context
|
|
39
|
+
:param context: The current webhook context.
|
|
40
40
|
:param attachment: The attachment record. Must be an existing data record that is an attachment type.
|
|
41
41
|
:param file_name: The name of the attachment.
|
|
42
42
|
:param file_bytes: The bytes of the attachment data.
|
|
@@ -45,12 +45,11 @@ class AttachmentUtil:
|
|
|
45
45
|
raise SapioException("Provided record cannot have its attachment data set, as it does not exist in the "
|
|
46
46
|
"system yet.")
|
|
47
47
|
attachment = AliasUtil.to_data_record(attachment)
|
|
48
|
-
dr_man = DataMgmtServer.get_data_record_manager(AliasUtil.to_sapio_user(context))
|
|
49
48
|
with io.BytesIO(file_bytes) as stream:
|
|
50
|
-
|
|
49
|
+
context.data_record_manager.set_attachment_data(attachment, file_name, stream)
|
|
51
50
|
|
|
52
51
|
@staticmethod
|
|
53
|
-
def create_attachment(context:
|
|
52
|
+
def create_attachment(context: SapioWebhookContext | SapioUser, file_name: str, file_bytes: bytes,
|
|
54
53
|
wrapper_type: type[WrappedType]) -> WrappedType:
|
|
55
54
|
"""
|
|
56
55
|
Create an attachment data type and initialize its attachment bytes at the same time.
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import time
|
|
4
2
|
from collections.abc import Mapping, Iterable
|
|
5
|
-
from weakref import WeakValueDictionary
|
|
6
3
|
|
|
7
|
-
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
8
4
|
from sapiopylib.rest.ELNService import ElnManager
|
|
9
|
-
from sapiopylib.rest.User import SapioUser
|
|
10
5
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
11
6
|
from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, TemplateExperimentQueryPojo, ElnTemplate, \
|
|
12
7
|
InitializeNotebookExperimentPojo, ElnExperimentUpdateCriteria
|
|
@@ -21,10 +16,8 @@ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
|
21
16
|
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
22
17
|
from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInstanceManager, RecordModelManager
|
|
23
18
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
24
|
-
from sapiopylib.rest.utils.recordmodel.properties import Child
|
|
25
19
|
|
|
26
|
-
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
27
|
-
DataTypeIdentifier, RecordModel
|
|
20
|
+
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
28
21
|
from sapiopycommons.general.exceptions import SapioException
|
|
29
22
|
|
|
30
23
|
Step = str | ElnEntryStep
|
|
@@ -34,8 +27,7 @@ itself."""
|
|
|
34
27
|
|
|
35
28
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
36
29
|
class ExperimentHandler:
|
|
37
|
-
|
|
38
|
-
context: SapioWebhookContext | None
|
|
30
|
+
context: SapioWebhookContext
|
|
39
31
|
"""The context that this handler is working from."""
|
|
40
32
|
|
|
41
33
|
# Basic experiment info from the context.
|
|
@@ -85,103 +77,44 @@ class ExperimentHandler:
|
|
|
85
77
|
ElnExperimentStatus.Canceled]
|
|
86
78
|
"""The set of statuses that an ELN experiment could have and be considered locked."""
|
|
87
79
|
|
|
88
|
-
|
|
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):
|
|
80
|
+
def __init__(self, context: SapioWebhookContext, experiment: ElnExperiment | None = None):
|
|
110
81
|
"""
|
|
111
82
|
Initialization will throw an exception if there is no ELN Experiment in the provided context and no experiment
|
|
112
83
|
is provided.
|
|
113
84
|
|
|
114
|
-
:param context: The current webhook context
|
|
85
|
+
:param context: The current webhook context.
|
|
115
86
|
:param experiment: If an experiment is provided that is separate from the experiment that is in the context,
|
|
116
|
-
that experiment will be used by this ExperimentHandler instead.
|
|
117
|
-
forms, including an ElnExperiment, ElnExperimentProtocol, an experiment record, or a notebook experiment ID.
|
|
87
|
+
that experiment will be used by this ExperimentHandler instead.
|
|
118
88
|
"""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
123
96
|
|
|
124
97
|
# Get the basic information about this experiment that already exists in the context and is often used.
|
|
125
|
-
self.__eln_exp = experiment
|
|
126
|
-
self.__protocol = ElnExperimentProtocol(experiment,
|
|
98
|
+
self.__eln_exp = experiment if experiment else context.eln_experiment
|
|
99
|
+
self.__protocol = ElnExperimentProtocol(experiment, context.user) if experiment else context.active_protocol
|
|
127
100
|
self.__exp_id = self.__protocol.get_id()
|
|
128
101
|
|
|
129
102
|
# Grab various managers that may be used.
|
|
130
|
-
self.__eln_man =
|
|
131
|
-
self.__inst_man = RecordModelManager(
|
|
103
|
+
self.__eln_man = context.eln_manager
|
|
104
|
+
self.__inst_man = RecordModelManager(context.user).instance_manager
|
|
132
105
|
|
|
133
106
|
# Create empty caches to fill when necessary.
|
|
134
107
|
self.__steps = {}
|
|
135
108
|
self.__step_options = {}
|
|
136
109
|
# CR-46330: Cache any experiment entry information that might already exist in the context.
|
|
137
110
|
self.__queried_all_steps = False
|
|
138
|
-
# We can only trust the entries in the context if
|
|
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:
|
|
145
|
-
self.__steps.update({entry.entry_name: ElnEntryStep(self.__protocol, entry)})
|
|
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.")
|
|
111
|
+
# We can only trust the entries in the context if no experiment was given as an input parameter.
|
|
181
112
|
if experiment is None:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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:
|
|
117
|
+
self.__steps.update({entry.entry_name: ElnEntryStep(self.__protocol, entry)})
|
|
185
118
|
|
|
186
119
|
# FR-46495: Split the creation of the experiment in launch_experiment into a create_experiment function.
|
|
187
120
|
@staticmethod
|
|
@@ -330,7 +263,11 @@ class ExperimentHandler:
|
|
|
330
263
|
:return: The data record for this experiment. None if it has no record.
|
|
331
264
|
"""
|
|
332
265
|
if not hasattr(self, "_ExperimentHandler__exp_record"):
|
|
333
|
-
|
|
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
|
|
334
271
|
if self.__exp_record is None and exception_on_none:
|
|
335
272
|
raise SapioException(f"Experiment record not found for experiment with ID {self.__exp_id}.")
|
|
336
273
|
return self.__exp_record
|
|
@@ -530,7 +467,7 @@ class ExperimentHandler:
|
|
|
530
467
|
ret_list.append(step)
|
|
531
468
|
return ret_list
|
|
532
469
|
|
|
533
|
-
def get_all_steps(self, data_type:
|
|
470
|
+
def get_all_steps(self, data_type: str | type[WrappedType] | None = None) -> list[ElnEntryStep]:
|
|
534
471
|
"""
|
|
535
472
|
Get a list of every entry in the experiment. Optionally filter the returned entries by a data type.
|
|
536
473
|
|
|
@@ -546,7 +483,8 @@ class ExperimentHandler:
|
|
|
546
483
|
all_steps: list[ElnEntryStep] = self.__protocol.get_sorted_step_list()
|
|
547
484
|
if data_type is None:
|
|
548
485
|
return all_steps
|
|
549
|
-
|
|
486
|
+
if not isinstance(data_type, str):
|
|
487
|
+
data_type: str = data_type.get_wrapper_data_type_name()
|
|
550
488
|
return [x for x in all_steps if data_type in x.get_data_type_names()]
|
|
551
489
|
|
|
552
490
|
def get_step_records(self, step: Step) -> list[DataRecord]:
|
|
@@ -598,10 +536,6 @@ class ExperimentHandler:
|
|
|
598
536
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
599
537
|
"""
|
|
600
538
|
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]}.")
|
|
605
539
|
step.add_records(AliasUtil.to_data_records(records))
|
|
606
540
|
|
|
607
541
|
def remove_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
@@ -620,10 +554,6 @@ class ExperimentHandler:
|
|
|
620
554
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
621
555
|
"""
|
|
622
556
|
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]}.")
|
|
627
557
|
step.remove_records(AliasUtil.to_data_records(records))
|
|
628
558
|
|
|
629
559
|
def set_step_records(self, step: Step, records: Iterable[SapioRecord]) -> None:
|
|
@@ -647,13 +577,11 @@ class ExperimentHandler:
|
|
|
647
577
|
The records may be provided as either DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
648
578
|
"""
|
|
649
579
|
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]}.")
|
|
654
580
|
step.set_records(AliasUtil.to_data_records(records))
|
|
655
581
|
|
|
656
582
|
# FR-46496 - Provide alias of set_step_records for use with form entries.
|
|
583
|
+
# TODO: Provide a similar aliased function for attachment entries once sapiopylib allows setting multiple
|
|
584
|
+
# attachments to an attachment step.
|
|
657
585
|
def set_form_record(self, step: Step, record: SapioRecord) -> None:
|
|
658
586
|
"""
|
|
659
587
|
Sets the record for a form entry.
|
|
@@ -671,8 +599,7 @@ class ExperimentHandler:
|
|
|
671
599
|
self.set_step_records(step, [record])
|
|
672
600
|
|
|
673
601
|
# FR-46496 - Provide functions for adding and removing rows from an ELN data type entry.
|
|
674
|
-
def add_eln_rows(self, step: Step, count: int
|
|
675
|
-
-> list[PyRecordModel | WrappedType]:
|
|
602
|
+
def add_eln_rows(self, step: Step, count: int) -> list[PyRecordModel]:
|
|
676
603
|
"""
|
|
677
604
|
Add rows to an ELNExperimentDetail or ELNSampleDetail table entry. The rows will not appear in the system
|
|
678
605
|
until a record manager store and commit has occurred.
|
|
@@ -684,37 +611,15 @@ class ExperimentHandler:
|
|
|
684
611
|
The step may be provided as either a string for the name of the step or an ElnEntryStep.
|
|
685
612
|
If given a name, throws an exception if no step of the given name exists in the experiment.
|
|
686
613
|
: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.
|
|
689
614
|
:return: A list of the newly created rows.
|
|
690
615
|
"""
|
|
691
616
|
step = self.__to_eln_step(step)
|
|
692
617
|
if step.eln_entry.entry_type != ElnEntryType.Table:
|
|
693
618
|
raise SapioException("The provided step is not a table entry.")
|
|
694
619
|
dt: str = step.get_data_type_names()[0]
|
|
695
|
-
if not
|
|
620
|
+
if not self.__is_eln_type(dt):
|
|
696
621
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
697
|
-
|
|
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]
|
|
622
|
+
return self.__inst_man.add_new_records(dt, count)
|
|
718
623
|
|
|
719
624
|
def remove_eln_rows(self, step: Step, records: list[SapioRecord]) -> None:
|
|
720
625
|
"""
|
|
@@ -736,12 +641,10 @@ class ExperimentHandler:
|
|
|
736
641
|
"""
|
|
737
642
|
step = self.__to_eln_step(step)
|
|
738
643
|
dt: str = step.get_data_type_names()[0]
|
|
739
|
-
if not
|
|
644
|
+
if not self.__is_eln_type(dt):
|
|
740
645
|
raise SapioException("The provided step is not an ELN data type entry.")
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
raise SapioException(f"Cannot remove {dt} records from entry {step.get_name()} of type "
|
|
744
|
-
f"{step.get_data_type_names()[0]}.")
|
|
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.")
|
|
745
648
|
# If any rows were provided as data records, turn them into record models before deleting them, as otherwise
|
|
746
649
|
# this function would need to make a webservice call to do the deletion.
|
|
747
650
|
data_records: list[DataRecord] = []
|
|
@@ -755,59 +658,16 @@ class ExperimentHandler:
|
|
|
755
658
|
for record in record_models:
|
|
756
659
|
record.delete()
|
|
757
660
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
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
|
|
811
671
|
|
|
812
672
|
def update_step(self, step: Step,
|
|
813
673
|
entry_name: str | None = None,
|
|
@@ -1096,27 +956,6 @@ class ExperimentHandler:
|
|
|
1096
956
|
step.unlock_step()
|
|
1097
957
|
step.eln_entry.entry_status = ExperimentEntryStatus.UnlockedChangesRequired
|
|
1098
958
|
|
|
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
|
-
|
|
1120
959
|
def step_is_submitted(self, step: Step) -> bool:
|
|
1121
960
|
"""
|
|
1122
961
|
Determine if the input step has already been submitted.
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import io
|
|
2
2
|
|
|
3
3
|
from sapiopylib.rest.User import SapioUser
|
|
4
|
-
|
|
5
|
-
from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
|
|
4
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class CDL:
|
|
9
8
|
@staticmethod
|
|
10
|
-
def load_cdl(context:
|
|
9
|
+
def load_cdl(context: SapioWebhookContext | SapioUser, config_name: str, file_name: str, file_data: bytes | str) \
|
|
11
10
|
-> list[int]:
|
|
12
11
|
"""
|
|
13
12
|
Create data records from a file using one of the complex data loader (CDL) configurations in the system.
|
|
@@ -23,8 +22,8 @@ class CDL:
|
|
|
23
22
|
"configName": config_name,
|
|
24
23
|
"fileName": file_name
|
|
25
24
|
}
|
|
26
|
-
user: SapioUser =
|
|
27
|
-
with io.
|
|
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:
|
|
28
27
|
response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
|
|
29
28
|
user.raise_for_status(response)
|
|
30
29
|
# The response content is returned as bytes for a comma separated string of record IDs.
|
|
@@ -4,21 +4,19 @@ import urllib.parse
|
|
|
4
4
|
|
|
5
5
|
from requests import Response
|
|
6
6
|
from sapiopylib.rest.User import SapioUser
|
|
7
|
-
|
|
8
|
-
from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
|
|
7
|
+
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
12
11
|
class FileBridge:
|
|
13
12
|
@staticmethod
|
|
14
|
-
def read_file(context:
|
|
13
|
+
def read_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str,
|
|
15
14
|
base64_decode: bool = True) -> bytes:
|
|
16
15
|
"""
|
|
17
16
|
Read a file from FileBridge.
|
|
18
17
|
|
|
19
18
|
:param context: The current webhook context or a user object to send requests from.
|
|
20
|
-
:param bridge_name: The name of the bridge to use.
|
|
21
|
-
file bridge configurations.
|
|
19
|
+
:param bridge_name: The name of the bridge to use.
|
|
22
20
|
:param file_path: The path to read the file from.
|
|
23
21
|
:param base64_decode: If true, base64 decode the file. Files are by default base64 encoded when retrieved from
|
|
24
22
|
FileBridge.
|
|
@@ -28,7 +26,7 @@ class FileBridge:
|
|
|
28
26
|
params = {
|
|
29
27
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
30
28
|
}
|
|
31
|
-
user: SapioUser =
|
|
29
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
32
30
|
response = user.get(sub_path, params)
|
|
33
31
|
user.raise_for_status(response)
|
|
34
32
|
|
|
@@ -38,14 +36,13 @@ class FileBridge:
|
|
|
38
36
|
return ret_val
|
|
39
37
|
|
|
40
38
|
@staticmethod
|
|
41
|
-
def write_file(context:
|
|
39
|
+
def write_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str,
|
|
42
40
|
file_data: bytes | str) -> None:
|
|
43
41
|
"""
|
|
44
42
|
Write a file to FileBridge.
|
|
45
43
|
|
|
46
44
|
:param context: The current webhook context or a user object to send requests from.
|
|
47
|
-
:param bridge_name: The name of the bridge to use.
|
|
48
|
-
file bridge configurations.
|
|
45
|
+
:param bridge_name: The name of the bridge to use.
|
|
49
46
|
:param file_path: The path to write the file to. If a file already exists at the given path then the file is
|
|
50
47
|
overwritten.
|
|
51
48
|
:param file_data: A string or bytes of the file to be written.
|
|
@@ -54,43 +51,41 @@ class FileBridge:
|
|
|
54
51
|
params = {
|
|
55
52
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
56
53
|
}
|
|
57
|
-
user: SapioUser =
|
|
58
|
-
with io.
|
|
54
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
55
|
+
with io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data) as data_stream:
|
|
59
56
|
response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
|
|
60
57
|
user.raise_for_status(response)
|
|
61
58
|
|
|
62
59
|
@staticmethod
|
|
63
|
-
def list_directory(context:
|
|
60
|
+
def list_directory(context: SapioWebhookContext | SapioUser, bridge_name: str,
|
|
64
61
|
file_path: str | None = "") -> list[str]:
|
|
65
62
|
"""
|
|
66
63
|
List the contents of a FileBridge directory.
|
|
67
64
|
|
|
68
65
|
:param context: The current webhook context or a user object to send requests from.
|
|
69
|
-
:param bridge_name: The name of the bridge to use.
|
|
70
|
-
file bridge configurations.
|
|
66
|
+
:param bridge_name: The name of the bridge to use.
|
|
71
67
|
:param file_path: The path to read the directory from.
|
|
72
|
-
:return: A list of
|
|
68
|
+
:return: A list of name of files and folders in the directory.
|
|
73
69
|
"""
|
|
74
70
|
sub_path = '/ext/filebridge/listDirectory'
|
|
75
71
|
params = {
|
|
76
72
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
77
73
|
}
|
|
78
|
-
user: SapioUser =
|
|
74
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
79
75
|
response: Response = user.get(sub_path, params=params)
|
|
80
76
|
user.raise_for_status(response)
|
|
81
77
|
|
|
82
78
|
response_body: list[str] = response.json()
|
|
83
79
|
path_length = len(f"bridge://{bridge_name}/")
|
|
84
|
-
return [urllib.parse.unquote(value
|
|
80
|
+
return [urllib.parse.unquote(value[path_length:]) for value in response_body]
|
|
85
81
|
|
|
86
82
|
@staticmethod
|
|
87
|
-
def create_directory(context:
|
|
83
|
+
def create_directory(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
|
|
88
84
|
"""
|
|
89
85
|
Create a new directory in FileBridge.
|
|
90
86
|
|
|
91
87
|
:param context: The current webhook context or a user object to send requests from.
|
|
92
|
-
:param bridge_name: The name of the bridge to use.
|
|
93
|
-
file bridge configurations.
|
|
88
|
+
:param bridge_name: The name of the bridge to use.
|
|
94
89
|
:param file_path: The path to create the directory at. If a directory already exists at the given path then an
|
|
95
90
|
exception is raised.
|
|
96
91
|
"""
|
|
@@ -98,42 +93,40 @@ class FileBridge:
|
|
|
98
93
|
params = {
|
|
99
94
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
100
95
|
}
|
|
101
|
-
user: SapioUser =
|
|
96
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
102
97
|
response = user.post(sub_path, params=params)
|
|
103
98
|
user.raise_for_status(response)
|
|
104
99
|
|
|
105
100
|
@staticmethod
|
|
106
|
-
def delete_file(context:
|
|
101
|
+
def delete_file(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
|
|
107
102
|
"""
|
|
108
103
|
Delete an existing file in FileBridge.
|
|
109
104
|
|
|
110
105
|
:param context: The current webhook context or a user object to send requests from.
|
|
111
|
-
:param bridge_name: The name of the bridge to use.
|
|
112
|
-
file bridge configurations.
|
|
106
|
+
:param bridge_name: The name of the bridge to use.
|
|
113
107
|
:param file_path: The path to the file to delete.
|
|
114
108
|
"""
|
|
115
109
|
sub_path = '/ext/filebridge/deleteFile'
|
|
116
110
|
params = {
|
|
117
111
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
118
112
|
}
|
|
119
|
-
user: SapioUser =
|
|
120
|
-
response = user.
|
|
113
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
114
|
+
response = user.post(sub_path, params=params)
|
|
121
115
|
user.raise_for_status(response)
|
|
122
116
|
|
|
123
117
|
@staticmethod
|
|
124
|
-
def delete_directory(context:
|
|
118
|
+
def delete_directory(context: SapioWebhookContext | SapioUser, bridge_name: str, file_path: str) -> None:
|
|
125
119
|
"""
|
|
126
120
|
Delete an existing directory in FileBridge.
|
|
127
121
|
|
|
128
122
|
:param context: The current webhook context or a user object to send requests from.
|
|
129
|
-
:param bridge_name: The name of the bridge to use.
|
|
130
|
-
file bridge configurations.
|
|
123
|
+
:param bridge_name: The name of the bridge to use.
|
|
131
124
|
:param file_path: The path to the directory to delete.
|
|
132
125
|
"""
|
|
133
126
|
sub_path = '/ext/filebridge/deleteDirectory'
|
|
134
127
|
params = {
|
|
135
128
|
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
136
129
|
}
|
|
137
|
-
user: SapioUser =
|
|
138
|
-
response = user.
|
|
130
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
131
|
+
response = user.post(sub_path, params=params)
|
|
139
132
|
user.raise_for_status(response)
|