data-sourcerer 0.1.0__py3-none-any.whl → 0.2.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 → data_sourcerer-0.2.0.dist-info}/METADATA +25 -4
- {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/RECORD +53 -50
- {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/WHEEL +1 -1
- sourcerer/__init__.py +1 -1
- sourcerer/domain/access_credentials/entities.py +17 -0
- sourcerer/domain/access_credentials/exceptions.py +1 -1
- sourcerer/domain/access_credentials/repositories.py +1 -1
- sourcerer/domain/access_credentials/services.py +14 -2
- sourcerer/domain/file_system/exceptions.py +1 -1
- sourcerer/domain/file_system/services.py +2 -2
- sourcerer/domain/shared/entities.py +1 -0
- sourcerer/domain/storage_provider/entities.py +3 -4
- sourcerer/domain/storage_provider/exceptions.py +1 -1
- sourcerer/domain/storage_provider/services.py +13 -9
- sourcerer/infrastructure/access_credentials/exceptions.py +15 -2
- sourcerer/infrastructure/access_credentials/registry.py +3 -4
- sourcerer/infrastructure/access_credentials/services.py +141 -44
- sourcerer/infrastructure/db/models.py +1 -1
- sourcerer/infrastructure/file_system/exceptions.py +9 -9
- sourcerer/infrastructure/file_system/services.py +16 -16
- sourcerer/infrastructure/storage_provider/exceptions.py +28 -8
- sourcerer/infrastructure/storage_provider/registry.py +2 -3
- sourcerer/infrastructure/storage_provider/services/__init__.py +0 -0
- sourcerer/infrastructure/storage_provider/services/azure.py +261 -0
- sourcerer/infrastructure/storage_provider/services/gcp.py +277 -0
- sourcerer/infrastructure/storage_provider/services/s3.py +290 -0
- sourcerer/infrastructure/utils.py +2 -4
- sourcerer/presentation/screens/critical_error/main.py +3 -4
- sourcerer/presentation/screens/file_system_finder/main.py +4 -4
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +12 -12
- sourcerer/presentation/screens/main/main.py +57 -33
- sourcerer/presentation/screens/main/messages/delete_request.py +1 -2
- sourcerer/presentation/screens/main/messages/download_request.py +1 -2
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +3 -3
- sourcerer/presentation/screens/main/widgets/gradient.py +2 -5
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +1 -1
- sourcerer/presentation/screens/main/widgets/storage_content.py +12 -13
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +8 -6
- sourcerer/presentation/screens/preview_content/main.py +15 -4
- sourcerer/presentation/screens/preview_content/styles.tcss +2 -1
- sourcerer/presentation/screens/provider_creds_list/main.py +2 -2
- sourcerer/presentation/screens/provider_creds_registration/main.py +26 -11
- sourcerer/presentation/screens/question/main.py +1 -1
- sourcerer/presentation/screens/shared/containers.py +1 -1
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -1
- sourcerer/presentation/screens/storage_action_progress/main.py +34 -20
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +11 -0
- sourcerer/presentation/utils.py +7 -3
- sourcerer/settings.py +4 -0
- sourcerer/utils.py +2 -2
- sourcerer/infrastructure/storage_provider/services.py +0 -509
- {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,290 @@
|
|
1
|
+
"""
|
2
|
+
Implementation of S3 compatible storage provider services.
|
3
|
+
|
4
|
+
This module provides concrete implementations of the BaseStorageProviderService
|
5
|
+
interface for various cloud storage providers.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from collections.abc import Callable
|
9
|
+
from itertools import groupby
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
import humanize
|
14
|
+
from platformdirs import user_downloads_dir
|
15
|
+
|
16
|
+
from sourcerer.domain.shared.entities import StorageProvider
|
17
|
+
from sourcerer.domain.storage_provider.entities import (
|
18
|
+
File,
|
19
|
+
Folder,
|
20
|
+
Storage,
|
21
|
+
StorageContent,
|
22
|
+
StoragePermissions,
|
23
|
+
)
|
24
|
+
from sourcerer.domain.storage_provider.services import BaseStorageProviderService
|
25
|
+
from sourcerer.infrastructure.storage_provider.exceptions import (
|
26
|
+
CredentialsNotFoundError,
|
27
|
+
DeleteStorageItemsError,
|
28
|
+
ListStorageItemsError,
|
29
|
+
ListStoragesError,
|
30
|
+
ReadStorageItemsError,
|
31
|
+
StoragePermissionError,
|
32
|
+
UploadStorageItemsError,
|
33
|
+
)
|
34
|
+
from sourcerer.infrastructure.storage_provider.registry import storage_provider
|
35
|
+
from sourcerer.infrastructure.utils import generate_uuid, is_text_file
|
36
|
+
from sourcerer.settings import PAGE_SIZE, PATH_DELIMITER
|
37
|
+
|
38
|
+
|
39
|
+
@storage_provider(StorageProvider.S3)
|
40
|
+
class S3ProviderService(BaseStorageProviderService):
|
41
|
+
"""
|
42
|
+
AWS S3 storage provider service implementation.
|
43
|
+
|
44
|
+
This class provides methods for interacting with AWS S3 storage,
|
45
|
+
implementing the BaseStorageProviderService interface.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, credentials: Any):
|
49
|
+
"""
|
50
|
+
Initialize the service with AWS credentials.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
credentials (Any): AWS session or credentials object
|
54
|
+
"""
|
55
|
+
self.credentials = credentials
|
56
|
+
|
57
|
+
@property
|
58
|
+
def client(self):
|
59
|
+
"""
|
60
|
+
Get the S3 client.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
boto3.client: S3 client object
|
64
|
+
"""
|
65
|
+
if not self.credentials:
|
66
|
+
raise CredentialsNotFoundError()
|
67
|
+
|
68
|
+
session = self.credentials.session
|
69
|
+
|
70
|
+
client_args = {}
|
71
|
+
if self.credentials.endpoint_url:
|
72
|
+
client_args["endpoint_url"] = self.credentials.endpoint_url
|
73
|
+
|
74
|
+
return session.client("s3", **client_args)
|
75
|
+
|
76
|
+
@property
|
77
|
+
def resource(self):
|
78
|
+
"""
|
79
|
+
Get the S3 resource.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
boto3.resource: S3 resource object
|
83
|
+
"""
|
84
|
+
if not self.credentials:
|
85
|
+
raise CredentialsNotFoundError()
|
86
|
+
|
87
|
+
session = self.credentials.session
|
88
|
+
|
89
|
+
client_args = {}
|
90
|
+
if self.credentials.endpoint_url:
|
91
|
+
client_args["endpoint_url"] = self.credentials.endpoint_url
|
92
|
+
return session.resource("s3", **client_args)
|
93
|
+
|
94
|
+
def list_storages(self) -> list[Storage]:
|
95
|
+
"""
|
96
|
+
Return a list of available S3 buckets.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
List[Storage]: List of storage objects representing S3 buckets
|
100
|
+
|
101
|
+
Raises:
|
102
|
+
ListStoragesError: If an error occurs while listing buckets
|
103
|
+
"""
|
104
|
+
try:
|
105
|
+
response = self.client.list_buckets()
|
106
|
+
except Exception as ex:
|
107
|
+
raise ListStoragesError(str(ex)) from ex
|
108
|
+
return [
|
109
|
+
Storage(StorageProvider.S3, i.get("Name"), i.get("CreationDate"))
|
110
|
+
for i in response.get("Buckets")
|
111
|
+
]
|
112
|
+
|
113
|
+
def get_storage_permissions(self, storage: str) -> list[StoragePermissions]:
|
114
|
+
"""
|
115
|
+
Return the permissions for the specified S3 bucket.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
storage (str): The bucket name
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
List[StoragePermissions]: List of permission objects for the bucket
|
122
|
+
|
123
|
+
Raises:
|
124
|
+
StoragePermissionError: If an error occurs while getting permissions
|
125
|
+
"""
|
126
|
+
try:
|
127
|
+
permissions = self.client.get_bucket_acl(Bucket=storage)
|
128
|
+
except Exception as ex:
|
129
|
+
raise StoragePermissionError(str(ex)) from ex
|
130
|
+
return [
|
131
|
+
StoragePermissions(name, [i["Permission"] for i in items])
|
132
|
+
for name, items in groupby(
|
133
|
+
permissions["Grants"],
|
134
|
+
key=lambda x: x["Grantee"]["DisplayName"] or x["Grantee"]["ID"],
|
135
|
+
)
|
136
|
+
]
|
137
|
+
|
138
|
+
def list_storage_items(
|
139
|
+
self, storage: str, path: str = "", prefix: str = ""
|
140
|
+
) -> StorageContent:
|
141
|
+
"""
|
142
|
+
List items in the specified S3 bucket path with the given prefix.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
storage (str): The bucket name
|
146
|
+
path (str, optional): The path within the bucket. Defaults to ''.
|
147
|
+
prefix (str, optional): Filter items by this prefix. Defaults to ''.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
StorageContent: Object containing files and folders at the specified location
|
151
|
+
|
152
|
+
Raises:
|
153
|
+
ListStorageItemsError: If an error occurs while listing items
|
154
|
+
"""
|
155
|
+
if path and not path.endswith("/"):
|
156
|
+
path += "/"
|
157
|
+
try:
|
158
|
+
result = self.client.list_objects_v2(
|
159
|
+
Bucket=storage,
|
160
|
+
Prefix=path + prefix,
|
161
|
+
Delimiter=PATH_DELIMITER,
|
162
|
+
MaxKeys=PAGE_SIZE,
|
163
|
+
)
|
164
|
+
except Exception as ex:
|
165
|
+
raise ListStorageItemsError(str(ex)) from ex
|
166
|
+
|
167
|
+
folders = [
|
168
|
+
Folder(i.get("Prefix").replace(path, ""))
|
169
|
+
for i in result.get("CommonPrefixes", [])
|
170
|
+
if i.get("Prefix")
|
171
|
+
]
|
172
|
+
files = [
|
173
|
+
File(
|
174
|
+
generate_uuid(),
|
175
|
+
i.get("Key").replace(path, ""),
|
176
|
+
humanize.naturalsize(i.get("Size")),
|
177
|
+
is_text_file(i.get("Key")),
|
178
|
+
i.get("LastModified"),
|
179
|
+
)
|
180
|
+
for i in result.get("Contents", [])
|
181
|
+
if i.get("Key").replace(path, "")
|
182
|
+
]
|
183
|
+
return StorageContent(files=files, folders=folders)
|
184
|
+
|
185
|
+
def read_storage_item(self, storage: str, key: str) -> str:
|
186
|
+
"""
|
187
|
+
Read and return the content of the specified S3 object.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
storage (str): The bucket name
|
191
|
+
key (str): The key/path of the item to read
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
bytes: The content of the S3 object
|
195
|
+
|
196
|
+
Raises:
|
197
|
+
ReadStorageItemsError: If an error occurs while reading the item
|
198
|
+
"""
|
199
|
+
try:
|
200
|
+
content_object = self.resource.Object(storage, key)
|
201
|
+
return content_object.get()["Body"].read().decode("utf-8")
|
202
|
+
except Exception as ex:
|
203
|
+
raise ReadStorageItemsError(str(ex)) from ex
|
204
|
+
|
205
|
+
def delete_storage_item(self, storage: str, key: str) -> None:
|
206
|
+
"""
|
207
|
+
Delete the specified S3 object.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
storage (str): The bucket name
|
211
|
+
key (str): The key/path of the item to delete
|
212
|
+
|
213
|
+
Raises:
|
214
|
+
DeleteStorageItemsError: If an error occurs while deleting the item
|
215
|
+
"""
|
216
|
+
try:
|
217
|
+
return self.resource.Object(storage, key).delete()
|
218
|
+
except Exception as ex:
|
219
|
+
raise DeleteStorageItemsError(str(ex)) from ex
|
220
|
+
|
221
|
+
def upload_storage_item(
|
222
|
+
self,
|
223
|
+
storage: str,
|
224
|
+
storage_path: str,
|
225
|
+
source_path: Path,
|
226
|
+
dest_path: str | None = None,
|
227
|
+
) -> None:
|
228
|
+
"""
|
229
|
+
Upload a file to the specified S3 bucket path.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
storage (str): The bucket name
|
233
|
+
storage_path (str): The path within the bucket
|
234
|
+
source_path (Path): Local file path to upload
|
235
|
+
dest_path (str, optional): Destination path in S3. Defaults to None.
|
236
|
+
|
237
|
+
Raises:
|
238
|
+
UploadStorageItemsError: If an error occurs while uploading the item
|
239
|
+
"""
|
240
|
+
try:
|
241
|
+
dest_path = str(Path(storage_path or "") / (dest_path or source_path.name))
|
242
|
+
self.client.upload_file(source_path, storage, dest_path)
|
243
|
+
except Exception as ex:
|
244
|
+
raise UploadStorageItemsError(str(ex)) from ex
|
245
|
+
|
246
|
+
def download_storage_item(
|
247
|
+
self, storage: str, key: str, progress_callback: Callable | None = None
|
248
|
+
) -> str:
|
249
|
+
"""
|
250
|
+
Download a file from S3 to local filesystem.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
storage (str): The bucket name
|
254
|
+
key (str): The key/path of the item to download
|
255
|
+
progress_callback (Callable, optional): Callback function for progress updates. Defaults to None.
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
str: Path to the downloaded file
|
259
|
+
|
260
|
+
Raises:
|
261
|
+
ReadStorageItemsError: If an error occurs while downloading the item
|
262
|
+
"""
|
263
|
+
try:
|
264
|
+
download_path = Path(user_downloads_dir()) / Path(key).name
|
265
|
+
self.client.download_file(
|
266
|
+
storage, key, download_path, Callback=progress_callback
|
267
|
+
)
|
268
|
+
return str(download_path)
|
269
|
+
except Exception as ex:
|
270
|
+
raise ReadStorageItemsError(str(ex)) from ex
|
271
|
+
|
272
|
+
def get_file_size(self, storage: str, key: str) -> int:
|
273
|
+
"""
|
274
|
+
Get metadata for an S3 object without downloading content.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
storage (str): The bucket name
|
278
|
+
key (str): The key/path of the item
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
int: Size of the storage item in bytes
|
282
|
+
|
283
|
+
Raises:
|
284
|
+
ReadStorageItemsError: If an error occurs while getting metadata
|
285
|
+
"""
|
286
|
+
try:
|
287
|
+
metadata = self.client.head_object(Bucket=storage, Key=key)
|
288
|
+
return metadata.get("ContentLength")
|
289
|
+
except Exception as ex:
|
290
|
+
raise ReadStorageItemsError(str(ex)) from ex
|
@@ -60,9 +60,7 @@ def is_text_file(file_name):
|
|
60
60
|
ext = Path(file_name).suffix.lower()
|
61
61
|
if ext in TEXT_EXTENSIONS:
|
62
62
|
return True
|
63
|
-
|
64
|
-
return True
|
65
|
-
return False
|
63
|
+
return bool(is_text_mime(file_name))
|
66
64
|
|
67
65
|
|
68
66
|
def custom_sort_key(s: str | Path):
|
@@ -102,5 +100,5 @@ class Singleton(type):
|
|
102
100
|
object: The singleton instance of the class.
|
103
101
|
"""
|
104
102
|
if cls not in cls._instances:
|
105
|
-
cls._instances[cls] = super(
|
103
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
106
104
|
return cls._instances[cls]
|
@@ -4,10 +4,10 @@ import webbrowser
|
|
4
4
|
|
5
5
|
from textual import on
|
6
6
|
from textual.app import ComposeResult
|
7
|
-
from textual.containers import
|
7
|
+
from textual.containers import Container, Horizontal
|
8
|
+
from textual.css.query import NoMatches
|
8
9
|
from textual.screen import ModalScreen
|
9
10
|
from textual.widgets import Label, RichLog
|
10
|
-
from textual.css.query import NoMatches
|
11
11
|
|
12
12
|
from sourcerer import __version__
|
13
13
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
@@ -30,7 +30,6 @@ class CriticalErrorScreen(ModalScreen[bool]):
|
|
30
30
|
with Horizontal():
|
31
31
|
yield Button("Report", name="report")
|
32
32
|
yield Button("Dismiss", name="dismiss")
|
33
|
-
|
34
33
|
|
35
34
|
def on_mount(self) -> None:
|
36
35
|
"""
|
@@ -40,7 +39,7 @@ class CriticalErrorScreen(ModalScreen[bool]):
|
|
40
39
|
try:
|
41
40
|
text_log = self.query_one(RichLog)
|
42
41
|
except NoMatches:
|
43
|
-
return
|
42
|
+
return
|
44
43
|
|
45
44
|
text_log.write(self.traceback)
|
46
45
|
|
@@ -1,12 +1,12 @@
|
|
1
|
+
from collections.abc import Callable
|
1
2
|
from dataclasses import dataclass
|
2
3
|
from pathlib import Path
|
3
|
-
from typing import List, Callable
|
4
4
|
|
5
5
|
from dependency_injector.wiring import Provide
|
6
6
|
from textual import on
|
7
7
|
from textual.app import ComposeResult
|
8
8
|
from textual.binding import Binding
|
9
|
-
from textual.containers import
|
9
|
+
from textual.containers import Container, Horizontal
|
10
10
|
from textual.css.query import NoMatches
|
11
11
|
from textual.reactive import reactive
|
12
12
|
from textual.screen import ModalScreen
|
@@ -38,11 +38,11 @@ class FileSystemNavigationModal(ModalScreen):
|
|
38
38
|
|
39
39
|
def __init__(
|
40
40
|
self,
|
41
|
+
*args,
|
41
42
|
file_system_service: FileSystemService = Provide[
|
42
43
|
DiContainer.file_system_service
|
43
44
|
],
|
44
|
-
validation_rules:
|
45
|
-
*args,
|
45
|
+
validation_rules: list[FileSystemSelectionValidationRule] | None = None,
|
46
46
|
**kwargs,
|
47
47
|
):
|
48
48
|
"""
|
@@ -1,8 +1,9 @@
|
|
1
|
+
from collections.abc import Callable, Sequence
|
1
2
|
from dataclasses import dataclass
|
2
3
|
from enum import Enum
|
3
4
|
from pathlib import Path
|
4
5
|
from time import time
|
5
|
-
from typing import ClassVar,
|
6
|
+
from typing import ClassVar, Literal
|
6
7
|
|
7
8
|
from textual import events, on
|
8
9
|
from textual.app import ComposeResult
|
@@ -11,17 +12,16 @@ from textual.css.query import NoMatches
|
|
11
12
|
from textual.message import Message
|
12
13
|
from textual.reactive import reactive
|
13
14
|
from textual.widget import Widget
|
14
|
-
from textual.widgets import
|
15
|
+
from textual.widgets import Label, Rule
|
15
16
|
|
16
|
-
from sourcerer.infrastructure.file_system.exceptions import
|
17
|
+
from sourcerer.infrastructure.file_system.exceptions import ListDirError
|
17
18
|
from sourcerer.infrastructure.file_system.services import FileSystemService
|
19
|
+
from sourcerer.infrastructure.utils import generate_uuid
|
18
20
|
from sourcerer.presentation.screens.shared.containers import (
|
19
21
|
ScrollHorizontalContainerWithNoBindings,
|
20
22
|
ScrollVerticalContainerWithNoBindings,
|
21
23
|
)
|
22
|
-
|
23
|
-
from sourcerer.infrastructure.utils import generate_uuid
|
24
|
-
from sourcerer.settings import DOUBLE_CLICK_THRESHOLD, FILE_ICON, DIRECTORY_ICON
|
24
|
+
from sourcerer.settings import DIRECTORY_ICON, DOUBLE_CLICK_THRESHOLD, FILE_ICON
|
25
25
|
|
26
26
|
|
27
27
|
class PathListingContainer(ScrollVerticalContainerWithNoBindings):
|
@@ -95,7 +95,7 @@ class FileSystemWidget(Widget):
|
|
95
95
|
"""
|
96
96
|
self.entity_name = entity_name
|
97
97
|
self.icon = icon
|
98
|
-
self.last_file_click:
|
98
|
+
self.last_file_click: tuple[float, Path | None] = (
|
99
99
|
time() - 2,
|
100
100
|
None,
|
101
101
|
)
|
@@ -231,7 +231,7 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
231
231
|
"""
|
232
232
|
|
233
233
|
# Consolidate key binding data
|
234
|
-
BINDINGS: ClassVar[
|
234
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
235
235
|
Binding("enter", "select_cursor", "Select", show=False),
|
236
236
|
Binding("up", "cursor_up", "Cursor up", show=False),
|
237
237
|
Binding("down", "cursor_down", "Cursor down", show=False),
|
@@ -489,15 +489,15 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
489
489
|
path (Path): The directory path to list contents for
|
490
490
|
|
491
491
|
Returns:
|
492
|
-
PathListingContainer | None: A container with folder and file widgets, or None if directory is empty or
|
492
|
+
- PathListingContainer | None: A container with folder and file widgets, or None if directory is empty or
|
493
493
|
listing fails
|
494
494
|
|
495
495
|
Raises:
|
496
|
-
Notifies user via self.notify() if directory listing encounters an error
|
496
|
+
- Notifies user via self.notify() if directory listing encounters an error
|
497
497
|
"""
|
498
498
|
try:
|
499
499
|
dir_list = self.file_system_service.list_dir(path, relative_paths=False)
|
500
|
-
except
|
500
|
+
except ListDirError as e:
|
501
501
|
self.notify(str(e), markup=False)
|
502
502
|
return
|
503
503
|
|
@@ -686,7 +686,7 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
686
686
|
elements: Sequence[Widget],
|
687
687
|
direction: Literal["up", "down"],
|
688
688
|
selector: Callable,
|
689
|
-
of_type:
|
689
|
+
of_type: type | None = None,
|
690
690
|
) -> Widget | None:
|
691
691
|
"""
|
692
692
|
Determine the next element in a sequence based on navigation direction and selection criteria.
|
@@ -1,7 +1,9 @@
|
|
1
|
+
import time
|
1
2
|
import traceback
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
2
4
|
from pathlib import Path
|
3
5
|
|
4
|
-
from textual import
|
6
|
+
from textual import on, work
|
5
7
|
from textual.app import App, ComposeResult
|
6
8
|
from textual.binding import Binding
|
7
9
|
from textual.containers import Horizontal
|
@@ -10,7 +12,7 @@ from textual.widgets import Footer
|
|
10
12
|
|
11
13
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
12
14
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
13
|
-
|
15
|
+
ListStorageItemsError,
|
14
16
|
)
|
15
17
|
from sourcerer.infrastructure.utils import generate_uuid
|
16
18
|
from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
|
@@ -44,16 +46,17 @@ from sourcerer.presentation.screens.provider_creds_list.main import (
|
|
44
46
|
ProviderCredsListScreen,
|
45
47
|
)
|
46
48
|
from sourcerer.presentation.screens.storage_action_progress.main import (
|
49
|
+
DeleteKey,
|
50
|
+
DownloadKey,
|
47
51
|
StorageActionProgressScreen,
|
48
52
|
UploadKey,
|
49
|
-
DownloadKey,
|
50
|
-
DeleteKey,
|
51
53
|
)
|
52
54
|
from sourcerer.presentation.themes.github_dark import github_dark_theme
|
53
55
|
from sourcerer.presentation.utils import (
|
54
|
-
get_provider_service_by_access_uuid,
|
55
56
|
get_provider_service_by_access_credentials,
|
57
|
+
get_provider_service_by_access_uuid,
|
56
58
|
)
|
59
|
+
from sourcerer.settings import MAX_PARALLEL_STORAGE_LIST_OPERATIONS
|
57
60
|
|
58
61
|
|
59
62
|
class Sourcerer(App, ResizeContainersWatcherMixin):
|
@@ -168,25 +171,11 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
168
171
|
if not access_credentials:
|
169
172
|
return
|
170
173
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
severity="error",
|
177
|
-
)
|
178
|
-
continue
|
179
|
-
try:
|
180
|
-
storages = provider_service.list_storages()
|
181
|
-
self.storage_list_sidebar.storages = {
|
182
|
-
credentials.uuid: storages,
|
183
|
-
**self.storage_list_sidebar.storages,
|
184
|
-
}
|
185
|
-
except Exception:
|
186
|
-
self.notify(
|
187
|
-
f"Could not get storages list for {credentials.name}!",
|
188
|
-
severity="error",
|
189
|
-
)
|
174
|
+
with ThreadPoolExecutor(
|
175
|
+
max_workers=MAX_PARALLEL_STORAGE_LIST_OPERATIONS
|
176
|
+
) as executor:
|
177
|
+
for credentials in access_credentials:
|
178
|
+
executor.submit(self._load_storages, credentials)
|
190
179
|
|
191
180
|
@on(SelectStorageItem)
|
192
181
|
def on_select_storage_item(self, event: SelectStorageItem):
|
@@ -386,23 +375,22 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
386
375
|
self.storage_content.storage = storage_name
|
387
376
|
self.storage_content.access_credentials_uuid = access_credentials_uuid
|
388
377
|
self.storage_content.search_prefix = prefix or ""
|
378
|
+
self.storage_content.storage_content = None
|
379
|
+
self.storage_content.selected_files = set()
|
380
|
+
self.storage_content.selected_files_n = 0
|
389
381
|
|
390
382
|
provider_service = get_provider_service_by_access_uuid(
|
391
383
|
access_credentials_uuid, self.credentials_service
|
392
384
|
)
|
393
385
|
|
394
386
|
if not provider_service:
|
395
|
-
self.
|
387
|
+
self.notify_error("Could not extract storage content")
|
396
388
|
return
|
397
389
|
params = {"storage": storage_name, "path": path or "", "prefix": prefix or ""}
|
398
390
|
try:
|
399
391
|
self.storage_content.storage_content = provider_service.list_storage_items(**params) # type: ignore
|
400
|
-
except
|
401
|
-
self.
|
402
|
-
f"""Could not extract storage content
|
403
|
-
{str(e)}""",
|
404
|
-
severity="error",
|
405
|
-
)
|
392
|
+
except ListStorageItemsError as e:
|
393
|
+
self.notify_error(f"""Could not extract storage content \n{str(e)}""")
|
406
394
|
|
407
395
|
def _upload_file(
|
408
396
|
self,
|
@@ -435,7 +423,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
435
423
|
"""
|
436
424
|
# Validate input parameters
|
437
425
|
if not source_path:
|
438
|
-
self.
|
426
|
+
self.notify_error("No file selected for upload")
|
439
427
|
return
|
440
428
|
|
441
429
|
# Get the provider service
|
@@ -443,7 +431,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
443
431
|
access_credentials_uuid, self.credentials_service
|
444
432
|
)
|
445
433
|
if not provider_service:
|
446
|
-
self.
|
434
|
+
self.notify_error("Could not get provider service for upload")
|
447
435
|
return
|
448
436
|
|
449
437
|
# Create upload key
|
@@ -467,3 +455,39 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
467
455
|
access_credentials_uuid, storage_name, path
|
468
456
|
),
|
469
457
|
)
|
458
|
+
|
459
|
+
def _load_storages(self, credentials):
|
460
|
+
"""
|
461
|
+
Loads the list of storages for the given access credentials.
|
462
|
+
|
463
|
+
This method retrieves the list of storages from the provider service
|
464
|
+
associated with the provided access credentials and updates the
|
465
|
+
storage list sidebar with the retrieved storages.
|
466
|
+
|
467
|
+
Args:
|
468
|
+
credentials (Credentials): The access credentials for which to load storages.
|
469
|
+
|
470
|
+
Note:
|
471
|
+
If an error occurs while retrieving the storages, a notification is shown
|
472
|
+
to the user.
|
473
|
+
"""
|
474
|
+
provider_service = get_provider_service_by_access_credentials(credentials)
|
475
|
+
if not provider_service:
|
476
|
+
self.notify_error(f"Could not get storages list for {credentials.name}!")
|
477
|
+
return
|
478
|
+
|
479
|
+
try:
|
480
|
+
storages = provider_service.list_storages()
|
481
|
+
self.storage_list_sidebar.storages[credentials.uuid] = storages
|
482
|
+
self.storage_list_sidebar.last_update_timestamp = time.time()
|
483
|
+
except Exception:
|
484
|
+
self.notify_error(f"Could not get storages list for {credentials.name}!")
|
485
|
+
|
486
|
+
def notify_error(self, message):
|
487
|
+
"""
|
488
|
+
Displays an error notification to the user.
|
489
|
+
|
490
|
+
Args:
|
491
|
+
message (str): The error message to display.
|
492
|
+
"""
|
493
|
+
self.notify(message, severity="error")
|
@@ -3,13 +3,13 @@ import time
|
|
3
3
|
from textual.events import MouseMove, MouseUp
|
4
4
|
|
5
5
|
from sourcerer.presentation.screens.main.messages.resizing_rule import (
|
6
|
-
ResizingRuleSelect,
|
7
|
-
ResizingRuleRelease,
|
8
6
|
ResizingRuleMove,
|
7
|
+
ResizingRuleRelease,
|
8
|
+
ResizingRuleSelect,
|
9
9
|
)
|
10
10
|
from sourcerer.presentation.screens.main.widgets.resizing_rule import (
|
11
|
-
ResizingRule,
|
12
11
|
MoveEvent,
|
12
|
+
ResizingRule,
|
13
13
|
)
|
14
14
|
from sourcerer.presentation.settings import MIN_SECTION_DIMENSION
|
15
15
|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
from textual.widgets import Static
|
2
1
|
from rich.text import Text
|
2
|
+
from textual.widgets import Static
|
3
3
|
|
4
4
|
|
5
5
|
def hex_to_rgb(hex_color: str):
|
@@ -20,10 +20,7 @@ def generate_gradient_text(text: str, start_color: str, end_color: str) -> Text:
|
|
20
20
|
gradient_text = Text()
|
21
21
|
|
22
22
|
for i, char in enumerate(text):
|
23
|
-
if len(text) > 1
|
24
|
-
blend = i / (len(text) - 1)
|
25
|
-
else:
|
26
|
-
blend = 0
|
23
|
+
blend = i / (len(text) - 1) if len(text) > 1 else 0
|
27
24
|
|
28
25
|
r = int(start_rgb[0] + (end_rgb[0] - start_rgb[0]) * blend)
|
29
26
|
g = int(start_rgb[1] + (end_rgb[1] - start_rgb[1]) * blend)
|