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.
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +50 -34
- sourcerer/__init__.py +1 -1
- sourcerer/domain/access_credentials/entities.py +3 -1
- sourcerer/domain/access_credentials/repositories.py +1 -1
- sourcerer/domain/storage/__init__.py +0 -0
- sourcerer/domain/storage/entities.py +27 -0
- sourcerer/domain/storage/repositories.py +31 -0
- sourcerer/domain/storage_provider/entities.py +1 -1
- sourcerer/infrastructure/access_credentials/repositories.py +3 -2
- sourcerer/infrastructure/access_credentials/services.py +9 -25
- sourcerer/infrastructure/db/models.py +33 -2
- sourcerer/infrastructure/storage/__init__.py +0 -0
- sourcerer/infrastructure/storage/repositories.py +72 -0
- sourcerer/infrastructure/storage/services.py +37 -0
- sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
- sourcerer/infrastructure/storage_provider/services/gcp.py +2 -3
- sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- sourcerer/infrastructure/utils.py +2 -1
- sourcerer/presentation/di_container.py +15 -0
- sourcerer/presentation/screens/file_system_finder/main.py +5 -10
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
- sourcerer/presentation/screens/main/main.py +89 -9
- sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
- sourcerer/presentation/screens/main/widgets/storage_content.py +197 -80
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
- sourcerer/presentation/screens/preview_content/main.py +216 -17
- sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
- sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +38 -13
- sourcerer/presentation/screens/provider_creds_registration/main.py +10 -7
- sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
- sourcerer/presentation/screens/storages_list/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/main.py +184 -0
- sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
- sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
- sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
- sourcerer/presentation/screens/storages_registration/main.py +100 -0
- sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
- sourcerer/presentation/settings.py +29 -16
- sourcerer/presentation/utils.py +9 -1
- sourcerer/settings.py +2 -0
- sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
- {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.
|
8
|
-
from textual.
|
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
|
-
|
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
|
-
|
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
|
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(
|
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"
|
223
|
+
"json"
|
224
|
+
if extension == ".tfstate"
|
225
|
+
else Syntax.guess_lexer(self.key, self.content)
|
61
226
|
)
|
62
|
-
|
63
|
-
|
64
|
-
|
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.
|
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:
|
15
|
+
width: 70%;
|
16
16
|
height: 40;
|
17
17
|
border: solid $secondary-background;
|
18
18
|
border-title-color: $primary-lighten-2;
|
19
19
|
|
20
|
-
|
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:
|
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__(
|
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
|
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(
|
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__(
|
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 =
|
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(
|
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,
|
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(
|
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.
|
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(
|
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.
|
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)
|