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.
Files changed (54) hide show
  1. {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/METADATA +25 -4
  2. {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/RECORD +53 -50
  3. {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/WHEEL +1 -1
  4. sourcerer/__init__.py +1 -1
  5. sourcerer/domain/access_credentials/entities.py +17 -0
  6. sourcerer/domain/access_credentials/exceptions.py +1 -1
  7. sourcerer/domain/access_credentials/repositories.py +1 -1
  8. sourcerer/domain/access_credentials/services.py +14 -2
  9. sourcerer/domain/file_system/exceptions.py +1 -1
  10. sourcerer/domain/file_system/services.py +2 -2
  11. sourcerer/domain/shared/entities.py +1 -0
  12. sourcerer/domain/storage_provider/entities.py +3 -4
  13. sourcerer/domain/storage_provider/exceptions.py +1 -1
  14. sourcerer/domain/storage_provider/services.py +13 -9
  15. sourcerer/infrastructure/access_credentials/exceptions.py +15 -2
  16. sourcerer/infrastructure/access_credentials/registry.py +3 -4
  17. sourcerer/infrastructure/access_credentials/services.py +141 -44
  18. sourcerer/infrastructure/db/models.py +1 -1
  19. sourcerer/infrastructure/file_system/exceptions.py +9 -9
  20. sourcerer/infrastructure/file_system/services.py +16 -16
  21. sourcerer/infrastructure/storage_provider/exceptions.py +28 -8
  22. sourcerer/infrastructure/storage_provider/registry.py +2 -3
  23. sourcerer/infrastructure/storage_provider/services/__init__.py +0 -0
  24. sourcerer/infrastructure/storage_provider/services/azure.py +261 -0
  25. sourcerer/infrastructure/storage_provider/services/gcp.py +277 -0
  26. sourcerer/infrastructure/storage_provider/services/s3.py +290 -0
  27. sourcerer/infrastructure/utils.py +2 -4
  28. sourcerer/presentation/screens/critical_error/main.py +3 -4
  29. sourcerer/presentation/screens/file_system_finder/main.py +4 -4
  30. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +12 -12
  31. sourcerer/presentation/screens/main/main.py +57 -33
  32. sourcerer/presentation/screens/main/messages/delete_request.py +1 -2
  33. sourcerer/presentation/screens/main/messages/download_request.py +1 -2
  34. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +3 -3
  35. sourcerer/presentation/screens/main/widgets/gradient.py +2 -5
  36. sourcerer/presentation/screens/main/widgets/resizing_rule.py +1 -1
  37. sourcerer/presentation/screens/main/widgets/storage_content.py +12 -13
  38. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +8 -6
  39. sourcerer/presentation/screens/preview_content/main.py +15 -4
  40. sourcerer/presentation/screens/preview_content/styles.tcss +2 -1
  41. sourcerer/presentation/screens/provider_creds_list/main.py +2 -2
  42. sourcerer/presentation/screens/provider_creds_registration/main.py +26 -11
  43. sourcerer/presentation/screens/question/main.py +1 -1
  44. sourcerer/presentation/screens/shared/containers.py +1 -1
  45. sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -1
  46. sourcerer/presentation/screens/storage_action_progress/main.py +34 -20
  47. sourcerer/presentation/screens/storage_action_progress/styles.tcss +11 -0
  48. sourcerer/presentation/utils.py +7 -3
  49. sourcerer/settings.py +4 -0
  50. sourcerer/utils.py +2 -2
  51. sourcerer/infrastructure/storage_provider/services.py +0 -509
  52. {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/entry_points.txt +0 -0
  53. {data_sourcerer-0.1.0.dist-info → data_sourcerer-0.2.0.dist-info}/licenses/LICENSE +0 -0
  54. {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
- if is_text_mime(file_name):
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(Singleton, cls).__call__(*args, **kwargs)
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 Horizontal, Container
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 Horizontal, Container
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: List[FileSystemSelectionValidationRule] | None = None,
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, Sequence, Literal, List, Callable, Tuple, Type
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 Rule, Label
15
+ from textual.widgets import Label, Rule
15
16
 
16
- from sourcerer.infrastructure.file_system.exceptions import ListDirException
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: Tuple[float, Path | None] = (
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[List[BindingType]] = [
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 ListDirException as e:
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: Type | None = None,
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 work, on
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
- ListStorageItemsException,
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
- for credentials in access_credentials:
172
- provider_service = get_provider_service_by_access_credentials(credentials)
173
- if not provider_service:
174
- self.notify(
175
- f"Could not get storages list for {credentials.name}!",
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.notify("Could not extract storage content", severity="error")
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 ListStorageItemsException as e:
401
- self.notify(
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.notify("No file selected for upload", severity="error")
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.notify("Could not get provider service for upload", severity="error")
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")
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import List
3
2
 
4
3
  from textual.message import Message
5
4
 
@@ -9,4 +8,4 @@ class DeleteRequest(Message):
9
8
  storage_name: str
10
9
  access_credentials_uuid: str
11
10
  path: str
12
- keys: List[str]
11
+ keys: list[str]
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import List
3
2
 
4
3
  from textual.message import Message
5
4
 
@@ -9,4 +8,4 @@ class DownloadRequest(Message):
9
8
  storage_name: str
10
9
  access_credentials_uuid: str
11
10
  path: str
12
- keys: List[str]
11
+ keys: list[str]
@@ -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)
@@ -6,8 +6,8 @@ from textual.widgets import Rule
6
6
 
7
7
  from sourcerer.presentation.screens.main.messages.resizing_rule import (
8
8
  ResizingRuleMove,
9
- ResizingRuleSelect,
10
9
  ResizingRuleRelease,
10
+ ResizingRuleSelect,
11
11
  )
12
12
 
13
13