data-sourcerer 0.2.1__py3-none-any.whl → 0.2.3__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.1.dist-info → data_sourcerer-0.2.3.dist-info}/METADATA +4 -1
- {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/RECORD +39 -35
- sourcerer/__init__.py +1 -1
- sourcerer/domain/access_credentials/entities.py +4 -7
- sourcerer/domain/access_credentials/repositories.py +12 -0
- sourcerer/domain/access_credentials/services.py +2 -1
- sourcerer/domain/file_system/entities.py +5 -7
- sourcerer/domain/storage_provider/entities.py +7 -11
- sourcerer/infrastructure/access_credentials/registry.py +1 -1
- sourcerer/infrastructure/access_credentials/repositories.py +17 -0
- sourcerer/infrastructure/access_credentials/services.py +30 -14
- sourcerer/infrastructure/db/config.py +1 -1
- sourcerer/infrastructure/storage_provider/services/azure.py +5 -3
- sourcerer/infrastructure/utils.py +115 -1
- sourcerer/presentation/screens/critical_error/main.py +4 -4
- sourcerer/presentation/screens/file_system_finder/main.py +7 -3
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +1 -1
- sourcerer/presentation/screens/main/main.py +37 -6
- sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py +8 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +3 -3
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +3 -1
- sourcerer/presentation/screens/main/widgets/storage_content.py +43 -18
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +38 -6
- sourcerer/presentation/screens/preview_content/main.py +5 -5
- sourcerer/presentation/screens/provider_creds_list/main.py +44 -0
- sourcerer/presentation/screens/provider_creds_list/messages/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py +8 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +5 -5
- sourcerer/presentation/screens/provider_creds_registration/main.py +6 -2
- sourcerer/presentation/screens/question/main.py +4 -3
- sourcerer/presentation/screens/question/styles.tcss +2 -8
- sourcerer/presentation/screens/shared/widgets/button.py +1 -1
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +5 -1
- sourcerer/presentation/screens/shared/widgets/loader.py +31 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +29 -29
- sourcerer/utils.py +1 -1
- {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -29,7 +29,7 @@ class CriticalErrorScreen(ModalScreen[bool]):
|
|
29
29
|
yield RichLog(highlight=True, markup=True)
|
30
30
|
with Horizontal():
|
31
31
|
yield Button("Report", name="report")
|
32
|
-
yield Button("
|
32
|
+
yield Button("Exit", name="exit")
|
33
33
|
|
34
34
|
def on_mount(self) -> None:
|
35
35
|
"""
|
@@ -50,8 +50,8 @@ class CriticalErrorScreen(ModalScreen[bool]):
|
|
50
50
|
"""
|
51
51
|
if event.action == "report":
|
52
52
|
webbrowser.open(self._build_github_issue_url(), new=0, autoraise=True)
|
53
|
-
elif event.action == "
|
54
|
-
self.
|
53
|
+
elif event.action == "exit":
|
54
|
+
self.app.exit()
|
55
55
|
|
56
56
|
def _build_github_issue_url(self) -> str:
|
57
57
|
"""
|
@@ -67,7 +67,7 @@ Steps to Reproduce:
|
|
67
67
|
…
|
68
68
|
…
|
69
69
|
Traceback:
|
70
|
-
```{self.traceback}```
|
70
|
+
```{self.traceback[-1000:]}```
|
71
71
|
"""
|
72
72
|
|
73
73
|
return (
|
@@ -155,11 +155,15 @@ class FileSystemNavigationModal(ModalScreen):
|
|
155
155
|
if not event.path:
|
156
156
|
return
|
157
157
|
try:
|
158
|
-
active_path_label
|
158
|
+
active_path_label = self.query_one("#active-path", Static)
|
159
159
|
except NoMatches:
|
160
160
|
return
|
161
|
-
label = str(
|
162
|
-
|
161
|
+
label = str(
|
162
|
+
event.path.relative_to( # ty: ignore[possibly-unbound-attribute]
|
163
|
+
self.work_dir
|
164
|
+
)
|
165
|
+
)
|
166
|
+
if event.path.is_dir(): # ty: ignore[possibly-unbound-attribute]
|
163
167
|
label += "/"
|
164
168
|
active_path_label.update(label)
|
165
169
|
self.active_path = event.path
|
@@ -474,7 +474,7 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
474
474
|
NoMatches: Silently caught internally if no widget matches the UUID.
|
475
475
|
"""
|
476
476
|
try:
|
477
|
-
return self.query_one(f"#{container_uuid}")
|
477
|
+
return self.query_one(f"#{container_uuid}", PathListingContainer)
|
478
478
|
except NoMatches:
|
479
479
|
return None
|
480
480
|
|
@@ -24,6 +24,9 @@ from sourcerer.presentation.screens.main.messages.download_request import (
|
|
24
24
|
DownloadRequest,
|
25
25
|
)
|
26
26
|
from sourcerer.presentation.screens.main.messages.preview_request import PreviewRequest
|
27
|
+
from sourcerer.presentation.screens.main.messages.refresh_storages_list_request import (
|
28
|
+
RefreshStoragesListRequest,
|
29
|
+
)
|
27
30
|
from sourcerer.presentation.screens.main.messages.select_storage_item import (
|
28
31
|
SelectStorageItem,
|
29
32
|
)
|
@@ -36,6 +39,7 @@ from sourcerer.presentation.screens.main.mixins.resize_containers_watcher_mixin
|
|
36
39
|
)
|
37
40
|
from sourcerer.presentation.screens.main.widgets.resizing_rule import ResizingRule
|
38
41
|
from sourcerer.presentation.screens.main.widgets.storage_content import (
|
42
|
+
FileItem,
|
39
43
|
StorageContentContainer,
|
40
44
|
)
|
41
45
|
from sourcerer.presentation.screens.main.widgets.storage_list_sidebar import (
|
@@ -171,11 +175,19 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
171
175
|
if not access_credentials:
|
172
176
|
return
|
173
177
|
|
178
|
+
self.storage_list_sidebar.is_loading = True
|
179
|
+
|
174
180
|
with ThreadPoolExecutor(
|
175
181
|
max_workers=MAX_PARALLEL_STORAGE_LIST_OPERATIONS
|
176
182
|
) as executor:
|
177
|
-
|
183
|
+
futures = [
|
178
184
|
executor.submit(self._load_storages, credentials)
|
185
|
+
for credentials in access_credentials
|
186
|
+
]
|
187
|
+
|
188
|
+
for future in futures:
|
189
|
+
future.result()
|
190
|
+
self.storage_list_sidebar.is_loading = False
|
179
191
|
|
180
192
|
@on(SelectStorageItem)
|
181
193
|
def on_select_storage_item(self, event: SelectStorageItem):
|
@@ -236,7 +248,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
236
248
|
),
|
237
249
|
path=event.path,
|
238
250
|
keys=[
|
239
|
-
DownloadKey(display_name=key, uuid=generate_uuid(), path=key)
|
251
|
+
DownloadKey(display_name=key, uuid=generate_uuid(), path=key)
|
240
252
|
for key in event.keys
|
241
253
|
],
|
242
254
|
action="download",
|
@@ -266,7 +278,8 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
266
278
|
),
|
267
279
|
path=event.path,
|
268
280
|
keys=[
|
269
|
-
DeleteKey(display_name=key, uuid=generate_uuid(), path=key)
|
281
|
+
DeleteKey(display_name=key, uuid=generate_uuid(), path=key)
|
282
|
+
for key in event.keys
|
270
283
|
],
|
271
284
|
action="delete",
|
272
285
|
),
|
@@ -305,8 +318,8 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
305
318
|
event (UncheckFilesRequest): The uncheck request event containing file keys
|
306
319
|
"""
|
307
320
|
for key_uuid in event.keys:
|
308
|
-
file_item = self.query_one(f"#{key_uuid}")
|
309
|
-
file_item.uncheck()
|
321
|
+
file_item = self.query_one(f"#{key_uuid}", FileItem)
|
322
|
+
file_item.uncheck()
|
310
323
|
self.storage_content.selected_files = set()
|
311
324
|
self.storage_content.selected_files_n = 0
|
312
325
|
|
@@ -334,6 +347,22 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
334
347
|
)
|
335
348
|
)
|
336
349
|
|
350
|
+
@on(RefreshStoragesListRequest)
|
351
|
+
def on_refresh_storages_list_request(self, _: RefreshStoragesListRequest):
|
352
|
+
"""
|
353
|
+
Handles requests to refresh the storage list.
|
354
|
+
|
355
|
+
This method is triggered when a RefreshStoragesListRequest event is received.
|
356
|
+
It refreshes the storage list if the storage list sidebar is not currently
|
357
|
+
loading.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
_ (RefreshStoragesListRequest): The refresh request event
|
361
|
+
"""
|
362
|
+
if self.storage_list_sidebar.is_loading:
|
363
|
+
return
|
364
|
+
self.refresh_storages()
|
365
|
+
|
337
366
|
def reset_storage_content(self):
|
338
367
|
"""
|
339
368
|
Resets the storage content attributes to their default state.
|
@@ -388,7 +417,9 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
388
417
|
return
|
389
418
|
params = {"storage": storage_name, "path": path or "", "prefix": prefix or ""}
|
390
419
|
try:
|
391
|
-
self.storage_content.storage_content = provider_service.list_storage_items(
|
420
|
+
self.storage_content.storage_content = provider_service.list_storage_items(
|
421
|
+
**params
|
422
|
+
)
|
392
423
|
except ListStorageItemsError as e:
|
393
424
|
self.notify_error(f"""Could not extract storage content \n{str(e)}""")
|
394
425
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from rich.text import Text
|
2
|
-
|
2
|
+
|
3
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
3
4
|
|
4
5
|
|
5
6
|
def hex_to_rgb(hex_color: str):
|
@@ -32,8 +33,7 @@ def generate_gradient_text(text: str, start_color: str, end_color: str) -> Text:
|
|
32
33
|
return gradient_text
|
33
34
|
|
34
35
|
|
35
|
-
class GradientWidget(
|
36
|
-
|
36
|
+
class GradientWidget(Button):
|
37
37
|
def on_mount(self):
|
38
38
|
"""
|
39
39
|
Generate a gradient text effect for the widget's content.
|
@@ -19,7 +19,9 @@ class MoveEvent:
|
|
19
19
|
|
20
20
|
class ResizingRule(Rule, can_focus=True):
|
21
21
|
dragging: reactive[bool] = reactive(False)
|
22
|
-
position: reactive[MoveEvent | None] = reactive(
|
22
|
+
position: reactive[MoveEvent | None] = reactive( # ty: ignore[invalid-assignment]
|
23
|
+
None
|
24
|
+
)
|
23
25
|
|
24
26
|
def __init__(self, prev_component_id, next_component_id, *args, **kwargs):
|
25
27
|
super().__init__(*args, **kwargs)
|
@@ -170,7 +170,7 @@ class FolderItem(Horizontal):
|
|
170
170
|
|
171
171
|
def compose(self):
|
172
172
|
"""Compose the folder item layout with folder name and icon."""
|
173
|
-
yield Label(f"{DIRECTORY_ICON}{self.folder.key}")
|
173
|
+
yield Label(f"{DIRECTORY_ICON}{self.folder.key}", markup=False)
|
174
174
|
|
175
175
|
def on_click(self, _: events.Click) -> None:
|
176
176
|
"""Handle click events to navigate into the folder."""
|
@@ -206,6 +206,9 @@ class FileItem(Horizontal):
|
|
206
206
|
background: $secondary;
|
207
207
|
color: $panel;
|
208
208
|
}
|
209
|
+
.file_size {
|
210
|
+
color: $primary
|
211
|
+
}
|
209
212
|
Checkbox {
|
210
213
|
border: none;
|
211
214
|
padding: 0 0;
|
@@ -255,11 +258,13 @@ class FileItem(Horizontal):
|
|
255
258
|
|
256
259
|
def compose(self):
|
257
260
|
yield Checkbox()
|
258
|
-
yield FileMetaLabel(f"{FILE_ICON} {self.file.key}", classes="file_name")
|
259
261
|
yield FileMetaLabel(
|
260
|
-
f"
|
262
|
+
f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
|
263
|
+
)
|
264
|
+
yield FileMetaLabel(f"{self.file.size}", classes="file_size", markup=False)
|
265
|
+
yield FileMetaLabel(
|
266
|
+
str(self.file.date_modified), classes="file_date", markup=False
|
261
267
|
)
|
262
|
-
yield FileMetaLabel(str(self.file.date_modified), classes="file_date")
|
263
268
|
if self.file.is_text:
|
264
269
|
yield Button(f"{PREVIEW_ICON}", name="preview", classes="download")
|
265
270
|
|
@@ -318,11 +323,21 @@ class StorageContentContainer(Vertical):
|
|
318
323
|
selected_files_n: Number of selected files
|
319
324
|
"""
|
320
325
|
|
321
|
-
storage: reactive[str | None] = reactive(
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
+
storage: reactive[str | None] = reactive( # ty: ignore[invalid-assignment]
|
327
|
+
None, recompose=True
|
328
|
+
)
|
329
|
+
path: reactive[str | None] = reactive( # ty: ignore[invalid-assignment]
|
330
|
+
None, recompose=False
|
331
|
+
)
|
332
|
+
search_prefix: reactive[str | None] = reactive( # ty: ignore[invalid-assignment]
|
333
|
+
None, recompose=False
|
334
|
+
)
|
335
|
+
access_credentials_uuid: reactive[ # ty: ignore[invalid-assignment]
|
336
|
+
str | None
|
337
|
+
] = reactive("", recompose=False)
|
338
|
+
storage_content: reactive[ # ty: ignore[invalid-assignment]
|
339
|
+
StorageContent | None
|
340
|
+
] = reactive(None, recompose=True)
|
326
341
|
selected_files: reactive[set] = reactive(set(), recompose=False)
|
327
342
|
selected_files_n: reactive[int] = reactive(0, recompose=False)
|
328
343
|
|
@@ -361,6 +376,15 @@ class StorageContentContainer(Vertical):
|
|
361
376
|
height: auto;
|
362
377
|
border-bottom: solid $secondary;
|
363
378
|
margin: 1 0;
|
379
|
+
|
380
|
+
PathSelector {
|
381
|
+
&.primary_color {
|
382
|
+
color: $primary;
|
383
|
+
}
|
384
|
+
&.secondary_color {
|
385
|
+
color: $secondary;
|
386
|
+
}
|
387
|
+
}
|
364
388
|
}
|
365
389
|
.storage_path_item {
|
366
390
|
padding: 0 0;
|
@@ -462,13 +486,14 @@ class StorageContentContainer(Vertical):
|
|
462
486
|
with Horizontal():
|
463
487
|
yield Label("Current Path: ", classes="storage_path_item")
|
464
488
|
for index, breadcrumb in enumerate(breadcrumbs):
|
465
|
-
|
489
|
+
color_classes = "primary" if index == 0 else "secondary"
|
466
490
|
yield PathSelector(
|
467
|
-
renderable=
|
491
|
+
renderable=breadcrumb,
|
468
492
|
storage=self.storage,
|
469
493
|
path="/".join(breadcrumbs[1 : index + 1]),
|
470
494
|
access_credentials_uuid=self.access_credentials_uuid,
|
471
|
-
classes="storage_path_item",
|
495
|
+
classes=f"storage_path_item {color_classes}_color",
|
496
|
+
markup=False,
|
472
497
|
)
|
473
498
|
yield Label("/", classes="storage_path_item")
|
474
499
|
with Horizontal():
|
@@ -630,15 +655,15 @@ class StorageContentContainer(Vertical):
|
|
630
655
|
)
|
631
656
|
)
|
632
657
|
elif action_type == ActionType.DELETE:
|
633
|
-
self.post_message(DeleteRequest(**params))
|
658
|
+
self.post_message(DeleteRequest(**params)) # ty: ignore[missing-argument]
|
634
659
|
elif action_type == ActionType.DOWNLOAD:
|
635
|
-
self.post_message(DownloadRequest(**params))
|
660
|
+
self.post_message(DownloadRequest(**params)) # ty: ignore[missing-argument]
|
636
661
|
elif action_type == ActionType.UNCHECK_ALL:
|
637
662
|
self.post_message(
|
638
663
|
UncheckFilesRequest(
|
639
664
|
keys=[
|
640
665
|
item.uuid
|
641
|
-
for item in self.storage_content
|
666
|
+
for item in getattr(self.storage_content, "files", [])
|
642
667
|
if item.key in self.selected_files
|
643
668
|
]
|
644
669
|
)
|
@@ -654,14 +679,14 @@ class StorageContentContainer(Vertical):
|
|
654
679
|
"""
|
655
680
|
try:
|
656
681
|
selected_actions = self.query_one("#selected_actions")
|
657
|
-
counter = self.query_one("#selected_n")
|
682
|
+
counter = self.query_one("#selected_n", Label)
|
658
683
|
except NoMatches:
|
659
684
|
return
|
660
685
|
|
661
686
|
if self.selected_files_n > 0:
|
662
687
|
if not selected_actions.has_class("-visible"):
|
663
688
|
selected_actions.add_class("-visible")
|
664
|
-
counter.update(f"❌Selected: {self.selected_files_n}")
|
689
|
+
counter.update(f"❌Selected: {self.selected_files_n}")
|
665
690
|
else:
|
666
691
|
selected_actions.remove_class("-visible")
|
667
692
|
self.query_one("#content").remove_class("-visible")
|
@@ -682,7 +707,7 @@ class StorageContentContainer(Vertical):
|
|
682
707
|
self.search_prefix = value
|
683
708
|
self.post_message(
|
684
709
|
SelectStorageItem(
|
685
|
-
self.storage,
|
710
|
+
self.storage,
|
686
711
|
self.path,
|
687
712
|
self.access_credentials_uuid,
|
688
713
|
value,
|
@@ -8,7 +8,7 @@ and selection of storage items.
|
|
8
8
|
from collections import namedtuple
|
9
9
|
from itertools import groupby
|
10
10
|
|
11
|
-
from textual import events
|
11
|
+
from textual import events, on
|
12
12
|
from textual.app import ComposeResult
|
13
13
|
from textual.containers import Container, Horizontal, VerticalScroll
|
14
14
|
from textual.reactive import reactive
|
@@ -16,10 +16,15 @@ from textual.widgets import Label, Rule
|
|
16
16
|
|
17
17
|
from sourcerer.domain.shared.entities import StorageProvider
|
18
18
|
from sourcerer.domain.storage_provider.entities import Storage
|
19
|
+
from sourcerer.presentation.screens.main.messages.refresh_storages_list_request import (
|
20
|
+
RefreshStoragesListRequest,
|
21
|
+
)
|
19
22
|
from sourcerer.presentation.screens.main.messages.select_storage_item import (
|
20
23
|
SelectStorageItem,
|
21
24
|
)
|
22
25
|
from sourcerer.presentation.screens.main.widgets.gradient import GradientWidget
|
26
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
27
|
+
from sourcerer.presentation.screens.shared.widgets.loader import Loader
|
23
28
|
|
24
29
|
STORAGE_ICONS = {
|
25
30
|
StorageProvider.S3: "🟠",
|
@@ -83,8 +88,11 @@ class StorageListSidebar(VerticalScroll):
|
|
83
88
|
storages: Dictionary mapping provider types to lists of storage instances
|
84
89
|
"""
|
85
90
|
|
91
|
+
is_loading: reactive[bool] = reactive(False, recompose=True)
|
86
92
|
storages: reactive[dict[str, list[Storage]]] = reactive({})
|
87
|
-
last_update_timestamp: reactive[float] = reactive(
|
93
|
+
last_update_timestamp: reactive[float] = reactive( # ty: ignore[invalid-assignment]
|
94
|
+
0.0, recompose=True
|
95
|
+
)
|
88
96
|
|
89
97
|
DEFAULT_CSS = """
|
90
98
|
StorageListSidebar {
|
@@ -96,14 +104,14 @@ class StorageListSidebar(VerticalScroll):
|
|
96
104
|
#rule-left {
|
97
105
|
width: 1;
|
98
106
|
}
|
99
|
-
|
107
|
+
|
100
108
|
Horizontal {
|
101
109
|
height: auto;
|
102
110
|
}
|
103
111
|
Rule.-horizontal {
|
104
112
|
height: 1;
|
105
113
|
margin: 0 0;
|
106
|
-
|
114
|
+
|
107
115
|
}
|
108
116
|
.storage-letter {
|
109
117
|
color: $secondary;
|
@@ -112,11 +120,29 @@ class StorageListSidebar(VerticalScroll):
|
|
112
120
|
|
113
121
|
}
|
114
122
|
}
|
123
|
+
#header {
|
124
|
+
width: 100%;
|
125
|
+
|
126
|
+
GradientWidget {
|
127
|
+
width: auto;
|
128
|
+
}
|
129
|
+
|
130
|
+
Loader {
|
131
|
+
width: 5%;
|
132
|
+
}
|
133
|
+
}
|
115
134
|
"""
|
116
135
|
|
117
136
|
def compose(self) -> ComposeResult:
|
118
|
-
with
|
119
|
-
|
137
|
+
with Horizontal(id="header"):
|
138
|
+
if self.is_loading:
|
139
|
+
yield Loader()
|
140
|
+
yield GradientWidget(
|
141
|
+
" SOURCERER" if self.is_loading else "🧙SOURCERER",
|
142
|
+
id="left-middle",
|
143
|
+
name="header_click",
|
144
|
+
)
|
145
|
+
|
120
146
|
StorageData = namedtuple("Storage", ["access_credentials_uuid", "storage"])
|
121
147
|
storages = [
|
122
148
|
StorageData(access_credentials_uuid, storage)
|
@@ -143,3 +169,9 @@ class StorageListSidebar(VerticalScroll):
|
|
143
169
|
storage_name=item.storage.storage,
|
144
170
|
access_credentials_uuid=item.access_credentials_uuid,
|
145
171
|
)
|
172
|
+
|
173
|
+
@on(Button.Click)
|
174
|
+
def on_button_click(self, event: Button.Click) -> None:
|
175
|
+
"""Handle button click events to refresh the storage list."""
|
176
|
+
if event.action == "header_click":
|
177
|
+
self.post_message(RefreshStoragesListRequest())
|
@@ -55,11 +55,11 @@ class PreviewContentScreen(ModalScreen):
|
|
55
55
|
text_log = self.query_one(RichLog)
|
56
56
|
|
57
57
|
extension = Path(self.key).suffix
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
|
59
|
+
lexer = (
|
60
|
+
"json" if extension == ".tfstate" else Syntax.guess_lexer(self.key, content)
|
61
|
+
)
|
62
|
+
|
63
63
|
content = Syntax(content, lexer, line_numbers=True, theme="ansi_dark")
|
64
64
|
text_log.write(content)
|
65
65
|
|
@@ -11,10 +11,14 @@ from textual.widgets import Checkbox, Label
|
|
11
11
|
|
12
12
|
from sourcerer.domain.access_credentials.entities import Credentials
|
13
13
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
14
|
+
from sourcerer.presentation.screens.provider_creds_list.messages.reload_credentials_request import (
|
15
|
+
ReloadCredentialsRequest,
|
16
|
+
)
|
14
17
|
from sourcerer.presentation.screens.provider_creds_registration.main import (
|
15
18
|
ProviderCredentialsEntry,
|
16
19
|
ProviderCredsRegistrationScreen,
|
17
20
|
)
|
21
|
+
from sourcerer.presentation.screens.question.main import QuestionScreen
|
18
22
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
19
23
|
|
20
24
|
|
@@ -39,6 +43,7 @@ class ProviderCredentialsRow(Horizontal):
|
|
39
43
|
yield Label(self.row.name, classes="credentials_name")
|
40
44
|
yield Label(self.row.provider, classes="credentials_provider")
|
41
45
|
yield Label(self.row.credentials_type, classes="credentials_auth_method")
|
46
|
+
yield Button("❌", name="delete", classes="credentials_auth_delete")
|
42
47
|
|
43
48
|
def on_mouse_move(self, _) -> None:
|
44
49
|
"""Change background color when hovered."""
|
@@ -59,6 +64,34 @@ class ProviderCredentialsRow(Horizontal):
|
|
59
64
|
self.row.active = event.value
|
60
65
|
self.post_message(self.ChangeActiveStatus(self.row.uuid, self.row.active))
|
61
66
|
|
67
|
+
@on(Button.Click)
|
68
|
+
def on_delete_button_click(self, _: Button.Click):
|
69
|
+
"""
|
70
|
+
Handle delete button click events by deleting the associated credentials using the credentials service.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
_ (Button.Click): The button click event.
|
74
|
+
"""
|
75
|
+
self.app.push_screen(
|
76
|
+
QuestionScreen(
|
77
|
+
f"Are you sure you want to delete {self.row.provider} {self.row.name} credentials?"
|
78
|
+
),
|
79
|
+
callback=self.delete_callback, # type: ignore
|
80
|
+
)
|
81
|
+
|
82
|
+
def delete_callback(self, result: bool):
|
83
|
+
"""
|
84
|
+
Callback function to handle the result of the confirmation screen.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
result (bool): True if the user confirmed, False otherwise.
|
88
|
+
"""
|
89
|
+
if not result:
|
90
|
+
return
|
91
|
+
credentials_service = CredentialsService()
|
92
|
+
credentials_service.delete(self.row.uuid)
|
93
|
+
self.post_message(ReloadCredentialsRequest())
|
94
|
+
|
62
95
|
|
63
96
|
class ProviderCredsListScreen(ModalScreen):
|
64
97
|
CSS_PATH = "styles.tcss"
|
@@ -94,6 +127,7 @@ class ProviderCredsListScreen(ModalScreen):
|
|
94
127
|
yield Label("Name", classes="credentials_name")
|
95
128
|
yield Label("Provider", classes="credentials_provider")
|
96
129
|
yield Label("Auth method", classes="credentials_auth_method")
|
130
|
+
yield Label("Delete", classes="credentials_auth_delete")
|
97
131
|
for row in self.credentials_list:
|
98
132
|
yield ProviderCredentialsRow(row, classes="credentials_row")
|
99
133
|
with Horizontal(id="controls"):
|
@@ -162,3 +196,13 @@ class ProviderCredsListScreen(ModalScreen):
|
|
162
196
|
else:
|
163
197
|
self.credentials_service.deactivate(event.uuid)
|
164
198
|
self.refresh_credentials_list()
|
199
|
+
|
200
|
+
@on(ReloadCredentialsRequest)
|
201
|
+
def on_reload_credentials_request(self, _: ReloadCredentialsRequest):
|
202
|
+
"""
|
203
|
+
Handle reload credentials request events by refreshing the credentials list.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
_ (ReloadCredentialsRequest): The reload credentials request event.
|
207
|
+
"""
|
208
|
+
self.refresh_credentials_list()
|
File without changes
|
@@ -43,23 +43,23 @@ ProviderCredsListScreen {
|
|
43
43
|
}
|
44
44
|
|
45
45
|
.credentials_name {
|
46
|
-
width:
|
46
|
+
width: 20%;
|
47
47
|
}
|
48
48
|
|
49
49
|
.credentials_provider {
|
50
|
-
width:
|
50
|
+
width: 25%;
|
51
51
|
}
|
52
52
|
|
53
53
|
.credentials_auth_method {
|
54
54
|
width: 30%;
|
55
55
|
}
|
56
56
|
|
57
|
+
.credentials_auth_delete {
|
58
|
+
width: 10%;
|
59
|
+
}
|
57
60
|
|
58
61
|
& > Select {
|
59
62
|
margin-bottom: 1;
|
60
63
|
}
|
61
64
|
}
|
62
65
|
}
|
63
|
-
|
64
|
-
|
65
|
-
|
@@ -15,6 +15,7 @@ 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.utils import generate_unique_name
|
18
19
|
from sourcerer.presentation.di_container import DiContainer
|
19
20
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
20
21
|
from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
|
@@ -155,7 +156,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
|
|
155
156
|
if event.control.name == self.PROVIDERS_NAME:
|
156
157
|
await self._process_selected_provider(str(event.value))
|
157
158
|
elif event.control.name == self.AUTH_METHODS_NAME:
|
158
|
-
provider = self.query_one(f"#{self.PROVIDER_SELECTOR_ID}").selection
|
159
|
+
provider = self.query_one(f"#{self.PROVIDER_SELECTOR_ID}", Select).selection
|
159
160
|
await self._process_selected_provider_auth_method(
|
160
161
|
provider, str(event.value)
|
161
162
|
)
|
@@ -215,7 +216,10 @@ class ProviderCredsRegistrationScreen(ModalScreen):
|
|
215
216
|
if isinstance(input_field, LabeledInput) and input_field.get().value
|
216
217
|
}
|
217
218
|
|
218
|
-
auth_name =
|
219
|
+
auth_name = (
|
220
|
+
self.query_one("#auth_name", LabeledInput).get().value
|
221
|
+
or generate_unique_name()
|
222
|
+
)
|
219
223
|
|
220
224
|
return ProviderCredentialsEntry(
|
221
225
|
name=auth_name,
|
@@ -1,8 +1,9 @@
|
|
1
|
+
from rich.text import Text
|
1
2
|
from textual import on
|
2
3
|
from textual.app import ComposeResult
|
3
4
|
from textual.containers import Container, Horizontal
|
4
5
|
from textual.screen import ModalScreen
|
5
|
-
from textual.widgets import
|
6
|
+
from textual.widgets import Static
|
6
7
|
|
7
8
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
8
9
|
|
@@ -18,7 +19,7 @@ class QuestionScreen(ModalScreen[bool]):
|
|
18
19
|
|
19
20
|
def compose(self) -> ComposeResult:
|
20
21
|
with Container():
|
21
|
-
yield
|
22
|
+
yield Static(Text(self.question, style="bold"), id="question_label")
|
22
23
|
with Horizontal():
|
23
24
|
yield Button("Yes", name="yes")
|
24
25
|
yield Button("No", name="no")
|
@@ -28,4 +29,4 @@ class QuestionScreen(ModalScreen[bool]):
|
|
28
29
|
"""
|
29
30
|
Handle button click events.
|
30
31
|
"""
|
31
|
-
self.dismiss(event.action) # type: ignore
|
32
|
+
self.dismiss(event.action == "yes") # type: ignore
|
@@ -2,8 +2,6 @@ QuestionScreen {
|
|
2
2
|
align: center middle;
|
3
3
|
content-align: center top;
|
4
4
|
|
5
|
-
|
6
|
-
|
7
5
|
& > Container {
|
8
6
|
padding: 1 2 0 2;
|
9
7
|
margin: 0 0;
|
@@ -11,11 +9,10 @@ QuestionScreen {
|
|
11
9
|
height: 8;
|
12
10
|
border: solid $warning-darken-1;
|
13
11
|
|
14
|
-
& >
|
12
|
+
& > Static {
|
15
13
|
margin: 1;
|
16
14
|
text-align: center;
|
17
|
-
|
18
|
-
width: auto;
|
15
|
+
text-wrap: wrap;
|
19
16
|
}
|
20
17
|
|
21
18
|
& > Horizontal {
|
@@ -28,6 +25,3 @@ QuestionScreen {
|
|
28
25
|
}
|
29
26
|
|
30
27
|
}
|
31
|
-
|
32
|
-
|
33
|
-
|
@@ -41,7 +41,7 @@ class Button(Label):
|
|
41
41
|
ValueError: If 'name' is not included in the keyword arguments.
|
42
42
|
"""
|
43
43
|
super().__init__(*args, **kwargs)
|
44
|
-
if "name" not in kwargs:
|
44
|
+
if "name" not in kwargs or not kwargs["name"]:
|
45
45
|
raise ValueError("Name is required attribute for button")
|
46
46
|
|
47
47
|
def on_click(self, _: events.Click) -> None:
|