data-sourcerer 0.2.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
  2. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +50 -34
  3. sourcerer/__init__.py +1 -1
  4. sourcerer/domain/access_credentials/entities.py +3 -1
  5. sourcerer/domain/access_credentials/repositories.py +1 -1
  6. sourcerer/domain/storage/__init__.py +0 -0
  7. sourcerer/domain/storage/entities.py +27 -0
  8. sourcerer/domain/storage/repositories.py +31 -0
  9. sourcerer/domain/storage_provider/entities.py +1 -1
  10. sourcerer/infrastructure/access_credentials/repositories.py +3 -2
  11. sourcerer/infrastructure/access_credentials/services.py +9 -25
  12. sourcerer/infrastructure/db/models.py +33 -2
  13. sourcerer/infrastructure/storage/__init__.py +0 -0
  14. sourcerer/infrastructure/storage/repositories.py +72 -0
  15. sourcerer/infrastructure/storage/services.py +37 -0
  16. sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
  17. sourcerer/infrastructure/storage_provider/services/gcp.py +2 -3
  18. sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
  19. sourcerer/infrastructure/utils.py +2 -1
  20. sourcerer/presentation/di_container.py +15 -0
  21. sourcerer/presentation/screens/file_system_finder/main.py +5 -10
  22. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
  23. sourcerer/presentation/screens/main/main.py +89 -9
  24. sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
  25. sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
  26. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
  27. sourcerer/presentation/screens/main/widgets/storage_content.py +197 -80
  28. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
  29. sourcerer/presentation/screens/preview_content/main.py +216 -17
  30. sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
  31. sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
  32. sourcerer/presentation/screens/provider_creds_list/main.py +38 -13
  33. sourcerer/presentation/screens/provider_creds_registration/main.py +10 -7
  34. sourcerer/presentation/screens/shared/modal_screens.py +37 -0
  35. sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
  36. sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
  37. sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  38. sourcerer/presentation/screens/storages_list/main.py +184 -0
  39. sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  40. sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
  41. sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
  42. sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  43. sourcerer/presentation/screens/storages_registration/main.py +100 -0
  44. sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
  45. sourcerer/presentation/settings.py +29 -16
  46. sourcerer/presentation/utils.py +9 -1
  47. sourcerer/settings.py +2 -0
  48. sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
  49. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
  50. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
  51. {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,70 +1,269 @@
1
+ import contextlib
2
+ import re
3
+ from dataclasses import dataclass
1
4
  from pathlib import Path
5
+ from typing import ClassVar
2
6
 
7
+ import humanize
8
+ from dependency_injector.wiring import Provide
3
9
  from rich.syntax import Syntax
4
- from textual import on
10
+ from textual import events, on
5
11
  from textual.app import ComposeResult
12
+ from textual.binding import Binding, BindingType
6
13
  from textual.containers import Container, Horizontal
7
- from textual.screen import ModalScreen
8
- from textual.widgets import LoadingIndicator, RichLog
14
+ from textual.css.query import NoMatches
15
+ from textual.document._document import Selection
16
+ from textual.message import Message
17
+ from textual.reactive import reactive
18
+ from textual.widgets import Input, Label, LoadingIndicator, Rule, TextArea
9
19
 
10
20
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
11
21
  from sourcerer.infrastructure.storage_provider.exceptions import (
12
22
  ReadStorageItemsError,
13
23
  )
24
+ from sourcerer.presentation.di_container import DiContainer
25
+ from sourcerer.presentation.screens.preview_content.text_area_style import (
26
+ SOURCERER_THEME_NAME,
27
+ sourcerer_text_area_theme,
28
+ )
29
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
14
30
  from sourcerer.presentation.screens.shared.widgets.button import Button
15
31
  from sourcerer.presentation.utils import get_provider_service_by_access_uuid
32
+ from sourcerer.settings import PREVIEW_LENGTH_LIMIT, PREVIEW_LIMIT_SIZE
33
+
34
+
35
+ @dataclass
36
+ class HighlightResult(Message):
37
+ line: int
38
+ start: int
39
+ end: int
40
+
41
+
42
+ @dataclass
43
+ class HideSearchBar(Message):
44
+ pass
45
+
46
+
47
+ class ClickableLabel(Label):
48
+ @dataclass
49
+ class Click(Message):
50
+ name: str
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ super().__init__(*args, **kwargs)
54
+
55
+ def on_click(self, _: events.Click) -> None:
56
+ self.post_message(self.Click(name=self.name)) # type: ignore
57
+
58
+
59
+ class Search(Container):
60
+ total = reactive(0, recompose=False)
61
+ current = reactive(0, recompose=False)
62
+ content = reactive("", recompose=False)
63
+
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.search_result_lines = []
67
+ self.search_value = ""
68
+
69
+ def compose(self) -> ComposeResult:
70
+ with Horizontal():
71
+ with Horizontal(id="left"):
72
+ yield Label("Search:")
73
+ yield Input(placeholder="...")
74
+
75
+ with Horizontal(id="right"):
76
+ yield ClickableLabel(
77
+ "◀", id="previous", name="previous", classes="search-button"
78
+ )
79
+ yield Label(f"{self.current}/{self.total}", id="search-result")
80
+ yield ClickableLabel(
81
+ "▶", id="next", name="next", classes="search-button"
82
+ )
83
+ yield ClickableLabel(
84
+ "❌", id="hide", name="hide", classes="search-button"
85
+ )
86
+ yield Rule()
87
+
88
+ @on(Input.Submitted)
89
+ def on_input_submitted(self, event: Input.Submitted) -> None:
90
+ """Handle input submitted events."""
91
+ if not event.value or not self.content:
92
+ self.total = 0
93
+ self.current = 0
94
+ self.search_value = ""
95
+ self.search_result_lines = []
96
+ return
97
+ if event.value == self.search_value:
98
+ self._increment_current()
99
+ return
100
+
101
+ self.search_value = event.value
102
+ lines = self.content.split("\n")
103
+ search_pattern = event.value.lower()
16
104
 
105
+ self.search_result_lines = [
106
+ (line_n, index)
107
+ for line_n, line in enumerate(lines)
108
+ if search_pattern in line.lower()
109
+ for index in [
110
+ match.start()
111
+ for match in re.finditer(rf"(?i){re.escape(search_pattern)}", line)
112
+ ]
113
+ ]
17
114
 
18
- class PreviewContentScreen(ModalScreen):
115
+ if not self.search_result_lines:
116
+ self.notify("No matches found", severity="warning")
117
+ self.total, self.current = 0, 0
118
+ return
119
+
120
+ self.total = len(self.search_result_lines)
121
+ self.current = 1
122
+
123
+ @on(ClickableLabel.Click)
124
+ def on_click(self, event: ClickableLabel.Click) -> None:
125
+ if event.name == "next":
126
+ self._increment_current()
127
+ elif event.name == "previous":
128
+ self._decrement_current()
129
+ elif event.name == "hide":
130
+ self.post_message(HideSearchBar())
131
+
132
+ def _increment_current(self):
133
+ self.current = self.current + 1 if self.current < self.total else 1
134
+
135
+ def _decrement_current(self):
136
+ self.current = self.current - 1 if self.current > 1 else self.total
137
+
138
+ def watch_current(self):
139
+ with contextlib.suppress(NoMatches):
140
+ search_result = self.query_one("#search-result", Label)
141
+ search_result.update(f"{self.current}/{self.total}")
142
+ if not self.search_result_lines:
143
+ return
144
+ line, start = self.search_result_lines[self.current - 1]
145
+ self.post_message(
146
+ HighlightResult(line, start=start, end=start + len(self.search_value))
147
+ )
148
+
149
+
150
+ class PreviewContentScreen(ExitBoundModalScreen):
19
151
  CSS_PATH = "styles.tcss"
20
152
 
21
- def __init__(self, storage_name, key, access_credentials_uuid, *args, **kwargs):
153
+ BINDINGS: ClassVar[list[BindingType]] = [
154
+ Binding("escape", "cancel", "Close the screen"),
155
+ ]
156
+
157
+ def __init__(
158
+ self,
159
+ storage_name,
160
+ key,
161
+ file_size,
162
+ access_credentials_uuid,
163
+ *args,
164
+ credentials_service: CredentialsService = Provide[
165
+ DiContainer.credentials_repository
166
+ ],
167
+ **kwargs,
168
+ ):
22
169
  super().__init__(*args, **kwargs)
23
170
 
24
171
  self.storage_name = storage_name
25
172
  self.key = key
173
+ self.file_size = file_size
26
174
  self.access_credentials_uuid = access_credentials_uuid
175
+ self.credentials_service = credentials_service
176
+ self.content = None
27
177
 
28
178
  def compose(self) -> ComposeResult:
29
179
  with Container(id="PreviewContentScreen"):
180
+ yield Search(id="search-bar")
30
181
  yield LoadingIndicator(id="loading")
31
- yield RichLog(highlight=True, markup=True, auto_scroll=False)
182
+ yield TextArea(read_only=True, show_line_numbers=True)
32
183
  with Horizontal(id="controls"):
33
184
  yield Button("Close", name="cancel")
34
185
 
35
186
  def on_mount(self) -> None:
36
187
  """Called when the DOM is ready."""
188
+ search = self.query_one(Search)
189
+ text_log = self.query_one(TextArea)
190
+ text_log.register_theme(sourcerer_text_area_theme)
191
+ text_log.theme = SOURCERER_THEME_NAME
37
192
 
38
- credentials_service = CredentialsService()
39
193
  provider_service = get_provider_service_by_access_uuid(
40
- self.access_credentials_uuid, credentials_service
194
+ self.access_credentials_uuid, self.credentials_service
41
195
  )
42
196
  if not provider_service:
43
197
  self.notify("Could not read file :(", severity="error")
44
198
  return
45
199
  try:
46
- content = provider_service.read_storage_item(self.storage_name, self.key)
200
+ self.content = provider_service.read_storage_item(
201
+ self.storage_name, self.key
202
+ )
203
+ if self.file_size > PREVIEW_LIMIT_SIZE:
204
+ self.content = self.content[:PREVIEW_LENGTH_LIMIT]
205
+ self.notify(
206
+ f"The file size {humanize.naturalsize(self.file_size)} "
207
+ f"exceeds {humanize.naturalsize(PREVIEW_LIMIT_SIZE)} preview limit. "
208
+ f"The content is truncated to {PREVIEW_LENGTH_LIMIT} characters.",
209
+ severity="warning",
210
+ )
211
+ search.content = self.content
47
212
  except ReadStorageItemsError:
48
213
  self.notify("Could not read file :(", severity="error")
49
214
  return
50
215
  self.query_one("#loading").remove()
51
- if content is None:
216
+ if self.content is None:
52
217
  self.notify("Empty file", severity="warning")
53
218
  return
54
219
 
55
- text_log = self.query_one(RichLog)
56
-
57
220
  extension = Path(self.key).suffix
58
221
 
59
222
  lexer = (
60
- "json" if extension == ".tfstate" else Syntax.guess_lexer(self.key, content)
223
+ "json"
224
+ if extension == ".tfstate"
225
+ else Syntax.guess_lexer(self.key, self.content)
61
226
  )
62
-
63
- content = Syntax(content, lexer, line_numbers=True, theme="ansi_dark")
64
- text_log.write(content)
227
+ if lexer in text_log.available_languages:
228
+ text_log.language = lexer
229
+ else:
230
+ text_log.language = "python"
231
+ text_log.blur()
232
+ text_log.load_text(self.content)
65
233
 
66
234
  @on(Button.Click)
67
235
  def on_button_click(self, event: Button.Click) -> None:
68
236
  """Handle button click events."""
69
237
  if event.action == "cancel":
70
- self.dismiss()
238
+ self.action_cancel_screen()
239
+
240
+ @on(HideSearchBar)
241
+ def on_hide_search_bar(self, _: HideSearchBar) -> None:
242
+ """Handle hide search bar events."""
243
+ search_bar = self.query_one("#search-bar", Search)
244
+ search_bar.remove_class("-visible")
245
+ search_bar.query_one(Input).value = ""
246
+ search_bar.total = 0
247
+ search_bar.current = 0
248
+ search_bar.search_result_lines = []
249
+ search_bar.search_value = ""
250
+
251
+ @on(HighlightResult)
252
+ def on_highlight_result(self, event: HighlightResult) -> None:
253
+ """Handle highlight result events."""
254
+
255
+ text_area = self.query_one(TextArea)
256
+ text_area.selection = Selection(
257
+ start=(event.line, event.start), end=(event.line, event.end)
258
+ )
259
+
260
+ def action_find(self):
261
+ self.query_one("#search-bar").add_class("-visible")
262
+ self.query_one(Input).focus()
263
+
264
+ def action_cancel(self):
265
+ self.action_cancel_screen()
266
+
267
+ def on_key(self, event: events.Key) -> None:
268
+ if event.key in ("ctrl+f", "super+f"):
269
+ self.action_find()
@@ -12,16 +12,51 @@ PreviewContentScreen {
12
12
  & > #PreviewContentScreen {
13
13
  padding: 1 2 0 2;
14
14
  margin: 0 0;
15
- width: 100;
15
+ width: 70%;
16
16
  height: 40;
17
17
  border: solid $secondary-background;
18
18
  border-title-color: $primary-lighten-2;
19
19
 
20
- RichLog {
20
+
21
+ #search-bar {
22
+ height: auto;
23
+ display: none;
24
+
25
+ &.-visible {
26
+ display: block;
27
+ }
28
+ }
29
+
30
+ Horizontal{
31
+ height: auto;
32
+
33
+ & > Static {
34
+ width: auto;
35
+ }
36
+
37
+ #left {
38
+ align: left middle;
39
+ width: 90%;
40
+ }
41
+ #right {
42
+ align: right middle;
43
+ width: 10%;
44
+ }
45
+ }
46
+
47
+ Input {
48
+ height: 1;
49
+ border: none;
21
50
  background: transparent;
22
- width: 95;
51
+ width: 78%;
23
52
  }
