sapiopycommons 2024.3.19a157__py3-none-any.whl → 2025.1.17a402__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (52) hide show
  1. sapiopycommons/callbacks/__init__.py +0 -0
  2. sapiopycommons/callbacks/callback_util.py +2041 -0
  3. sapiopycommons/callbacks/field_builder.py +545 -0
  4. sapiopycommons/chem/IndigoMolecules.py +46 -1
  5. sapiopycommons/chem/Molecules.py +100 -21
  6. sapiopycommons/customreport/__init__.py +0 -0
  7. sapiopycommons/customreport/column_builder.py +60 -0
  8. sapiopycommons/customreport/custom_report_builder.py +137 -0
  9. sapiopycommons/customreport/term_builder.py +315 -0
  10. sapiopycommons/datatype/attachment_util.py +14 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +355 -91
  14. sapiopycommons/eln/experiment_report_util.py +649 -0
  15. sapiopycommons/eln/plate_designer.py +152 -0
  16. sapiopycommons/files/complex_data_loader.py +31 -0
  17. sapiopycommons/files/file_bridge.py +149 -25
  18. sapiopycommons/files/file_bridge_handler.py +555 -0
  19. sapiopycommons/files/file_data_handler.py +633 -0
  20. sapiopycommons/files/file_util.py +263 -163
  21. sapiopycommons/files/file_validator.py +569 -0
  22. sapiopycommons/files/file_writer.py +377 -0
  23. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  24. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  25. sapiopycommons/general/accession_service.py +375 -0
  26. sapiopycommons/general/aliases.py +250 -15
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +251 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +59 -7
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +91 -7
  35. sapiopycommons/multimodal/multimodal.py +146 -0
  36. sapiopycommons/multimodal/multimodal_data.py +490 -0
  37. sapiopycommons/processtracking/__init__.py +0 -0
  38. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  39. sapiopycommons/processtracking/endpoints.py +192 -0
  40. sapiopycommons/recordmodel/record_handler.py +621 -148
  41. sapiopycommons/rules/eln_rule_handler.py +87 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +87 -12
  43. sapiopycommons/sftpconnect/__init__.py +0 -0
  44. sapiopycommons/sftpconnect/sftp_builder.py +70 -0
  45. sapiopycommons/webhook/webhook_context.py +39 -0
  46. sapiopycommons/webhook/webhook_handlers.py +614 -71
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
  49. sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
  50. {sapiopycommons-2024.3.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.19a157.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,152 @@
1
+ from sapiopylib.rest.utils.Protocols import ElnEntryStep
2
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
3
+
4
+ from sapiopycommons.eln.experiment_handler import ExperimentHandler
5
+ from sapiopycommons.general.aliases import SapioRecord, RecordIdentifier, AliasUtil
6
+ from sapiopycommons.general.exceptions import SapioException
7
+ from sapiopycommons.recordmodel.record_handler import RecordHandler
8
+
9
+ PLATE_IDS_TAG: str = "MultiLayerPlating_Plate_RecordIdList"
10
+
11
+
12
+ class PlateDesignerEntry:
13
+ """
14
+ A wrapper for 3D plate designer entries in experiments, providing functions for common actions when dealing with
15
+ such entries.
16
+ """
17
+ step: ElnEntryStep
18
+ __exp_handler: ExperimentHandler
19
+ __rec_handler: RecordHandler
20
+ __plates: list[SapioRecord] | None
21
+ __aliquots: list[SapioRecord] | None
22
+ __sources: list[SapioRecord] | None
23
+ __designer_elements: list[SapioRecord] | None
24
+ __plate_ids: list[int] | None
25
+
26
+ def __init__(self, step: ElnEntryStep, exp_handler: ExperimentHandler):
27
+ """
28
+ :param step: The ElnEntryStep that is the 3D plate designer entry.
29
+ :param exp_handler: An ExperimentHandler for the experiment that this entry comes from.
30
+ """
31
+ self.step = step
32
+ self.__exp_handler = exp_handler
33
+ self.__rec_handler = RecordHandler(exp_handler.context)
34
+ self.__plates = None
35
+ self.__aliquots = None
36
+ self.__sources = None
37
+ self.__designer_elements = None
38
+ self.__plate_ids = None
39
+
40
+ def get_plates(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
41
+ """
42
+ Get the plates that are in the designer entry.
43
+
44
+ Makes a webservice query to get the plates from the entry and caches the result for future calls. This cache
45
+ will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the next time
46
+ this function is called.
47
+
48
+ :param wrapper_type: The record model wrapper to use.
49
+ :return: A list of the plates in the designer entry.
50
+ """
51
+ if self.__plates is not None:
52
+ return self.__plates
53
+ self.__plates = self.__rec_handler.query_models_by_id(wrapper_type, self.__get_plate_ids())
54
+ return self.__plates
55
+
56
+ def set_plates(self, plates: list[RecordIdentifier]) -> None:
57
+ """
58
+ Set the plates that are in the plate designer entry. This removes any existing plates that are in the entry
59
+ but not in the given list.
60
+
61
+ Makes a webservice call to update the plate designer entry's entry options.
62
+
63
+ :param plates: The plates to set the plate designer entry with.
64
+ """
65
+ record_ids: list[int] = AliasUtil.to_record_ids(plates)
66
+ self.__set_plate_ids(record_ids)
67
+
68
+ def add_plates(self, plates: list[RecordIdentifier]) -> None:
69
+ """
70
+ Add the given plates to the plate designer entry. This preserves any existing plates that are in the entry.
71
+
72
+ Makes a webservice call to update the plate designer entry's entry options.
73
+
74
+ :param plates: The plates to add to the plate designer entry.
75
+ """
76
+ record_ids: list[int] = AliasUtil.to_record_ids(plates)
77
+ self.__set_plate_ids(self.__get_plate_ids() + record_ids)
78
+
79
+ def get_sources(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
80
+ """
81
+ 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 data type name
83
+ of the given wrapper.
84
+
85
+ Makes a webservice call to retrieve the dependent entries if the experiment handler had not already cached it.
86
+ Makes another webservice call to get the records from the dependent entry and caches them for future calls.
87
+
88
+ :param wrapper_type: The record model wrapper to use.
89
+ :return: A list of the source records that populate the plate designer entry's sample table.
90
+ """
91
+ if self.__sources is not None:
92
+ return self.__sources
93
+
94
+ records: list[WrappedType] = []
95
+ dependent_ids: list[int] = self.step.eln_entry.dependency_set
96
+ for step in self.__exp_handler.get_all_steps(wrapper_type):
97
+ if step.get_id() in dependent_ids:
98
+ records.extend(self.__exp_handler.get_step_models(step, wrapper_type))
99
+
100
+ self.__sources = records
101
+ return self.__sources
102
+
103
+ def get_aliquots(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
104
+ """
105
+ Get the aliquots that were created from this plate designer entry upon its submission.
106
+
107
+ Makes a webservice call to retrieve the aliquots from the plate designer entry and caches them for future calls.
108
+
109
+ :param wrapper_type: The record model wrapper to use.
110
+ :return: A list of the aliquots created by the plate designer entry.
111
+ """
112
+ if not self.__exp_handler.step_is_submitted(self.step):
113
+ raise SapioException("The plate designer entry must be submitted before its aliquots can be retrieved.")
114
+ if self.__aliquots is not None:
115
+ return self.__aliquots
116
+ self.__aliquots = self.__exp_handler.get_step_models(self.step, wrapper_type)
117
+ return self.__aliquots
118
+
119
+ def get_plate_designer_well_elements(self, wrapper_type: type[WrappedType]) -> list[WrappedType]:
120
+ """
121
+ Get the plate designer well elements for the plates in the plate designer entry. These are the records in the
122
+ system that determine how wells are displayed on each plate in the entry.
123
+
124
+ Makes a webservice call to get the plate designer well elements of the entry and caches them for future calls.
125
+ This cache will be invalidated if a set_plates or add_plates call is made, requiring a new webservice call the
126
+ next time this function is called.
127
+
128
+ :param wrapper_type: The record model wrapper to use.
129
+ :return: A list of the plate designer well elements in the designer entry.
130
+ """
131
+ if self.__designer_elements is not None:
132
+ return self.__designer_elements
133
+ self.__designer_elements = self.__rec_handler.query_models(wrapper_type, "PlateRecordId",
134
+ self.__get_plate_ids())
135
+ return self.__designer_elements
136
+
137
+ def __get_plate_ids(self) -> list[int]:
138
+ if self.__plate_ids is not None:
139
+ return self.__plate_ids
140
+ id_tag: str = self.__exp_handler.get_step_option(self.step, PLATE_IDS_TAG)
141
+ if not id_tag:
142
+ raise SapioException("No plates in the plate designer entry")
143
+ self.__plate_ids = [int(x) for x in id_tag.split(",")]
144
+ return self.__plate_ids
145
+
146
+ def __set_plate_ids(self, record_ids: list[int]) -> None:
147
+ record_ids.sort()
148
+ self.__exp_handler.add_step_options(self.step, {PLATE_IDS_TAG: ",".join([str(x) for x in record_ids])})
149
+ self.__plate_ids = record_ids
150
+ # The plates and designer elements caches have been invalidated.
151
+ self.__plates = None
152
+ self.__designer_elements = None
@@ -0,0 +1,31 @@
1
+ import io
2
+
3
+ from sapiopylib.rest.User import SapioUser
4
+
5
+ from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
6
+
7
+
8
+ class CDL:
9
+ @staticmethod
10
+ def load_cdl(context: UserIdentifier, config_name: str, file_name: str, file_data: bytes | str) \
11
+ -> list[int]:
12
+ """
13
+ Create data records from a file using one of the complex data loader (CDL) configurations in the system.
14
+
15
+ :param context: The current webhook context or a user object to send requests from.
16
+ :param config_name: The name of the CDL configuration to run.
17
+ :param file_name: The name of the file being read by the CDL.
18
+ :param file_data: A string or bytes of the file to be read by the CDL.
19
+ :return: A list of the record IDs of the data records created by the CDL.
20
+ """
21
+ sub_path = "/ext/cdl/load"
22
+ params = {
23
+ "configName": config_name,
24
+ "fileName": file_name
25
+ }
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:
28
+ response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
29
+ user.raise_for_status(response)
30
+ # The response content is returned as bytes for a comma separated string of record IDs.
31
+ return [int(x) for x in bytes.decode(response.content).split(",")]
@@ -1,20 +1,55 @@
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
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
7
+ from sapiopylib.rest.User import SapioUser
8
+
9
+ from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
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']
7
40
 
8
41
 
9
42
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
10
43
  class FileBridge:
11
44
  @staticmethod
12
- def read_file(context: SapioWebhookContext, bridge_name: str, file_path: str, base64_decode: bool = True) -> bytes:
45
+ def read_file(context: UserIdentifier, bridge_name: str, file_path: str,
46
+ base64_decode: bool = True) -> bytes:
13
47
  """
14
48
  Read a file from FileBridge.
15
49
 
16
- :param context: The current webhook context.
17
- :param bridge_name: The name of the bridge to use.
50
+ :param context: The current webhook context or a user object to send requests from.
51
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
52
+ file bridge configurations.
18
53
  :param file_path: The path to read the file from.
19
54
  :param base64_decode: If true, base64 decode the file. Files are by default base64 encoded when retrieved from
20
55
  FileBridge.
@@ -24,8 +59,9 @@ class FileBridge:
24
59
  params = {
25
60
  'Filepath': f"bridge://{bridge_name}/{file_path}"
26
61
  }
27
- response = context.user.get(sub_path, params)
28
- context.user.raise_for_status(response)
62
+ user: SapioUser = AliasUtil.to_sapio_user(context)
63
+ response = user.get(sub_path, params)
64
+ user.raise_for_status(response)
29
65
 
30
66
  ret_val = response.content
31
67
  if base64_decode:
@@ -33,12 +69,14 @@ class FileBridge:
33
69
  return ret_val
34
70
 
35
71
  @staticmethod
36
- def write_file(context: SapioWebhookContext, bridge_name: str, file_path: str, file_data: bytes | str) -> None:
72
+ def write_file(context: UserIdentifier, bridge_name: str, file_path: str,
73
+ file_data: bytes | str) -> None:
37
74
  """
38
75
  Write a file to FileBridge.
39
76
 
40
- :param context: The current webhook context.
41
- :param bridge_name: The name of the bridge to use.
77
+ :param context: The current webhook context or a user object to send requests from.
78
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
79
+ file bridge configurations.
42
80
  :param file_path: The path to write the file to. If a file already exists at the given path then the file is
43
81
  overwritten.
44
82
  :param file_data: A string or bytes of the file to be written.
@@ -47,39 +85,43 @@ class FileBridge:
47
85
  params = {
48
86
  'Filepath': f"bridge://{bridge_name}/{file_path}"
49
87
  }
50
- with io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data) as data_stream:
51
- # noinspection PyTypeChecker
52
- response = context.user.post_data_stream(sub_path, params=params, data_stream=data_stream)
53
- context.user.raise_for_status(response)
88
+ user: SapioUser = AliasUtil.to_sapio_user(context)
89
+ with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data_stream:
90
+ response = user.post_data_stream(sub_path, params=params, data_stream=data_stream)
91
+ user.raise_for_status(response)
54
92
 
55
93
  @staticmethod
56
- def list_directory(context: SapioWebhookContext, bridge_name: str, file_path: str | None = "") -> list[str]:
94
+ def list_directory(context: UserIdentifier, bridge_name: str,
95
+ file_path: str | None = "") -> list[str]:
57
96
  """
58
97
  List the contents of a FileBridge directory.
59
98
 
60
- :param context: The current webhook context.
61
- :param bridge_name: The name of the bridge to use.
99
+ :param context: The current webhook context or a user object to send requests from.
100
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
101
+ file bridge configurations.
62
102
  :param file_path: The path to read the directory from.
63
- :return: A list of name of files and folders in the directory.
103
+ :return: A list of names of files and folders in the directory.
64
104
  """
65
105
  sub_path = '/ext/filebridge/listDirectory'
66
106
  params = {
67
107
  'Filepath': f"bridge://{bridge_name}/{file_path}"
68
108
  }
69
- response: Response = context.user.get(sub_path, params=params)
70
- context.user.raise_for_status(response)
109
+ user: SapioUser = AliasUtil.to_sapio_user(context)
110
+ response: Response = user.get(sub_path, params=params)
111
+ user.raise_for_status(response)
71
112
 
72
113
  response_body: list[str] = response.json()
73
114
  path_length = len(f"bridge://{bridge_name}/")
74
- return [urllib.parse.unquote(value[path_length:]) for value in response_body]
115
+ return [urllib.parse.unquote(value)[path_length:] for value in response_body]
75
116
 
76
117
  @staticmethod
77
- def create_directory(context: SapioWebhookContext, bridge_name: str, file_path: str) -> None:
118
+ def create_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
78
119
  """
79
120
  Create a new directory in FileBridge.
80
121
 
81
- :param context: The current webhook context.
82
- :param bridge_name: The name of the bridge to use.
122
+ :param context: The current webhook context or a user object to send requests from.
123
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
124
+ file bridge configurations.
83
125
  :param file_path: The path to create the directory at. If a directory already exists at the given path then an
84
126
  exception is raised.
85
127
  """
@@ -87,5 +129,87 @@ class FileBridge:
87
129
  params = {
88
130
  'Filepath': f"bridge://{bridge_name}/{file_path}"
89
131
  }
90
- response = context.user.post(sub_path, params=params)
91
- context.user.raise_for_status(response)
132
+ user: SapioUser = AliasUtil.to_sapio_user(context)
133
+ response = user.post(sub_path, params=params)
134
+ user.raise_for_status(response)
135
+
136
+ @staticmethod
137
+ def delete_file(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
138
+ """
139
+ Delete an existing file in FileBridge.
140
+
141
+ :param context: The current webhook context or a user object to send requests from.
142
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
143
+ file bridge configurations.
144
+ :param file_path: The path to the file to delete.
145
+ """
146
+ sub_path = '/ext/filebridge/deleteFile'
147
+ params = {
148
+ 'Filepath': f"bridge://{bridge_name}/{file_path}"
149
+ }
150
+ user: SapioUser = AliasUtil.to_sapio_user(context)
151
+ response = user.delete(sub_path, params=params)
152
+ user.raise_for_status(response)
153
+
154
+ @staticmethod
155
+ def delete_directory(context: UserIdentifier, bridge_name: str, file_path: str) -> None:
156
+ """
157
+ Delete an existing directory in FileBridge.
158
+
159
+ :param context: The current webhook context or a user object to send requests from.
160
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
161
+ file bridge configurations.
162
+ :param file_path: The path to the directory to delete.
163
+ """
164
+ sub_path = '/ext/filebridge/deleteDirectory'
165
+ params = {
166
+ 'Filepath': f"bridge://{bridge_name}/{file_path}"
167
+ }
168
+ user: SapioUser = AliasUtil.to_sapio_user(context)
169
+ response = user.delete(sub_path, params=params)
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]