data-sourcerer 0.2.2__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 (44) hide show
  1. {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/METADATA +1 -1
  2. {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/RECORD +43 -29
  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 +7 -6
  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 +191 -85
  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/labeled_input.py +2 -2
  29. sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
  30. sourcerer/presentation/screens/storage_action_progress/main.py +7 -11
  31. sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  32. sourcerer/presentation/screens/storages_list/main.py +180 -0
  33. sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  34. sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
  35. sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
  36. sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  37. sourcerer/presentation/screens/storages_registration/main.py +100 -0
  38. sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
  39. sourcerer/presentation/settings.py +29 -16
  40. sourcerer/presentation/utils.py +9 -1
  41. sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
  42. {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/WHEEL +0 -0
  43. {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/entry_points.txt +0 -0
  44. {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -75,10 +75,10 @@ class LabeledInput(Container):
75
75
  Value: A dataclass containing the name and value of the input field.
76
76
 
77
77
  """
78
- input_area = self.query_one(".form_input", expect_type=Input)
78
+ input_area = self.query_one(".form_input")
79
79
  text = (
80
80
  input_area.document.text
81
81
  if isinstance(input_area, TextArea)
82
- else input_area.value
82
+ else input_area.value # type: ignore
83
83
  )
84
84
  return self.Value(name=self.key, value=text)
@@ -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
@@ -185,9 +185,9 @@ class StorageActionProgressScreen(ModalScreen):
185
185
  Sets the border title and starts the appropriate worker thread based on the action type
186
186
  (download, delete, or upload).
187
187
  """
188
- self.query_one("#StorageActionProgress").border_title = (
189
- f"{self.action.capitalize()} {len(self.keys)} files from {self.storage_name}"
190
- )
188
+ self.query_one(
189
+ "#StorageActionProgress"
190
+ ).border_title = f"{self.action.capitalize()} {len(self.keys)} files from {self.storage_name}"
191
191
 
192
192
  if self.action == "download":
193
193
  self.active_worker = self.run_worker(self.download_files, thread=True)
@@ -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)
@@ -447,9 +445,7 @@ class StorageActionProgressScreen(ModalScreen):
447
445
  self.files_has_been_processed = True
448
446
  self.active_executor = None
449
447
  try:
450
- await self.query_one(
451
- f"#progress_file_details_{key.uuid}", Label
452
- ).remove()
448
+ self.query_one(f"#progress_file_details_{key.uuid}", Label).remove()
453
449
  except NoMatches:
454
450
  self.log(f"Failed to remove progress details for {key.uuid}")
455
451
  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
@@ -0,0 +1,55 @@
1
+
2
+ Container {
3
+ height: auto;
4
+ }
5
+
6
+ Label {
7
+ padding-left: 1;
8
+ }
9
+
10
+ StoragesListScreen {
11
+ align: center middle;
12
+ content-align: center top;
13
+
14
+ & > #StoragesListScreen {
15
+ padding: 1 2 0 2;
16
+ margin: 0 0;
17
+ width: 60%;
18
+ height: 60%;
19
+ border: solid $secondary-background;
20
+
21
+ #right-top {
22
+ align: right top;
23
+
24
+ .add_storage_button {
25
+ color: $success-darken-2;
26
+ }
27
+
28
+ margin-bottom: 1;
29
+ }
30
+
31
+ ProviderCredentialsRow.active {
32
+ background: $primary-lighten-2;
33
+ }
34
+
35
+ ProviderCredentialsRow, Horizontal {
36
+ height: auto;
37
+ }
38
+
39
+ .storage_name {
40
+ width: 45%;
41
+ }
42
+
43
+ .credentials_name {
44
+ width: 35%;
45
+ }
46
+
47
+ .storage_delete {
48
+ width: 20%;
49
+ }
50
+
51
+ & > Select {
52
+ margin-bottom: 1;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,100 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from dependency_injector.wiring import Provide
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Container, Horizontal, VerticalScroll
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Label, Select
10
+
11
+ from sourcerer.infrastructure.access_credentials.services import CredentialsService
12
+ from sourcerer.presentation.di_container import DiContainer
13
+ from sourcerer.presentation.screens.shared.widgets.button import Button
14
+ from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
15
+
16
+
17
+ class ControlsEnum(Enum):
18
+ CANCEL = "Cancel"
19
+ CREATE = "Create"
20
+
21
+
22
+ @dataclass
23
+ class StorageEntry:
24
+ name: str
25
+ credentials_uuid: str
26
+
27
+
28
+ class StoragesRegistrationScreen(ModalScreen):
29
+ CSS_PATH = "styles.tcss"
30
+
31
+ MAIN_CONTAINER_ID = "StoragesRegistrationScreen"
32
+ SETTINGS_CONTAINER_ID = "settings"
33
+ CREDENTIALS_SELECTOR_ID = "credentials_selector"
34
+
35
+ PROVIDERS_NAME = "credentials"
36
+
37
+ def __init__(
38
+ self,
39
+ credentials_service: CredentialsService = Provide[
40
+ DiContainer.credentials_repository
41
+ ],
42
+ *args,
43
+ **kwargs,
44
+ ):
45
+ super().__init__(*args, **kwargs)
46
+ self.credentials = credentials_service.list()
47
+ self.auth_method = None
48
+
49
+ def compose(self) -> ComposeResult:
50
+ with Container(id=self.MAIN_CONTAINER_ID):
51
+ with VerticalScroll(id=self.SETTINGS_CONTAINER_ID):
52
+ yield LabeledInput(
53
+ "storage_name",
54
+ "Name:",
55
+ True,
56
+ multiline=False,
57
+ id="storage_name",
58
+ )
59
+ yield Label("* Credentials:", classes="form_label")
60
+ yield Select(
61
+ options=((item.name, item.uuid) for item in self.credentials),
62
+ name=self.PROVIDERS_NAME,
63
+ id=self.CREDENTIALS_SELECTOR_ID,
64
+ )
65
+ with Horizontal(id="controls"):
66
+ yield Button(ControlsEnum.CANCEL.value, name=ControlsEnum.CANCEL.name)
67
+ yield Button(ControlsEnum.CREATE.value, name=ControlsEnum.CREATE.name)
68
+
69
+ @on(Button.Click)
70
+ def on_control_button_click(self, event: Button.Click):
71
+ """
72
+ Handle click events for control buttons on the registration screen.
73
+
74
+ Depending on the action associated with the button click event, either dismiss
75
+ the screen or gather authentication fields and then dismiss the screen with
76
+ the collected data.
77
+
78
+ Args:
79
+ event (Button.Click): The click event containing the action to be performed.
80
+
81
+ Flow:
82
+ 1. Check if the event.action is ControlsEnum.cancel.name. If true, dismiss the screen.
83
+ 2. If event.action is ControlsEnum.create.name, gather authentication fields. Dismiss the screen with the
84
+ collected authentication fields.
85
+ """
86
+ if event.action == ControlsEnum.CANCEL.name:
87
+ self.dismiss()
88
+ elif event.action == ControlsEnum.CREATE.name:
89
+ storage_name = self.query_one("#storage_name", LabeledInput).get().value
90
+ if not storage_name:
91
+ self.notify("Storage name is required", severity="error")
92
+ return
93
+ credentials_uuid = self.query_one(
94
+ f"#{self.CREDENTIALS_SELECTOR_ID}", Select
95
+ ).value
96
+ if not credentials_uuid or credentials_uuid == Select.BLANK:
97
+ self.notify("Credentials are required", severity="error")
98
+ return
99
+
100
+ self.dismiss(StorageEntry(storage_name, credentials_uuid)) # type: ignore