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.
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/METADATA +1 -1
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/RECORD +42 -28
- sourcerer/__init__.py +1 -1
- sourcerer/domain/access_credentials/entities.py +3 -1
- sourcerer/domain/access_credentials/repositories.py +1 -1
- sourcerer/domain/storage/__init__.py +0 -0
- sourcerer/domain/storage/entities.py +27 -0
- sourcerer/domain/storage/repositories.py +31 -0
- sourcerer/infrastructure/access_credentials/repositories.py +3 -2
- sourcerer/infrastructure/access_credentials/services.py +9 -25
- sourcerer/infrastructure/db/models.py +33 -2
- sourcerer/infrastructure/storage/__init__.py +0 -0
- sourcerer/infrastructure/storage/repositories.py +72 -0
- sourcerer/infrastructure/storage/services.py +37 -0
- sourcerer/infrastructure/storage_provider/services/gcp.py +1 -1
- sourcerer/infrastructure/utils.py +2 -1
- sourcerer/presentation/di_container.py +15 -0
- sourcerer/presentation/screens/file_system_finder/main.py +5 -4
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
- sourcerer/presentation/screens/main/main.py +63 -8
- sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
- sourcerer/presentation/screens/main/widgets/storage_content.py +187 -77
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
- sourcerer/presentation/screens/preview_content/main.py +15 -3
- sourcerer/presentation/screens/provider_creds_list/main.py +29 -8
- sourcerer/presentation/screens/provider_creds_registration/main.py +7 -4
- sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
- sourcerer/presentation/screens/storages_list/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/main.py +180 -0
- sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
- sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
- sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
- sourcerer/presentation/screens/storages_registration/main.py +100 -0
- sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
- sourcerer/presentation/settings.py +29 -16
- sourcerer/presentation/utils.py +9 -1
- sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/entry_points.txt +0 -0
- {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
|
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.
|
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: $
|
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(
|
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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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
|
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__(
|
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__(
|
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
|
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__(
|
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 =
|
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(
|
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,
|
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(
|
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}: {
|
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}: {
|
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)
|
File without changes
|
@@ -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()
|
File without changes
|