24
53
 
54
+ TextArea {
55
+ background-tint: $background;
56
+
57
+ &:focus {
58
+ border: tall $border-blurred;
59
+ }
60
+ }
25
61
  }
26
62
  }
27
-
@@ -0,0 +1,60 @@
1
+ from rich.style import Style
2
+ from textual._text_area_theme import TextAreaTheme
3
+
4
+ SOURCERER_THEME_NAME = "sourcerer"
5
+ # Terraform theme for the text area.
6
+ sourcerer_text_area_theme = TextAreaTheme(
7
+ name=SOURCERER_THEME_NAME,
8
+ cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
9
+ cursor_line_style=Style(bgcolor="#2b2b2b"),
10
+ bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
11
+ cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
12
+ selection_style=Style(bgcolor="#FFA656"),
13
+ syntax_styles={
14
+ "string": Style(color="#79C0FF"),
15
+ "string.documentation": Style(color="#79C0FF"),
16
+ "comment": Style(color="#6A9955"),
17
+ "heading.marker": Style(color="#6E7681"),
18
+ "keyword": Style(color="#C586C0"),
19
+ "operator": Style(color="#CCCCCC"),
20
+ "conditional": Style(color="#569cd6"),
21
+ "keyword.function": Style(color="#F97970"),
22
+ "keyword.return": Style(color="#569cd6"),
23
+ "keyword.operator": Style(color="#569cd6"),
24
+ "repeat": Style(color="#569cd6"),
25
+ "exception": Style(color="#569cd6"),
26
+ "include": Style(color="#569cd6"),
27
+ "number": Style(color="#b5cea8"),
28
+ "float": Style(color="#b5cea8"),
29
+ "class": Style(color="#4EC9B0"),
30
+ "type": Style(color="#EFCB43"),
31
+ "type.class": Style(color="#FFA656"),
32
+ "type.builtin": Style(color="#CDD9E5"),
33
+ "function": Style(color="#CCB0EA"),
34
+ "function.call": Style(color="#CCB0EA"),
35
+ "method": Style(color="#CCB0EA"),
36
+ "method.call": Style(color="#CCB0EA"),
37
+ "constructor": Style(color="#DCBDFB"),
38
+ "boolean": Style(color="#7DAF9C"),
39
+ "constant.builtin": Style(color="#7DAF9C"),
40
+ "json.null": Style(color="#FDA556"),
41
+ "tag": Style(color="#EFCB43"),
42
+ "yaml.field": Style(color="#569cd6", bold=True),
43
+ "json.label": Style(color="#8EDB8C", bold=True),
44
+ "toml.type": Style(color="#569cd6"),
45
+ "toml.datetime": Style(color="#C586C0", italic=True),
46
+ "css.property": Style(color="#569cd6"),
47
+ "heading": Style(color="#569cd6", bold=True),
48
+ "bold": Style(bold=True),
49
+ "italic": Style(italic=True),
50
+ "strikethrough": Style(strike=True),
51
+ "link.uri": Style(color="#40A6FF", underline=True),
52
+ "link.label": Style(color="#569cd6"),
53
+ "list.marker": Style(color="#6E7681"),
54
+ "inline_code": Style(color="#ce9178"),
55
+ "info_string": Style(color="#ce9178", bold=True, italic=True),
56
+ "punctuation.bracket": Style(color="#CCCCCC"),
57
+ "punctuation.delimiter": Style(color="#CCCCCC"),
58
+ "punctuation.special": Style(color="#CCCCCC"),
59
+ },
60
+ )
@@ -1,16 +1,18 @@
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
7
8
  from textual.message import Message
