sapiopycommons 2024.3.18a156__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.
- sapiopycommons/callbacks/__init__.py +0 -0
- sapiopycommons/callbacks/callback_util.py +2041 -0
- sapiopycommons/callbacks/field_builder.py +545 -0
- sapiopycommons/chem/IndigoMolecules.py +52 -5
- sapiopycommons/chem/Molecules.py +114 -30
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +137 -0
- sapiopycommons/customreport/term_builder.py +315 -0
- sapiopycommons/datatype/attachment_util.py +17 -15
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +390 -90
- sapiopycommons/eln/experiment_report_util.py +649 -0
- sapiopycommons/eln/plate_designer.py +152 -0
- sapiopycommons/files/complex_data_loader.py +31 -0
- sapiopycommons/files/file_bridge.py +153 -25
- sapiopycommons/files/file_bridge_handler.py +555 -0
- sapiopycommons/files/file_data_handler.py +633 -0
- sapiopycommons/files/file_util.py +270 -158
- sapiopycommons/files/file_validator.py +569 -0
- sapiopycommons/files/file_writer.py +377 -0
- sapiopycommons/flowcyto/flow_cyto.py +77 -0
- sapiopycommons/flowcyto/flowcyto_data.py +75 -0
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +259 -18
- sapiopycommons/general/audit_log.py +185 -0
- sapiopycommons/general/custom_report_util.py +252 -31
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +69 -7
- sapiopycommons/general/popup_util.py +85 -18
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/storage_util.py +148 -0
- sapiopycommons/general/time_util.py +97 -7
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/__init__.py +0 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +192 -0
- sapiopycommons/recordmodel/record_handler.py +653 -149
- sapiopycommons/rules/eln_rule_handler.py +89 -8
- sapiopycommons/rules/on_save_rule_handler.py +89 -12
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +70 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +617 -69
- sapiopycommons/webhook/webservice_handlers.py +317 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
- sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod, ABC
|
|
4
|
+
from typing import cast
|
|
5
|
+
from weakref import WeakValueDictionary
|
|
6
|
+
|
|
7
|
+
from sapiopylib.rest.User import SapioUser
|
|
8
|
+
|
|
9
|
+
from sapiopycommons.files.file_bridge import FileBridge, FileBridgeMetadata
|
|
10
|
+
from sapiopycommons.general.aliases import AliasUtil, UserIdentifier
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileBridgeHandler:
|
|
14
|
+
"""
|
|
15
|
+
The FileBridgeHandler provides caching of the results of file bridge endpoint calls while also containing quality
|
|
16
|
+
of life functions for common file bridge actions.
|
|
17
|
+
"""
|
|
18
|
+
user: SapioUser
|
|
19
|
+
__bridge: str
|
|
20
|
+
__file_data_cache: dict[str, bytes]
|
|
21
|
+
"""A cache of file paths to file bytes."""
|
|
22
|
+
__file_objects: dict[str, File]
|
|
23
|
+
"""A cache of file paths to File objects."""
|
|
24
|
+
__dir_file_name_cache: dict[str, list[str]]
|
|
25
|
+
"""A cache of directory file paths to the names of the files or nested directories within them."""
|
|
26
|
+
__dir_objects: dict[str, Directory]
|
|
27
|
+
"""A cache of directory file paths to Directory objects."""
|
|
28
|
+
__file_metadata_cache: dict[str, FileBridgeMetadata]
|
|
29
|
+
"""A cache of file or directory paths to file metadata."""
|
|
30
|
+
__dir_metadata_cache: dict[str, list[FileBridgeMetadata]]
|
|
31
|
+
"""A cache of directory file paths to the metadata of the files or nested directories within them."""
|
|
32
|
+
|
|
33
|
+
__instances: WeakValueDictionary[str, FileBridgeHandler] = WeakValueDictionary()
|
|
34
|
+
__initialized: bool
|
|
35
|
+
|
|
36
|
+
def __new__(cls, context: UserIdentifier, bridge_name: str):
|
|
37
|
+
"""
|
|
38
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
39
|
+
"""
|
|
40
|
+
user = AliasUtil.to_sapio_user(context)
|
|
41
|
+
key = f"{user.__hash__()}:{bridge_name}"
|
|
42
|
+
obj = cls.__instances.get(key)
|
|
43
|
+
if not obj:
|
|
44
|
+
obj = object.__new__(cls)
|
|
45
|
+
obj.__initialized = False
|
|
46
|
+
cls.__instances[key] = obj
|
|
47
|
+
return obj
|
|
48
|
+
|
|
49
|
+
def __init__(self, context: UserIdentifier, bridge_name: str):
|
|
50
|
+
"""
|
|
51
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
52
|
+
:param bridge_name: The name of the bridge to communicate with. This is the "connection name" in the
|
|
53
|
+
file bridge configurations.
|
|
54
|
+
"""
|
|
55
|
+
if self.__initialized:
|
|
56
|
+
return
|
|
57
|
+
self.__initialized = True
|
|
58
|
+
|
|
59
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
60
|
+
self.__bridge = bridge_name
|
|
61
|
+
self.__file_data_cache = {}
|
|
62
|
+
self.__file_objects = {}
|
|
63
|
+
self.__dir_file_name_cache = {}
|
|
64
|
+
self.__dir_objects = {}
|
|
65
|
+
self.__file_metadata_cache = {}
|
|
66
|
+
self.__dir_metadata_cache = {}
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def connection_name(self) -> str:
|
|
70
|
+
return self.__bridge
|
|
71
|
+
|
|
72
|
+
def clear_caches(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Clear the file and directory caches of this handler.
|
|
75
|
+
"""
|
|
76
|
+
self.__file_data_cache.clear()
|
|
77
|
+
self.__file_objects.clear()
|
|
78
|
+
self.__dir_file_name_cache.clear()
|
|
79
|
+
self.__dir_objects.clear()
|
|
80
|
+
self.__file_metadata_cache.clear()
|
|
81
|
+
self.__dir_metadata_cache.clear()
|
|
82
|
+
|
|
83
|
+
# CR-47388: Allow the FileBridgeHandler's File and Directory objects to be provided as file path parameters.
|
|
84
|
+
def file_exists(self, file_path: str | File | Directory) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Determine if a file or directory exists in FileBridge at the provided path. This is achieved by calling for the
|
|
87
|
+
metadata of the provided file path. If the file does not exist, then an exception is raised, which is caught and
|
|
88
|
+
handled by this function as a return value of False.
|
|
89
|
+
|
|
90
|
+
:param file_path: A file path, File object, or Directory object.
|
|
91
|
+
:return: True if the file exists. False if it does not.
|
|
92
|
+
"""
|
|
93
|
+
if isinstance(file_path, FileBridgeObject):
|
|
94
|
+
file_path = file_path.path
|
|
95
|
+
try:
|
|
96
|
+
self.file_metadata(file_path)
|
|
97
|
+
return True
|
|
98
|
+
except Exception:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def read_file(self, file_path: str | File, base64_decode: bool = True) -> bytes:
|
|
102
|
+
"""
|
|
103
|
+
Read a file from FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
104
|
+
file will not make an additional webservice call.
|
|
105
|
+
|
|
106
|
+
:param file_path: The file path or File object to read from.
|
|
107
|
+
:param base64_decode: If true, base64 decode the file. Files are base64 encoded by default when retrieved from
|
|
108
|
+
FileBridge.
|
|
109
|
+
:return: The bytes of the file.
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(file_path, File):
|
|
112
|
+
file_path = file_path.path
|
|
113
|
+
if file_path in self.__file_data_cache:
|
|
114
|
+
return self.__file_data_cache[file_path]
|
|
115
|
+
file_bytes: bytes = FileBridge.read_file(self.user, self.__bridge, file_path, base64_decode)
|
|
116
|
+
self.__file_data_cache[file_path] = file_bytes
|
|
117
|
+
return file_bytes
|
|
118
|
+
|
|
119
|
+
def write_file(self, file_path: str | File, file_data: bytes | str) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Write a file to FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
122
|
+
file will not make an additional webservice call.
|
|
123
|
+
|
|
124
|
+
:param file_path: The file path or File object to write to. If a file already exists at the given path then the
|
|
125
|
+
file is overwritten.
|
|
126
|
+
:param file_data: A string or bytes of the file to be written.
|
|
127
|
+
"""
|
|
128
|
+
if isinstance(file_path, File):
|
|
129
|
+
file_path = file_path.path
|
|
130
|
+
FileBridge.write_file(self.user, self.__bridge, file_path, file_data)
|
|
131
|
+
self.__file_data_cache[file_path] = file_data if isinstance(file_data, bytes) else file_data.encode()
|
|
132
|
+
|
|
133
|
+
# Find the directory path to this file and the name of the file. Add the file name to the cached list of
|
|
134
|
+
# files for the directory, assuming we have this directory cached and the file isn't already in it.
|
|
135
|
+
file_name, path_to = split_path(file_path)
|
|
136
|
+
if path_to in self.__dir_file_name_cache and file_name not in self.__dir_file_name_cache[path_to]:
|
|
137
|
+
self.__dir_file_name_cache[path_to].append(file_name)
|
|
138
|
+
|
|
139
|
+
def delete_file(self, file_path: str | File) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Delete an existing file in FileBridge. If this file is in the cache, it will also be deleted from the cache.
|
|
142
|
+
|
|
143
|
+
:param file_path: The file path or File object to delete.
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(file_path, File):
|
|
146
|
+
file_path = file_path.path
|
|
147
|
+
FileBridge.delete_file(self.user, self.__bridge, file_path)
|
|
148
|
+
if file_path in self.__file_data_cache:
|
|
149
|
+
self.__file_data_cache.pop(file_path)
|
|
150
|
+
if file_path in self.__file_objects:
|
|
151
|
+
self.__file_objects.pop(file_path)
|
|
152
|
+
if file_path in self.__file_metadata_cache:
|
|
153
|
+
self.__file_metadata_cache.pop(file_path)
|
|
154
|
+
|
|
155
|
+
def list_directory(self, file_path: str | Directory) -> list[str]:
|
|
156
|
+
"""
|
|
157
|
+
List the contents of a FileBridge directory. The contents of this directory will be cached so that any
|
|
158
|
+
subsequent lists of this directory will not make an additional webservice call.
|
|
159
|
+
|
|
160
|
+
:param file_path: The directory path or Directory object to list from.
|
|
161
|
+
:return: A list of names of files and folders in the directory.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(file_path, Directory):
|
|
164
|
+
file_path = file_path.path
|
|
165
|
+
if file_path in self.__dir_file_name_cache:
|
|
166
|
+
return self.__dir_file_name_cache[file_path]
|
|
167
|
+
files: list[str] = FileBridge.list_directory(self.user, self.__bridge, file_path)
|
|
168
|
+
self.__dir_file_name_cache[file_path] = files
|
|
169
|
+
return files
|
|
170
|
+
|
|
171
|
+
def create_directory(self, file_path: str | Directory) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Create a new directory in FileBridge. This new directory will be added to the cache as empty so that listing
|
|
174
|
+
the same directory does not make an additional webservice call.
|
|
175
|
+
|
|
176
|
+
:param file_path: The directory path or Directory object to create the directory at. If a directory already
|
|
177
|
+
exists at the given path then an exception is raised.
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(file_path, Directory):
|
|
180
|
+
file_path = file_path.path
|
|
181
|
+
FileBridge.create_directory(self.user, self.__bridge, file_path)
|
|
182
|
+
# This directory was just created, so we know it's empty.
|
|
183
|
+
self.__dir_file_name_cache[file_path] = []
|
|
184
|
+
self.__dir_metadata_cache[file_path] = []
|
|
185
|
+
|
|
186
|
+
# Find the directory path to this directory and the name of the directory. Add the directory name to the cached
|
|
187
|
+
# list of files for the directory, assuming we have this directory cached and the directory isn't already in it.
|
|
188
|
+
dir_name, path_to = split_path(file_path)
|
|
189
|
+
if path_to in self.__dir_file_name_cache and dir_name not in self.__dir_file_name_cache[path_to]:
|
|
190
|
+
self.__dir_file_name_cache[path_to].append(dir_name)
|
|
191
|
+
|
|
192
|
+
def delete_directory(self, file_path: str | Directory) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Delete an existing directory in FileBridge. If this directory is in the cache, it will also be deleted
|
|
195
|
+
from the cache.
|
|
196
|
+
|
|
197
|
+
:param file_path: The directory path or Directory object to delete.
|
|
198
|
+
"""
|
|
199
|
+
if isinstance(file_path, Directory):
|
|
200
|
+
file_path = file_path.path
|
|
201
|
+
FileBridge.delete_directory(self.user, self.__bridge, file_path)
|
|
202
|
+
# The deletion of a directory also deletes all the files within it, so we need to check every cache for file
|
|
203
|
+
# paths that start with this directory path and remove them.
|
|
204
|
+
for key in list(self.__file_data_cache.keys()):
|
|
205
|
+
if key.startswith(file_path):
|
|
206
|
+
self.__file_data_cache.pop(key)
|
|
207
|
+
for key in list(self.__file_objects.keys()):
|
|
208
|
+
if key.startswith(file_path):
|
|
209
|
+
self.__file_objects.pop(key)
|
|
210
|
+
for key in list(self.__file_metadata_cache.keys()):
|
|
211
|
+
if key.startswith(file_path):
|
|
212
|
+
self.__file_metadata_cache.pop(key)
|
|
213
|
+
for key in list(self.__dir_file_name_cache.keys()):
|
|
214
|
+
if key.startswith(file_path):
|
|
215
|
+
self.__dir_file_name_cache.pop(key)
|
|
216
|
+
for key in list(self.__dir_objects.keys()):
|
|
217
|
+
if key.startswith(file_path):
|
|
218
|
+
self.__dir_objects.pop(key)
|
|
219
|
+
for key in list(self.__dir_metadata_cache.keys()):
|
|
220
|
+
if key.startswith(file_path):
|
|
221
|
+
self.__dir_metadata_cache.pop(key)
|
|
222
|
+
|
|
223
|
+
# FR-47387: Add support for the metadata endpoints in FileBridge.
|
|
224
|
+
def file_metadata(self, file_path: str | File | Directory) -> FileBridgeMetadata:
|
|
225
|
+
"""
|
|
226
|
+
Get metadata for a file in FileBridge. If this metadata is already cached, then it will be returned from the
|
|
227
|
+
cache.
|
|
228
|
+
|
|
229
|
+
The file path may be to a directory, in which case only the metadata for that directory will be returned. If you
|
|
230
|
+
want the metadata for the contents of a directory, then use the directory_metadata function.
|
|
231
|
+
|
|
232
|
+
:param file_path: The file path, File object, or Directory object to retrieve the metadata from.
|
|
233
|
+
:return: The metadata for the file.
|
|
234
|
+
"""
|
|
235
|
+
if isinstance(file_path, FileBridgeObject):
|
|
236
|
+
file_path = file_path.path
|
|
237
|
+
if file_path in self.__file_metadata_cache:
|
|
238
|
+
return self.__file_metadata_cache[file_path]
|
|
239
|
+
metadata: FileBridgeMetadata = FileBridge.file_metadata(self.user, self.__bridge, file_path)
|
|
240
|
+
self.__file_metadata_cache[file_path] = metadata
|
|
241
|
+
|
|
242
|
+
# It's possible that this file is newly created, but the directory it's in was already cached. If that's the
|
|
243
|
+
# case, then we need to add this file's metadata to the directory's cache. (The write_file/create_directory
|
|
244
|
+
# methods will have already handled the directory's file name cache.)
|
|
245
|
+
file_name, path_to = split_path(file_path)
|
|
246
|
+
if (path_to in self.__dir_metadata_cache
|
|
247
|
+
and not any([file_name == x.file_name for x in self.__dir_metadata_cache[path_to]])):
|
|
248
|
+
self.__dir_metadata_cache[path_to].append(metadata)
|
|
249
|
+
return metadata
|
|
250
|
+
|
|
251
|
+
def directory_metadata(self, file_path: str | Directory) -> list[FileBridgeMetadata]:
|
|
252
|
+
"""
|
|
253
|
+
Get metadata for every file in a directory in FileBridge. If this metadata is already cached, then it will be
|
|
254
|
+
returned from the cache.
|
|
255
|
+
|
|
256
|
+
:param file_path: The path to the directory to retrieve the metadata of the contents, or the Directory object.
|
|
257
|
+
:return: A list of the metadata for each file in the directory.
|
|
258
|
+
"""
|
|
259
|
+
if isinstance(file_path, Directory):
|
|
260
|
+
file_path = file_path.path
|
|
261
|
+
# If the directory metadata is already cached, then use the cached value instead of making an additional
|
|
262
|
+
# webservice call. The only exception to this is if the size of the directory's file name cache differs from
|
|
263
|
+
# the size of the directory's metadata cache. This can happen if a new file or directory was added to the
|
|
264
|
+
# directory using write_file/create_directory after the metadata of the directory's contents was cached.
|
|
265
|
+
# In this case, we need to make an additional webservice call to get the metadata of the new file or directory.
|
|
266
|
+
# Since there could be multiple new files or directories, just re-query the metadata for the entire directory.
|
|
267
|
+
if (file_path in self.__dir_metadata_cache
|
|
268
|
+
and len(self.__dir_metadata_cache[file_path]) == len(self.__dir_file_name_cache[file_path])):
|
|
269
|
+
return self.__dir_metadata_cache[file_path]
|
|
270
|
+
metadata: list[FileBridgeMetadata] = FileBridge.directory_metadata(self.user, self.__bridge, file_path)
|
|
271
|
+
# Save the metadata to the directory cache.
|
|
272
|
+
self.__dir_metadata_cache[file_path] = metadata
|
|
273
|
+
# We can also save the metadata to the file cache so that we don't have to make additional webservice calls if
|
|
274
|
+
# an individual file's metadata is requested.
|
|
275
|
+
for file_metadata in metadata:
|
|
276
|
+
self.__file_metadata_cache[file_path + "/" + file_metadata.file_name] = file_metadata
|
|
277
|
+
# This also doubles as a list directory call since it contains the file names of the contents of the directory.
|
|
278
|
+
self.__dir_file_name_cache[file_path] = [x.file_name for x in metadata]
|
|
279
|
+
return metadata
|
|
280
|
+
|
|
281
|
+
def is_file(self, file_path: str | File | Directory) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Determine if the given file path points to a file. This is achieved by checking the metadata of the provided
|
|
284
|
+
file path. If the metadata is not cached, then this will make a webservice call to get the metadata.
|
|
285
|
+
|
|
286
|
+
:param file_path: A file path, File object, or Directory object.
|
|
287
|
+
:return: True if the file path points to a file. False if it points to a directory.
|
|
288
|
+
"""
|
|
289
|
+
return self.file_metadata(file_path).is_file
|
|
290
|
+
|
|
291
|
+
def is_directory(self, file_path: str | Directory) -> bool:
|
|
292
|
+
"""
|
|
293
|
+
Determine if the given file path points to a directory. This is achieved by checking the metadata of the
|
|
294
|
+
provided file path. If the metadata is not cached, then this will make a webservice call to get the metadata.
|
|
295
|
+
|
|
296
|
+
:param file_path: A file path or Directory object.
|
|
297
|
+
:return: True if the file path points to a directory. False if it points to a file.
|
|
298
|
+
"""
|
|
299
|
+
return self.file_metadata(file_path).is_directory
|
|
300
|
+
|
|
301
|
+
def move_file(self, move_from: str | Directory, move_to: str | Directory, old_name: str | File,
|
|
302
|
+
new_name: str | File | None = None) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Move a file from one location to another within File Bridge. This is done be reading the file into memory,
|
|
305
|
+
writing a copy of the file in the new location, then deleting the original file.
|
|
306
|
+
|
|
307
|
+
:param move_from: The path or Directory object to the current location of the file.
|
|
308
|
+
:param move_to: The path or Directory object to move the file to.
|
|
309
|
+
:param old_name: The current name of the file, or a File object.
|
|
310
|
+
:param new_name: The name that the file should have after it is moved, or a File object. If this is not
|
|
311
|
+
provided, then the new name will be the same as the old name.
|
|
312
|
+
"""
|
|
313
|
+
if isinstance(move_from, Directory):
|
|
314
|
+
move_from = move_from.path
|
|
315
|
+
if isinstance(move_to, Directory):
|
|
316
|
+
move_to = move_to.path
|
|
317
|
+
if isinstance(old_name, File):
|
|
318
|
+
old_name = old_name.name
|
|
319
|
+
if isinstance(new_name, File):
|
|
320
|
+
new_name = new_name.name
|
|
321
|
+
if not new_name:
|
|
322
|
+
new_name = old_name
|
|
323
|
+
|
|
324
|
+
# Read the file into memory.
|
|
325
|
+
file_bytes: bytes = self.read_file(move_from + "/" + old_name)
|
|
326
|
+
# Write the file into the new location.
|
|
327
|
+
self.write_file(move_to + "/" + new_name, file_bytes)
|
|
328
|
+
# Delete the file from the old location. We do this last in case the write call fails.
|
|
329
|
+
self.delete_file(move_from + "/" + old_name)
|
|
330
|
+
|
|
331
|
+
def get_file_object(self, file_path: str) -> File:
|
|
332
|
+
"""
|
|
333
|
+
Get a File object from a file path. This object can be used to get the contents of the file at this path
|
|
334
|
+
and traverse up the file hierarchy to the directory that the file is contained within.
|
|
335
|
+
|
|
336
|
+
There is no guarantee that this file actually exists within the current file bridge connection when it is
|
|
337
|
+
constructed. If the file doesn't exist, then retrieving its contents will fail.
|
|
338
|
+
|
|
339
|
+
:param file_path: A file path.
|
|
340
|
+
:return: A File object constructed form the given file path.
|
|
341
|
+
"""
|
|
342
|
+
if file_path in self.__file_objects:
|
|
343
|
+
return self.__file_objects[file_path]
|
|
344
|
+
file = File(self, file_path)
|
|
345
|
+
self.__file_objects[file_path] = file
|
|
346
|
+
return file
|
|
347
|
+
|
|
348
|
+
def get_directory_object(self, file_path: str) -> Directory:
|
|
349
|
+
"""
|
|
350
|
+
Get a Directory object from a file path. This object can be used to traverse up and down the file hierarchy
|
|
351
|
+
by going up to the parent directory that this directory is contained within or going down to the contents of
|
|
352
|
+
this directory. A file path of "" (a blank string) equates to the root directory of this file bridge connection.
|
|
353
|
+
|
|
354
|
+
There is no guarantee that this directory actually exists within the current file bridge connection when it is
|
|
355
|
+
constructed. If the directory doesn't exist, then retrieving its contents will fail.
|
|
356
|
+
|
|
357
|
+
:param file_path: A file path.
|
|
358
|
+
:return: A Directory object constructed from the given file path.
|
|
359
|
+
"""
|
|
360
|
+
if file_path in self.__dir_objects:
|
|
361
|
+
return self.__dir_objects[file_path]
|
|
362
|
+
directory = Directory(self, file_path)
|
|
363
|
+
self.__dir_objects[file_path] = directory
|
|
364
|
+
return directory
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class FileBridgeObject(ABC):
|
|
368
|
+
"""
|
|
369
|
+
A FileBridgeObject is either a file or a directory that is contained within file bridge. Every object has a
|
|
370
|
+
name and a parent directory that it is contained within, unless the object is the root directory, in
|
|
371
|
+
which case the parent is None. The root directory has a path and name of "" (a blank string).
|
|
372
|
+
|
|
373
|
+
Note that this object may not actually exist within the file bridge connection that it is associated with.
|
|
374
|
+
Retrieving the contents of an object that doesn't exist will fail. You can use the write_file or create_directory
|
|
375
|
+
functions of the FileBridgeHandler to create new files or directories from these objects.
|
|
376
|
+
"""
|
|
377
|
+
_handler: FileBridgeHandler
|
|
378
|
+
_name: str
|
|
379
|
+
_path_to: str
|
|
380
|
+
_parent: Directory | None
|
|
381
|
+
|
|
382
|
+
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
383
|
+
self._handler = handler
|
|
384
|
+
|
|
385
|
+
# Remove any leading or trailing slashes from the file path.
|
|
386
|
+
if file_path.startswith("/") or file_path.endswith("/"):
|
|
387
|
+
file_path = file_path.strip("/")
|
|
388
|
+
|
|
389
|
+
# If the file path is an empty string, then this is the root directory.
|
|
390
|
+
if file_path == "":
|
|
391
|
+
self._name = ""
|
|
392
|
+
self._path_to = ""
|
|
393
|
+
self._parent = None
|
|
394
|
+
return
|
|
395
|
+
name, path_to = split_path(file_path)
|
|
396
|
+
self._name = name
|
|
397
|
+
self._path_to = path_to
|
|
398
|
+
self._parent = handler.get_directory_object(path_to)
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def name(self) -> str:
|
|
402
|
+
"""
|
|
403
|
+
:return: The name of this object.
|
|
404
|
+
"""
|
|
405
|
+
return self._name
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def path_to(self) -> str:
|
|
409
|
+
"""
|
|
410
|
+
:return: The file path that leads to this object. Excludes the name of the object itself.
|
|
411
|
+
"""
|
|
412
|
+
return self._path_to
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def path(self) -> str:
|
|
416
|
+
"""
|
|
417
|
+
:return: The full file path that leads to this object. Includes the name of the object itself.
|
|
418
|
+
"""
|
|
419
|
+
if self._path_to == "":
|
|
420
|
+
return self._name
|
|
421
|
+
return self._path_to + "/" + self._name
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def parent(self) -> Directory | None:
|
|
425
|
+
"""
|
|
426
|
+
:return: The parent directory of this object. If this object is the root directory, then this will be None.
|
|
427
|
+
"""
|
|
428
|
+
return self._parent
|
|
429
|
+
|
|
430
|
+
@abstractmethod
|
|
431
|
+
def is_file(self) -> bool:
|
|
432
|
+
"""
|
|
433
|
+
:return: True if this object is a file. False if it is a directory.
|
|
434
|
+
"""
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
@abstractmethod
|
|
438
|
+
def is_directory(self) -> bool:
|
|
439
|
+
"""
|
|
440
|
+
:return: True if this object is a directory. False if it is a file.
|
|
441
|
+
"""
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
def exists(self) -> bool:
|
|
445
|
+
"""
|
|
446
|
+
:return: True if this object exists in the file bridge connection that it is associated with.
|
|
447
|
+
False if it does not.
|
|
448
|
+
"""
|
|
449
|
+
return self._handler.file_exists(self.path)
|
|
450
|
+
|
|
451
|
+
def get_metadata(self) -> FileBridgeMetadata:
|
|
452
|
+
"""
|
|
453
|
+
:return: The metadata for this object.
|
|
454
|
+
"""
|
|
455
|
+
return self._handler.file_metadata(self.path)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class File(FileBridgeObject):
|
|
459
|
+
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
460
|
+
"""
|
|
461
|
+
:param handler: A FileBridgeHandler for the connection that this file came from.
|
|
462
|
+
:param file_path: The path to this file.
|
|
463
|
+
"""
|
|
464
|
+
super().__init__(handler, file_path)
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def contents(self) -> bytes:
|
|
468
|
+
"""
|
|
469
|
+
Read the bytes of this file.
|
|
470
|
+
This pulls from the cache of this object's related FileBridgeHandler.
|
|
471
|
+
|
|
472
|
+
:return: The bytes of this file.
|
|
473
|
+
"""
|
|
474
|
+
return self._handler.read_file(self.path)
|
|
475
|
+
|
|
476
|
+
def is_file(self) -> bool:
|
|
477
|
+
return True
|
|
478
|
+
|
|
479
|
+
def is_directory(self) -> bool:
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class Directory(FileBridgeObject):
|
|
484
|
+
_contents: dict[str, FileBridgeObject] | None
|
|
485
|
+
|
|
486
|
+
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
487
|
+
"""
|
|
488
|
+
:param handler: A FileBridgeHandler for the connection that this directory came from.
|
|
489
|
+
:param file_path: The path to this directory.
|
|
490
|
+
"""
|
|
491
|
+
super().__init__(handler, file_path)
|
|
492
|
+
self._contents = None
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def contents(self) -> dict[str, FileBridgeObject]:
|
|
496
|
+
"""
|
|
497
|
+
Get all the objects in this Directory.
|
|
498
|
+
This pulls from the cache of this object's related FileBridgeHandler.
|
|
499
|
+
|
|
500
|
+
:return: A dictionary of object names to the objects (Files or Directories) contained within this Directory.
|
|
501
|
+
"""
|
|
502
|
+
if self._contents is not None:
|
|
503
|
+
return self._contents
|
|
504
|
+
|
|
505
|
+
# Load the metadata of the directory to get the names of the files and directories within it.
|
|
506
|
+
# We don't need the return value of this function, but we need to call it to populate the cache.
|
|
507
|
+
self._handler.directory_metadata(self._path_to)
|
|
508
|
+
|
|
509
|
+
# Construct the objects for the contents of this directory.
|
|
510
|
+
self._contents = {}
|
|
511
|
+
for name in self._handler.list_directory(self._path_to):
|
|
512
|
+
file_path: str = self._path_to + "/" + name
|
|
513
|
+
if self._handler.is_file(file_path):
|
|
514
|
+
self._contents[name] = self._handler.get_file_object(file_path)
|
|
515
|
+
else:
|
|
516
|
+
self._contents[name] = self._handler.get_directory_object(file_path)
|
|
517
|
+
return self._contents
|
|
518
|
+
|
|
519
|
+
def is_file(self) -> bool:
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
def is_directory(self) -> bool:
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
def get_files(self) -> dict[str, File]:
|
|
526
|
+
"""
|
|
527
|
+
Get all the files in this Directory.
|
|
528
|
+
This pulls from the cache of this object's related FileBridgeHandler.
|
|
529
|
+
|
|
530
|
+
:return: A mapping of file name to File for every file in this Directory.
|
|
531
|
+
"""
|
|
532
|
+
return {x: cast(File, y) for x, y in self.contents.items() if y.is_file()}
|
|
533
|
+
|
|
534
|
+
def get_directories(self) -> dict[str, Directory]:
|
|
535
|
+
"""
|
|
536
|
+
Get all the nested directories in this Directory.
|
|
537
|
+
This pulls from the cache of this object's related FileBridgeHandler.
|
|
538
|
+
|
|
539
|
+
:return: A mapping of directory name to Directory for every nested directory in this Directory.
|
|
540
|
+
"""
|
|
541
|
+
return {x: cast(Directory, y) for x, y in self.contents.items() if not y.is_file()}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def split_path(file_path: str) -> tuple[str, str]:
|
|
545
|
+
"""
|
|
546
|
+
:param file_path: A file path where directories are separated the "/" characters. If there is no "/" character, then
|
|
547
|
+
that means that the provided path is in the root directory, or is the root directory itself.
|
|
548
|
+
:return: A tuple of two strings that splits the path on its last slash. The first string is the name of the
|
|
549
|
+
file/directory at the given file path and the second string is the location to that file. If there is no slash
|
|
550
|
+
character, then the second string is an empty string.
|
|
551
|
+
"""
|
|
552
|
+
last_slash: int = file_path.rfind("/")
|
|
553
|
+
if last_slash == -1:
|
|
554
|
+
return file_path, ""
|
|
555
|
+
return file_path[last_slash + 1:], file_path[:last_slash]
|