sapiopycommons 2025.4.9a150__py3-none-any.whl → 2025.4.9a476__py3-none-any.whl

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

Potentially problematic release.


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

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