8
9
  from textual.reactive import reactive
9
- from textual.screen import ModalScreen
10
10
  from textual.widgets import Checkbox, Label
11
11
 
12
12
  from sourcerer.domain.access_credentials.entities import Credentials
13
+ from sourcerer.domain.access_credentials.repositories import BaseCredentialsRepository
13
14
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
15
+ from sourcerer.presentation.di_container import DiContainer
14
16
  from sourcerer.presentation.screens.provider_creds_list.messages.reload_credentials_request import (
15
17
  ReloadCredentialsRequest,
16
18
  )
@@ -19,6 +21,9 @@ from sourcerer.presentation.screens.provider_creds_registration.main import (
19
21
  ProviderCredsRegistrationScreen,
20
22
  )
21
23
  from sourcerer.presentation.screens.question.main import QuestionScreen
24
+ from sourcerer.presentation.screens.shared.modal_screens import (
25
+ RefreshTriggerableModalScreen,
26
+ )
22
27
  from sourcerer.presentation.screens.shared.widgets.button import Button
23
28
 
24
29
 
@@ -32,9 +37,12 @@ class ProviderCredentialsRow(Horizontal):
32
37
  uuid: str
33
38
  active: bool
34
39
 
35
- def __init__(self, row: Credentials, *args, **kwargs):
40
+ def __init__(
41
+ self, row: Credentials, credentials_service: CredentialsService, *args, **kwargs
42
+ ):
36
43
  super().__init__(*args, **kwargs)
