sapiopycommons 2025.4.8a473__py3-none-any.whl → 2025.4.9a150__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 +392 -1262
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/chem/Molecules.py +2 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +2 -4
- sapiopycommons/datatype/data_fields.py +1 -23
- sapiopycommons/eln/experiment_handler.py +279 -933
- sapiopycommons/eln/experiment_report_util.py +10 -15
- sapiopycommons/eln/plate_designer.py +59 -159
- sapiopycommons/files/file_bridge.py +0 -76
- sapiopycommons/files/file_bridge_handler.py +110 -325
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +15 -40
- sapiopycommons/files/file_validator.py +5 -6
- 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 +28 -51
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +1 -24
- sapiopycommons/general/exceptions.py +2 -41
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +0 -1
- sapiopycommons/processtracking/custom_workflow_handler.py +30 -46
- sapiopycommons/recordmodel/record_handler.py +159 -547
- sapiopycommons/rules/eln_rule_handler.py +30 -41
- sapiopycommons/rules/on_save_rule_handler.py +30 -41
- sapiopycommons/webhook/webhook_handlers.py +55 -448
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a150.dist-info/RECORD +59 -0
- sapiopycommons/customreport/auto_pagers.py +0 -281
- sapiopycommons/eln/experiment_cache.py +0 -173
- sapiopycommons/eln/experiment_step_factory.py +0 -474
- sapiopycommons/eln/experiment_tags.py +0 -7
- sapiopycommons/eln/step_creation.py +0 -235
- sapiopycommons/general/data_structure_util.py +0 -115
- sapiopycommons/general/directive_util.py +0 -86
- sapiopycommons/samples/aliquot.py +0 -48
- sapiopycommons-2025.4.8a473.dist-info/RECORD +0 -67
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod, ABC
|
|
4
|
-
from typing import cast
|
|
5
4
|
from weakref import WeakValueDictionary
|
|
6
5
|
|
|
7
6
|
from sapiopylib.rest.User import SapioUser
|
|
8
7
|
|
|
9
|
-
from sapiopycommons.files.file_bridge import FileBridge
|
|
8
|
+
from sapiopycommons.files.file_bridge import FileBridge
|
|
10
9
|
from sapiopycommons.general.aliases import AliasUtil, UserIdentifier
|
|
11
10
|
|
|
12
11
|
|
|
@@ -17,18 +16,14 @@ class FileBridgeHandler:
|
|
|
17
16
|
"""
|
|
18
17
|
user: SapioUser
|
|
19
18
|
__bridge: str
|
|
20
|
-
|
|
19
|
+
__file_cache: dict[str, bytes]
|
|
21
20
|
"""A cache of file paths to file bytes."""
|
|
22
|
-
|
|
21
|
+
__files: dict[str, File]
|
|
23
22
|
"""A cache of file paths to File objects."""
|
|
24
|
-
|
|
25
|
-
"""A cache of directory file paths to the names of the files or nested directories within
|
|
26
|
-
|
|
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]
|
|
27
26
|
"""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
27
|
|
|
33
28
|
__instances: WeakValueDictionary[str, FileBridgeHandler] = WeakValueDictionary()
|
|
34
29
|
__initialized: bool
|
|
@@ -58,12 +53,10 @@ class FileBridgeHandler:
|
|
|
58
53
|
|
|
59
54
|
self.user = AliasUtil.to_sapio_user(context)
|
|
60
55
|
self.__bridge = bridge_name
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.__file_metadata_cache = {}
|
|
66
|
-
self.__dir_metadata_cache = {}
|
|
56
|
+
self.__file_cache = {}
|
|
57
|
+
self.__files = {}
|
|
58
|
+
self.__dir_cache = {}
|
|
59
|
+
self.__directories = {}
|
|
67
60
|
|
|
68
61
|
@property
|
|
69
62
|
def connection_name(self) -> str:
|
|
@@ -73,251 +66,125 @@ class FileBridgeHandler:
|
|
|
73
66
|
"""
|
|
74
67
|
Clear the file and directory caches of this handler.
|
|
75
68
|
"""
|
|
76
|
-
self.
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
79
|
-
self.
|
|
80
|
-
self.__file_metadata_cache.clear()
|
|
81
|
-
self.__dir_metadata_cache.clear()
|
|
69
|
+
self.__file_cache.clear()
|
|
70
|
+
self.__files.clear()
|
|
71
|
+
self.__dir_cache.clear()
|
|
72
|
+
self.__directories.clear()
|
|
82
73
|
|
|
83
|
-
|
|
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:
|
|
74
|
+
def read_file(self, file_path: str, base64_decode: bool = True) -> bytes:
|
|
102
75
|
"""
|
|
103
76
|
Read a file from FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
104
77
|
file will not make an additional webservice call.
|
|
105
78
|
|
|
106
|
-
:param file_path: The
|
|
107
|
-
:param base64_decode: If true, base64 decode the file. Files are base64 encoded
|
|
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
|
|
108
81
|
FileBridge.
|
|
109
82
|
:return: The bytes of the file.
|
|
110
83
|
"""
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
if file_path in self.__file_data_cache:
|
|
114
|
-
return self.__file_data_cache[file_path]
|
|
84
|
+
if file_path in self.__file_cache:
|
|
85
|
+
return self.__file_cache[file_path]
|
|
115
86
|
file_bytes: bytes = FileBridge.read_file(self.user, self.__bridge, file_path, base64_decode)
|
|
116
|
-
self.
|
|
87
|
+
self.__file_cache[file_path] = file_bytes
|
|
117
88
|
return file_bytes
|
|
118
89
|
|
|
119
|
-
def write_file(self, file_path: str
|
|
90
|
+
def write_file(self, file_path: str, file_data: bytes | str) -> None:
|
|
120
91
|
"""
|
|
121
92
|
Write a file to FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
122
93
|
file will not make an additional webservice call.
|
|
123
94
|
|
|
124
|
-
:param file_path: The
|
|
125
|
-
|
|
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.
|
|
126
97
|
:param file_data: A string or bytes of the file to be written.
|
|
127
98
|
"""
|
|
128
|
-
if isinstance(file_path, File):
|
|
129
|
-
file_path = file_path.path
|
|
130
99
|
FileBridge.write_file(self.user, self.__bridge, file_path, file_data)
|
|
131
|
-
self.
|
|
100
|
+
self.__file_cache[file_path] = file_data if isinstance(file_data, bytes) else file_data.encode()
|
|
132
101
|
|
|
133
102
|
# Find the directory path to this file and the name of the file. Add the file name to the cached list of
|
|
134
103
|
# files for the directory, assuming we have this directory cached and the file isn't already in it.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
109
|
|
|
139
|
-
def delete_file(self, file_path: str
|
|
110
|
+
def delete_file(self, file_path: str) -> None:
|
|
140
111
|
"""
|
|
141
112
|
Delete an existing file in FileBridge. If this file is in the cache, it will also be deleted from the cache.
|
|
142
113
|
|
|
143
|
-
:param file_path: The
|
|
114
|
+
:param file_path: The path to the file to delete.
|
|
144
115
|
"""
|
|
145
|
-
if isinstance(file_path, File):
|
|
146
|
-
file_path = file_path.path
|
|
147
116
|
FileBridge.delete_file(self.user, self.__bridge, file_path)
|
|
148
|
-
if file_path in self.
|
|
149
|
-
self.
|
|
150
|
-
if file_path in self.
|
|
151
|
-
self.
|
|
152
|
-
if file_path in self.__file_metadata_cache:
|
|
153
|
-
self.__file_metadata_cache.pop(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)
|
|
154
121
|
|
|
155
|
-
def list_directory(self, file_path: str
|
|
122
|
+
def list_directory(self, file_path: str) -> list[str]:
|
|
156
123
|
"""
|
|
157
124
|
List the contents of a FileBridge directory. The contents of this directory will be cached so that any
|
|
158
125
|
subsequent lists of this directory will not make an additional webservice call.
|
|
159
126
|
|
|
160
|
-
:param file_path: The
|
|
127
|
+
:param file_path: The path to read the directory from.
|
|
161
128
|
:return: A list of names of files and folders in the directory.
|
|
162
129
|
"""
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
if file_path in self.__dir_file_name_cache:
|
|
166
|
-
return self.__dir_file_name_cache[file_path]
|
|
130
|
+
if file_path in self.__dir_cache:
|
|
131
|
+
return self.__dir_cache[file_path]
|
|
167
132
|
files: list[str] = FileBridge.list_directory(self.user, self.__bridge, file_path)
|
|
168
|
-
self.
|
|
133
|
+
self.__dir_cache[file_path] = files
|
|
169
134
|
return files
|
|
170
135
|
|
|
171
|
-
def create_directory(self, file_path: str
|
|
136
|
+
def create_directory(self, file_path: str) -> None:
|
|
172
137
|
"""
|
|
173
138
|
Create a new directory in FileBridge. This new directory will be added to the cache as empty so that listing
|
|
174
139
|
the same directory does not make an additional webservice call.
|
|
175
140
|
|
|
176
|
-
:param file_path: The
|
|
177
|
-
|
|
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.
|
|
178
143
|
"""
|
|
179
|
-
if isinstance(file_path, Directory):
|
|
180
|
-
file_path = file_path.path
|
|
181
144
|
FileBridge.create_directory(self.user, self.__bridge, file_path)
|
|
182
145
|
# This directory was just created, so we know it's empty.
|
|
183
|
-
self.
|
|
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)
|
|
146
|
+
self.__dir_cache[file_path] = []
|
|
191
147
|
|
|
192
|
-
def delete_directory(self, file_path: str
|
|
148
|
+
def delete_directory(self, file_path: str) -> None:
|
|
193
149
|
"""
|
|
194
150
|
Delete an existing directory in FileBridge. If this directory is in the cache, it will also be deleted
|
|
195
151
|
from the cache.
|
|
196
152
|
|
|
197
|
-
:param file_path: The
|
|
153
|
+
:param file_path: The path to the directory to delete.
|
|
198
154
|
"""
|
|
199
|
-
if isinstance(file_path, Directory):
|
|
200
|
-
file_path = file_path.path
|
|
201
155
|
FileBridge.delete_directory(self.user, self.__bridge, file_path)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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)
|
|
290
160
|
|
|
291
|
-
def
|
|
161
|
+
def is_file(self, file_path: str) -> bool:
|
|
292
162
|
"""
|
|
293
|
-
Determine if the given file path points to a directory. This is achieved by
|
|
294
|
-
|
|
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.
|
|
295
167
|
|
|
296
|
-
:param file_path: A file path
|
|
297
|
-
:return: True if the file path points to a
|
|
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.
|
|
298
170
|
"""
|
|
299
|
-
|
|
171
|
+
try:
|
|
172
|
+
self.list_directory(file_path)
|
|
173
|
+
return False
|
|
174
|
+
except Exception:
|
|
175
|
+
return True
|
|
300
176
|
|
|
301
|
-
def move_file(self, move_from: str
|
|
302
|
-
new_name: str | File | None = None) -> None:
|
|
177
|
+
def move_file(self, move_from: str, move_to: str, old_name: str, new_name: str | None = None) -> None:
|
|
303
178
|
"""
|
|
304
179
|
Move a file from one location to another within File Bridge. This is done be reading the file into memory,
|
|
305
180
|
writing a copy of the file in the new location, then deleting the original file.
|
|
306
181
|
|
|
307
|
-
:param move_from: The path
|
|
308
|
-
:param move_to: The path
|
|
309
|
-
:param old_name: The current name of the file
|
|
310
|
-
:param new_name: The name that the file should have after it is moved
|
|
311
|
-
|
|
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
|
|
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
|
+
"""
|
|
321
188
|
if not new_name:
|
|
322
189
|
new_name = old_name
|
|
323
190
|
|
|
@@ -339,93 +206,49 @@ class FileBridgeHandler:
|
|
|
339
206
|
:param file_path: A file path.
|
|
340
207
|
:return: A File object constructed form the given file path.
|
|
341
208
|
"""
|
|
342
|
-
if file_path in self.
|
|
343
|
-
return self.
|
|
209
|
+
if file_path in self.__files:
|
|
210
|
+
return self.__files[file_path]
|
|
344
211
|
file = File(self, file_path)
|
|
345
|
-
self.
|
|
212
|
+
self.__files[file_path] = file
|
|
346
213
|
return file
|
|
347
214
|
|
|
348
|
-
def get_directory_object(self, file_path: str) -> Directory:
|
|
215
|
+
def get_directory_object(self, file_path: str) -> Directory | None:
|
|
349
216
|
"""
|
|
350
217
|
Get a Directory object from a file path. This object can be used to traverse up and down the file hierarchy
|
|
351
218
|
by going up to the parent directory that this directory is contained within or going down to the contents of
|
|
352
|
-
this directory.
|
|
219
|
+
this directory.
|
|
353
220
|
|
|
354
221
|
There is no guarantee that this directory actually exists within the current file bridge connection when it is
|
|
355
222
|
constructed. If the directory doesn't exist, then retrieving its contents will fail.
|
|
356
223
|
|
|
357
224
|
:param file_path: A file path.
|
|
358
|
-
:return: A Directory object constructed
|
|
225
|
+
:return: A Directory object constructed form the given file path.
|
|
359
226
|
"""
|
|
360
|
-
if file_path
|
|
361
|
-
return
|
|
227
|
+
if file_path is None:
|
|
228
|
+
return None
|
|
229
|
+
if file_path in self.__directories:
|
|
230
|
+
return self.__directories[file_path]
|
|
362
231
|
directory = Directory(self, file_path)
|
|
363
|
-
self.
|
|
232
|
+
self.__directories[file_path] = directory
|
|
364
233
|
return directory
|
|
365
234
|
|
|
366
235
|
|
|
367
236
|
class FileBridgeObject(ABC):
|
|
368
237
|
"""
|
|
369
238
|
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
|
|
371
|
-
which case the parent is None.
|
|
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.
|
|
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.
|
|
376
241
|
"""
|
|
377
242
|
_handler: FileBridgeHandler
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
_parent: Directory | None
|
|
243
|
+
name: str
|
|
244
|
+
parent: Directory | None
|
|
381
245
|
|
|
382
246
|
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
383
247
|
self._handler = handler
|
|
384
248
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
249
|
+
name, root = split_path(file_path)
|
|
250
|
+
self.name = name
|
|
251
|
+
self.parent = handler.get_directory_object(root)
|
|
429
252
|
|
|
430
253
|
@abstractmethod
|
|
431
254
|
def is_file(self) -> bool:
|
|
@@ -434,25 +257,13 @@ class FileBridgeObject(ABC):
|
|
|
434
257
|
"""
|
|
435
258
|
pass
|
|
436
259
|
|
|
437
|
-
|
|
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:
|
|
260
|
+
def get_path(self) -> str:
|
|
445
261
|
"""
|
|
446
|
-
:return:
|
|
447
|
-
False if it does not.
|
|
262
|
+
:return: The file path that leads to this object.
|
|
448
263
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"""
|
|
453
|
-
:return: The metadata for this object.
|
|
454
|
-
"""
|
|
455
|
-
return self._handler.file_metadata(self.path)
|
|
264
|
+
if self.parent is None:
|
|
265
|
+
return self.name
|
|
266
|
+
return self.parent.get_path() + "/" + self.name
|
|
456
267
|
|
|
457
268
|
|
|
458
269
|
class File(FileBridgeObject):
|
|
@@ -466,90 +277,64 @@ class File(FileBridgeObject):
|
|
|
466
277
|
@property
|
|
467
278
|
def contents(self) -> bytes:
|
|
468
279
|
"""
|
|
469
|
-
Read the bytes of this file.
|
|
470
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
471
|
-
|
|
472
280
|
:return: The bytes of this file.
|
|
281
|
+
This pulls from the cache of this object's related FileBridgeHandler.
|
|
473
282
|
"""
|
|
474
|
-
return self._handler.read_file(self.
|
|
283
|
+
return self._handler.read_file(self.get_path())
|
|
475
284
|
|
|
476
285
|
def is_file(self) -> bool:
|
|
477
286
|
return True
|
|
478
287
|
|
|
479
|
-
def is_directory(self) -> bool:
|
|
480
|
-
return False
|
|
481
|
-
|
|
482
288
|
|
|
483
289
|
class Directory(FileBridgeObject):
|
|
484
|
-
_contents: dict[str, FileBridgeObject] | None
|
|
485
|
-
|
|
486
290
|
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
487
291
|
"""
|
|
488
292
|
:param handler: A FileBridgeHandler for the connection that this directory came from.
|
|
489
293
|
:param file_path: The path to this directory.
|
|
490
294
|
"""
|
|
491
295
|
super().__init__(handler, file_path)
|
|
492
|
-
self._contents = None
|
|
493
296
|
|
|
494
297
|
@property
|
|
495
298
|
def contents(self) -> dict[str, FileBridgeObject]:
|
|
496
299
|
"""
|
|
497
|
-
Get all the objects in this Directory.
|
|
498
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
499
|
-
|
|
500
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.
|
|
501
302
|
"""
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
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
|
|
513
307
|
if self._handler.is_file(file_path):
|
|
514
|
-
|
|
308
|
+
contents[name] = self._handler.get_file_object(file_path)
|
|
515
309
|
else:
|
|
516
|
-
|
|
517
|
-
return
|
|
310
|
+
contents[name] = self._handler.get_directory_object(file_path)
|
|
311
|
+
return contents
|
|
518
312
|
|
|
519
313
|
def is_file(self) -> bool:
|
|
520
314
|
return False
|
|
521
315
|
|
|
522
|
-
def is_directory(self) -> bool:
|
|
523
|
-
return True
|
|
524
|
-
|
|
525
316
|
def get_files(self) -> dict[str, File]:
|
|
526
317
|
"""
|
|
527
|
-
Get all the files in this Directory.
|
|
528
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
529
|
-
|
|
530
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.
|
|
531
320
|
"""
|
|
532
|
-
return {x:
|
|
321
|
+
return {x: y for x, y in self.contents.items() if y.is_file()}
|
|
533
322
|
|
|
534
323
|
def get_directories(self) -> dict[str, Directory]:
|
|
535
324
|
"""
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
:return: A mapping of directory name to Directory for every nested directory in this Directory.
|
|
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.
|
|
540
327
|
"""
|
|
541
|
-
return {x:
|
|
328
|
+
return {x: y for x, y in self.contents.items() if not y.is_file()}
|
|
542
329
|
|
|
543
330
|
|
|
544
331
|
def split_path(file_path: str) -> tuple[str, str]:
|
|
545
332
|
"""
|
|
546
|
-
:param file_path: A file path where directories are separated the "/" characters.
|
|
547
|
-
that means that the provided path is in the root directory, or is the root directory itself.
|
|
333
|
+
:param file_path: A file path where directories are separated the "/" characters.
|
|
548
334
|
: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.
|
|
550
|
-
character, then the second string is an empty string.
|
|
335
|
+
file/directory at the given file path and the second string is the location to that file.
|
|
551
336
|
"""
|
|
552
337
|
last_slash: int = file_path.rfind("/")
|
|
553
338
|
if last_slash == -1:
|
|
554
|
-
return file_path,
|
|
339
|
+
return file_path, None
|
|
555
340
|
return file_path[last_slash + 1:], file_path[:last_slash]
|
|
@@ -312,7 +312,7 @@ class FileDataHandler:
|
|
|
312
312
|
"""
|
|
313
313
|
return self.get_by_function(lambda i, row: row.get(header) not in values, whitelist=whitelist, blacklist=blacklist)
|
|
314
314
|
|
|
315
|
-
def get_matches(self, header: str, pattern: str
|
|
315
|
+
def get_matches(self, header: str, pattern: str,
|
|
316
316
|
*, whitelist: FilterList = None, blacklist: FilterList = None) -> list[int]:
|
|
317
317
|
"""
|
|
318
318
|
Get the index of every row with a value under the given header than matches a regex pattern. Unless you set up
|
|
@@ -332,7 +332,7 @@ class FileDataHandler:
|
|
|
332
332
|
|
|
333
333
|
return self.get_by_function(func, whitelist=whitelist, blacklist=blacklist)
|
|
334
334
|
|
|
335
|
-
def get_mismatches(self, header: str, pattern: str
|
|
335
|
+
def get_mismatches(self, header: str, pattern: str,
|
|
336
336
|
*, whitelist: FilterList = None, blacklist: FilterList = None) -> list[int]:
|
|
337
337
|
"""
|
|
338
338
|
Get the index of every row with a value under the given header than doesn't match a regex pattern.
|