data-sourcerer 0.2.3__py3-none-any.whl → 0.4.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 (51) hide show
  1. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
  2. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +50 -34
  3. sourcerer/__init__.py +1 -1
  4. sourcerer/domain/access_credentials/entities.py +3 -1
  5. sourcerer/domain/access_credentials/repositories.py +1 -1
  6. sourcerer/domain/storage/__init__.py +0 -0
  7. sourcerer/domain/storage/entities.py +27 -0
  8. sourcerer/domain/storage/repositories.py +31 -0
  9. sourcerer/domain/storage_provider/entities.py +1 -1
  10. sourcerer/infrastructure/access_credentials/repositories.py +3 -2
  11. sourcerer/infrastructure/access_credentials/services.py +9 -25
  12. sourcerer/infrastructure/db/models.py +33 -2
  13. sourcerer/infrastructure/storage/__init__.py +0 -0
  14. sourcerer/infrastructure/storage/repositories.py +72 -0
  15. sourcerer/infrastructure/storage/services.py +37 -0
  16. sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
  17. sourcerer/infrastructure/storage_provider/services/gcp.py +2 -3
  18. sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
  19. sourcerer/infrastructure/utils.py +2 -1
  20. sourcerer/presentation/di_container.py +15 -0
  21. sourcerer/presentation/screens/file_system_finder/main.py +5 -10
  22. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
  23. sourcerer/presentation/screens/main/main.py +89 -9
  24. sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
  25. sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
  26. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
  27. sourcerer/presentation/screens/main/widgets/storage_content.py +197 -80
  28. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
  29. sourcerer/presentation/screens/preview_content/main.py +216 -17
  30. sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
  31. sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
  32. sourcerer/presentation/screens/provider_creds_list/main.py +38 -13
  33. sourcerer/presentation/screens/provider_creds_registration/main.py +10 -7
  34. sourcerer/presentation/screens/shared/modal_screens.py +37 -0
  35. sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
  36. sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
  37. sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  38. sourcerer/presentation/screens/storages_list/main.py +184 -0
  39. sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  40. sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
  41. sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
  42. sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  43. sourcerer/presentation/screens/storages_registration/main.py +100 -0
  44. sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
  45. sourcerer/presentation/settings.py +29 -16
  46. sourcerer/presentation/utils.py +9 -1
  47. sourcerer/settings.py +2 -0
  48. sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
  49. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
  50. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
  51. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,8 +13,11 @@ from dependency_injector import containers, providers
13
13
  from sourcerer.infrastructure.access_credentials.repositories import (
14
14
  SQLAlchemyCredentialsRepository,
15
15
  )
16
+ from sourcerer.infrastructure.access_credentials.services import CredentialsService
16
17
  from sourcerer.infrastructure.db.config import Database
17
18
  from sourcerer.infrastructure.file_system.services import FileSystemService
19
+ from sourcerer.infrastructure.storage.repositories import SQLAlchemyStoragesRepository
20
+ from sourcerer.infrastructure.storage.services import StoragesService
18
21
  from sourcerer.settings import APP_DIR, DB_NAME
19
22
 
20
23
  DB_URL = f"sqlite:////{APP_DIR}/{DB_NAME}"
@@ -43,4 +46,16 @@ class DiContainer(containers.DeclarativeContainer):
43
46
  SQLAlchemyCredentialsRepository, session_factory
44
47
  )
45
48
 
49
+ storages_repository = providers.Factory(
50
+ SQLAlchemyStoragesRepository, session_factory
51
+ )
52
+
53
+ credentials_service = providers.Factory(
54
+ CredentialsService, repository=credentials_repository
55
+ )
56
+ storages_service = providers.Factory(
57
+ StoragesService,
58
+ repository=storages_repository,
59
+ )
60
+
46
61
  file_system_service = providers.Factory(FileSystemService, Path.home())
@@ -5,11 +5,9 @@ from pathlib import Path
5
5
  from dependency_injector.wiring import Provide
6
6
  from textual import on
7
7
  from textual.app import ComposeResult
8
- from textual.binding import Binding
9
8
  from textual.containers import Container, Horizontal
10
9
  from textual.css.query import NoMatches
11
10
  from textual.reactive import reactive
12
- from textual.screen import ModalScreen
13
11
  from textual.widgets import Static
14
12
 
15
13
  from sourcerer.infrastructure.file_system.services import FileSystemService
@@ -17,6 +15,7 @@ from sourcerer.presentation.di_container import DiContainer
17
15
  from sourcerer.presentation.screens.file_system_finder.widgets.file_system_navigator import (
18
16
  FileSystemNavigator,
19
17
  )
