data-sourcerer 0.2.3__py3-none-any.whl → 0.3.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 (43) hide show
  1. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/METADATA +1 -1
  2. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/RECORD +42 -28
  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/infrastructure/access_credentials/repositories.py +3 -2
  10. sourcerer/infrastructure/access_credentials/services.py +9 -25
  11. sourcerer/infrastructure/db/models.py +33 -2
  12. sourcerer/infrastructure/storage/__init__.py +0 -0
  13. sourcerer/infrastructure/storage/repositories.py +72 -0
  14. sourcerer/infrastructure/storage/services.py +37 -0
  15. sourcerer/infrastructure/storage_provider/services/gcp.py +1 -1
  16. sourcerer/infrastructure/utils.py +2 -1
  17. sourcerer/presentation/di_container.py +15 -0
  18. sourcerer/presentation/screens/file_system_finder/main.py +5 -4
  19. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
  20. sourcerer/presentation/screens/main/main.py +63 -8
  21. sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
  22. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
  23. sourcerer/presentation/screens/main/widgets/storage_content.py +187 -77
  24. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
  25. sourcerer/presentation/screens/preview_content/main.py +15 -3
  26. sourcerer/presentation/screens/provider_creds_list/main.py +29 -8
  27. sourcerer/presentation/screens/provider_creds_registration/main.py +7 -4
  28. sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
  29. sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
  30. sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  31. sourcerer/presentation/screens/storages_list/main.py +180 -0
  32. sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  33. sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
  34. sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
  35. sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  36. sourcerer/presentation/screens/storages_registration/main.py +100 -0
  37. sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
  38. sourcerer/presentation/settings.py +29 -16
  39. sourcerer/presentation/utils.py +9 -1
  40. sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
  41. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/WHEEL +0 -0
  42. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/entry_points.txt +0 -0
  43. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,10 +7,12 @@ and selection of storage items.
7
7
 
8
8
  from collections import namedtuple
9
9
  from itertools import groupby
10
+ from typing import Self
10
11
 
11
12
  from textual import events, on
12
13
  from textual.app import ComposeResult
13
- from textual.containers import Container, Horizontal, VerticalScroll
14
+ from textual.containers import Horizontal, Vertical
15
+ from textual.css.query import NoMatches
14
16
  from textual.reactive import reactive
15
17
  from textual.widgets import Label, Rule
16
18
 
@@ -23,8 +25,12 @@ from sourcerer.presentation.screens.main.messages.select_storage_item import (
23
25
  SelectStorageItem,
24
26
  )
25
27
  from sourcerer.presentation.screens.main.widgets.gradient import GradientWidget
28
+ from sourcerer.presentation.screens.shared.containers import (
29
+ ScrollVerticalContainerWithNoBindings,
30
+ )
26
31
  from sourcerer.presentation.screens.shared.widgets.button import Button
27
- from sourcerer.presentation.screens.shared.widgets.loader import Loader
32
+ from sourcerer.presentation.screens.shared.widgets.spinner import Spinner
33
+ from sourcerer.presentation.settings import KeyBindings
28
34
 
