sapiopycommons 2024.8.27a312__py3-none-any.whl → 2024.8.28a313__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 +35 -277
- sapiopycommons/chem/IndigoMolecules.py +0 -1
- sapiopycommons/chem/Molecules.py +0 -1
- sapiopycommons/files/file_bridge.py +10 -16
- sapiopycommons/files/file_util.py +6 -13
- sapiopycommons/files/file_validator.py +0 -71
- sapiopycommons/general/custom_report_util.py +27 -199
- sapiopycommons/recordmodel/record_handler.py +45 -278
- sapiopycommons/webhook/webhook_handlers.py +1 -58
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/METADATA +2 -4
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/RECORD +13 -18
- sapiopycommons/eln/experiment_report_util.py +0 -214
- sapiopycommons/files/file_bridge_handler.py +0 -318
- sapiopycommons/general/accession_service.py +0 -375
- sapiopycommons/multimodal/multimodal.py +0 -146
- sapiopycommons/multimodal/multimodal_data.py +0 -487
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.27a312.dist-info → sapiopycommons-2024.8.28a313.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
from sapiopylib.rest.User import SapioUser
|
|
2
|
-
from sapiopylib.rest.pojo.CustomReport import (
|
|
3
|
-
CompositeReportTerm,
|
|
4
|
-
CompositeTermOperation,
|
|
5
|
-
CustomReportCriteria,
|
|
6
|
-
ExplicitJoinDefinition,
|
|
7
|
-
FieldCompareReportTerm,
|
|
8
|
-
RawReportTerm,
|
|
9
|
-
RawTermOperation,
|
|
10
|
-
ReportColumn,
|
|
11
|
-
)
|
|
12
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
13
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
14
|
-
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
15
|
-
|
|
16
|
-
from sapiopycommons.general.aliases import SapioRecord
|
|
17
|
-
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
18
|
-
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
19
|
-
|
|
20
|
-
_NOTEBOOK_ID = "EXPERIMENTID"
|
|
21
|
-
_RECORD_ID = "RECORDID"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
|
|
25
|
-
# that given records were used in or getting all records of a datatype used in given experiments.
|
|
26
|
-
class ExperimentReportUtil:
|
|
27
|
-
@staticmethod
|
|
28
|
-
def map_records_to_experiment_ids(
|
|
29
|
-
context: SapioWebhookContext | SapioUser,
|
|
30
|
-
records: list[SapioRecord],
|
|
31
|
-
) -> dict[SapioRecord, list[int]]:
|
|
32
|
-
"""
|
|
33
|
-
Return a dictionary mapping each record to a list of ids of experiments that they were used in.
|
|
34
|
-
If a record wasn't used in any experiments then it will be mapped to an empty list.
|
|
35
|
-
|
|
36
|
-
:param context: The current webhook context or a user object to send requests from.
|
|
37
|
-
:param records: a list of records of the same data type.
|
|
38
|
-
:return: a dictionary mapping each record to a list of ids of each experiment it was used in.
|
|
39
|
-
"""
|
|
40
|
-
if not records:
|
|
41
|
-
return {}
|
|
42
|
-
|
|
43
|
-
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
44
|
-
|
|
45
|
-
data_type_name = records[0].data_type_name
|
|
46
|
-
|
|
47
|
-
record_ids = [record.record_id for record in records]
|
|
48
|
-
|
|
49
|
-
rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
|
|
50
|
-
user, data_type_name, record_ids=record_ids
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
|
|
54
|
-
|
|
55
|
-
record_to_exps: dict[SapioRecord, set[int]] = {
|
|
56
|
-
record: set() for record in records
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for row in rows:
|
|
60
|
-
record_id: int = row[_RECORD_ID]
|
|
61
|
-
exp_id: int = row[_NOTEBOOK_ID]
|
|
62
|
-
|
|
63
|
-
record = id_to_record[record_id]
|
|
64
|
-
|
|
65
|
-
record_to_exps[record].add(exp_id)
|
|
66
|
-
|
|
67
|
-
return {record: list(exps) for record, exps in record_to_exps.items()}
|
|
68
|
-
|
|
69
|
-
@staticmethod
|
|
70
|
-
def map_experiments_to_records_of_type(
|
|
71
|
-
context: SapioWebhookContext | SapioUser,
|
|
72
|
-
exp_ids: list[int],
|
|
73
|
-
wrapper_type: type[WrappedType],
|
|
74
|
-
) -> dict[int, list[WrappedType]]:
|
|
75
|
-
"""
|
|
76
|
-
Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
|
|
77
|
-
If an experiment didn't use any records of the given type then it will be mapped to an empty list.
|
|
78
|
-
|
|
79
|
-
:param context: The current webhook context or a user object to send requests from.
|
|
80
|
-
:param exp_ids: a list of experiment ids. These are specifically the Notebook Experiment ids which can be found in the title of the experiment.
|
|
81
|
-
:param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
|
|
82
|
-
:return: a dictionary mapping each experiment id to a list of records of the given type that were used in that experiment.
|
|
83
|
-
"""
|
|
84
|
-
if not exp_ids:
|
|
85
|
-
return {}
|
|
86
|
-
|
|
87
|
-
user = context if isinstance(context, SapioUser) else context.user
|
|
88
|
-
|
|
89
|
-
record_handler = RecordHandler(user)
|
|
90
|
-
|
|
91
|
-
data_type_name: str = wrapper_type.get_wrapper_data_type_name()
|
|
92
|
-
|
|
93
|
-
rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
|
|
94
|
-
user, data_type_name, exp_ids=exp_ids
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
record_ids: set[int] = {row[_RECORD_ID] for row in rows}
|
|
98
|
-
|
|
99
|
-
records = record_handler.query_models_by_id(wrapper_type, record_ids)
|
|
100
|
-
|
|
101
|
-
id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
|
|
102
|
-
|
|
103
|
-
exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
|
|
104
|
-
|
|
105
|
-
for row in rows:
|
|
106
|
-
record_id: int = row[_RECORD_ID]
|
|
107
|
-
exp_id: int = row[_NOTEBOOK_ID]
|
|
108
|
-
|
|
109
|
-
record = id_to_record[record_id]
|
|
110
|
-
|
|
111
|
-
exp_to_records[exp_id].add(record)
|
|
112
|
-
|
|
113
|
-
return {exp: list(records) for exp, records in exp_to_records.items()}
|
|
114
|
-
|
|
115
|
-
@staticmethod
|
|
116
|
-
def __get_record_experiment_relation_rows(
|
|
117
|
-
user: SapioUser,
|
|
118
|
-
data_type_name: str,
|
|
119
|
-
record_ids: list[int] | None = None,
|
|
120
|
-
exp_ids: list[int] | None = None,
|
|
121
|
-
) -> list[dict[str, int]]:
|
|
122
|
-
"""
|
|
123
|
-
Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
|
|
124
|
-
At least one of record_ids and exp_ids should be provided.
|
|
125
|
-
"""
|
|
126
|
-
assert (record_ids or exp_ids)
|
|
127
|
-
|
|
128
|
-
if record_ids:
|
|
129
|
-
rec_ids = [str(record_id) for record_id in record_ids]
|
|
130
|
-
|
|
131
|
-
ids_str = "{" + ", ".join(rec_ids) + "}"
|
|
132
|
-
|
|
133
|
-
records_term = RawReportTerm(
|
|
134
|
-
data_type_name, "RECORDID", RawTermOperation.EQUAL_TO_OPERATOR, ids_str
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
else:
|
|
138
|
-
# Get all records of the given type
|
|
139
|
-
records_term = RawReportTerm(
|
|
140
|
-
data_type_name,
|
|
141
|
-
"RECORDID",
|
|
142
|
-
RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
|
|
143
|
-
"0",
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
if exp_ids:
|
|
147
|
-
exp_ids = [str(exp_id) for exp_id in exp_ids]
|
|
148
|
-
|
|
149
|
-
ids_str = "{" + ", ".join(exp_ids) + "}"
|
|
150
|
-
|
|
151
|
-
exp_term = RawReportTerm(
|
|
152
|
-
"NOTEBOOKEXPERIMENT",
|
|
153
|
-
"EXPERIMENTID",
|
|
154
|
-
RawTermOperation.EQUAL_TO_OPERATOR,
|
|
155
|
-
ids_str,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
else:
|
|
159
|
-
# Get all experiments
|
|
160
|
-
exp_term = RawReportTerm(
|
|
161
|
-
"NOTEBOOKEXPERIMENT",
|
|
162
|
-
"EXPERIMENTID",
|
|
163
|
-
RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
|
|
164
|
-
"0",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
root_term = CompositeReportTerm(
|
|
168
|
-
records_term, CompositeTermOperation.AND_OPERATOR, exp_term
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# The columns the resulting dataframe will have
|
|
172
|
-
column_list = [
|
|
173
|
-
ReportColumn(data_type_name, "RECORDID", FieldType.LONG),
|
|
174
|
-
ReportColumn("NOTEBOOKEXPERIMENT", "EXPERIMENTID", FieldType.LONG),
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
# Join records on the experiment entry records that correspond to them.
|
|
178
|
-
records_entry_join = FieldCompareReportTerm(
|
|
179
|
-
data_type_name,
|
|
180
|
-
"RECORDID",
|
|
181
|
-
RawTermOperation.EQUAL_TO_OPERATOR,
|
|
182
|
-
"EXPERIMENTENTRYRECORD",
|
|
183
|
-
"RECORDID",
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Join entry records on the experiment entries they are in.
|
|
187
|
-
experiment_entry_enb_entry_join = FieldCompareReportTerm(
|
|
188
|
-
"EXPERIMENTENTRYRECORD",
|
|
189
|
-
"ENTRYID",
|
|
190
|
-
RawTermOperation.EQUAL_TO_OPERATOR,
|
|
191
|
-
"ENBENTRY",
|
|
192
|
-
"ENTRYID",
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
# Join entries on the experiments they are in.
|
|
196
|
-
enb_entry_experiment_join = FieldCompareReportTerm(
|
|
197
|
-
"ENBENTRY",
|
|
198
|
-
"EXPERIMENTID",
|
|
199
|
-
RawTermOperation.EQUAL_TO_OPERATOR,
|
|
200
|
-
"NOTEBOOKEXPERIMENT",
|
|
201
|
-
"EXPERIMENTID",
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
report_criteria = CustomReportCriteria(
|
|
205
|
-
column_list,
|
|
206
|
-
root_term,
|
|
207
|
-
join_list=[
|
|
208
|
-
ExplicitJoinDefinition("EXPERIMENTENTRYRECORD", records_entry_join),
|
|
209
|
-
ExplicitJoinDefinition("ENBENTRY", experiment_entry_enb_entry_join),
|
|
210
|
-
ExplicitJoinDefinition("NOTEBOOKEXPERIMENT", enb_entry_experiment_join),
|
|
211
|
-
],
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return CustomReportUtil.run_custom_report(user, report_criteria)
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import abstractmethod, ABC
|
|
4
|
-
|
|
5
|
-
from sapiopycommons.files.file_bridge import FileBridge
|
|
6
|
-
from sapiopylib.rest.User import SapioUser
|
|
7
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class FileBridgeHandler:
|
|
11
|
-
"""
|
|
12
|
-
The FileBridgeHandler provides caching of the results of file bridge endpoint calls while also containing quality
|
|
13
|
-
of life functions for common file bridge actions.
|
|
14
|
-
"""
|
|
15
|
-
user: SapioUser
|
|
16
|
-
__bridge: str
|
|
17
|
-
__file_cache: dict[str, bytes]
|
|
18
|
-
"""A cache of file paths to file bytes."""
|
|
19
|
-
__files: dict[str, File]
|
|
20
|
-
"""A cache of file paths to File objects."""
|
|
21
|
-
__dir_cache: dict[str, list[str]]
|
|
22
|
-
"""A cache of directory file paths to the names of the files or nested directories within it."""
|
|
23
|
-
__directories: dict[str, Directory]
|
|
24
|
-
"""A cache of directory file paths to Directory objects."""
|
|
25
|
-
|
|
26
|
-
def __init__(self, context: SapioWebhookContext | SapioUser, bridge_name: str):
|
|
27
|
-
"""
|
|
28
|
-
:param context: The current webhook context or a user object to send requests from.
|
|
29
|
-
:param bridge_name: The name of the bridge to communicate with. This is the "connection name" in the
|
|
30
|
-
file bridge configurations.
|
|
31
|
-
"""
|
|
32
|
-
self.user = context if isinstance(context, SapioUser) else context.user
|
|
33
|
-
self.__bridge = bridge_name
|
|
34
|
-
self.__file_cache = {}
|
|
35
|
-
self.__files = {}
|
|
36
|
-
self.__dir_cache = {}
|
|
37
|
-
self.__directories = {}
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def connection_name(self) -> str:
|
|
41
|
-
return self.__bridge
|
|
42
|
-
|
|
43
|
-
def clear_caches(self) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Clear the file and directory caches of this handler.
|
|
46
|
-
"""
|
|
47
|
-
self.__file_cache.clear()
|
|
48
|
-
self.__files.clear()
|
|
49
|
-
self.__dir_cache.clear()
|
|
50
|
-
self.__directories.clear()
|
|
51
|
-
|
|
52
|
-
def read_file(self, file_path: str, base64_decode: bool = True) -> bytes:
|
|
53
|
-
"""
|
|
54
|
-
Read a file from FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
55
|
-
file will not make an additional webservice call.
|
|
56
|
-
|
|
57
|
-
:param file_path: The path to read the file from.
|
|
58
|
-
:param base64_decode: If true, base64 decode the file. Files are by default base64 encoded when retrieved from
|
|
59
|
-
FileBridge.
|
|
60
|
-
:return: The bytes of the file.
|
|
61
|
-
"""
|
|
62
|
-
if file_path in self.__file_cache:
|
|
63
|
-
return self.__file_cache[file_path]
|
|
64
|
-
file_bytes: bytes = FileBridge.read_file(self.user, self.__bridge, file_path, base64_decode)
|
|
65
|
-
self.__file_cache[file_path] = file_bytes
|
|
66
|
-
return file_bytes
|
|
67
|
-
|
|
68
|
-
def write_file(self, file_path: str, file_data: bytes | str) -> None:
|
|
69
|
-
"""
|
|
70
|
-
Write a file to FileBridge. The bytes of the given file will be cached so that any subsequent reads of this
|
|
71
|
-
file will not make an additional webservice call.
|
|
72
|
-
|
|
73
|
-
:param file_path: The path to write the file to. If a file already exists at the given path then the file is
|
|
74
|
-
overwritten.
|
|
75
|
-
:param file_data: A string or bytes of the file to be written.
|
|
76
|
-
"""
|
|
77
|
-
FileBridge.write_file(self.user, self.__bridge, file_path, file_data)
|
|
78
|
-
self.__file_cache[file_path] = file_data if isinstance(file_data, bytes) else file_data.encode()
|
|
79
|
-
|
|
80
|
-
# Find the directory path to this file and the name of the file. Add the file name to the cached list of
|
|
81
|
-
# files for the directory, assuming we have this directory cached and the file isn't already in it.
|
|
82
|
-
last_slash: int = file_path.rfind("/")
|
|
83
|
-
dir_path: str = file_path[:last_slash]
|
|
84
|
-
file_name: str = file_path[last_slash + 1:]
|
|
85
|
-
if dir_path in self.__dir_cache and file_path not in self.__dir_cache[dir_path]:
|
|
86
|
-
self.__dir_cache[dir_path].append(file_name)
|
|
87
|
-
|
|
88
|
-
def delete_file(self, file_path: str) -> None:
|
|
89
|
-
"""
|
|
90
|
-
Delete an existing file in FileBridge. If this file is in the cache, it will also be deleted from the cache.
|
|
91
|
-
|
|
92
|
-
:param file_path: The path to the file to delete.
|
|
93
|
-
"""
|
|
94
|
-
FileBridge.delete_file(self.user, self.__bridge, file_path)
|
|
95
|
-
if file_path in self.__file_cache:
|
|
96
|
-
self.__file_cache.pop(file_path)
|
|
97
|
-
if file_path in self.__files:
|
|
98
|
-
self.__files.pop(file_path)
|
|
99
|
-
|
|
100
|
-
def list_directory(self, file_path: str) -> list[str]:
|
|
101
|
-
"""
|
|
102
|
-
List the contents of a FileBridge directory. The contents of this directory will be cached so that any
|
|
103
|
-
subsequent lists of this directory will not make an additional webservice call.
|
|
104
|
-
|
|
105
|
-
:param file_path: The path to read the directory from.
|
|
106
|
-
:return: A list of names of files and folders in the directory.
|
|
107
|
-
"""
|
|
108
|
-
if file_path in self.__dir_cache:
|
|
109
|
-
return self.__dir_cache[file_path]
|
|
110
|
-
files: list[str] = FileBridge.list_directory(self.user, self.__bridge, file_path)
|
|
111
|
-
self.__dir_cache[file_path] = files
|
|
112
|
-
return files
|
|
113
|
-
|
|
114
|
-
def create_directory(self, file_path: str) -> None:
|
|
115
|
-
"""
|
|
116
|
-
Create a new directory in FileBridge. This new directory will be added to the cache as empty so that listing
|
|
117
|
-
the same directory does not make an additional webservice call.
|
|
118
|
-
|
|
119
|
-
:param file_path: The path to create the directory at. If a directory already exists at the given path then an
|
|
120
|
-
exception is raised.
|
|
121
|
-
"""
|
|
122
|
-
FileBridge.create_directory(self.user, self.__bridge, file_path)
|
|
123
|
-
# This directory was just created, so we know it's empty.
|
|
124
|
-
self.__dir_cache[file_path] = []
|
|
125
|
-
|
|
126
|
-
def delete_directory(self, file_path: str) -> None:
|
|
127
|
-
"""
|
|
128
|
-
Delete an existing directory in FileBridge. If this directory is in the cache, it will also be deleted
|
|
129
|
-
from the cache.
|
|
130
|
-
|
|
131
|
-
:param file_path: The path to the directory to delete.
|
|
132
|
-
"""
|
|
133
|
-
FileBridge.delete_directory(self.user, self.__bridge, file_path)
|
|
134
|
-
if file_path in self.__dir_cache:
|
|
135
|
-
self.__dir_cache.pop(file_path)
|
|
136
|
-
if file_path in self.__directories:
|
|
137
|
-
self.__directories.pop(file_path)
|
|
138
|
-
|
|
139
|
-
def is_file(self, file_path: str) -> bool:
|
|
140
|
-
"""
|
|
141
|
-
Determine if the given file path points to a file or a directory. This is achieved by trying to call
|
|
142
|
-
list_directory on the given file path. If an exception is thrown, that's because the function was called
|
|
143
|
-
on a file. If no exception is thrown, then we know that this is a directory, and we have now also cached
|
|
144
|
-
the contents of that directory if it wasn't cached already.
|
|
145
|
-
|
|
146
|
-
:param file_path: A file path.
|
|
147
|
-
:return: True if the file path points to a file. False if it points to a directory.
|
|
148
|
-
"""
|
|
149
|
-
try:
|
|
150
|
-
self.list_directory(file_path)
|
|
151
|
-
return False
|
|
152
|
-
except Exception:
|
|
153
|
-
return True
|
|
154
|
-
|
|
155
|
-
def move_file(self, move_from: str, move_to: str, old_name: str, new_name: str | None = None) -> None:
|
|
156
|
-
"""
|
|
157
|
-
Move a file from one location to another within File Bridge. This is done be reading the file into memory,
|
|
158
|
-
writing a copy of the file in the new location, then deleting the original file.
|
|
159
|
-
|
|
160
|
-
:param move_from: The path to the current location of the file.
|
|
161
|
-
:param move_to: The path to move the file to.
|
|
162
|
-
:param old_name: The current name of the file.
|
|
163
|
-
:param new_name: The name that the file should have after it is moved. if this is not provided, then the new
|
|
164
|
-
name will be the same as the old name.
|
|
165
|
-
"""
|
|
166
|
-
if not new_name:
|
|
167
|
-
new_name = old_name
|
|
168
|
-
|
|
169
|
-
# Read the file into memory.
|
|
170
|
-
file_bytes: bytes = self.read_file(move_from + "/" + old_name)
|
|
171
|
-
# Write the file into the new location.
|
|
172
|
-
self.write_file(move_to + "/" + new_name, file_bytes)
|
|
173
|
-
# Delete the file from the old location. We do this last in case the write call fails.
|
|
174
|
-
self.delete_file(move_from + "/" + old_name)
|
|
175
|
-
|
|
176
|
-
def get_file_object(self, file_path: str) -> File:
|
|
177
|
-
"""
|
|
178
|
-
Get a File object from a file path. This object can be used to get the contents of the file at this path
|
|
179
|
-
and traverse up the file hierarchy to the directory that the file is contained within.
|
|
180
|
-
|
|
181
|
-
There is no guarantee that this file actually exists within the current file bridge connection when it is
|
|
182
|
-
constructed. If the file doesn't exist, then retrieving its contents will fail.
|
|
183
|
-
|
|
184
|
-
:param file_path: A file path.
|
|
185
|
-
:return: A File object constructed form the given file path.
|
|
186
|
-
"""
|
|
187
|
-
if file_path in self.__files:
|
|
188
|
-
return self.__files[file_path]
|
|
189
|
-
file = File(self, file_path)
|
|
190
|
-
self.__files[file_path] = file
|
|
191
|
-
return file
|
|
192
|
-
|
|
193
|
-
def get_directory_object(self, file_path: str) -> Directory | None:
|
|
194
|
-
"""
|
|
195
|
-
Get a Directory object from a file path. This object can be used to traverse up and down the file hierarchy
|
|
196
|
-
by going up to the parent directory that this directory is contained within or going down to the contents of
|
|
197
|
-
this directory.
|
|
198
|
-
|
|
199
|
-
There is no guarantee that this directory actually exists within the current file bridge connection when it is
|
|
200
|
-
constructed. If the directory doesn't exist, then retrieving its contents will fail.
|
|
201
|
-
|
|
202
|
-
:param file_path: A file path.
|
|
203
|
-
:return: A Directory object constructed form the given file path.
|
|
204
|
-
"""
|
|
205
|
-
if file_path is None:
|
|
206
|
-
return None
|
|
207
|
-
if file_path in self.__directories:
|
|
208
|
-
return self.__directories[file_path]
|
|
209
|
-
directory = Directory(self, file_path)
|
|
210
|
-
self.__directories[file_path] = directory
|
|
211
|
-
return directory
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class FileBridgeObject(ABC):
|
|
215
|
-
"""
|
|
216
|
-
A FileBridgeObject is either a file or a directory that is contained within file bridge. Every object has a
|
|
217
|
-
name and a parent directory that it is contained within (unless the object is located in the bridge root, in
|
|
218
|
-
which case the parent is None). From the name and the parent, a path can be constructed to that object.
|
|
219
|
-
"""
|
|
220
|
-
_handler: FileBridgeHandler
|
|
221
|
-
name: str
|
|
222
|
-
parent: Directory | None
|
|
223
|
-
|
|
224
|
-
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
225
|
-
self._handler = handler
|
|
226
|
-
|
|
227
|
-
name, root = split_path(file_path)
|
|
228
|
-
self.name = name
|
|
229
|
-
self.parent = handler.get_directory_object(root)
|
|
230
|
-
|
|
231
|
-
@abstractmethod
|
|
232
|
-
def is_file(self) -> bool:
|
|
233
|
-
"""
|
|
234
|
-
:return: True if this object is a file. False if it is a directory.
|
|
235
|
-
"""
|
|
236
|
-
pass
|
|
237
|
-
|
|
238
|
-
def get_path(self) -> str:
|
|
239
|
-
"""
|
|
240
|
-
:return: The file path that leads to this object.
|
|
241
|
-
"""
|
|
242
|
-
if self.parent is None:
|
|
243
|
-
return self.name
|
|
244
|
-
return self.parent.get_path() + "/" + self.name
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class File(FileBridgeObject):
|
|
248
|
-
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
249
|
-
"""
|
|
250
|
-
:param handler: A FileBridgeHandler for the connection that this file came from.
|
|
251
|
-
:param file_path: The path to this file.
|
|
252
|
-
"""
|
|
253
|
-
super().__init__(handler, file_path)
|
|
254
|
-
|
|
255
|
-
@property
|
|
256
|
-
def contents(self) -> bytes:
|
|
257
|
-
"""
|
|
258
|
-
:return: The bytes of this file.
|
|
259
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
260
|
-
"""
|
|
261
|
-
return self._handler.read_file(self.get_path())
|
|
262
|
-
|
|
263
|
-
def is_file(self) -> bool:
|
|
264
|
-
return True
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class Directory(FileBridgeObject):
|
|
268
|
-
def __init__(self, handler: FileBridgeHandler, file_path: str):
|
|
269
|
-
"""
|
|
270
|
-
:param handler: A FileBridgeHandler for the connection that this directory came from.
|
|
271
|
-
:param file_path: The path to this directory.
|
|
272
|
-
"""
|
|
273
|
-
super().__init__(handler, file_path)
|
|
274
|
-
|
|
275
|
-
@property
|
|
276
|
-
def contents(self) -> dict[str, FileBridgeObject]:
|
|
277
|
-
"""
|
|
278
|
-
:return: A dictionary of object names to the objects (Files or Directories) contained within this Directory.
|
|
279
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
280
|
-
"""
|
|
281
|
-
contents: dict[str, FileBridgeObject] = {}
|
|
282
|
-
path: str = self.get_path()
|
|
283
|
-
for name in self._handler.list_directory(path):
|
|
284
|
-
file_path: str = path + "/" + name
|
|
285
|
-
if self._handler.is_file(file_path):
|
|
286
|
-
contents[name] = self._handler.get_file_object(file_path)
|
|
287
|
-
else:
|
|
288
|
-
contents[name] = self._handler.get_directory_object(file_path)
|
|
289
|
-
return contents
|
|
290
|
-
|
|
291
|
-
def is_file(self) -> bool:
|
|
292
|
-
return False
|
|
293
|
-
|
|
294
|
-
def get_files(self) -> dict[str, File]:
|
|
295
|
-
"""
|
|
296
|
-
:return: A mapping of file name to File for every file in this Directory.
|
|
297
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
298
|
-
"""
|
|
299
|
-
return {x: y for x, y in self.contents.items() if y.is_file()}
|
|
300
|
-
|
|
301
|
-
def get_directories(self) -> dict[str, Directory]:
|
|
302
|
-
"""
|
|
303
|
-
:return: A mapping of directory name to Directory for every directory in this Directory.
|
|
304
|
-
This pulls from the cache of this object's related FileBridgeHandler.
|
|
305
|
-
"""
|
|
306
|
-
return {x: y for x, y in self.contents.items() if not y.is_file()}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def split_path(file_path: str) -> (str, str):
|
|
310
|
-
"""
|
|
311
|
-
:param file_path: A file path where directories are separated the "/" characters.
|
|
312
|
-
:return: A tuple of two strings that splits the path on its last slash. The first string is the name of the
|
|
313
|
-
file/directory at the given file path and the second string is the location to that file.
|
|
314
|
-
"""
|
|
315
|
-
last_slash: int = file_path.rfind("/")
|
|
316
|
-
if last_slash == -1:
|
|
317
|
-
return file_path, None
|
|
318
|
-
return file_path[last_slash + 1:], file_path[:last_slash]
|