18
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
20
19
  from sourcerer.presentation.screens.shared.widgets.button import Button
21
20
 
22
21
 
@@ -26,21 +25,17 @@ class FileSystemSelectionValidationRule:
26
25
  error_message: str
27
26
 
28
27
 
29
- class FileSystemNavigationModal(ModalScreen):
28
+ class FileSystemNavigationModal(ExitBoundModalScreen):
30
29
  CONTAINER_ID = "file_system_view_container"
31
30
  CSS_PATH = "styles.tcss"
32
31
 
33
- BINDINGS = [
34
- Binding("escape", "app.pop_screen", "Pop screen"),
35
- ]
36
-
37
32
  active_path: reactive[Path] = reactive(Path())
38
33
 
39
34
  def __init__(
40
35
  self,
41
36
  *args,
42
- file_system_service: FileSystemService = Provide[
43
- DiContainer.file_system_service
37
+ file_system_service: FileSystemService = Provide[ # type: ignore
38
+ DiContainer.file_system_service # type: ignore
44
39
  ],
45
40
  validation_rules: list[FileSystemSelectionValidationRule] | None = None,
46
41
  **kwargs,
@@ -125,7 +120,7 @@ class FileSystemNavigationModal(ModalScreen):
125
120
  event (Button.Click): The event containing the button that was clicked.
126
121
  """
127
122
  if event.action == "close":
128
- self.on_close()
123
+ self.action_cancel_screen()
129
124
  else:
130
125
  self.on_apply()
131
126
 
@@ -21,6 +21,7 @@ from sourcerer.presentation.screens.shared.containers import (
21
21
  ScrollHorizontalContainerWithNoBindings,
22
22
  ScrollVerticalContainerWithNoBindings,
23
23
  )
24
+ from sourcerer.presentation.settings import KeyBindings
24
25
  from sourcerer.settings import DIRECTORY_ICON, DOUBLE_CLICK_THRESHOLD, FILE_ICON
25
26
 
26
27
 
@@ -48,7 +49,7 @@ class FileSystemWidget(Widget):
48
49
  background: $block-cursor-blurred-background;
49
50
  text-style: $block-cursor-blurred-text-style;
50
51
  }
51
-
52
+
52
53
  .folder-name {
53
54
  text-overflow: ellipsis;
54
55
  text-wrap: nowrap;
@@ -161,7 +162,7 @@ class FileSystemWidget(Widget):
161
162
  Calls the `on_click` method if the pressed key is "enter", which may trigger
162
163
  folder navigation or file opening depending on the widget's context.
163
164
  """
164
- if event.key == "enter":
165
+ if event.key == KeyBindings.ENTER.value:
165
166
  self.on_file_select()
166
167
 
167
168
  def on_file_select(self):
@@ -232,11 +233,13 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
232
233
 
233
234
  # Consolidate key binding data
234
235
  BINDINGS: ClassVar[list[BindingType]] = [
235
- Binding("enter", "select_cursor", "Select", show=False),
236
- Binding("up", "cursor_up", "Cursor up", show=False),
237
- Binding("down", "cursor_down", "Cursor down", show=False),
238
- Binding("left", "cursor_left", "Cursor left", show=False),
239
- Binding("right", "cursor_right", "Cursor right", show=False),
236
+ Binding(KeyBindings.ENTER.value, "select_cursor", "Select", show=False),
237
+ Binding(KeyBindings.ARROW_UP.value, "cursor_up", "Cursor up", show=False),
238
+ Binding(KeyBindings.ARROW_DOWN.value, "cursor_down", "Cursor down", show=False),
239
+ Binding(KeyBindings.ARROW_LEFT.value, "cursor_left", "Cursor left", show=False),
240
+ Binding(
241
+ KeyBindings.ARROW_RIGHT.value, "cursor_right", "Cursor right", show=False
242
+ ),
240
243
  ]
241
244
 
242
245
  MAIN_CONTAINER_ID: ClassVar[str] = "dirs_content"
@@ -306,9 +309,9 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
306
309
  )
307
310
  self._focus_first_child(path_listing_container)
308
311
 
309
- self.path_listing_containers_uuids[str(self.work_dir)] = (
310
- path_listing_container.id
311
- )
312
+ self.path_listing_containers_uuids[
313
+ str(self.work_dir)
314
+ ] = path_listing_container.id
312
315
 
313
316
  def action_cursor_down(self) -> None:
314
317
  """
@@ -626,9 +629,9 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
626
629
  await self._mount_path_listing_container(path_listing_container)
627
630
 
628
631
  if str(folder_path) not in self.path_listing_containers_uuids:
629
- self.path_listing_containers_uuids[str(folder_path)] = (
630
- path_listing_container.id
631
- )
632
+ self.path_listing_containers_uuids[
633
+ str(folder_path)
634
+ ] = path_listing_container.id
632
635
 
633
636
  @on(FileSystemWidget.Focus)
634
637
  def on_folder_focus(self, event: FileSystemWidget.Focus):
@@ -1,20 +1,26 @@
1
+ import contextlib
1
2
  import time
2
3
  import traceback
3
4
  from concurrent.futures import ThreadPoolExecutor
4
5
  from pathlib import Path
6
+ from typing import ClassVar
5
7
 
8
+ from dependency_injector.wiring import Provide
6
9
  from textual import on, work
7
10
  from textual.app import App, ComposeResult
8
- from textual.binding import Binding
11
+ from textual.binding import Binding, BindingType
9
12
  from textual.containers import Horizontal
13
+ from textual.css.query import NoMatches
10
14
  from textual.reactive import reactive
11
15
  from textual.widgets import Footer
12
16
 
17
+ from sourcerer.domain.storage_provider.entities import Storage
13
18
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
14
19
  from sourcerer.infrastructure.storage_provider.exceptions import (
15
20
  ListStorageItemsError,
16
21
  )
17
22
  from sourcerer.infrastructure.utils import generate_uuid
23
+ from sourcerer.presentation.di_container import DiContainer
18
24
  from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
19
25
  from sourcerer.presentation.screens.file_system_finder.main import (
20
26
  FileSystemNavigationModal,
@@ -55,6 +61,8 @@ from sourcerer.presentation.screens.storage_action_progress.main import (
55
61
  StorageActionProgressScreen,
56
62
  UploadKey,
57
63
  )
64
+ from sourcerer.presentation.screens.storages_list.main import StoragesListScreen
65
+ from sourcerer.presentation.settings import KeyBindings
58
66
  from sourcerer.presentation.themes.github_dark import github_dark_theme
59
67
  from sourcerer.presentation.utils import (
60
68
  get_provider_service_by_access_credentials,
@@ -93,12 +101,29 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
93
101
  """
94
102
 
95
103
  CSS_PATH = "styles.tcss"
96
- BINDINGS = [Binding("ctrl+r", "registrations", "Registrations list")]
104
+ BINDINGS: ClassVar[list[BindingType]] = [
105
+ Binding("ctrl+r", "registrations", "Registrations list"),
106
+ Binding("ctrl+s", "storages", "Storages list"),
107
+ Binding("ctrl+f", "find", show=False),
108
+ Binding(
109
+ KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
110
+ ),
111
+ Binding(
112
+ KeyBindings.ARROW_RIGHT.value, "focus_content", "Focus content", show=False
113
+ ),
114
+ ]
97
115
  is_storage_list_loading = reactive(False, recompose=True)
98
116
 
99
- def __init__(self, *args, **kwargs):
117
+ def __init__(
118
+ self,
119
+ credentials_service: CredentialsService = Provide[
120
+ DiContainer.credentials_service
121
+ ],
122
+ *args,
123
+ **kwargs,
124
+ ):
100
125
  super().__init__(*args, **kwargs)
101
- self.credentials_service = CredentialsService()
126
+ self.credentials_service = credentials_service
102
127
  self.storage_list_sidebar = StorageListSidebar(id="storage_list_sidebar")
103
128
  self.storage_content = StorageContentContainer(id="storage_content_container")
104
129
  self.load_percentage = 0
@@ -130,6 +155,25 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
130
155
  self.theme = "github-dark"
131
156
  self.init_storages_list()
132
157
 
158
+ def action_find(self):
159
+ """
160
+ Focus search input.
161
+ """
162
+ with contextlib.suppress(NoMatches):
163
+ self.query_one(f"#{self.storage_content.search_input_id}").focus()
164
+
165
+ def action_focus_content(self):
166
+ """
167
+ Focuses the storage content container.
168
+ """
169
+ self.storage_content.focus()
170
+
171
+ def action_focus_sidebar(self):
172
+ """
173
+ Focuses the storage list sidebar.
174
+ """
175
+ self.storage_list_sidebar.focus()
176
+
133
177
  def action_registrations(self):
134
178
  """
135
179
  Opens the provider credentials list screen and refreshes the storage list.
@@ -140,7 +184,23 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
140
184
  This method is typically used to allow users to add their
141
185
  cloud storage credentials, which will then be reflected in the storage
142
186
  """
143
- self.app.push_screen(ProviderCredsListScreen(), callback=self.refresh_storages)
187
+ self.app.push_screen(
188
+ ProviderCredsListScreen(), callback=self.modal_screen_callback
189
+ )
190
+
191
+ def action_storages(self):
192
+ self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
193
+
194
+ def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
195
+ """
196
+ Callback for modal screens to refresh the storage list if required.
197
+
198
+ This method is called when a modal screen is closed. If the
199
+ `requires_storage_refresh` flag is set to True, it refreshes the
200
+ storage list by calling the `refresh_storages` method.
201
+ """
202
+ if requires_storage_refresh:
203
+ self.refresh_storages()
144
204
 
145
205
  def refresh_storages(self, *args, **kwargs):
146
206
  """
@@ -150,6 +210,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
150
210
  configurations.
151
211
  """
152
212
  self.storage_list_sidebar.storages = {}
213
+ self.storage_list_sidebar.last_update_timestamp = time.time()
153
214
  self.init_storages_list()
154
215
 
155
216
  @work(thread=True)
@@ -206,7 +267,11 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
206
267
  5. Notify the user if an error occurs during the retrieval process.
207
268
  """
208
269
  self.refresh_storage_content(
209
- event.access_credentials_uuid, event.name, event.path, event.prefix
270
+ event.access_credentials_uuid,
271
+ event.name,
272
+ event.path,
273
+ event.prefix,
274
+ event.focus_content,
210
275
  )
211
276
 
212
277
  @on(UploadRequest)
@@ -343,6 +408,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
343
408
  PreviewContentScreen(
344
409
  storage_name=event.storage_name,
345
410
  key=event.path,
411
+ file_size=event.size,
346
412
  access_credentials_uuid=event.access_credentials_uuid,
347
413
  )
348
414
  )
@@ -381,7 +447,12 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
381
447
  self.uncheck_files_request(UncheckFilesRequest(keys=[]))
382
448
 
383
449
  def refresh_storage_content(
384
- self, access_credentials_uuid, storage_name, path, prefix=None
450
+ self,
451
+ access_credentials_uuid,
452
+ storage_name,
453
+ path,
454
+ prefix=None,
455
+ focus_content=False,
385
456
  ):
386
457
  """
387
458
  Refreshes the storage content display with items from the specified storage path.
@@ -407,6 +478,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
407
478
  self.storage_content.storage_content = None
408
479
  self.storage_content.selected_files = set()
409
480
  self.storage_content.selected_files_n = 0
481
+ self.storage_content.focus_content = focus_content
410
482
 
411
483
  provider_service = get_provider_service_by_access_uuid(
412
484
  access_credentials_uuid, self.credentials_service
@@ -421,7 +493,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
421
493
  **params
422
494
  )
423
495
  except ListStorageItemsError as e:
424
- self.notify_error(f"""Could not extract storage content \n{str(e)}""")
496
+ self.notify_error(f"""Could not extract storage content \n{e}""")
425
497
 
426
498
  def _upload_file(
427
499
  self,
@@ -509,7 +581,15 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
509
581
 
510
582
  try:
511
583
  storages = provider_service.list_storages()
512
- self.storage_list_sidebar.storages[credentials.uuid] = storages
584
+ storage_names = [storage.storage for storage in storages]
585
+ registered_storages = [
586
+ Storage(credentials.provider, storage.name, storage.created_at)
587
+ for storage in credentials.storages
588
+ if storage.name not in storage_names
589
+ ]
590
+ self.storage_list_sidebar.storages[credentials.uuid] = (
591
+ storages + registered_storages
592
+ )
513
593
  self.storage_list_sidebar.last_update_timestamp = time.time()
514
594
  except Exception:
515
595
  self.notify_error(f"Could not get storages list for {credentials.name}!")
@@ -8,3 +8,4 @@ class PreviewRequest(Message):
8
8
  storage_name: str
9
9
  access_credentials_uuid: str
10
10
  path: str
11
+ size: int
@@ -9,3 +9,4 @@ class SelectStorageItem(Message):
9
9
  path: str | None = None
10
10
  access_credentials_uuid: str | None = None
11
11
  prefix: str | None = None
12
+ focus_content: bool = False
@@ -1,4 +1,5 @@
1
1
  import time
2
+ from typing import ClassVar
2
3
 
3
4
  from textual.events import MouseMove, MouseUp
4
5
 
@@ -28,7 +29,7 @@ class ResizeContainersWatcherMixin:
28
29
  and is inherited from textual App.
29
30
  """
30
31
 
31
- required_methods = ["query_one", "refresh"]
32
+ required_methods: ClassVar[list[str]] = ["query_one", "refresh"]
32
33
 
33
34
  def __init_subclass__(cls, **kwargs):
34
35
  super().__init_subclass__(**kwargs)