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.
Files changed (39) hide show
  1. {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/METADATA +4 -1
  2. {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/RECORD +39 -35
  3. sourcerer/__init__.py +1 -1
  4. sourcerer/domain/access_credentials/entities.py +4 -7
  5. sourcerer/domain/access_credentials/repositories.py +12 -0
  6. sourcerer/domain/access_credentials/services.py +2 -1
  7. sourcerer/domain/file_system/entities.py +5 -7
  8. sourcerer/domain/storage_provider/entities.py +7 -11
  9. sourcerer/infrastructure/access_credentials/registry.py +1 -1
  10. sourcerer/infrastructure/access_credentials/repositories.py +17 -0
  11. sourcerer/infrastructure/access_credentials/services.py +30 -14
  12. sourcerer/infrastructure/db/config.py +1 -1
  13. sourcerer/infrastructure/storage_provider/services/azure.py +5 -3
  14. sourcerer/infrastructure/utils.py +115 -1
  15. sourcerer/presentation/screens/critical_error/main.py +4 -4
  16. sourcerer/presentation/screens/file_system_finder/main.py +7 -3
  17. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +1 -1
  18. sourcerer/presentation/screens/main/main.py +37 -6
  19. sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py +8 -0
  20. sourcerer/presentation/screens/main/widgets/gradient.py +3 -3
  21. sourcerer/presentation/screens/main/widgets/resizing_rule.py +3 -1
  22. sourcerer/presentation/screens/main/widgets/storage_content.py +43 -18
  23. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +38 -6
  24. sourcerer/presentation/screens/preview_content/main.py +5 -5
  25. sourcerer/presentation/screens/provider_creds_list/main.py +44 -0
  26. sourcerer/presentation/screens/provider_creds_list/messages/__init__.py +0 -0
  27. sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py +8 -0
  28. sourcerer/presentation/screens/provider_creds_list/styles.tcss +5 -5
  29. sourcerer/presentation/screens/provider_creds_registration/main.py +6 -2
  30. sourcerer/presentation/screens/question/main.py +4 -3
  31. sourcerer/presentation/screens/question/styles.tcss +2 -8
  32. sourcerer/presentation/screens/shared/widgets/button.py +1 -1
  33. sourcerer/presentation/screens/shared/widgets/labeled_input.py +5 -1
  34. sourcerer/presentation/screens/shared/widgets/loader.py +31 -0
  35. sourcerer/presentation/screens/storage_action_progress/main.py +29 -29
  36. sourcerer/utils.py +1 -1
  37. {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/WHEEL +0 -0
  38. {data_sourcerer-0.2.1.dist-info → data_sourcerer-0.2.3.dist-info}/entry_points.txt +0 -0
  39. {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("Dismiss", name="dismiss")
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 == "dismiss":
54
- self.dismiss() # type: ignore
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: Static = self.query_one("#active-path") # type: ignore
158
+ active_path_label = self.query_one("#active-path", Static)
159
159
  except NoMatches:
160
160
  return
161
- label = str(event.path.relative_to(self.work_dir))
162
- if event.path.is_dir():
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}") # type: ignore
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
- for credentials in access_credentials:
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) # type: ignore
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) for key in event.keys # type: ignore
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() # type: ignore
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(**params) # type: ignore
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
 
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from textual.message import Message
4
+
5
+
6
+ @dataclass
7
+ class RefreshStoragesListRequest(Message):
8
+ pass
@@ -1,5 +1,6 @@
1
1
  from rich.text import Text
2
- from textual.widgets import Static
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(Static):
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(None)
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"[$primary]{self.file.size}[/$primary]", classes="file_size"
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(None, recompose=True)
322
- path: reactive[str | None] = reactive(None, recompose=False)
323
- search_prefix: reactive[str | None] = reactive(None, recompose=False)
324
- access_credentials_uuid: reactive[str | None] = reactive("", recompose=False)
325
- storage_content: reactive[StorageContent | None] = reactive(None, recompose=True)
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
- color = "$primary" if index == 0 else "$secondary"
489
+ color_classes = "primary" if index == 0 else "secondary"
466
490
  yield PathSelector(
467
- renderable=f"[{color}]{breadcrumb}[/]",
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.files # type: ignore
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}") # type: ignore
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, # type: ignore
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(0, recompose=True)
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 Container(id="header"):
119
- yield GradientWidget("🧙SOURCERER", id="left-middle")
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
- match extension:
59
- case ".tfstate":
60
- lexer = "json"
61
- case _:
62
- lexer = Syntax.guess_lexer(self.key, content)
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()
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from textual.message import Message
4
+
5
+
6
+ @dataclass
7
+ class ReloadCredentialsRequest(Message):
8
+ pass
@@ -43,23 +43,23 @@ ProviderCredsListScreen {
43
43
  }
44
44
 
45
45
  .credentials_name {
46
- width: 30%;
46
+ width: 20%;
47
47
  }
48
48
 
49
49
  .credentials_provider {
50
- width: 30%;
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 # type: ignore
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 = self.query_one("#auth_name").get().value or "default" # type: ignore
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 Label
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 Label(self.question)
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
- & > Label {
12
+ & > Static {
15
13
  margin: 1;
16
14
  text-align: center;
17
- column-span: 2;
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: