sapiopycommons 2024.11.7a354__py3-none-any.whl → 2024.11.8a359__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 (47) hide show
  1. sapiopycommons/callbacks/callback_util.py +83 -532
  2. sapiopycommons/chem/IndigoMolecules.py +0 -2
  3. sapiopycommons/chem/Molecules.py +18 -77
  4. sapiopycommons/datatype/attachment_util.py +10 -11
  5. sapiopycommons/eln/experiment_handler.py +70 -272
  6. sapiopycommons/files/complex_data_loader.py +4 -5
  7. sapiopycommons/files/file_bridge.py +24 -31
  8. sapiopycommons/files/file_data_handler.py +5 -2
  9. sapiopycommons/files/file_util.py +9 -59
  10. sapiopycommons/files/file_validator.py +6 -92
  11. sapiopycommons/files/file_writer.py +15 -44
  12. sapiopycommons/general/aliases.py +6 -207
  13. sapiopycommons/general/custom_report_util.py +37 -212
  14. sapiopycommons/general/exceptions.py +8 -21
  15. sapiopycommons/general/popup_util.py +0 -21
  16. sapiopycommons/general/time_util.py +2 -8
  17. sapiopycommons/processtracking/endpoints.py +22 -22
  18. sapiopycommons/recordmodel/record_handler.py +97 -481
  19. sapiopycommons/rules/eln_rule_handler.py +25 -34
  20. sapiopycommons/rules/on_save_rule_handler.py +31 -34
  21. sapiopycommons/webhook/webhook_handlers.py +42 -201
  22. {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/METADATA +2 -4
  23. sapiopycommons-2024.11.8a359.dist-info/RECORD +38 -0
  24. sapiopycommons/callbacks/field_builder.py +0 -537
  25. sapiopycommons/customreport/__init__.py +0 -0
  26. sapiopycommons/customreport/column_builder.py +0 -60
  27. sapiopycommons/customreport/custom_report_builder.py +0 -130
  28. sapiopycommons/customreport/term_builder.py +0 -299
  29. sapiopycommons/datatype/data_fields.py +0 -61
  30. sapiopycommons/datatype/pseudo_data_types.py +0 -440
  31. sapiopycommons/eln/experiment_report_util.py +0 -653
  32. sapiopycommons/files/file_bridge_handler.py +0 -340
  33. sapiopycommons/flowcyto/flow_cyto.py +0 -77
  34. sapiopycommons/flowcyto/flowcyto_data.py +0 -75
  35. sapiopycommons/general/accession_service.py +0 -375
  36. sapiopycommons/general/audit_log.py +0 -189
  37. sapiopycommons/general/sapio_links.py +0 -50
  38. sapiopycommons/multimodal/multimodal.py +0 -146
  39. sapiopycommons/multimodal/multimodal_data.py +0 -489
  40. sapiopycommons/processtracking/custom_workflow_handler.py +0 -406
  41. sapiopycommons/sftpconnect/__init__.py +0 -0
  42. sapiopycommons/sftpconnect/sftp_builder.py +0 -69
  43. sapiopycommons/webhook/webhook_context.py +0 -39
  44. sapiopycommons/webhook/webservice_handlers.py +0 -67
  45. sapiopycommons-2024.11.7a354.dist-info/RECORD +0 -59
  46. {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/WHEEL +0 -0
  47. {sapiopycommons-2024.11.7a354.dist-info → sapiopycommons-2024.11.8a359.dist-info}/licenses/LICENSE +0 -0
@@ -1,340 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import abstractmethod, ABC
4
- from weakref import WeakValueDictionary
5
-
6
- from sapiopylib.rest.User import SapioUser
7
-
8
- from sapiopycommons.files.file_bridge import FileBridge
9
- from sapiopycommons.general.aliases import AliasUtil, UserIdentifier
10
-
11
-
12
- class FileBridgeHandler:
13
- """
14
- The FileBridgeHandler provides caching of the results of file bridge endpoint calls while also containing quality
15
- of life functions for common file bridge actions.
16
- """
17
- user: SapioUser
18
- __bridge: str
19
- __file_cache: dict[str, bytes]
20
- """A cache of file paths to file bytes."""
21
- __files: dict[str, File]
22
- """A cache of file paths to File objects."""
23
- __dir_cache: dict[str, list[str]]
24
- """A cache of directory file paths to the names of the files or nested directories within it."""
25
- __directories: dict[str, Directory]
26
- """A cache of directory file paths to Directory objects."""
27
-
28
- __instances: WeakValueDictionary[str, FileBridgeHandler] = WeakValueDictionary()
29
- __initialized: bool
30
-
31
- def __new__(cls, context: UserIdentifier, bridge_name: str):
32
- """
33
- :param context: The current webhook context or a user object to send requests from.
34
- """
35
- user = AliasUtil.to_sapio_user(context)
36
- key = f"{user.__hash__()}:{bridge_name}"
37
- obj = cls.__instances.get(key)
38
- if not obj:
39
- obj = object.__new__(cls)
40
- obj.__initialized = False
41
- cls.__instances[key] = obj
42
- return obj
43
-
44
- def __init__(self, context: UserIdentifier, bridge_name: str):
45
- """
46
- :param context: The current webhook context or a user object to send requests from.
47
- :param bridge_name: The name of the bridge to communicate with. This is the "connection name" in the
48
- file bridge configurations.
49
- """
50
- if self.__initialized:
51
- return
52
- self.__initialized = True
53
-
54
- self.user = AliasUtil.to_sapio_user(context)
55
- self.__bridge = bridge_name
56
- self.__file_cache = {}
57
- self.__files = {}
58
- self.__dir_cache = {}
59
- self.__directories = {}
60
-
61
- @property
62
- def connection_name(self) -> str:
63
- return self.__bridge
64
-
65
- def clear_caches(self) -> None:
66
- """
67
- Clear the file and directory caches of this handler.
68
- """
69
- self.__file_cache.clear()
70
- self.__files.clear()
71
- self.__dir_cache.clear()
72
- self.__directories.clear()
73
-
74
- def read_file(self, file_path: str, base64_decode: bool = True) -> bytes:
75
- """
76
- Read a file from FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
77
- file will not make an additional webservice call.
78
-
79
- :param file_path: The path to read the file from.
80
- :param base64_decode: If true, base64 decode the file. Files are by default base64 encoded when retrieved from
81
- FileBridge.
82
- :return: The bytes of the file.
83
- """
84
- if file_path in self.__file_cache:
85
- return self.__file_cache[file_path]
86
- file_bytes: bytes = FileBridge.read_file(self.user, self.__bridge, file_path, base64_decode)
87
- self.__file_cache[file_path] = file_bytes
88
- return file_bytes
89
-
90
- def write_file(self, file_path: str, file_data: bytes | str) -> None:
91
- """
92
- Write a file to FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
93
- file will not make an additional webservice call.
94
-
95
- :param file_path: The path to write the file to. If a file already exists at the given path then the file is
96
- overwritten.
97
- :param file_data: A string or bytes of the file to be written.
98
- """
99
- FileBridge.write_file(self.user, self.__bridge, file_path, file_data)
100
- self.__file_cache[file_path] = file_data if isinstance(file_data, bytes) else file_data.encode()
101
-
102
- # Find the directory path to this file and the name of the file. Add the file name to the cached list of
103
- # files for the directory, assuming we have this directory cached and the file isn't already in it.
104
- last_slash: int = file_path.rfind("/")
105
- dir_path: str = file_path[:last_slash]
106
- file_name: str = file_path[last_slash + 1:]
107
- if dir_path in self.__dir_cache and file_path not in self.__dir_cache[dir_path]:
108
- self.__dir_cache[dir_path].append(file_name)
109
-
110
- def delete_file(self, file_path: str) -> None:
111
- """
112
- Delete an existing file in FileBridge. If this file is in the cache, it will also be deleted from the cache.
113
-
114
- :param file_path: The path to the file to delete.
115
- """
116
- FileBridge.delete_file(self.user, self.__bridge, file_path)
117
- if file_path in self.__file_cache:
118
- self.__file_cache.pop(file_path)
119
- if file_path in self.__files:
120
- self.__files.pop(file_path)
121
-
122
- def list_directory(self, file_path: str) -> list[str]:
123
- """
124
- List the contents of a FileBridge directory. The contents of this directory will be cached so that any
125
- subsequent lists of this directory will not make an additional webservice call.
126
-
127
- :param file_path: The path to read the directory from.
128
- :return: A list of names of files and folders in the directory.
129
- """
130
- if file_path in self.__dir_cache:
131
- return self.__dir_cache[file_path]
132
- files: list[str] = FileBridge.list_directory(self.user, self.__bridge, file_path)
133
- self.__dir_cache[file_path] = files
134
- return files
135
-
136
- def create_directory(self, file_path: str) -> None:
137
- """
138
- Create a new directory in FileBridge. This new directory will be added to the cache as empty so that listing
139
- the same directory does not make an additional webservice call.
140
-
141
- :param file_path: The path to create the directory at. If a directory already exists at the given path then an
142
- exception is raised.
143
- """
144
- FileBridge.create_directory(self.user, self.__bridge, file_path)
145
- # This directory was just created, so we know it's empty.
146
- self.__dir_cache[file_path] = []
147
-
148
- def delete_directory(self, file_path: str) -> None:
149
- """
150
- Delete an existing directory in FileBridge. If this directory is in the cache, it will also be deleted
151
- from the cache.
152
-
153
- :param file_path: The path to the directory to delete.
154
- """
155
- FileBridge.delete_directory(self.user, self.__bridge, file_path)
156
- if file_path in self.__dir_cache:
157
- self.__dir_cache.pop(file_path)
158
- if file_path in self.__directories:
159
- self.__directories.pop(file_path)
160
-
161
- def is_file(self, file_path: str) -> bool:
162
- """
163
- Determine if the given file path points to a file or a directory. This is achieved by trying to call
164
- list_directory on the given file path. If an exception is thrown, that's because the function was called
165
- on a file. If no exception is thrown, then we know that this is a directory, and we have now also cached
166
- the contents of that directory if it wasn't cached already.
167
-
168
- :param file_path: A file path.
169
- :return: True if the file path points to a file. False if it points to a directory.
170
- """
171
- try:
172
- self.list_directory(file_path)
173
- return False
174
- except Exception:
175
- return True
176
-
177
- def move_file(self, move_from: str, move_to: str, old_name: str, new_name: str | None = None) -> None:
178
- """
179
- Move a file from one location to another within File Bridge. This is done be reading the file into memory,
180
- writing a copy of the file in the new location, then deleting the original file.
181
-
182
- :param move_from: The path to the current location of the file.
183
- :param move_to: The path to move the file to.
184
- :param old_name: The current name of the file.
185
- :param new_name: The name that the file should have after it is moved. if this is not provided, then the new
186
- name will be the same as the old name.
187
- """
188
- if not new_name:
189
- new_name = old_name
190
-
191
- # Read the file into memory.
192
- file_bytes: bytes = self.read_file(move_from + "/" + old_name)
193
- # Write the file into the new location.
194
- self.write_file(move_to + "/" + new_name, file_bytes)
195
- # Delete the file from the old location. We do this last in case the write call fails.
196
- self.delete_file(move_from + "/" + old_name)
197
-
198
- def get_file_object(self, file_path: str) -> File:
199
- """
200
- Get a File object from a file path. This object can be used to get the contents of the file at this path
201
- and traverse up the file hierarchy to the directory that the file is contained within.
202
-
203
- There is no guarantee that this file actually exists within the current file bridge connection when it is
204
- constructed. If the file doesn't exist, then retrieving its contents will fail.
205
-
206
- :param file_path: A file path.
207
- :return: A File object constructed form the given file path.
208
- """
209
- if file_path in self.__files:
210
- return self.__files[file_path]
211
- file = File(self, file_path)
212
- self.__files[file_path] = file
213
- return file
214
-
215
- def get_directory_object(self, file_path: str) -> Directory | None:
216
- """
217
- Get a Directory object from a file path. This object can be used to traverse up and down the file hierarchy
218
- by going up to the parent directory that this directory is contained within or going down to the contents of
219
- this directory.
220
-
221
- There is no guarantee that this directory actually exists within the current file bridge connection when it is
222
- constructed. If the directory doesn't exist, then retrieving its contents will fail.
223
-
224
- :param file_path: A file path.
225
- :return: A Directory object constructed form the given file path.
226
- """
227
- if file_path is None:
228
- return None
229
- if file_path in self.__directories:
230
- return self.__directories[file_path]
231
- directory = Directory(self, file_path)
232
- self.__directories[file_path] = directory
233
- return directory
234
-
235
-
236
- class FileBridgeObject(ABC):
237
- """
238
- A FileBridgeObject is either a file or a directory that is contained within file bridge. Every object has a
239
- name and a parent directory that it is contained within (unless the object is located in the bridge root, in
240
- which case the parent is None). From the name and the parent, a path can be constructed to that object.
241
- """
242
- _handler: FileBridgeHandler
243
- name: str
244
- parent: Directory | None
245
-
246
- def __init__(self, handler: FileBridgeHandler, file_path: str):
247
- self._handler = handler
248
-
249
- name, root = split_path(file_path)
250
- self.name = name
251
- self.parent = handler.get_directory_object(root)
252
-
253
- @abstractmethod
254
- def is_file(self) -> bool:
255
- """
256
- :return: True if this object is a file. False if it is a directory.
257
- """
258
- pass
259
-
260
- def get_path(self) -> str:
261
- """
262
- :return: The file path that leads to this object.
263
- """
264
- if self.parent is None:
265
- return self.name
266
- return self.parent.get_path() + "/" + self.name
267
-
268
-
269
- class File(FileBridgeObject):
270
- def __init__(self, handler: FileBridgeHandler, file_path: str):
271
- """
272
- :param handler: A FileBridgeHandler for the connection that this file came from.
273
- :param file_path: The path to this file.
274
- """
275
- super().__init__(handler, file_path)
276
-
277
- @property
278
- def contents(self) -> bytes:
279
- """
280
- :return: The bytes of this file.
281
- This pulls from the cache of this object's related FileBridgeHandler.
282
- """
283
- return self._handler.read_file(self.get_path())
284
-
285
- def is_file(self) -> bool:
286
- return True
287
-
288
-
289
- class Directory(FileBridgeObject):
290
- def __init__(self, handler: FileBridgeHandler, file_path: str):
291
- """
292
- :param handler: A FileBridgeHandler for the connection that this directory came from.
293
- :param file_path: The path to this directory.
294
- """
295
- super().__init__(handler, file_path)
296
-
297
- @property
298
- def contents(self) -> dict[str, FileBridgeObject]:
299
- """
300
- :return: A dictionary of object names to the objects (Files or Directories) contained within this Directory.
301
- This pulls from the cache of this object's related FileBridgeHandler.
302
- """
303
- contents: dict[str, FileBridgeObject] = {}
304
- path: str = self.get_path()
305
- for name in self._handler.list_directory(path):
306
- file_path: str = path + "/" + name
307
- if self._handler.is_file(file_path):
308
- contents[name] = self._handler.get_file_object(file_path)
309
- else:
310
- contents[name] = self._handler.get_directory_object(file_path)
311
- return contents
312
-
313
- def is_file(self) -> bool:
314
- return False
315
-
316
- def get_files(self) -> dict[str, File]:
317
- """
318
- :return: A mapping of file name to File for every file in this Directory.
319
- This pulls from the cache of this object's related FileBridgeHandler.
320
- """
321
- return {x: y for x, y in self.contents.items() if y.is_file()}
322
-
323
- def get_directories(self) -> dict[str, Directory]:
324
- """
325
- :return: A mapping of directory name to Directory for every directory in this Directory.
326
- This pulls from the cache of this object's related FileBridgeHandler.
327
- """
328
- return {x: y for x, y in self.contents.items() if not y.is_file()}
329
-
330
-
331
- def split_path(file_path: str) -> tuple[str, str]:
332
- """
333
- :param file_path: A file path where directories are separated the "/" characters.
334
- :return: A tuple of two strings that splits the path on its last slash. The first string is the name of the
335
- file/directory at the given file path and the second string is the location to that file.
336
- """
337
- last_slash: int = file_path.rfind("/")
338
- if last_slash == -1:
339
- return file_path, None
340
- return file_path[last_slash + 1:], file_path[:last_slash]
@@ -1,77 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from weakref import WeakValueDictionary
4
-
5
- from sapiopylib.rest.User import SapioUser
6
- from databind.json import dumps
7
-
8
- from sapiopycommons.flowcyto.flowcyto_data import FlowJoWorkspaceInputJson, UploadFCSInputJson, \
9
- ComputeFlowStatisticsInputJson
10
-
11
-
12
- class FlowCytoManager:
13
- """
14
- This manager includes flow cytometry analysis tools that would require FlowCyto license to use.
15
- """
16
- _user: SapioUser
17
-
18
- __instances: WeakValueDictionary[SapioUser, FlowCytoManager] = WeakValueDictionary()
19
- __initialized: bool
20
-
21
- def __new__(cls, user: SapioUser):
22
- """
23
- Observes singleton pattern per record model manager object.
24
-
25
- :param user: The user that will make the webservice request to the application.
26
- """
27
- obj = cls.__instances.get(user)
28
- if not obj:
29
- obj = object.__new__(cls)
30
- obj.__initialized = False
31
- cls.__instances[user] = obj
32
- return obj
33
-
34
- def __init__(self, user: SapioUser):
35
- if self.__initialized:
36
- return
37
- self._user = user
38
- self.__initialized = True
39
-
40
- def create_flowjo_workspace(self, workspace_input: FlowJoWorkspaceInputJson) -> int:
41
- """
42
- Create FlowJo Workspace and return the workspace record ID of workspace root record,
43
- after successful creation.
44
- :param workspace_input: the request data payload.
45
- :return: The new workspace record ID.
46
- """
47
- payload = dumps(workspace_input, FlowJoWorkspaceInputJson)
48
- response = self._user.plugin_post("flowcyto/workspace", payload=payload, is_payload_plain_text=True)
49
- self._user.raise_for_status(response)
50
- return int(response.json())
51
-
52
- def upload_fcs_for_sample(self, upload_input: UploadFCSInputJson) -> int:
53
- """
54
- Upload FCS file as root of the sample FCS.
55
- :param upload_input: The request data payload
56
- :return: The root FCS file uploaded under sample.
57
- """
58
- payload = dumps(upload_input, UploadFCSInputJson)
59
- response = self._user.plugin_post("flowcyto/fcs", payload=payload, is_payload_plain_text=True)
60
- self._user.raise_for_status(response)
61
- return int(response.json())
62
-
63
- def compute_statistics(self, stat_compute_input: ComputeFlowStatisticsInputJson) -> list[int]:
64
- """
65
- Requests to compute flow cytometry statistics.
66
- The children are of type FCSStatistic.
67
- If the FCS files have not been evaluated yet,
68
- then the lazy evaluation will be performed immediately prior to computing statistics, which can take longer.
69
- If any new statistics are computed as children of FCS, they will be returned in the result record id list.
70
- Note: if input has multiple FCS files, the client should try to get parent FCS file from each record to figure out which one is for which FCS.
71
- :param stat_compute_input:
72
- :return:
73
- """
74
- payload = dumps(stat_compute_input, ComputeFlowStatisticsInputJson)
75
- response = self._user.plugin_post("flowcyto/statistics", payload=payload, is_payload_plain_text=True)
76
- self._user.raise_for_status(response)
77
- return list(response.json())
@@ -1,75 +0,0 @@
1
- import base64
2
- from enum import Enum
3
-
4
- from databind.core.dataclasses import dataclass
5
-
6
-
7
- class ChannelStatisticType(Enum):
8
- """
9
- All supported channel statistics type.
10
- """
11
- MEAN = "(Mean) MFI"
12
- MEDIAN = "(Median) MFI"
13
- STD_EV = "Std. Dev."
14
- COEFFICIENT_OF_VARIATION = "CV"
15
-
16
- display_name: str
17
-
18
- def __init__(self, display_name: str):
19
- self.display_name = display_name
20
-
21
-
22
- @dataclass
23
- class ChannelStatisticsParameterJSON:
24
- channelNameList: list[str]
25
- statisticsType: ChannelStatisticType
26
-
27
- def __init__(self, channel_name_list: list[str], stat_type: ChannelStatisticType):
28
- self.channelNameList = channel_name_list
29
- self.statisticsType = stat_type
30
-
31
-
32
- @dataclass
33
- class ComputeFlowStatisticsInputJson:
34
- fcsFileRecordIdList: list[int]
35
- statisticsParameterList: list[ChannelStatisticsParameterJSON]
36
-
37
- def __init__(self, fcs_file_record_id_list: list[int], statistics_parameter_list: list[ChannelStatisticsParameterJSON]):
38
- self.fcsFileRecordIdList = fcs_file_record_id_list
39
- self.statisticsParameterList = statistics_parameter_list
40
-
41
-
42
- @dataclass
43
- class FlowJoWorkspaceInputJson:
44
- filePath: str
45
- base64Data: str
46
-
47
- def __init__(self, filePath: str, file_data: bytes):
48
- self.filePath = filePath
49
- self.base64Data = base64.b64encode(file_data).decode('utf-8')
50
-
51
-
52
- @dataclass
53
- class UploadFCSInputJson:
54
- """
55
- Request to upload new FCS file
56
- Attributes:
57
- filePath: The file name of the FCS file to be uploaded. For FlowJo workspace, this is important to match the file in group (via file names).
58
- attachmentDataType: the attachment data type that contains already-uploaded FCS data.
59
- attachmentRecordId: the attachment record ID that contains already-uploaded FCS data.
60
- associatedRecordDataType: the "parent" association for the FCS. Can either be a workspace or a sample record.
61
- associatedRecordId: the "parent" association for the FCS. Can either be a workspace or a sample record.
62
- """
63
- filePath: str
64
- attachmentDataType: str
65
- attachmentRecordId: int
66
- associatedRecordDataType: str
67
- associatedRecordId: int
68
-
69
- def __init__(self, associated_record_data_type: str, associated_record_id: int,
70
- file_path: str, attachment_data_type: str, attachment_record_id: int):
71
- self.filePath = file_path
72
- self.attachmentDataType = attachment_data_type
73
- self.attachmentRecordId = attachment_record_id
74
- self.associatedRecordDataType = associated_record_data_type
75
- self.associatedRecordId = associated_record_id