sapiopycommons 2025.4.9a150__py3-none-any.whl → 2025.4.9a476__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 +1262 -392
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/Molecules.py +0 -2
- sapiopycommons/customreport/auto_pagers.py +281 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +4 -2
- sapiopycommons/datatype/data_fields.py +23 -1
- sapiopycommons/eln/experiment_cache.py +173 -0
- sapiopycommons/eln/experiment_handler.py +933 -279
- sapiopycommons/eln/experiment_report_util.py +15 -10
- sapiopycommons/eln/experiment_step_factory.py +474 -0
- sapiopycommons/eln/experiment_tags.py +7 -0
- sapiopycommons/eln/plate_designer.py +159 -59
- sapiopycommons/eln/step_creation.py +235 -0
- sapiopycommons/files/file_bridge.py +76 -0
- sapiopycommons/files/file_bridge_handler.py +325 -110
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +40 -15
- sapiopycommons/files/file_validator.py +6 -5
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +3 -3
- sapiopycommons/general/aliases.py +51 -28
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +24 -1
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +41 -2
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +1 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +46 -30
- sapiopycommons/recordmodel/record_handler.py +547 -159
- sapiopycommons/rules/eln_rule_handler.py +41 -30
- sapiopycommons/rules/on_save_rule_handler.py +41 -30
- sapiopycommons/samples/aliquot.py +48 -0
- sapiopycommons/webhook/webhook_handlers.py +448 -55
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a476.dist-info/RECORD +67 -0
- sapiopycommons-2025.4.9a150.dist-info/RECORD +0 -59
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.9a150.dist-info → sapiopycommons-2025.4.9a476.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,43 +1,64 @@
|
|
|
1
1
|
from sapiopylib.rest.utils.Protocols import ElnEntryStep
|
|
2
|
+
from sapiopylib.rest.utils.plates.PlatingUtils import PlateLocation
|
|
3
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
2
4
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
3
5
|
|
|
6
|
+
from sapiopycommons.datatype.data_fields import PlateDesignerWellElementFields
|
|
4
7
|
from sapiopycommons.eln.experiment_handler import ExperimentHandler
|
|
5
|
-
from sapiopycommons.
|
|
8
|
+
from sapiopycommons.eln.experiment_tags import PLATE_IDS_TAG
|
|
9
|
+
from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, RecordModel, FieldValue
|
|
6
10
|
from sapiopycommons.general.exceptions import SapioException
|
|
7
11
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
# Shorthands for longer type names.
|
|
14
|
+
WellElement = PlateDesignerWellElementFields
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
class
|
|
17
|
+
# FR-47486: Change the PlateDesignerEntry class to extend ElnEntryStep instead of containing one.
|
|
18
|
+
class PlateDesignerEntry(ElnEntryStep):
|
|
13
19
|
"""
|
|
14
20
|
A wrapper for 3D plate designer entries in experiments, providing functions for common actions when dealing with
|
|
15
21
|
such entries.
|
|
16
22
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
_exp_handler: ExperimentHandler
|
|
24
|
+
_rec_handler: RecordHandler
|
|
25
|
+
_plates: list[RecordModel] | None
|
|
26
|
+
_aliquots: list[RecordModel] | None
|
|
27
|
+
_sources: list[RecordModel] | None
|
|
28
|
+
_designer_elements: list[RecordModel] | None
|
|
29
|
+
_designer_elements_by_plate: dict[int, list[RecordModel]] | None
|
|
30
|
+
_plate_ids: list[int] | None
|
|
25
31
|
|
|
26
32
|
def __init__(self, step: ElnEntryStep, exp_handler: ExperimentHandler):
|
|
27
33
|
"""
|
|
28
34
|
:param step: The ElnEntryStep that is the 3D plate designer entry.
|
|
29
35
|
:param exp_handler: An ExperimentHandler for the experiment that this entry comes from.
|
|
30
36
|
"""
|
|
31
|
-
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
37
|
+
super().__init__(exp_handler.protocol, step.eln_entry)
|
|
38
|
+
self._exp_handler = exp_handler
|
|
39
|
+
self._rec_handler = RecordHandler(exp_handler.user)
|
|
40
|
+
self._plates = None
|
|
41
|
+
self._aliquots = None
|
|
42
|
+
self._sources = None
|
|
43
|
+
self._designer_elements = None
|
|
44
|
+
self._designer_elements_by_plate = None
|
|
45
|
+
self._plate_ids = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def step(self) -> ElnEntryStep:
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def clear_cache(self) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Clear the caches for the plates and plate designer well elements in this plate designer entry. This will require
|
|
54
|
+
a new webservice call to get the plates and designer elements the next time they are requested.
|
|
55
|
+
"""
|
|
56
|
+
self._plates = None
|
|
57
|
+
self._designer_elements = None
|
|
58
|
+
self._designer_elements_by_plate = None
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
# CR-47491: Support not providing a wrapper type to receive PyRecordModels instead of WrappedRecordModels.
|
|
61
|
+
def get_plates(self, wrapper_type: type[WrappedType] | None = None) -> list[WrappedType] | list[PyRecordModel]:
|
|
41
62
|
"""
|
|
42
63
|
Get the plates that are in the designer entry.
|
|
43
64
|
|
|
@@ -45,13 +66,16 @@ class PlateDesignerEntry:
|
|
|
45
66
|
will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the next time
|
|
46
67
|
this function is called.
|
|
47
68
|
|
|
48
|
-
:param wrapper_type: The record model wrapper to use.
|
|
69
|
+
:param wrapper_type: The record model wrapper to use on the plates. If not provided, the returned records will
|
|
70
|
+
be PyRecordModels instead of WrappedRecordModels.
|
|
49
71
|
:return: A list of the plates in the designer entry.
|
|
50
72
|
"""
|
|
51
|
-
if self.
|
|
52
|
-
return self.
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
if self._plates is not None:
|
|
74
|
+
return self._plates
|
|
75
|
+
if wrapper_type is None:
|
|
76
|
+
wrapper_type = "Plate"
|
|
77
|
+
self._plates = self._rec_handler.query_models_by_id(wrapper_type, self.__get_plate_ids())
|
|
78
|
+
return self._plates
|
|
55
79
|
|
|
56
80
|
def set_plates(self, plates: list[RecordIdentifier]) -> None:
|
|
57
81
|
"""
|
|
@@ -76,47 +100,50 @@ class PlateDesignerEntry:
|
|
|
76
100
|
record_ids: list[int] = AliasUtil.to_record_ids(plates)
|
|
77
101
|
self.__set_plate_ids(self.__get_plate_ids() + record_ids)
|
|
78
102
|
|
|
79
|
-
def get_sources(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
103
|
+
def get_sources(self, wrapper_type: type[WrappedType] | str) -> list[WrappedType] | list[PyRecordModel]:
|
|
80
104
|
"""
|
|
81
105
|
Get the source records that were used to populate the plate designer entry's sample table. This looks for the
|
|
82
|
-
entries that the plate designer entry is dependent upon and gets their records if they match the
|
|
83
|
-
of the given wrapper.
|
|
106
|
+
entries that the plate designer entry is implicitly dependent upon and gets their records if they match the
|
|
107
|
+
data type name of the given wrapper.
|
|
84
108
|
|
|
85
109
|
Makes a webservice call to retrieve the dependent entries if the experiment handler had not already cached it.
|
|
86
110
|
Makes another webservice call to get the records from the dependent entry and caches them for future calls.
|
|
87
111
|
|
|
88
|
-
:param wrapper_type: The record model wrapper to
|
|
112
|
+
:param wrapper_type: The record model wrapper or data type name of the records to retrieve. If a data type name
|
|
113
|
+
is provided, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
89
114
|
:return: A list of the source records that populate the plate designer entry's sample table.
|
|
90
115
|
"""
|
|
91
|
-
if self.
|
|
92
|
-
return self.
|
|
116
|
+
if self._sources is not None:
|
|
117
|
+
return self._sources
|
|
93
118
|
|
|
94
119
|
records: list[WrappedType] = []
|
|
95
|
-
dependent_ids: list[int] = self.step.eln_entry.
|
|
96
|
-
for step in self.
|
|
120
|
+
dependent_ids: list[int] = self.step.eln_entry.related_entry_id_set
|
|
121
|
+
for step in self._exp_handler.get_all_steps(wrapper_type):
|
|
97
122
|
if step.get_id() in dependent_ids:
|
|
98
|
-
records.extend(self.
|
|
123
|
+
records.extend(self._exp_handler.get_step_models(step, wrapper_type))
|
|
99
124
|
|
|
100
|
-
self.
|
|
101
|
-
return self.
|
|
125
|
+
self._sources = records
|
|
126
|
+
return self._sources
|
|
102
127
|
|
|
103
|
-
def get_aliquots(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
128
|
+
def get_aliquots(self, wrapper_type: type[WrappedType] | None = None) -> list[WrappedType] | list[PyRecordModel]:
|
|
104
129
|
"""
|
|
105
130
|
Get the aliquots that were created from this plate designer entry upon its submission.
|
|
106
131
|
|
|
107
132
|
Makes a webservice call to retrieve the aliquots from the plate designer entry and caches them for future calls.
|
|
108
133
|
|
|
109
|
-
:param wrapper_type: The record model wrapper to use.
|
|
134
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
|
|
135
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
110
136
|
:return: A list of the aliquots created by the plate designer entry.
|
|
111
137
|
"""
|
|
112
|
-
if not self.
|
|
138
|
+
if not self._exp_handler.step_is_submitted(self.step):
|
|
113
139
|
raise SapioException("The plate designer entry must be submitted before its aliquots can be retrieved.")
|
|
114
|
-
if self.
|
|
115
|
-
return self.
|
|
116
|
-
self.
|
|
117
|
-
return self.
|
|
140
|
+
if self._aliquots is not None:
|
|
141
|
+
return self._aliquots
|
|
142
|
+
self._aliquots = self._exp_handler.get_step_models(self.step, wrapper_type)
|
|
143
|
+
return self._aliquots
|
|
118
144
|
|
|
119
|
-
def get_plate_designer_well_elements(self, wrapper_type: type[WrappedType])
|
|
145
|
+
def get_plate_designer_well_elements(self, wrapper_type: type[WrappedType] | None = None) \
|
|
146
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
120
147
|
"""
|
|
121
148
|
Get the plate designer well elements for the plates in the plate designer entry. These are the records in the
|
|
122
149
|
system that determine how wells are displayed on each plate in the entry.
|
|
@@ -125,28 +152,101 @@ class PlateDesignerEntry:
|
|
|
125
152
|
This cache will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the
|
|
126
153
|
next time this function is called.
|
|
127
154
|
|
|
128
|
-
:param wrapper_type: The record model wrapper to use.
|
|
155
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
|
|
156
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
157
|
+
:return: A list of the plate designer well elements in the designer entry.
|
|
158
|
+
"""
|
|
159
|
+
if self._designer_elements is not None:
|
|
160
|
+
return self._designer_elements
|
|
161
|
+
if wrapper_type is None:
|
|
162
|
+
wrapper_type: str = WellElement.DATA_TYPE_NAME
|
|
163
|
+
self._designer_elements = self._rec_handler.query_models(wrapper_type,
|
|
164
|
+
WellElement.PLATE_RECORD_ID__FIELD,
|
|
165
|
+
self.__get_plate_ids())
|
|
166
|
+
if self._designer_elements_by_plate is None:
|
|
167
|
+
self._designer_elements_by_plate = {}
|
|
168
|
+
self._designer_elements_by_plate.clear()
|
|
169
|
+
for element in self._designer_elements:
|
|
170
|
+
plate_id: int = element.get(WellElement.PLATE_RECORD_ID__FIELD)
|
|
171
|
+
self._designer_elements_by_plate.setdefault(plate_id, []).append(element)
|
|
172
|
+
return self._designer_elements
|
|
173
|
+
|
|
174
|
+
def get_well_elements_by_plate(self, plate: RecordIdentifier, wrapper_type: type[WrappedType] | None = None) \
|
|
175
|
+
-> list[WrappedType] | list[PyRecordModel]:
|
|
176
|
+
"""
|
|
177
|
+
Get the plate designer well elements for the given plate in the plate designer entry. These are the records in
|
|
178
|
+
the system that determine how wells are displayed on the plate in the entry.
|
|
179
|
+
|
|
180
|
+
Makes a webservice call to get the plate designer well elements of the entry and caches them for future calls.
|
|
181
|
+
This cache will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the
|
|
182
|
+
next time this function is called.
|
|
183
|
+
|
|
184
|
+
:param plate: The plate to get the well elements for.
|
|
185
|
+
:param wrapper_type: The record model wrapper to use. If not provided, the returned records will be
|
|
186
|
+
PyRecordModels instead of WrappedRecordModels.
|
|
129
187
|
:return: A list of the plate designer well elements in the designer entry.
|
|
130
188
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
plate: int = AliasUtil.to_record_id(plate)
|
|
190
|
+
if plate not in self.__get_plate_ids():
|
|
191
|
+
raise SapioException(f"Plate record ID {plate} is not in this plate designer entry.")
|
|
192
|
+
if self._designer_elements_by_plate is None:
|
|
193
|
+
self.get_plate_designer_well_elements(wrapper_type)
|
|
194
|
+
return self._designer_elements_by_plate[plate]
|
|
195
|
+
|
|
196
|
+
def create_well_element(self, sample: RecordModel, plate: RecordModel, location: PlateLocation | None = None,
|
|
197
|
+
wrapper_type: type[WrappedType] | None = None) -> WrappedType | PyRecordModel:
|
|
198
|
+
"""
|
|
199
|
+
Create a new plate designer well element for the input sample and plate. A record model manager store and commit
|
|
200
|
+
must be called to save this new well element to the server.
|
|
201
|
+
|
|
202
|
+
:param sample: The sample that the element is for. Must exist in the system (i.e. have a >0 record ID).
|
|
203
|
+
:param plate: The plate that the element is for. Must exist in the system (i.e. have a >0 record ID).
|
|
204
|
+
:param location: The location of the well element. If not provided, the row and column position fields of the
|
|
205
|
+
sample will be used.
|
|
206
|
+
:param wrapper_type: The record model wrapper to use for the plate designer well element. If not provided, the
|
|
207
|
+
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
208
|
+
:return: The newly created PlateDesignerWellElementModel.
|
|
209
|
+
"""
|
|
210
|
+
# Confirm that we can actually make a designer element for the input records.
|
|
211
|
+
if AliasUtil.to_record_id(sample) <= 0:
|
|
212
|
+
raise SapioException("Cannot create plate designer well element for sample without a record ID.")
|
|
213
|
+
if AliasUtil.to_record_id(plate) <= 0:
|
|
214
|
+
raise SapioException("Cannot create plate designer well element for plate without a record ID.")
|
|
215
|
+
if AliasUtil.to_data_type_name(sample) != "Sample":
|
|
216
|
+
raise SapioException("Sample record must be of type Sample.")
|
|
217
|
+
if AliasUtil.to_data_type_name(plate) != "Plate":
|
|
218
|
+
raise SapioException("Plate record must be of type Plate.")
|
|
219
|
+
|
|
220
|
+
dt: type[WrappedType] | str = wrapper_type if wrapper_type else WellElement.DATA_TYPE_NAME
|
|
221
|
+
plate_id: int = AliasUtil.to_record_id(plate)
|
|
222
|
+
fields: dict[str, FieldValue] = {
|
|
223
|
+
WellElement.SOURCE_RECORD_ID__FIELD: AliasUtil.to_record_id(sample),
|
|
224
|
+
WellElement.PLATE_RECORD_ID__FIELD: plate_id,
|
|
225
|
+
WellElement.ROW_POSITION__FIELD: location.row_pos if location else sample.get_field_value("RowPosition"),
|
|
226
|
+
WellElement.COL_POSITION__FIELD: str(location.col_pos) if location else sample.get_field_value("ColPosition"),
|
|
227
|
+
WellElement.SOURCE_DATA_TYPE_NAME__FIELD: "Sample",
|
|
228
|
+
}
|
|
229
|
+
element = self._rec_handler.add_models_with_data(dt, [fields])[0]
|
|
230
|
+
|
|
231
|
+
# Add the new element to the cache.
|
|
232
|
+
if self._designer_elements is not None:
|
|
233
|
+
self._designer_elements.append(element)
|
|
234
|
+
if self._designer_elements_by_plate is not None and plate_id in self._designer_elements_by_plate:
|
|
235
|
+
self._designer_elements_by_plate.setdefault(plate_id, []).append(element)
|
|
236
|
+
return element
|
|
136
237
|
|
|
137
238
|
def __get_plate_ids(self) -> list[int]:
|
|
138
|
-
if self.
|
|
139
|
-
return self.
|
|
140
|
-
id_tag: str = self.
|
|
239
|
+
if self._plate_ids is not None:
|
|
240
|
+
return self._plate_ids
|
|
241
|
+
id_tag: str = self._exp_handler.get_step_option(self.step, PLATE_IDS_TAG)
|
|
141
242
|
if not id_tag:
|
|
142
243
|
raise SapioException("No plates in the plate designer entry")
|
|
143
|
-
self.
|
|
144
|
-
return self.
|
|
244
|
+
self._plate_ids = [int(x) for x in id_tag.split(",")]
|
|
245
|
+
return self._plate_ids
|
|
145
246
|
|
|
146
247
|
def __set_plate_ids(self, record_ids: list[int]) -> None:
|
|
147
248
|
record_ids.sort()
|
|
148
|
-
self.
|
|
149
|
-
self.
|
|
249
|
+
self._exp_handler.add_step_options(self.step, {PLATE_IDS_TAG: ",".join([str(x) for x in record_ids])})
|
|
250
|
+
self._plate_ids = record_ids
|
|
150
251
|
# The plates and designer elements caches have been invalidated.
|
|
151
|
-
self.
|
|
152
|
-
self.__designer_elements = None
|
|
252
|
+
self.clear_cache()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, ExperimentEntryIdentifier
|
|
4
|
+
from sapiopylib.rest.pojo.TableColumn import TableColumn
|
|
5
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
|
|
6
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnEntryType
|
|
7
|
+
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# CR-47564: Created these classes to streamline entry creation using the ExperimentEntryFactory.
|
|
11
|
+
class StepCreation:
|
|
12
|
+
"""
|
|
13
|
+
An object that contains the criteria for creating a new entry in the experiment.
|
|
14
|
+
"""
|
|
15
|
+
_entry_type: ElnEntryType
|
|
16
|
+
"""The type of the entry to be created."""
|
|
17
|
+
is_shown_in_template: bool | None
|
|
18
|
+
"""Whether the entry will appear in the template if the experiment this entry is in is saved to a new template."""
|
|
19
|
+
is_removable: bool | None
|
|
20
|
+
"""Whether the entry can be removed by users."""
|
|
21
|
+
is_renamable: bool | None
|
|
22
|
+
"""Whether the entry can be renamed by users."""
|
|
23
|
+
is_static_view: bool | None
|
|
24
|
+
"""Whether the entry's attachment is static. For attachment entries only. Static attachment entries will store
|
|
25
|
+
their attachment data in the template."""
|
|
26
|
+
related_entry_set: Iterable[ExperimentEntryIdentifier | str] | None
|
|
27
|
+
"""The IDs of the entries this entry is implicitly dependent on. If any of the entries are deleted then this entry
|
|
28
|
+
is also deleted."""
|
|
29
|
+
dependency_set: Iterable[ExperimentEntryIdentifier | str] | None
|
|
30
|
+
"""The IDs of the entries this entry is dependent on. Requires the entries to be completed before this entry will
|
|
31
|
+
be enabled."""
|
|
32
|
+
requires_grabber_plugin: bool
|
|
33
|
+
"""Whether to run a grabber plugin when this entry is initialized."""
|
|
34
|
+
entry_singleton_id: str | None
|
|
35
|
+
"""When this field is present (i.e. not null or blank) it will enforce that only one entry with this singleton
|
|
36
|
+
value is present in the experiment. If you attempt to create an entry with the singletonId of an entry already
|
|
37
|
+
present in the experiment it will return the existing entry instead of creating a new one. If an entry isn't
|
|
38
|
+
present in the Notebook Experiment with a matching singletonId it will create a new entry like normal."""
|
|
39
|
+
is_hidden: bool | None
|
|
40
|
+
"""Whether the user is able to visibly see this entry within the experiment."""
|
|
41
|
+
entry_height: int | None
|
|
42
|
+
"""The height of this entry in pixels. Setting the height to 0 will cause the entry to auto-size to its contents."""
|
|
43
|
+
description: str | None
|
|
44
|
+
"""The description of the entry."""
|
|
45
|
+
is_initialization_required: bool | None
|
|
46
|
+
"""Whether the user must manually initialize this entry by clicking on it."""
|
|
47
|
+
collapse_entry: bool | None
|
|
48
|
+
"""Whether the entry should be collapsed by default."""
|
|
49
|
+
entry_status: ExperimentEntryStatus | None
|
|
50
|
+
"""The current status of the entry."""
|
|
51
|
+
template_item_fulfilled_timestamp: int | None
|
|
52
|
+
"""The time in milliseconds since the epoch that this entry became initialized."""
|
|
53
|
+
entry_options: dict[str, str] | None
|
|
54
|
+
"""The entry options of the entry."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, entry_type: ElnEntryType):
|
|
57
|
+
self._entry_type = entry_type
|
|
58
|
+
self.is_shown_in_template = None
|
|
59
|
+
self.is_removable = None
|
|
60
|
+
self.is_renamable = None
|
|
61
|
+
self.is_static_view = None
|
|
62
|
+
self._related_entry_set = None
|
|
63
|
+
self._dependency_set = None
|
|
64
|
+
self.requires_grabber_plugin = False
|
|
65
|
+
self.entry_singleton_id = None
|
|
66
|
+
self.is_hidden = None
|
|
67
|
+
self.entry_height = None
|
|
68
|
+
self.description = None
|
|
69
|
+
self.is_initialization_required = None
|
|
70
|
+
self.collapse_entry = None
|
|
71
|
+
self.entry_status = None
|
|
72
|
+
self.template_item_fulfilled_timestamp = None
|
|
73
|
+
self.entry_options = None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def entry_type(self) -> ElnEntryType:
|
|
77
|
+
return self._entry_type
|
|
78
|
+
|
|
79
|
+
class AttachmentStepCreation(StepCreation):
|
|
80
|
+
"""
|
|
81
|
+
An object that contains criteria for creating a new attachment entry in an experiment.
|
|
82
|
+
"""
|
|
83
|
+
def __init__(self):
|
|
84
|
+
super().__init__(ElnEntryType.Attachment)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DashboardStepCreation(StepCreation):
|
|
88
|
+
"""
|
|
89
|
+
An object that contains criteria for creating a new dashboard entry in an experiment.
|
|
90
|
+
"""
|
|
91
|
+
source_entry: ExperimentEntryIdentifier | str | None
|
|
92
|
+
"""The entry that contains the source data for this entry's dashboard(s)."""
|
|
93
|
+
dashboard_guids: Iterable[str] | None
|
|
94
|
+
"""The GUIDs of the dashboards to display in this entry."""
|
|
95
|
+
|
|
96
|
+
def __init__(self):
|
|
97
|
+
super().__init__(ElnEntryType.Dashboard)
|
|
98
|
+
self.source_entry = None
|
|
99
|
+
self.dashboard_guids = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class GlobalDtFormStepCreation(StepCreation):
|
|
103
|
+
"""
|
|
104
|
+
An object that contains criteria for creating a new global data type form entry in an experiment.
|
|
105
|
+
"""
|
|
106
|
+
layout_name: str | None
|
|
107
|
+
"""The name of the data type layout to be displayed in this form. The layout must be for the data type for this
|
|
108
|
+
entry."""
|
|
109
|
+
form_names: Iterable[str] | None
|
|
110
|
+
"""The names of the components in the chosen data type layout to display in this form."""
|
|
111
|
+
extension_types: Iterable[DataTypeIdentifier] | None
|
|
112
|
+
"""The names of the extension data types to display fields from within the form."""
|
|
113
|
+
field_names: Iterable[FieldIdentifier] | None
|
|
114
|
+
"""A list of data field names for the fields to be displayed in the form."""
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
super().__init__(ElnEntryType.Form)
|
|
118
|
+
self.form_names = None
|
|
119
|
+
self.layout_name = None
|
|
120
|
+
self.extension_types = None
|
|
121
|
+
self.field_names = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ELnDtFormStepCreation(StepCreation):
|
|
125
|
+
"""
|
|
126
|
+
An object that contains criteria for creating a new ELN data type form entry in an experiment.
|
|
127
|
+
"""
|
|
128
|
+
is_field_addable: bool | None
|
|
129
|
+
"""Whether new fields can be added to the entry by users."""
|
|
130
|
+
is_existing_field_removable: bool | None
|
|
131
|
+
"""Whether existing fields on the entry can be removed by users."""
|
|
132
|
+
field_sets: Iterable[int | str | ElnFieldSetInfo] | None
|
|
133
|
+
"""The predefined field sets to display in this form."""
|
|
134
|
+
field_definitions: Iterable[AbstractVeloxFieldDefinition] | None
|
|
135
|
+
"""New field definitions to be created for this entry."""
|
|
136
|
+
predefined_field_names: Iterable[str] | None
|
|
137
|
+
"""The names of the predefined fields to display in this form."""
|
|
138
|
+
|
|
139
|
+
def __init__(self):
|
|
140
|
+
super().__init__(ElnEntryType.Form)
|
|
141
|
+
self.is_field_addable = None
|
|
142
|
+
self.is_existing_field_removable = None
|
|
143
|
+
self.field_sets = None
|
|
144
|
+
self.field_definitions = None
|
|
145
|
+
self.predefined_field_names = None
|
|
146
|
+
self.table_columns = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class PluginStepCreation(StepCreation):
|
|
150
|
+
"""
|
|
151
|
+
An object that contains criteria for creating a new plugin entry in an experiment.
|
|
152
|
+
"""
|
|
153
|
+
plugin_name: str | None
|
|
154
|
+
"""The client side plugin name to render this entry with."""
|
|
155
|
+
using_template_data: bool | None
|
|
156
|
+
"""Whether this entry will use the data from the template."""
|
|
157
|
+
provides_template_data: bool | None
|
|
158
|
+
"""Whether this entry can provide data to copy into a new template."""
|
|
159
|
+
|
|
160
|
+
def __init__(self):
|
|
161
|
+
super().__init__(ElnEntryType.Plugin)
|
|
162
|
+
self.plugin_name = None
|
|
163
|
+
self.using_template_data = None
|
|
164
|
+
self.provides_template_data = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class GlobalDtTableStepCreation(StepCreation):
|
|
168
|
+
"""
|
|
169
|
+
An object that contains criteria for creating a new global data type table entry in an experiment.
|
|
170
|
+
"""
|
|
171
|
+
layout_name: str | None
|
|
172
|
+
"""The name of the data type layout to display in this table."""
|
|
173
|
+
extension_types: Iterable[str] | None
|
|
174
|
+
"""The names of the extension data types to display fields from within the table."""
|
|
175
|
+
table_columns: Iterable[TableColumn] | None
|
|
176
|
+
"""The columns to display in the table. This can be used to change the sort order and direction of columns."""
|
|
177
|
+
field_names: Iterable[FieldIdentifier] | None
|
|
178
|
+
"""A list of data field names for the fields to be displayed in the table. These will be added as TableColumns and
|
|
179
|
+
placed after any of the existing columns specified in the table_columns parameter without any sorting."""
|
|
180
|
+
show_key_fields: bool | None
|
|
181
|
+
"""Whether the key fields of the data type should be shown in the entry."""
|
|
182
|
+
|
|
183
|
+
def __init__(self):
|
|
184
|
+
super().__init__(ElnEntryType.Table)
|
|
185
|
+
self.layout_name = None
|
|
186
|
+
self.extension_types = None
|
|
187
|
+
self.table_columns = None
|
|
188
|
+
self.show_key_fields = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ELnDtTableStepCreation(StepCreation):
|
|
192
|
+
"""
|
|
193
|
+
An object that contains criteria for creating a new ELN data type table entry in an experiment.
|
|
194
|
+
"""
|
|
195
|
+
is_field_addable: bool | None
|
|
196
|
+
"""Whether new fields can be added to the entry by users."""
|
|
197
|
+
is_existing_field_removable: bool | None
|
|
198
|
+
"""Whether existing fields on the entry can be removed by users."""
|
|
199
|
+
field_sets: Iterable[int | str | ElnFieldSetInfo] | None
|
|
200
|
+
"""The predefined field sets to display in this form."""
|
|
201
|
+
field_definitions: Iterable[AbstractVeloxFieldDefinition] | None
|
|
202
|
+
"""New field definitions to be created for this entry."""
|
|
203
|
+
predefined_field_names: Iterable[str] | None
|
|
204
|
+
"""The names of the predefined fields to display in this form."""
|
|
205
|
+
table_columns: Iterable[TableColumn] | None
|
|
206
|
+
"""The columns to display in the table."""
|
|
207
|
+
|
|
208
|
+
def __init__(self):
|
|
209
|
+
super().__init__(ElnEntryType.Table)
|
|
210
|
+
self.is_field_addable = None
|
|
211
|
+
self.is_existing_field_removable = None
|
|
212
|
+
self.field_sets = None
|
|
213
|
+
self.field_definitions = None
|
|
214
|
+
self.predefined_field_names = None
|
|
215
|
+
self.table_columns = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TempDataStepCreation(StepCreation):
|
|
219
|
+
"""
|
|
220
|
+
An object that contains criteria for creating a new temp data entry in an experiment.
|
|
221
|
+
"""
|
|
222
|
+
plugin_path: str | None
|
|
223
|
+
"""The temp data plugin path to run to populate the entry."""
|
|
224
|
+
|
|
225
|
+
def __init__(self):
|
|
226
|
+
super().__init__(ElnEntryType.TempData)
|
|
227
|
+
self.plugin_path = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TextStepCreation(StepCreation):
|
|
231
|
+
"""
|
|
232
|
+
An object that contains criteria for creating a new text entry in an experiment.
|
|
233
|
+
"""
|
|
234
|
+
def __init__(self):
|
|
235
|
+
super().__init__(ElnEntryType.Text)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import io
|
|
3
3
|
import urllib.parse
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from requests import Response
|
|
6
7
|
from sapiopylib.rest.User import SapioUser
|
|
@@ -8,6 +9,36 @@ from sapiopylib.rest.User import SapioUser
|
|
|
8
9
|
from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
# FR-47387: Add support for the metadata endpoints in FileBridge.
|
|
13
|
+
class FileBridgeMetadata:
|
|
14
|
+
"""
|
|
15
|
+
Metadata for a file or directory in FileBridge.
|
|
16
|
+
"""
|
|
17
|
+
file_name: str
|
|
18
|
+
"""The name of the file or directory."""
|
|
19
|
+
is_file: bool
|
|
20
|
+
"""True if the metadata is for a file, False if it is for a directory."""
|
|
21
|
+
is_directory: bool
|
|
22
|
+
"""True if the metadata is for a directory, False if it is for a file."""
|
|
23
|
+
size: int
|
|
24
|
+
"""The size of the file in bytes. For directories, this value will always be zero."""
|
|
25
|
+
creation_time: int
|
|
26
|
+
"""The time the file or directory was created, in milliseconds since the epoch."""
|
|
27
|
+
last_accessed_time: int
|
|
28
|
+
"""The time the file or directory was last accessed, in milliseconds since the epoch."""
|
|
29
|
+
last_modified_time: int
|
|
30
|
+
"""The time the file or directory was last modified, in milliseconds since the epoch."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, json_dict: dict[str, Any]):
|
|
33
|
+
self.file_name = json_dict['fileName']
|
|
34
|
+
self.is_file = json_dict['isFile']
|
|
35
|
+
self.is_directory = json_dict['isDirectory']
|
|
36
|
+
self.size = json_dict['size']
|
|
37
|
+
self.creation_time = json_dict['creationTime']
|
|
38
|
+
self.last_accessed_time = json_dict['lastAccessTime']
|
|
39
|
+
self.last_modified_time = json_dict['lastModifiedTime']
|
|
40
|
+
|
|
41
|
+
|
|
11
42
|
# FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
|
|
12
43
|
class FileBridge:
|
|
13
44
|
@staticmethod
|
|
@@ -137,3 +168,48 @@ class FileBridge:
|
|
|
137
168
|
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
138
169
|
response = user.delete(sub_path, params=params)
|
|
139
170
|
user.raise_for_status(response)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def file_metadata(context: UserIdentifier, bridge_name: str, file_path: str) -> FileBridgeMetadata:
|
|
174
|
+
"""
|
|
175
|
+
Get metadata for a file or directory in FileBridge.
|
|
176
|
+
|
|
177
|
+
The file path may be to a directory, in which case only the metadata for that directory will be returned. If you
|
|
178
|
+
want the metadata for the contents of a directory, then use the directory_metadata function.
|
|
179
|
+
|
|
180
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
181
|
+
:param bridge_name: The name of the bridge to use. This is the "connection name" in the
|
|
182
|
+
file bridge configurations.
|
|
183
|
+
:param file_path: The path to the file to retrieve the metadata from.
|
|
184
|
+
:return: The metadata for the file.
|
|
185
|
+
"""
|
|
186
|
+
sub_path = '/ext/filebridge/file/metadata'
|
|
187
|
+
params = {
|
|
188
|
+
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
189
|
+
}
|
|
190
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
191
|
+
response = user.get(sub_path, params=params)
|
|
192
|
+
user.raise_for_status(response)
|
|
193
|
+
response_body: dict[str, Any] = response.json()
|
|
194
|
+
return FileBridgeMetadata(response_body)
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def directory_metadata(context: UserIdentifier, bridge_name: str, file_path: str) -> list[FileBridgeMetadata]:
|
|
198
|
+
"""
|
|
199
|
+
Get metadata for every file or nested directory in a directory in FileBridge.
|
|
200
|
+
|
|
201
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
202
|
+
:param bridge_name: The name of the bridge to use. This is the "connection name" in the
|
|
203
|
+
file bridge configurations.
|
|
204
|
+
:param file_path: The path to the directory to retrieve the metadata of the contents.
|
|
205
|
+
:return: A list of the metadata for the contents of the directory.
|
|
206
|
+
"""
|
|
207
|
+
sub_path = '/ext/filebridge/directory/metadata'
|
|
208
|
+
params = {
|
|
209
|
+
'Filepath': f"bridge://{bridge_name}/{file_path}"
|
|
210
|
+
}
|
|
211
|
+
user: SapioUser = AliasUtil.to_sapio_user(context)
|
|
212
|
+
response = user.get(sub_path, params=params)
|
|
213
|
+
user.raise_for_status(response)
|
|
214
|
+
response_body: list[dict[str, Any]] = response.json()
|
|
215
|
+
return [FileBridgeMetadata(x) for x in response_body]
|