data-sourcerer 0.1.0__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.
- data_sourcerer-0.1.0.dist-info/METADATA +52 -0
- data_sourcerer-0.1.0.dist-info/RECORD +95 -0
- data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
- data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
- data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
- data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
- sourcerer/__init__.py +15 -0
- sourcerer/domain/__init__.py +13 -0
- sourcerer/domain/access_credentials/__init__.py +7 -0
- sourcerer/domain/access_credentials/entities.py +53 -0
- sourcerer/domain/access_credentials/exceptions.py +17 -0
- sourcerer/domain/access_credentials/repositories.py +84 -0
- sourcerer/domain/access_credentials/services.py +91 -0
- sourcerer/domain/file_system/__init__.py +7 -0
- sourcerer/domain/file_system/entities.py +70 -0
- sourcerer/domain/file_system/exceptions.py +17 -0
- sourcerer/domain/file_system/services.py +64 -0
- sourcerer/domain/shared/__init__.py +6 -0
- sourcerer/domain/shared/entities.py +18 -0
- sourcerer/domain/storage_provider/__init__.py +7 -0
- sourcerer/domain/storage_provider/entities.py +86 -0
- sourcerer/domain/storage_provider/exceptions.py +17 -0
- sourcerer/domain/storage_provider/services.py +130 -0
- sourcerer/infrastructure/__init__.py +13 -0
- sourcerer/infrastructure/access_credentials/__init__.py +7 -0
- sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
- sourcerer/infrastructure/access_credentials/registry.py +120 -0
- sourcerer/infrastructure/access_credentials/repositories.py +119 -0
- sourcerer/infrastructure/access_credentials/services.py +396 -0
- sourcerer/infrastructure/db/__init__.py +6 -0
- sourcerer/infrastructure/db/config.py +73 -0
- sourcerer/infrastructure/db/models.py +47 -0
- sourcerer/infrastructure/file_system/__init__.py +7 -0
- sourcerer/infrastructure/file_system/exceptions.py +89 -0
- sourcerer/infrastructure/file_system/services.py +147 -0
- sourcerer/infrastructure/storage_provider/__init__.py +7 -0
- sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
- sourcerer/infrastructure/storage_provider/registry.py +84 -0
- sourcerer/infrastructure/storage_provider/services.py +509 -0
- sourcerer/infrastructure/utils.py +106 -0
- sourcerer/presentation/__init__.py +12 -0
- sourcerer/presentation/app.py +36 -0
- sourcerer/presentation/di_container.py +46 -0
- sourcerer/presentation/screens/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/main.py +78 -0
- sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
- sourcerer/presentation/screens/file_system_finder/main.py +248 -0
- sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
- sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
- sourcerer/presentation/screens/main/__init__.py +0 -0
- sourcerer/presentation/screens/main/main.py +469 -0
- sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
- sourcerer/presentation/screens/main/messages/download_request.py +12 -0
- sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
- sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
- sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
- sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
- sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
- sourcerer/presentation/screens/main/styles.tcss +32 -0
- sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
- sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- sourcerer/presentation/screens/preview_content/main.py +59 -0
- sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
- sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
- sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
- sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
- sourcerer/presentation/screens/question/__init__.py +0 -0
- sourcerer/presentation/screens/question/main.py +31 -0
- sourcerer/presentation/screens/question/styles.tcss +33 -0
- sourcerer/presentation/screens/shared/__init__.py +0 -0
- sourcerer/presentation/screens/shared/containers.py +13 -0
- sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/shared/widgets/button.py +54 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
- sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
- sourcerer/presentation/settings.py +31 -0
- sourcerer/presentation/themes/__init__.py +0 -0
- sourcerer/presentation/themes/github_dark.py +21 -0
- sourcerer/presentation/utils.py +69 -0
- sourcerer/settings.py +72 -0
- sourcerer/utils.py +32 -0
@@ -0,0 +1,509 @@
|
|
1
|
+
"""
|
2
|
+
Implementation of storage provider services.
|
3
|
+
|
4
|
+
This module provides concrete implementations of the BaseStorageProviderService
|
5
|
+
interface for various cloud storage providers.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from itertools import groupby
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import List, Any, Callable
|
11
|
+
|
12
|
+
import humanize
|
13
|
+
from platformdirs import user_downloads_dir
|
14
|
+
|
15
|
+
from sourcerer.domain.shared.entities import StorageProvider
|
16
|
+
from sourcerer.domain.storage_provider.entities import (
|
17
|
+
StoragePermissions,
|
18
|
+
StorageContent,
|
19
|
+
Folder,
|
20
|
+
File,
|
21
|
+
Storage,
|
22
|
+
)
|
23
|
+
from sourcerer.domain.storage_provider.services import BaseStorageProviderService
|
24
|
+
from sourcerer.infrastructure.storage_provider.exceptions import (
|
25
|
+
ListStoragesException,
|
26
|
+
StoragePermissionException,
|
27
|
+
ListStorageItemsException,
|
28
|
+
ReadStorageItemsException,
|
29
|
+
DeleteStorageItemsException,
|
30
|
+
UploadStorageItemsException,
|
31
|
+
CredentialsNotFoundException,
|
32
|
+
)
|
33
|
+
from sourcerer.infrastructure.storage_provider.registry import storage_provider
|
34
|
+
from sourcerer.infrastructure.utils import generate_uuid, is_text_file
|
35
|
+
from sourcerer.settings import PATH_DELIMITER, PAGE_SIZE
|
36
|
+
|
37
|
+
|
38
|
+
@storage_provider(StorageProvider.S3)
|
39
|
+
class S3ProviderService(BaseStorageProviderService):
|
40
|
+
"""
|
41
|
+
AWS S3 storage provider service implementation.
|
42
|
+
|
43
|
+
This class provides methods for interacting with AWS S3 storage,
|
44
|
+
implementing the BaseStorageProviderService interface.
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self, credentials: Any):
|
48
|
+
"""
|
49
|
+
Initialize the service with AWS credentials.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
credentials (Any): AWS session or credentials object
|
53
|
+
"""
|
54
|
+
self.credentials = credentials
|
55
|
+
|
56
|
+
@property
|
57
|
+
def client(self):
|
58
|
+
"""
|
59
|
+
Get the S3 client.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
boto3.client: S3 client object
|
63
|
+
"""
|
64
|
+
if not self.credentials:
|
65
|
+
raise CredentialsNotFoundException()
|
66
|
+
|
67
|
+
session = self.credentials.session
|
68
|
+
|
69
|
+
client_args = {}
|
70
|
+
if self.credentials.endpoint_url:
|
71
|
+
client_args["endpoint_url"] = self.credentials.endpoint_url
|
72
|
+
|
73
|
+
return session.client("s3", **client_args)
|
74
|
+
|
75
|
+
@property
|
76
|
+
def resource(self):
|
77
|
+
"""
|
78
|
+
Get the S3 resource.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
boto3.resource: S3 resource object
|
82
|
+
"""
|
83
|
+
if not self.credentials:
|
84
|
+
raise CredentialsNotFoundException()
|
85
|
+
|
86
|
+
session = self.credentials.session
|
87
|
+
|
88
|
+
client_args = {}
|
89
|
+
if self.credentials.endpoint_url:
|
90
|
+
client_args["endpoint_url"] = self.credentials.endpoint_url
|
91
|
+
return session.resource("s3", **client_args)
|
92
|
+
|
93
|
+
def list_storages(self) -> List[Storage]:
|
94
|
+
"""
|
95
|
+
Return a list of available S3 buckets.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
List[Storage]: List of storage objects representing S3 buckets
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
ListStoragesException: If an error occurs while listing buckets
|
102
|
+
"""
|
103
|
+
try:
|
104
|
+
response = self.client.list_buckets()
|
105
|
+
except Exception as ex:
|
106
|
+
raise ListStoragesException(str(ex)) from ex
|
107
|
+
return [
|
108
|
+
Storage(StorageProvider.S3, i.get("Name"), i.get("CreationDate"))
|
109
|
+
for i in response.get("Buckets")
|
110
|
+
]
|
111
|
+
|
112
|
+
def get_storage_permissions(self, storage: str) -> List[StoragePermissions]:
|
113
|
+
"""
|
114
|
+
Return the permissions for the specified S3 bucket.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
storage (str): The bucket name
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
List[StoragePermissions]: List of permission objects for the bucket
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
StoragePermissionException: If an error occurs while getting permissions
|
124
|
+
"""
|
125
|
+
try:
|
126
|
+
permissions = self.client.get_bucket_acl(Bucket=storage)
|
127
|
+
except Exception as ex:
|
128
|
+
raise StoragePermissionException(str(ex)) from ex
|
129
|
+
return [
|
130
|
+
StoragePermissions(name, [i["Permission"] for i in items])
|
131
|
+
for name, items in groupby(
|
132
|
+
permissions["Grants"],
|
133
|
+
key=lambda x: x["Grantee"]["DisplayName"] or x["Grantee"]["ID"],
|
134
|
+
)
|
135
|
+
]
|
136
|
+
|
137
|
+
def list_storage_items(
|
138
|
+
self, storage: str, path: str = "", prefix: str = ""
|
139
|
+
) -> StorageContent:
|
140
|
+
"""
|
141
|
+
List items in the specified S3 bucket path with the given prefix.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
storage (str): The bucket name
|
145
|
+
path (str, optional): The path within the bucket. Defaults to ''.
|
146
|
+
prefix (str, optional): Filter items by this prefix. Defaults to ''.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
StorageContent: Object containing files and folders at the specified location
|
150
|
+
|
151
|
+
Raises:
|
152
|
+
ListStorageItemsException: If an error occurs while listing items
|
153
|
+
"""
|
154
|
+
if path and not path.endswith("/"):
|
155
|
+
path += "/"
|
156
|
+
try:
|
157
|
+
result = self.client.list_objects_v2(
|
158
|
+
Bucket=storage,
|
159
|
+
Prefix=path + prefix,
|
160
|
+
Delimiter=PATH_DELIMITER,
|
161
|
+
MaxKeys=PAGE_SIZE,
|
162
|
+
)
|
163
|
+
except Exception as ex:
|
164
|
+
raise ListStorageItemsException(str(ex)) from ex
|
165
|
+
|
166
|
+
folders = [
|
167
|
+
Folder(i.get("Prefix").replace(path, ""))
|
168
|
+
for i in result.get("CommonPrefixes", [])
|
169
|
+
if i.get("Prefix")
|
170
|
+
]
|
171
|
+
files = [
|
172
|
+
File(
|
173
|
+
generate_uuid(),
|
174
|
+
i.get("Key").replace(path, ""),
|
175
|
+
humanize.naturalsize(i.get("Size")),
|
176
|
+
is_text_file(i.get("Key")),
|
177
|
+
i.get("LastModified"),
|
178
|
+
)
|
179
|
+
for i in result.get("Contents", [])
|
180
|
+
if i.get("Key").replace(path, "")
|
181
|
+
]
|
182
|
+
return StorageContent(files=files, folders=folders)
|
183
|
+
|
184
|
+
def read_storage_item(self, storage: str, key: str) -> str:
|
185
|
+
"""
|
186
|
+
Read and return the content of the specified S3 object.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
storage (str): The bucket name
|
190
|
+
key (str): The key/path of the item to read
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
bytes: The content of the S3 object
|
194
|
+
|
195
|
+
Raises:
|
196
|
+
ReadStorageItemsException: If an error occurs while reading the item
|
197
|
+
"""
|
198
|
+
try:
|
199
|
+
content_object = self.resource.Object(storage, key)
|
200
|
+
return content_object.get()["Body"].read().decode("utf-8")
|
201
|
+
except Exception as ex:
|
202
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
203
|
+
|
204
|
+
def delete_storage_item(self, storage: str, key: str) -> None:
|
205
|
+
"""
|
206
|
+
Delete the specified S3 object.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
storage (str): The bucket name
|
210
|
+
key (str): The key/path of the item to delete
|
211
|
+
|
212
|
+
Raises:
|
213
|
+
DeleteStorageItemsException: If an error occurs while deleting the item
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
return self.resource.Object(storage, key).delete()
|
217
|
+
except Exception as ex:
|
218
|
+
raise DeleteStorageItemsException(str(ex)) from ex
|
219
|
+
|
220
|
+
def upload_storage_item(
|
221
|
+
self, storage: str, source_path: Path, dest_path: str | None = None
|
222
|
+
) -> None:
|
223
|
+
"""
|
224
|
+
Upload a file to the specified S3 bucket path.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
storage (str): The bucket name
|
228
|
+
source_path (Path): Local file path to upload
|
229
|
+
dest_path (str, optional): Destination path in S3. Defaults to None.
|
230
|
+
|
231
|
+
Raises:
|
232
|
+
UploadStorageItemsException: If an error occurs while uploading the item
|
233
|
+
"""
|
234
|
+
try:
|
235
|
+
self.client.upload_file(source_path, storage, dest_path or source_path.name)
|
236
|
+
except Exception as ex:
|
237
|
+
raise UploadStorageItemsException(str(ex)) from ex
|
238
|
+
|
239
|
+
def download_storage_item(
|
240
|
+
self, storage: str, key: str, progress_callback: Callable | None = None
|
241
|
+
) -> str:
|
242
|
+
"""
|
243
|
+
Download a file from S3 to local filesystem.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
storage (str): The bucket name
|
247
|
+
key (str): The key/path of the item to download
|
248
|
+
progress_callback (Callable, optional): Callback function for progress updates. Defaults to None.
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
str: Path to the downloaded file
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
ReadStorageItemsException: If an error occurs while downloading the item
|
255
|
+
"""
|
256
|
+
try:
|
257
|
+
download_path = Path(user_downloads_dir()) / Path(key).name
|
258
|
+
self.client.download_file(
|
259
|
+
storage, key, download_path, Callback=progress_callback
|
260
|
+
)
|
261
|
+
return str(download_path)
|
262
|
+
except Exception as ex:
|
263
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
264
|
+
|
265
|
+
def get_file_size(self, storage: str, key: str) -> dict:
|
266
|
+
"""
|
267
|
+
Get metadata for an S3 object without downloading content.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
storage (str): The bucket name
|
271
|
+
key (str): The key/path of the item
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
dict: Metadata for the specified S3 object
|
275
|
+
|
276
|
+
Raises:
|
277
|
+
ReadStorageItemsException: If an error occurs while getting metadata
|
278
|
+
"""
|
279
|
+
try:
|
280
|
+
metadata = self.client.head_object(Bucket=storage, Key=key)
|
281
|
+
return metadata.get("ContentLength")
|
282
|
+
except Exception as ex:
|
283
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
284
|
+
|
285
|
+
|
286
|
+
@storage_provider(StorageProvider.GoogleCloudStorage)
|
287
|
+
class GCPStorageProviderService(BaseStorageProviderService):
|
288
|
+
"""
|
289
|
+
Google Cloud Platform storage provider service implementation.
|
290
|
+
|
291
|
+
This class provides methods for interacting with GCP Cloud Storage,
|
292
|
+
implementing the BaseStorageProviderService interface.
|
293
|
+
"""
|
294
|
+
|
295
|
+
def __init__(self, credentials: Any):
|
296
|
+
"""
|
297
|
+
Initialize the service with GCP credentials.
|
298
|
+
|
299
|
+
Args:
|
300
|
+
credentials (Any): GCP client or credentials object
|
301
|
+
"""
|
302
|
+
self.client = credentials
|
303
|
+
|
304
|
+
def list_storages(self) -> List[Storage]:
|
305
|
+
"""
|
306
|
+
Return a list of available GCP buckets.
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
List[Storage]: List of storage objects representing GCP buckets
|
310
|
+
|
311
|
+
Raises:
|
312
|
+
ListStoragesException: If an error occurs while listing buckets
|
313
|
+
"""
|
314
|
+
try:
|
315
|
+
return [
|
316
|
+
Storage(StorageProvider.GoogleCloudStorage, i.name, i.time_created)
|
317
|
+
for i in self.client.list_buckets()
|
318
|
+
]
|
319
|
+
except Exception as ex:
|
320
|
+
raise ListStoragesException(str(ex)) from ex
|
321
|
+
|
322
|
+
def get_storage_permissions(self, storage: str) -> List[StoragePermissions]:
|
323
|
+
"""
|
324
|
+
Return the permissions for the specified GCP bucket.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
storage (str): The bucket name
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
List[StoragePermissions]: List of permission objects for the bucket
|
331
|
+
|
332
|
+
Raises:
|
333
|
+
StoragePermissionException: If an error occurs while getting permissions
|
334
|
+
"""
|
335
|
+
try:
|
336
|
+
bucket = self.client.get_bucket(storage)
|
337
|
+
policy = bucket.get_iam_policy()
|
338
|
+
|
339
|
+
result = {}
|
340
|
+
for role, members in policy.items():
|
341
|
+
for member in members:
|
342
|
+
member = member.split(":")[-1]
|
343
|
+
if member not in result:
|
344
|
+
result[member] = set()
|
345
|
+
result[member].add(role)
|
346
|
+
return [
|
347
|
+
StoragePermissions(member, roles) for member, roles in result.items()
|
348
|
+
]
|
349
|
+
except Exception as ex:
|
350
|
+
raise StoragePermissionException(str(ex)) from ex
|
351
|
+
|
352
|
+
def list_storage_items(
|
353
|
+
self, storage: str, path: str = "", prefix: str = ""
|
354
|
+
) -> StorageContent:
|
355
|
+
"""
|
356
|
+
List items in the specified GCP bucket path with the given prefix.
|
357
|
+
|
358
|
+
Args:
|
359
|
+
storage (str): The bucket name
|
360
|
+
path (str, optional): The path within the bucket. Defaults to ''.
|
361
|
+
prefix (str, optional): Filter items by this prefix. Defaults to ''.
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
StorageContent: Object containing files and folders at the specified location
|
365
|
+
|
366
|
+
Raises:
|
367
|
+
ListStorageItemsException: If an error occurs while listing items
|
368
|
+
"""
|
369
|
+
try:
|
370
|
+
|
371
|
+
files = []
|
372
|
+
folders = []
|
373
|
+
if path and not path.endswith("/"):
|
374
|
+
path += "/"
|
375
|
+
|
376
|
+
bucket = self.client.bucket(storage)
|
377
|
+
|
378
|
+
blobs = bucket.list_blobs(
|
379
|
+
prefix=path + prefix, delimiter=PATH_DELIMITER, max_results=PAGE_SIZE
|
380
|
+
)
|
381
|
+
|
382
|
+
for blob in blobs:
|
383
|
+
files.append(
|
384
|
+
File(
|
385
|
+
generate_uuid(),
|
386
|
+
blob.name[len(path) :],
|
387
|
+
size=humanize.naturalsize(blob.size),
|
388
|
+
date_modified=blob.updated.date(),
|
389
|
+
is_text=is_text_file(blob.name),
|
390
|
+
)
|
391
|
+
)
|
392
|
+
|
393
|
+
for folder in blobs.prefixes:
|
394
|
+
relative_path = folder[len(path) :]
|
395
|
+
folders.append(Folder(relative_path))
|
396
|
+
|
397
|
+
return StorageContent(files=files, folders=folders)
|
398
|
+
|
399
|
+
except Exception as ex:
|
400
|
+
raise ListStorageItemsException(
|
401
|
+
f"Failed to list items in {storage}: {str(ex)}"
|
402
|
+
) from ex
|
403
|
+
|
404
|
+
def read_storage_item(self, storage: str, key: str) -> str:
|
405
|
+
"""
|
406
|
+
Read and return the content of the specified GCP object.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
storage (str): The bucket name
|
410
|
+
key (str): The key/path of the item to read
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
bytes: The content of the GCP object
|
414
|
+
|
415
|
+
Raises:
|
416
|
+
ReadStorageItemsException: If an error occurs while reading the item
|
417
|
+
"""
|
418
|
+
try:
|
419
|
+
bucket = self.client.bucket(storage)
|
420
|
+
blob = bucket.get_blob(key)
|
421
|
+
content = blob.download_as_bytes()
|
422
|
+
return content.decode("utf-8")
|
423
|
+
except Exception as ex:
|
424
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
425
|
+
|
426
|
+
def delete_storage_item(self, storage: str, key: str) -> None:
|
427
|
+
"""
|
428
|
+
Delete the specified GCP object.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
storage (str): The bucket name
|
432
|
+
key (str): The key/path of the item to delete
|
433
|
+
|
434
|
+
Raises:
|
435
|
+
DeleteStorageItemsException: If an error occurs while deleting the item
|
436
|
+
"""
|
437
|
+
try:
|
438
|
+
bucket = self.client.bucket(storage)
|
439
|
+
blob = bucket.get_blob(key)
|
440
|
+
blob.delete()
|
441
|
+
except Exception as ex:
|
442
|
+
raise DeleteStorageItemsException(str(ex)) from ex
|
443
|
+
|
444
|
+
def upload_storage_item(
|
445
|
+
self, storage: str, source_path: Path, dest_path: str | None = None
|
446
|
+
) -> None:
|
447
|
+
"""
|
448
|
+
Upload a file to the specified GCP bucket path.
|
449
|
+
|
450
|
+
Args:
|
451
|
+
storage (str): The bucket name
|
452
|
+
source_path (Path): Local file path to upload
|
453
|
+
dest_path (str, optional): Destination path in GCP. Defaults to None.
|
454
|
+
|
455
|
+
Raises:
|
456
|
+
UploadStorageItemsException: If an error occurs while uploading the item
|
457
|
+
"""
|
458
|
+
try:
|
459
|
+
bucket = self.client.bucket(storage)
|
460
|
+
bucket.blob(dest_path or source_path.name).upload_from_filename(source_path)
|
461
|
+
except Exception as ex:
|
462
|
+
raise UploadStorageItemsException(str(ex)) from ex
|
463
|
+
|
464
|
+
def download_storage_item(
|
465
|
+
self, storage: str, key: str, progress_callback: Callable | None = None
|
466
|
+
) -> str:
|
467
|
+
"""
|
468
|
+
Download a file from GCP to the local filesystem.
|
469
|
+
|
470
|
+
Args:
|
471
|
+
storage (str): The bucket name
|
472
|
+
key (str): The key/path of the item to download
|
473
|
+
progress_callback (Callable, optional): Callback function for progress updates. Defaults to None.
|
474
|
+
|
475
|
+
Returns:
|
476
|
+
str: Path to the downloaded file
|
477
|
+
|
478
|
+
Raises:
|
479
|
+
ReadStorageItemsException: If an error occurs while downloading the item
|
480
|
+
"""
|
481
|
+
try:
|
482
|
+
bucket = self.client.bucket(storage)
|
483
|
+
blob = bucket.get_blob(key)
|
484
|
+
download_path = Path(user_downloads_dir()) / Path(key).name
|
485
|
+
blob.download_to_filename(str(download_path))
|
486
|
+
return str(download_path)
|
487
|
+
except Exception as ex:
|
488
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
489
|
+
|
490
|
+
def get_file_size(self, storage: str, key: str) -> dict:
|
491
|
+
"""
|
492
|
+
Get metadata for a GCP object without downloading content.
|
493
|
+
|
494
|
+
Args:
|
495
|
+
storage (str): The bucket name
|
496
|
+
key (str): The key/path of the item
|
497
|
+
|
498
|
+
Returns:
|
499
|
+
dict: Metadata for the specified GCP object
|
500
|
+
|
501
|
+
Raises:
|
502
|
+
ReadStorageItemsException: If an error occurs while getting metadata
|
503
|
+
"""
|
504
|
+
try:
|
505
|
+
bucket = self.client.bucket(storage)
|
506
|
+
blob = bucket.get_blob(key)
|
507
|
+
return blob.size
|
508
|
+
except Exception as ex:
|
509
|
+
raise ReadStorageItemsException(str(ex)) from ex
|
@@ -0,0 +1,106 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for the Sourcerer application.
|
3
|
+
|
4
|
+
This module provides various utility functions used throughout the application,
|
5
|
+
including UUID generation, MIME type detection, and file type checking.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import mimetypes
|
9
|
+
import uuid
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
from sourcerer.settings import TEXT_EXTENSIONS
|
13
|
+
|
14
|
+
|
15
|
+
def is_text_mime(filename):
|
16
|
+
"""
|
17
|
+
Determine if a file has a text MIME type.
|
18
|
+
|
19
|
+
This function uses the mimetypes module to guess the MIME type of a file
|
20
|
+
based on its filename, then checks if the MIME type starts with "text/".
|
21
|
+
|
22
|
+
Args:
|
23
|
+
filename (str): The name of the file to check
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
bool: True if the file has a text MIME type, False otherwise
|
27
|
+
"""
|
28
|
+
mime, _ = mimetypes.guess_type(filename)
|
29
|
+
return mime is not None and mime.startswith("text/")
|
30
|
+
|
31
|
+
|
32
|
+
def generate_uuid():
|
33
|
+
"""
|
34
|
+
Generate a unique identifier (UUID).
|
35
|
+
|
36
|
+
This function creates a UUID using the uuid4() function from the uuid module.
|
37
|
+
The UUID is prefixed with 'a' to ensure compatibility for ID usage in various
|
38
|
+
contexts.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
str: A unique identifier string prefixed with 'a'
|
42
|
+
"""
|
43
|
+
return f"a{uuid.uuid4()}"
|
44
|
+
|
45
|
+
|
46
|
+
def is_text_file(file_name):
|
47
|
+
"""
|
48
|
+
Check if the given file is a text file based on its extension or MIME type.
|
49
|
+
|
50
|
+
This function determines if a file is a text file by checking if its extension
|
51
|
+
is in the predefined TEXT_EXTENSIONS list or if its MIME type indicates it's
|
52
|
+
a text file.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
file_name (str): The name of the file to check
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
bool: True if the file is a text file, False otherwise
|
59
|
+
"""
|
60
|
+
ext = Path(file_name).suffix.lower()
|
61
|
+
if ext in TEXT_EXTENSIONS:
|
62
|
+
return True
|
63
|
+
if is_text_mime(file_name):
|
64
|
+
return True
|
65
|
+
return False
|
66
|
+
|
67
|
+
|
68
|
+
def custom_sort_key(s: str | Path):
|
69
|
+
"""
|
70
|
+
Converts a string by replacing '.' with a character '{' (ASCII 123)
|
71
|
+
to ensure that strings are sorted in a specific order where '.'
|
72
|
+
is considered after all letters in ASCII comparison.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
s (str|Path): The string to be transformed for custom sorting.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
str: A string transformed to facilitate the desired sorting order.
|
79
|
+
"""
|
80
|
+
return str(s).replace(".", "{")
|
81
|
+
|
82
|
+
|
83
|
+
class Singleton(type):
|
84
|
+
"""
|
85
|
+
Metaclass that implements the singleton pattern, ensuring only one instance of a class exists.
|
86
|
+
"""
|
87
|
+
|
88
|
+
_instances = {}
|
89
|
+
|
90
|
+
def __call__(cls, *args, **kwargs):
|
91
|
+
"""
|
92
|
+
Create or return the singleton instance of the class.
|
93
|
+
|
94
|
+
If an instance does not exist, instantiate the class with the given arguments.
|
95
|
+
Otherwise, return the existing instance.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
*args: Positional arguments for class instantiation.
|
99
|
+
**kwargs: Keyword arguments for class instantiation.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
object: The singleton instance of the class.
|
103
|
+
"""
|
104
|
+
if cls not in cls._instances:
|
105
|
+
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
106
|
+
return cls._instances[cls]
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
Presentation layer for the Sourcerer application.
|
3
|
+
|
4
|
+
This package contains the user interface components and controllers that
|
5
|
+
handle user interactions. It uses the Textual library to create a terminal-based
|
6
|
+
user interface for interacting with cloud storage providers.
|
7
|
+
|
8
|
+
The presentation layer is organized into several subpackages:
|
9
|
+
- screens: Different screens and views of the application
|
10
|
+
- themes: Visual themes and styling
|
11
|
+
- utils: Presentation-specific utilities
|
12
|
+
"""
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"""Main application module for the Sourcerer application.
|
2
|
+
|
3
|
+
This module initializes the dependency injection container, sets up the database,
|
4
|
+
and runs the main application window.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from sourcerer.infrastructure.access_credentials.registry import (
|
8
|
+
access_credential_method_registry,
|
9
|
+
)
|
10
|
+
from sourcerer.presentation.di_container import DiContainer
|
11
|
+
from sourcerer.presentation.screens.main.main import Sourcerer
|
12
|
+
|
13
|
+
|
14
|
+
def main():
|
15
|
+
"""Initialize and run the Sourcerer application.
|
16
|
+
|
17
|
+
This function:
|
18
|
+
1. Creates and configures the dependency injection container
|
19
|
+
2. Sets up the access credential method registry
|
20
|
+
3. Prepares the database
|
21
|
+
4. Creates and runs the main application window
|
22
|
+
"""
|
23
|
+
di_container = DiContainer()
|
24
|
+
di_container.config.access_credential_method_registry.from_value(
|
25
|
+
access_credential_method_registry
|
26
|
+
)
|
27
|
+
di_container.wire(packages=["sourcerer"])
|
28
|
+
|
29
|
+
DiContainer.db().prepare_db()
|
30
|
+
|
31
|
+
app = Sourcerer()
|
32
|
+
app.run()
|
33
|
+
|
34
|
+
|
35
|
+
if __name__ == "__main__":
|
36
|
+
main()
|