data-sourcerer 0.2.0__py3-none-any.whl → 0.2.2__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.0.dist-info → data_sourcerer-0.2.2.dist-info}/METADATA +23 -21
- {data_sourcerer-0.2.0.dist-info → data_sourcerer-0.2.2.dist-info}/RECORD +39 -36
- {data_sourcerer-0.2.0.dist-info → data_sourcerer-0.2.2.dist-info}/WHEEL +1 -2
- 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 +47 -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 +6 -2
- sourcerer/presentation/screens/shared/widgets/loader.py +31 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +28 -26
- sourcerer/utils.py +1 -1
- data_sourcerer-0.2.0.dist-info/top_level.txt +0 -1
- {data_sourcerer-0.2.0.dist-info → data_sourcerer-0.2.2.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.2.0.dist-info → data_sourcerer-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -4,8 +4,8 @@ Utility functions for the Sourcerer application.
|
|
4
4
|
This module provides various utility functions used throughout the application,
|
5
5
|
including UUID generation, MIME type detection, and file type checking.
|
6
6
|
"""
|
7
|
-
|
8
7
|
import mimetypes
|
8
|
+
import secrets
|
9
9
|
import uuid
|
10
10
|
from pathlib import Path
|
11
11
|
|
@@ -102,3 +102,117 @@ class Singleton(type):
|
|
102
102
|
if cls not in cls._instances:
|
103
103
|
cls._instances[cls] = super().__call__(*args, **kwargs)
|
104
104
|
return cls._instances[cls]
|
105
|
+
|
106
|
+
|
107
|
+
def generate_unique_name() -> str:
|
108
|
+
"""
|
109
|
+
Generate a unique name for a file or directory.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
str: A unique name
|
113
|
+
"""
|
114
|
+
adjectives = [
|
115
|
+
"goofy",
|
116
|
+
"quirky",
|
117
|
+
"snappy",
|
118
|
+
"witty",
|
119
|
+
"bubbly",
|
120
|
+
"zany",
|
121
|
+
"wonky",
|
122
|
+
"clumsy",
|
123
|
+
"cheeky",
|
124
|
+
"sassy",
|
125
|
+
"cryptic",
|
126
|
+
"shadowy",
|
127
|
+
"enigmatic",
|
128
|
+
"arcane",
|
129
|
+
"veiled",
|
130
|
+
"hidden",
|
131
|
+
"phantasmal",
|
132
|
+
"whispering",
|
133
|
+
"hollow",
|
134
|
+
"ghostly",
|
135
|
+
"mighty",
|
136
|
+
"fierce",
|
137
|
+
"brave",
|
138
|
+
"bold",
|
139
|
+
"ironclad",
|
140
|
+
"unyielding",
|
141
|
+
"stormforged",
|
142
|
+
"grim",
|
143
|
+
"feral",
|
144
|
+
"dauntless",
|
145
|
+
"stellar",
|
146
|
+
"vivid",
|
147
|
+
"luminous",
|
148
|
+
"glimmering",
|
149
|
+
"blazing",
|
150
|
+
"radiant",
|
151
|
+
"shimmering",
|
152
|
+
"snazzy",
|
153
|
+
"flashy",
|
154
|
+
"dazzling",
|
155
|
+
]
|
156
|
+
|
157
|
+
names = [
|
158
|
+
"gandalf",
|
159
|
+
"merlin",
|
160
|
+
"morgana",
|
161
|
+
"radagast",
|
162
|
+
"saruman",
|
163
|
+
"galadriel",
|
164
|
+
"smaug",
|
165
|
+
"elminster",
|
166
|
+
"ambrosius",
|
167
|
+
"balrog",
|
168
|
+
"sauron",
|
169
|
+
"rivendell",
|
170
|
+
"azkaban",
|
171
|
+
"gryffindor",
|
172
|
+
"slytherin",
|
173
|
+
"rowena",
|
174
|
+
"helga",
|
175
|
+
"alatar",
|
176
|
+
"pellinore",
|
177
|
+
"nimue",
|
178
|
+
"glorfindel",
|
179
|
+
"melian",
|
180
|
+
"feanor",
|
181
|
+
"titania",
|
182
|
+
"oberon",
|
183
|
+
"cerberus",
|
184
|
+
"phoenix",
|
185
|
+
"gryphon",
|
186
|
+
"hydra",
|
187
|
+
"basilisk",
|
188
|
+
"leviathan",
|
189
|
+
"wraith",
|
190
|
+
"djinn",
|
191
|
+
"fae",
|
192
|
+
"nymph",
|
193
|
+
"dryad",
|
194
|
+
"selkie",
|
195
|
+
"witchking",
|
196
|
+
"morwen",
|
197
|
+
"telvanni",
|
198
|
+
"daedric",
|
199
|
+
"argonian",
|
200
|
+
"auriel",
|
201
|
+
"azura",
|
202
|
+
"boethiah",
|
203
|
+
"nocturnal",
|
204
|
+
"zarthos",
|
205
|
+
"shadar",
|
206
|
+
"kobold",
|
207
|
+
"lich",
|
208
|
+
"oracle",
|
209
|
+
"summoner",
|
210
|
+
"geomancer",
|
211
|
+
"runeweaver",
|
212
|
+
"voidcaller",
|
213
|
+
"planewalker",
|
214
|
+
]
|
215
|
+
|
216
|
+
adjective = secrets.choice(adjectives)
|
217
|
+
name = secrets.choice(names)
|
218
|
+
return f"{adjective}_{name}"
|
@@ -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(
|
163
|
+
self.work_dir
|
164
|
+
) # ty: ignore[possibly-unbound-attribute]
|
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,25 @@ 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[
|
336
|
+
str | None
|
337
|
+
] = reactive( # ty: ignore[invalid-assignment]
|
338
|
+
"", recompose=False
|
339
|
+
)
|
340
|
+
storage_content: reactive[
|
341
|
+
StorageContent | None
|
342
|
+
] = reactive( # ty: ignore[invalid-assignment]
|
343
|
+
None, recompose=True
|
344
|
+
)
|
326
345
|
selected_files: reactive[set] = reactive(set(), recompose=False)
|
327
346
|
selected_files_n: reactive[int] = reactive(0, recompose=False)
|
328
347
|
|
@@ -361,6 +380,15 @@ class StorageContentContainer(Vertical):
|
|
361
380
|
height: auto;
|
362
381
|
border-bottom: solid $secondary;
|
363
382
|
margin: 1 0;
|
383
|
+
|
384
|
+
PathSelector {
|
385
|
+
&.primary_color {
|
386
|
+
color: $primary;
|
387
|
+
}
|
388
|
+
&.secondary_color {
|
389
|
+
color: $secondary;
|
390
|
+
}
|
391
|
+
}
|
364
392
|
}
|
365
393
|
.storage_path_item {
|
366
394
|
padding: 0 0;
|
@@ -462,13 +490,14 @@ class StorageContentContainer(Vertical):
|
|
462
490
|
with Horizontal():
|
463
491
|
yield Label("Current Path: ", classes="storage_path_item")
|
464
492
|
for index, breadcrumb in enumerate(breadcrumbs):
|
465
|
-
|
493
|
+
color_classes = "primary" if index == 0 else "secondary"
|
466
494
|
yield PathSelector(
|
467
|
-
renderable=
|
495
|
+
renderable=breadcrumb,
|
468
496
|
storage=self.storage,
|
469
497
|
path="/".join(breadcrumbs[1 : index + 1]),
|
470
498
|
access_credentials_uuid=self.access_credentials_uuid,
|
471
|
-
classes="storage_path_item",
|
499
|
+
classes=f"storage_path_item {color_classes}_color",
|
500
|
+
markup=False,
|
472
501
|
)
|
473
502
|
yield Label("/", classes="storage_path_item")
|
474
503
|
with Horizontal():
|
@@ -630,15 +659,15 @@ class StorageContentContainer(Vertical):
|
|
630
659
|
)
|
631
660
|
)
|
632
661
|
elif action_type == ActionType.DELETE:
|
633
|
-
self.post_message(DeleteRequest(**params))
|
662
|
+
self.post_message(DeleteRequest(**params)) # ty: ignore[missing-argument]
|
634
663
|
elif action_type == ActionType.DOWNLOAD:
|
635
|
-
self.post_message(DownloadRequest(**params))
|
664
|
+
self.post_message(DownloadRequest(**params)) # ty: ignore[missing-argument]
|
636
665
|
elif action_type == ActionType.UNCHECK_ALL:
|
637
666
|
self.post_message(
|
638
667
|
UncheckFilesRequest(
|
639
668
|
keys=[
|
640
669
|
item.uuid
|
641
|
-
for item in self.storage_content
|
670
|
+
for item in getattr(self.storage_content, "files", [])
|
642
671
|
if item.key in self.selected_files
|
643
672
|
]
|
644
673
|
)
|
@@ -654,14 +683,14 @@ class StorageContentContainer(Vertical):
|
|
654
683
|
"""
|
655
684
|
try:
|
656
685
|
selected_actions = self.query_one("#selected_actions")
|
657
|
-
counter = self.query_one("#selected_n")
|
686
|
+
counter = self.query_one("#selected_n", Label)
|
658
687
|
except NoMatches:
|
659
688
|
return
|
660
689
|
|
661
690
|
if self.selected_files_n > 0:
|
662
691
|
if not selected_actions.has_class("-visible"):
|
663
692
|
selected_actions.add_class("-visible")
|
664
|
-
counter.update(f"❌Selected: {self.selected_files_n}")
|
693
|
+
counter.update(f"❌Selected: {self.selected_files_n}")
|
665
694
|
else:
|
666
695
|
selected_actions.remove_class("-visible")
|
667
696
|
self.query_one("#content").remove_class("-visible")
|
@@ -682,7 +711,7 @@ class StorageContentContainer(Vertical):
|
|
682
711
|
self.search_prefix = value
|
683
712
|
self.post_message(
|
684
713
|
SelectStorageItem(
|
685
|
-
self.storage,
|
714
|
+
self.storage,
|
686
715
|
self.path,
|
687
716
|
self.access_credentials_uuid,
|
688
717
|
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
|