37
44
  self.row = row
45
+ self.credentials_service = credentials_service
38
46
 
39
47
  def compose(self) -> ComposeResult:
40
48
  yield Checkbox(
@@ -88,12 +96,11 @@ class ProviderCredentialsRow(Horizontal):
88
96
  """
89
97
  if not result:
90
98
  return
91
- credentials_service = CredentialsService()
92
- credentials_service.delete(self.row.uuid)
99
+ self.credentials_service.delete(self.row.uuid)
93
100
  self.post_message(ReloadCredentialsRequest())
94
101
 
95
102
 
96
- class ProviderCredsListScreen(ModalScreen):
103
+ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
97
104
  CSS_PATH = "styles.tcss"
98
105
 
99
106
  MAIN_CONTAINER_ID = "ProviderCredsListScreen"
@@ -107,9 +114,16 @@ class ProviderCredsListScreen(ModalScreen):
107
114
 
108
115
  credentials_list = reactive([], recompose=True)
109
116
 
110
- def __init__(self, *args, **kwargs):
117
+ def __init__(
118
+ self,
119
+ credentials_service: CredentialsService = Provide[
120
+ DiContainer.credentials_service
121
+ ],
122
+ *args,
123
+ **kwargs,
124
+ ):
111
125
  super().__init__(*args, **kwargs)
112
- self.credentials_service = CredentialsService()
126
+ self.credentials_service = credentials_service
113
127
 
114
128
  def compose(self) -> ComposeResult:
115
129
  with Container(id=self.MAIN_CONTAINER_ID):
@@ -129,7 +143,9 @@ class ProviderCredsListScreen(ModalScreen):
129
143
  yield Label("Auth method", classes="credentials_auth_method")
130
144
  yield Label("Delete", classes="credentials_auth_delete")
131
145
  for row in self.credentials_list:
132
- yield ProviderCredentialsRow(row, classes="credentials_row")
146
+ yield ProviderCredentialsRow(
147
+ row, self.credentials_service, classes="credentials_row"
148
+ )
133
149
  with Horizontal(id="controls"):
134
150
  yield Button(ControlsEnum.CANCEL.value, name=ControlsEnum.CANCEL.name)
135
151
 
@@ -137,16 +153,22 @@ class ProviderCredsListScreen(ModalScreen):
137
153
  """
138
154
  Initialize the screen by refreshing the credentials list when the screen is composed.
139
155
  """
140
- self.refresh_credentials_list()
156
+ self.refresh_credentials_list(set_refresh_flag=False)
141
157
 
142
- def refresh_credentials_list(self):
158
+ def refresh_credentials_list(self, set_refresh_flag: bool = True):
143
159
  """
144
160
  Refresh the credentials list by retrieving the latest credentials from the credentials service.
145
161
  """
146
162
  self.credentials_list = self.credentials_service.list()
163
+ if set_refresh_flag:
164
+ self._requires_storage_refresh = True
147
165
 
148
166
  def create_provider_creds_registration(
149
- self, credentials_entry: ProviderCredentialsEntry
167
+ self,
168
+ credentials_entry: ProviderCredentialsEntry,
169
+ credentials_repo: BaseCredentialsRepository = Provide[
170
+ DiContainer.credentials_repository
171
+ ],
150
172
  ):
151
173
  """
152
174
  Create a new provider credentials registration.
@@ -155,10 +177,13 @@ class ProviderCredsListScreen(ModalScreen):
155
177
 
156
178
  Args:
157
179
  credentials_entry (ProviderCredentialsEntry): The credentials entry to register.
180
+ credentials_repo (BaseCredentialsRepository): The repository to store the credentials.
158
181
  """
159
182
  if not credentials_entry:
160
183
  return
161
- service = credentials_entry.cloud_storage_provider_credentials_service() # type: ignore
184
+ service = credentials_entry.cloud_storage_provider_credentials_service(
185
+ credentials_repo
186
+ )
162
187
  service.store(credentials_entry.name, credentials_entry.fields)
163
188
  self.refresh_credentials_list()
164
189
 
@@ -174,7 +199,7 @@ class ProviderCredsListScreen(ModalScreen):
174
199
  event (Button.Click): The button click event.
175
200
  """
176
201
  if event.action == ControlsEnum.CANCEL.name:
177
- self.dismiss()
202
+ self.action_cancel_screen()
178
203
  if event.action == "add_registration":
179
204
  self.app.push_screen(
180
205
  ProviderCredsRegistrationScreen(),
@@ -5,7 +5,6 @@ from dependency_injector.wiring import Provide
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container, Horizontal, VerticalScroll
8
- from textual.screen import ModalScreen
9
8
  from textual.widgets import Label, Select
10
9
 
11
10
  from sourcerer.domain.access_credentials.services import (
@@ -15,8 +14,12 @@ from sourcerer.domain.access_credentials.services import (
15
14
  from sourcerer.infrastructure.access_credentials.exceptions import (
16
15
  MissingAuthFieldsError,
17
16
  )
17
+ from sourcerer.infrastructure.access_credentials.registry import (
18
+ AccessCredentialsRegistry,
19
+ )
18
20
  from sourcerer.infrastructure.utils import generate_unique_name
19
21
  from sourcerer.presentation.di_container import DiContainer
22
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
20
23
  from sourcerer.presentation.screens.shared.widgets.button import Button
21
24
  from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
22
25
 
@@ -33,7 +36,7 @@ class ProviderCredentialsEntry:
33
36
  fields: dict[str, str]
34
37
 
35
38
 
36
- class ProviderCredsRegistrationScreen(ModalScreen):
39
+ class ProviderCredsRegistrationScreen(ExitBoundModalScreen):
37
40
  CSS_PATH = "styles.tcss"
38
41
 
39
42
  MAIN_CONTAINER_ID = "ProviderCredsRegistrationScreen"
@@ -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)
@@ -179,7 +182,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
179
182
  collected authentication fields.
180
183
  """
181
184
  if event.action == ControlsEnum.CANCEL.name:
182
- self.dismiss()
185
+ self.action_cancel_screen()
183
186
  elif event.action == ControlsEnum.CREATE.name:
184
187
  if not self.auth_method:
185
188
  self.notify("Please select provider and auth method", severity="error")
@@ -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,37 @@
1
+ from typing import ClassVar
2
+
3
+ from textual.binding import Binding, BindingType
4
+ from textual.screen import ModalScreen
5
+
6
+
7
+ class ExitBoundModalScreen(ModalScreen):
8
+ """
9
+ A base class for modal screens that can be exited.
10
+ It provides a method to exit the screen and a flag to indicate if the screen should be exited.
11
+ """
12
+
13
+ BINDINGS: ClassVar[list[BindingType]] = [
14
+ Binding("escape", "cancel_screen", "Pop screen"),
15
+ ]
16
+
17
+ def action_cancel_screen(self):
18
+ """
19
+ Action to exit the screen.
20
+ """
21
+ self.dismiss()
22
+
23
+
24
+ class RefreshTriggerableModalScreen(ExitBoundModalScreen):
25
+ """
26
+ A base class for modal screens that can be refreshed.
27
+ It provides a method to refresh the screen and a flag to indicate if the screen should be refreshed.
28
+ """
29
+
30
+ def __init__(self, *args, **kwargs):
31
+ super().__init__(*args, **kwargs)
32
+ self._requires_storage_refresh = False
33
+
34
+ def action_cancel_screen(self):
35
+ requires_storage_refresh = self._requires_storage_refresh
36
+ self._requires_storage_refresh = False
37
+ self.dismiss(requires_storage_refresh)