29
35
  STORAGE_ICONS = {
30
36
  StorageProvider.S3: "🟠",
@@ -41,6 +47,9 @@ class StorageItem(Label):
41
47
  selection and visual feedback on hover.
42
48
  """
43
49
 
50
+ can_focus = True
51
+ selected = reactive(False, recompose=True, toggle_class="selected")
52
+
44
53
  DEFAULT_CSS = """
45
54
  StorageItem {
46
55
  width: 90%;
@@ -51,7 +60,17 @@ class StorageItem(Label):
51
60
  text-wrap: nowrap;
52
61
 
53
62
  & > :hover {
54
- background: $warning;
63
+ background: $primary-lighten-2;
64
+ color: $panel;
65
+ }
66
+
67
+ & > :focus {
68
+ background: $primary-lighten-2;
69
+ color: $panel;
70
+ }
71
+
72
+ &.selected {
73
+ background: $primary;
55
74
  color: $panel;
56
75
  }
57
76
  }
@@ -71,6 +90,38 @@ class StorageItem(Label):
71
90
 
72
91
  def on_click(self, _: events.Click) -> None:
73
92
  """Handle click events to select the storage item."""
93
+ self._select_storage()
94
+
95
+ def on_key(self, event: events.Key) -> None:
96
+ """Handle key events to select the storage item."""
97
+ if event.key == KeyBindings.ENTER.value:
98
+ self._select_storage()
99
+ return
100
+ storages = [
101
+ component
102
+ for component in self.screen.focus_chain
103
+ if isinstance(component, StorageItem)
104
+ ]
105
+ if not storages:
106
+ return
107
+ if event.key == KeyBindings.ARROW_DOWN.value:
108
+ if self.screen.focused == storages[-1]:
109
+ storages[0].focus()
110
+ return
111
+ self.screen.focus_next(StorageItem)
112
+ elif event.key == KeyBindings.ARROW_UP.value:
113
+ if self.screen.focused == storages[0]:
114
+ storages[-1].focus()
115
+ return
116
+ self.screen.focus_previous(StorageItem)
117
+
118
+ def _select_storage(self):
119
+ """
120
+ Select the storage item and notify the application.
121
+ This method posts a message to select the storage item based on its
122
+ name and access credentials UUID.
123
+
124
+ """
74
125
  self.post_message(
75
126
  SelectStorageItem(
76
127
  self.storage_name, access_credentials_uuid=self.access_credentials_uuid
@@ -78,7 +129,7 @@ class StorageItem(Label):
78
129
  )
79
130
 
80
131
 
81
- class StorageListSidebar(VerticalScroll):
132
+ class StorageListSidebar(Vertical):
82
133
  """Sidebar widget for displaying the list of storage providers.
83
134
 
84
135
  This widget manages the display of storage providers grouped by their type,
@@ -98,27 +149,29 @@ class StorageListSidebar(VerticalScroll):
98
149
  StorageListSidebar {
99
150
  padding-right: 0;
100
151
  margin-right: 0;
101
- & > .storage-group {
152
+ height: 100%;
153
+ margin-bottom: 1;
154
+ #rule-left {
155
+ width: 1;
156
+ }
157
+
158
+ ScrollVerticalContainerWithNoBindings{
159
+ height: 95%;
160
+ }
161
+
162
+ Horizontal {
102
163
  height: auto;
103
- margin-bottom: 1;
104
- #rule-left {
105
- width: 1;
106
- }
107
-
108
- Horizontal {
109
- height: auto;
110
- }
111
- Rule.-horizontal {
112
- height: 1;
113
- margin: 0 0;
114
-
115
- }
116
- .storage-letter {
117
- color: $secondary;
118
- padding: 0 1;
119
- }
164
+ }
165
+ Rule.-horizontal {
166
+ height: 1;
167
+ margin: 0 0;
120
168
 
121
169
  }
170
+ .storage-letter {
171
+ color: $secondary;
172
+ padding: 0 1;
173
+ }
174
+
122
175
  }
123
176
  #header {
124
177
  width: 100%;
@@ -127,7 +180,7 @@ class StorageListSidebar(VerticalScroll):
127
180
  width: auto;
128
181
  }
129
182
 
130
- Loader {
183
+ Spinner {
131
184
  width: 5%;
132
185
  }
133
186
  }
@@ -136,7 +189,7 @@ class StorageListSidebar(VerticalScroll):
136
189
  def compose(self) -> ComposeResult:
137
190
  with Horizontal(id="header"):
138
191
  if self.is_loading:
139
- yield Loader()
192
+ yield Spinner()
140
193
  yield GradientWidget(
141
194
  " SOURCERER" if self.is_loading else "🧙SOURCERER",
142
195
  id="left-middle",
@@ -150,11 +203,11 @@ class StorageListSidebar(VerticalScroll):
150
203
  for storage in storages
151
204
  ]
152
205
  storages = sorted(storages, key=lambda x: x.storage.storage)
206
+ with ScrollVerticalContainerWithNoBindings():
207
+ for letter, storages_group in groupby(
208
+ storages, key=lambda x: x.storage.storage[0]
209
+ ):
153
210
 
154
- for letter, storages_group in groupby(
155
- storages, key=lambda x: x.storage.storage[0]
156
- ):
157
- with Container(id=f"group-{letter}", classes="storage-group"):
158
211
  yield Horizontal(
159
212
  Rule(id="rule-left"),
160
213
  Label(letter.upper(), classes="storage-letter"),
@@ -163,15 +216,30 @@ class StorageListSidebar(VerticalScroll):
163
216
 
164
217
  for item in storages_group:
165
218
  yield StorageItem(
166
- renderable=STORAGE_ICONS.get(item.storage.provider, "")
167
- + " "
168
- + item.storage.storage,
219
+ renderable=f'{STORAGE_ICONS.get(item.storage.provider, "")} {item.storage.storage}',
169
220
  storage_name=item.storage.storage,
170
221
  access_credentials_uuid=item.access_credentials_uuid,
171
222
  )
172
223
 
224
+ def focus(self, scroll_visible: bool = True) -> Self:
225
+ try:
226
+ content = self.query_one(StorageItem)
227
+ except NoMatches:
228
+ return self
229
+ content.focus()
230
+ return self
231
+
173
232
  @on(Button.Click)
174
233
  def on_button_click(self, event: Button.Click) -> None:
175
234
  """Handle button click events to refresh the storage list."""
176
235
  if event.action == "header_click":
177
236
  self.post_message(RefreshStoragesListRequest())
237
+
238
+ @on(SelectStorageItem)
239
+ def on_select_storage_item(self, event: SelectStorageItem) -> None:
240
+ """Handle selection of a storage item."""
241
+ for child in self.query(StorageItem):
242
+ child.selected = (
243
+ child.storage_name == event.name
244
+ and child.access_credentials_uuid == event.access_credentials_uuid
245
+ )
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
+ from dependency_injector.wiring import Provide
3
4
  from rich.syntax import Syntax
4
5
  from textual import on
5
6
  from textual.app import ComposeResult
@@ -11,6 +12,7 @@ from sourcerer.infrastructure.access_credentials.services import CredentialsServ
11
12
  from sourcerer.infrastructure.storage_provider.exceptions import (
12
13
  ReadStorageItemsError,
13
14
  )
15
+ from sourcerer.presentation.di_container import DiContainer
14
16
  from sourcerer.presentation.screens.shared.widgets.button import Button
15
17
  from sourcerer.presentation.utils import get_provider_service_by_access_uuid
16
18
 
@@ -18,12 +20,23 @@ from sourcerer.presentation.utils import get_provider_service_by_access_uuid
18
20
  class PreviewContentScreen(ModalScreen):
19
21
  CSS_PATH = "styles.tcss"
20
22
 
21
- def __init__(self, storage_name, key, access_credentials_uuid, *args, **kwargs):
23
+ def __init__(
24
+ self,
25
+ storage_name,
26
+ key,
27
+ access_credentials_uuid,
28
+ *args,
29
+ credentials_service: CredentialsService = Provide[
30
+ DiContainer.credentials_repository
31
+ ],
32
+ **kwargs
33
+ ):
22
34
  super().__init__(*args, **kwargs)
23
35
 
24
36
  self.storage_name = storage_name
25
37
  self.key = key
26
38
  self.access_credentials_uuid = access_credentials_uuid
39
+ self.credentials_service = credentials_service
27
40
 
28
41
  def compose(self) -> ComposeResult:
29
42
  with Container(id="PreviewContentScreen"):
@@ -35,9 +48,8 @@ class PreviewContentScreen(ModalScreen):
35
48
  def on_mount(self) -> None:
36
49
  """Called when the DOM is ready."""
37
50
 
38
- credentials_service = CredentialsService()
39
51
  provider_service = get_provider_service_by_access_uuid(
40
- self.access_credentials_uuid, credentials_service
52
+ self.access_credentials_uuid, self.credentials_service
41
53
  )
42
54
  if not provider_service:
43
55
  self.notify("Could not read file :(", severity="error")
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
3
 
4
+ from dependency_injector.wiring import Provide
4
5
  from textual import on
5
6
  from textual.app import ComposeResult
6
7
  from textual.containers import Container, Horizontal, VerticalScroll
@@ -10,7 +11,9 @@ from textual.screen import ModalScreen
10
11
  from textual.widgets import Checkbox, Label
11
12
 
12
13
  from sourcerer.domain.access_credentials.entities import Credentials
14
+ from sourcerer.domain.access_credentials.repositories import BaseCredentialsRepository
13
15
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
16
+ from sourcerer.presentation.di_container import DiContainer
14
17
  from sourcerer.presentation.screens.provider_creds_list.messages.reload_credentials_request import (
15
18
  ReloadCredentialsRequest,
16
19
  )
@@ -32,9 +35,12 @@ class ProviderCredentialsRow(Horizontal):
32
35
  uuid: str
33
36
  active: bool
34
37
 
35
- def __init__(self, row: Credentials, *args, **kwargs):
38
+ def __init__(
39
+ self, row: Credentials, credentials_service: CredentialsService, *args, **kwargs
40
+ ):
36
41
  super().__init__(*args, **kwargs)
37
42
  self.row = row
43
+ self.credentials_service = credentials_service
38
44
 
39
45
  def compose(self) -> ComposeResult:
40
46
  yield Checkbox(
@@ -88,8 +94,7 @@ class ProviderCredentialsRow(Horizontal):
88
94
  """
89
95
  if not result:
90
96
  return
91
- credentials_service = CredentialsService()
92
- credentials_service.delete(self.row.uuid)
97
+ self.credentials_service.delete(self.row.uuid)
93
98
  self.post_message(ReloadCredentialsRequest())
94
99
 
95
100
 
@@ -107,9 +112,16 @@ class ProviderCredsListScreen(ModalScreen):
107
112
 
108
113
  credentials_list = reactive([], recompose=True)
109
114
 
110
- def __init__(self, *args, **kwargs):
115
+ def __init__(
116
+ self,
117
+ credentials_service: CredentialsService = Provide[
118
+ DiContainer.credentials_service
119
+ ],
120
+ *args,
121
+ **kwargs,
122
+ ):
111
123
  super().__init__(*args, **kwargs)
112
- self.credentials_service = CredentialsService()
124
+ self.credentials_service = credentials_service
113
125
 
114
126
  def compose(self) -> ComposeResult:
115
127
  with Container(id=self.MAIN_CONTAINER_ID):
@@ -129,7 +141,9 @@ class ProviderCredsListScreen(ModalScreen):
129
141
  yield Label("Auth method", classes="credentials_auth_method")
130
142
  yield Label("Delete", classes="credentials_auth_delete")
131
143
  for row in self.credentials_list:
132
- yield ProviderCredentialsRow(row, classes="credentials_row")
144
+ yield ProviderCredentialsRow(
145
+ row, self.credentials_service, classes="credentials_row"
146
+ )
133
147
  with Horizontal(id="controls"):
134
148
  yield Button(ControlsEnum.CANCEL.value, name=ControlsEnum.CANCEL.name)
135
149
 
@@ -146,7 +160,11 @@ class ProviderCredsListScreen(ModalScreen):
146
160
  self.credentials_list = self.credentials_service.list()
147
161
 
148
162
  def create_provider_creds_registration(
149
- self, credentials_entry: ProviderCredentialsEntry
163
+ self,
164
+ credentials_entry: ProviderCredentialsEntry,
165
+ credentials_repo: BaseCredentialsRepository = Provide[
166
+ DiContainer.credentials_repository
167
+ ],
150
168
  ):
151
169
  """
152
170
  Create a new provider credentials registration.
@@ -155,10 +173,13 @@ class ProviderCredsListScreen(ModalScreen):
155
173
 
156
174
  Args:
157
175
  credentials_entry (ProviderCredentialsEntry): The credentials entry to register.
176
+ credentials_repo (BaseCredentialsRepository): The repository to store the credentials.
158
177
  """
159
178
  if not credentials_entry:
160
179
  return
161
- service = credentials_entry.cloud_storage_provider_credentials_service() # type: ignore
180
+ service = credentials_entry.cloud_storage_provider_credentials_service(
181
+ credentials_repo
182
+ )
162
183
  service.store(credentials_entry.name, credentials_entry.fields)
163
184
  self.refresh_credentials_list()
164
185
 
@@ -15,6 +15,9 @@ from sourcerer.domain.access_credentials.services import (
15
15
  from sourcerer.infrastructure.access_credentials.exceptions import (
16
16
  MissingAuthFieldsError,
17
17
  )
18
+ from sourcerer.infrastructure.access_credentials.registry import (
19
+ AccessCredentialsRegistry,
20
+ )
18
21
  from sourcerer.infrastructure.utils import generate_unique_name
19
22
  from sourcerer.presentation.di_container import DiContainer
20
23
  from sourcerer.presentation.screens.shared.widgets.button import Button
@@ -48,8 +51,8 @@ class ProviderCredsRegistrationScreen(ModalScreen):
48
51
  def __init__(
49
52
  self,
50
53
  *args,
51
- credentials_type_registry=Provide[
52
- DiContainer.config.access_credential_method_registry
54
+ credentials_type_registry: AccessCredentialsRegistry = Provide[ # type: ignore
55
+ DiContainer.config.access_credential_method_registry # type: ignore
53
56
  ],
54
57
  **kwargs,
55
58
  ):
@@ -123,7 +126,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
123
126
 
124
127
  # If only one authentication method exists, set it and mount its fields
125
128
  self.auth_method = next(iter(auth_methods.values()))
126
- cls: BaseAccessCredentialsService = self.auth_method
129
+ cls: type[BaseAccessCredentialsService] = self.auth_method
127
130
  await self._mount_credentials_fields(cls.auth_fields())
128
131
 
129
132
  @on(Select.Changed)
@@ -206,7 +209,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
206
209
  ProviderCredentialsEntry: An object containing the authentication name, method, and fields.
207
210
  """
208
211
  if not self.auth_method:
209
- return
212
+ return None
210
213
 
211
214
  fields = {
212
215
  input_field.get().name: input_field.get().value
@@ -0,0 +1,57 @@
1
+ from collections.abc import Iterator
2
+ from enum import Enum
3
+ from itertools import cycle
4
+
5
+ from textual.app import RenderResult
6
+ from textual.widgets import Static
7
+
8
+
9
+ class SpinnerType(Enum):
10
+ """Enumeration of various spinner animations."""
11
+
12
+ dots_1 = "⣷⣯⣟⡿⢿⣻⣽⣾"
13
+ dots_2 = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
14
+ dots_3 = "⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓"
15
+ dots_4 = "⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆"
16
+ dots_5 = "⠈⠐⠠⢀⡀⠄⠂⠁"
17
+ dots_6 = "⋯⋱⋮⋰"
18
+ circles = "◐◓◑◒"
19
+ angles = "┐┤┘┴└├┌┬"
20
+ arrows = "←↖↑↗→↘↓↙"
21
+ moon = "🌑🌒🌓🌔🌕🌖🌗🌘"
22
+ clock = "🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚"
23
+ histogram = "▁▃▄▅▆▇█▇▆▅▄▃"
24
+ shade = "░▒▓█▓▒░"
25
+ colors = "🟨🟨🟧🟧🟥🟥🟦🟦🟪🟪🟩🟩"
26
+ triangles = "◢◣◤◥"
27
+
28
+
29
+ class Spinner(Static):
30
+ """A loading spinner widget for Textual apps."""
31
+
32
+ DEFAULT_CSS = """
33
+ Spinner {
34
+ color: #9E53E0;
35
+ }
36
+ """
37
+
38
+ def __init__(
39
+ self, spinner: SpinnerType = SpinnerType.dots_1, interval: float = 0.1, **kwargs
40
+ ):
41
+ """
42
+ Initialize the Loader widget.
43
+
44
+ Args:
45
+ spinner (SpinnerType): The spinner animation type.
46
+ interval (float): Time in seconds between frames.
47
+ **kwargs: Additional keyword arguments for Static.
48
+ """
49
+ super().__init__(**kwargs)
50
+ self._frames: Iterator[str] = cycle(spinner.value)
51
+ self._interval = interval
52
+
53
+ def render(self) -> RenderResult:
54
+ return next(self._frames)
55
+
56
+ def on_mount(self) -> None:
57
+ self.auto_refresh = self._interval
@@ -298,7 +298,7 @@ class StorageActionProgressScreen(ModalScreen):
298
298
  progress_bar.total = file_size
299
299
  except Exception as ex:
300
300
  self.notify(
301
- f"Failed to get file size for {key}: {str(ex)}", severity="error"
301
+ f"Failed to get file size for {key}: {ex}", severity="error"
302
302
  )
303
303
  self.log.error(f"Error getting file size: {ex}")
304
304
  return
@@ -311,7 +311,7 @@ class StorageActionProgressScreen(ModalScreen):
311
311
  lambda chunk: progress_callback(progress_bar, chunk),
312
312
  )
313
313
  except Exception as ex:
314
- self.notify(f"Failed to download {key}: {str(ex)}", severity="error")
314
+ self.notify(f"Failed to download {key}: {ex}", severity="error")
315
315
  self.log.error(f"Error downloading file: {ex}")
316
316
  return
317
317
 
@@ -324,9 +324,7 @@ class StorageActionProgressScreen(ModalScreen):
324
324
  # Non-critical error, continue execution
325
325
  except Exception as ex:
326
326
  # Catch any unexpected exceptions
327
- self.notify(
328
- f"Unexpected error downloading {key}: {str(ex)}", severity="error"
329
- )
327
+ self.notify(f"Unexpected error downloading {key}: {ex}", severity="error")
330
328
  self.log.error(f"Unexpected error: {ex}")
331
329
  finally:
332
330
  main_progress_bar.advance(1)
@@ -0,0 +1,180 @@
1
+ import datetime
2
+ import uuid
3
+ from enum import Enum
4
+
5
+ from dependency_injector.wiring import Provide
6
+ from textual import on
7
+ from textual.app import ComposeResult
8
+ from textual.containers import Container, Horizontal, VerticalScroll
9
+ from textual.reactive import reactive
10
+ from textual.screen import ModalScreen
11
+ from textual.widgets import Label
12
+
13
+ from sourcerer.domain.storage.entities import Storage
14
+ from sourcerer.infrastructure.access_credentials.services import CredentialsService
15
+ from sourcerer.infrastructure.storage.services import StoragesService
16
+ from sourcerer.presentation.di_container import DiContainer
17
+ from sourcerer.presentation.screens.question.main import QuestionScreen
18
+ from sourcerer.presentation.screens.shared.widgets.button import Button
19
+ from sourcerer.presentation.screens.storages_list.messages.reload_storages_request import (
20
+ ReloadStoragesRequest,
21
+ )
22
+ from sourcerer.presentation.screens.storages_registration.main import (
23
+ StorageEntry,
24
+ StoragesRegistrationScreen,
25
+ )
26
+
27
+
28
+ class ControlsEnum(Enum):
29
+ CANCEL = "Cancel"
30
+ ADD_STORAGE = "Add Storage"
31
+
32
+
33
+ class StorageRow(Horizontal):
34
+ def __init__(
35
+ self, storage: Storage, storages_service: StoragesService, *args, **kwargs
36
+ ):
37
+ super().__init__(*args, **kwargs)
38
+ self.storage = storage
39
+ self.storages_service = storages_service
40
+
41
+ def compose(self):
42
+ yield Label(self.storage.name, classes="storage_name")
43
+ yield Label(self.storage.credentials_name or "🚫", classes="credentials_name")
44
+ yield Button(
45
+ "❌",
46
+ name="delete_storage",
47
+ classes="storage_delete",
48
+ )
49
+
50
+ @on(Button.Click)
51
+ def on_button_click(self, _: Button.Click):
52
+ """
53
+ Handle delete button click events by showing a confirmation dialog for storage deletion.
54
+ Args:
55
+ _ (Button.Click): The button click event.
56
+ """
57
+ self.app.push_screen(
58
+ QuestionScreen(
59
+ f"Are you sure you want to delete {self.storage.credentials_name} {self.storage.name} storage?"
60
+ ),
61
+ callback=self.delete_callback, # type: ignore
62
+ )
63
+
64
+ def delete_callback(self, result: bool):
65
+ """
66
+ Callback function to handle the result of the confirmation screen.
67
+
68
+ Args:
69
+ result (bool): True if the user confirmed, False otherwise.
70
+ """
71
+ if not result:
72
+ return
73
+ self.storages_service.delete(self.storage.uuid)
74
+ self.post_message(ReloadStoragesRequest())
75
+
76
+
77
+ class StoragesListScreen(ModalScreen):
78
+ CSS_PATH = "styles.tcss"
79
+
80
+ MAIN_CONTAINER_ID = "StoragesListScreen"
81
+ SETTINGS_CONTAINER_ID = "settings"
82
+
83
+ storages_list = reactive([], recompose=True)
84
+
85
+ def __init__(
86
+ self,
87
+ credentials_service: CredentialsService = Provide[
88
+ DiContainer.credentials_service
89
+ ],
90
+ storages_service: StoragesService = Provide[DiContainer.storages_service],
91
+ *args,
92
+ **kwargs,
93
+ ):
94
+ super().__init__(*args, **kwargs)
95
+ self.storage_service = storages_service
96
+ self.credentials_service = credentials_service
97
+
98
+ def compose(self) -> ComposeResult:
99
+ with Container(id=self.MAIN_CONTAINER_ID):
100
+ yield Container(
101
+ Button(
102
+ "+Add new storage",
103
+ name=ControlsEnum.ADD_STORAGE.name,
104
+ classes="add_storage_button",
105
+ ),
106
+ id="right-top",
107
+ )
108
+ with VerticalScroll(id=self.SETTINGS_CONTAINER_ID):
109
+ with Horizontal():
110
+ yield Label("Storage Name", classes="storage_name")
111
+ yield Label("Credentials Name", classes="credentials_name")
112
+ yield Label("Delete", classes="storage_delete")
113
+ for storage in self.storages_list:
114
+ yield StorageRow(storage, self.storage_service)
115
+ with Horizontal(id="controls"):
116
+ yield Button(ControlsEnum.CANCEL.value, name=ControlsEnum.CANCEL.name)
117
+
118
+ def on_compose(self):
119
+ """
120
+ Initialize the screen by refreshing the credentials list when the screen is composed.
121
+ """
122
+ self.refresh_storages_list()
123
+
124
+ def refresh_storages_list(self):
125
+ """
126
+ Refresh the storages list by retrieving the latest storages from the storage service.
127
+ """
128
+ self.storages_list = self.storage_service.list()
129
+
130
+ @on(ReloadStoragesRequest)
131
+ def on_reload_storages_request(self, _: ReloadStoragesRequest):
132
+ """
133
+ Handle the reload storages request by refreshing the storages list.
134
+
135
+ Args:
136
+ _: ReloadStoragesRequest: The reload storages request message.
137
+ """
138
+ self.refresh_storages_list()
139
+
140
+ @on(Button.Click)
141
+ def on_control_button_click(self, event: Button.Click):
142
+ """
143
+ Handle click events for control buttons.
144
+
145
+ Dismisses the screen if the cancel button is clicked, or opens the provider credentials registration screen if
146
+ the add registration button is clicked.
147
+
148
+ Args:
149
+ event (Button.Click): The button click event.
150
+ """
151
+ if event.action == ControlsEnum.CANCEL.name:
152
+ self.dismiss()
153
+ if event.action == ControlsEnum.ADD_STORAGE.name:
154
+ self.app.push_screen(
155
+ StoragesRegistrationScreen(),
156
+ callback=self.create_storage_entry, # type: ignore
157
+ )
158
+
159
+ def create_storage_entry(self, storage: StorageEntry | None):
160
+ """
161
+ Create a new storage entry.
162
+
163
+ Creates a new storage entry using the provided data and refreshes the storage list.
164
+ """
165
+ if storage is None:
166
+ return
167
+
168
+ credentials = self.credentials_service.get(storage.credentials_uuid)
169
+ if not credentials:
170
+ self.notify("Credentials not found", severity="error")
171
+ return
172
+ self.storage_service.create(
173
+ Storage(
174
+ uuid=str(uuid.uuid4()),
175
+ name=storage.name,
176
+ credentials_id=credentials.id,
177
+ date_created=datetime.datetime.now(),
178
+ )
179
+ )
180
+ self.refresh_storages_list()
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from textual.message import Message
4
+
5
+
6
+ @dataclass
7
+ class ReloadStoragesRequest(Message):
8
+ pass