data-sourcerer 0.1.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.1.0.dist-info/METADATA +52 -0
- data_sourcerer-0.1.0.dist-info/RECORD +95 -0
- data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
- data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
- data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
- data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
- sourcerer/__init__.py +15 -0
- sourcerer/domain/__init__.py +13 -0
- sourcerer/domain/access_credentials/__init__.py +7 -0
- sourcerer/domain/access_credentials/entities.py +53 -0
- sourcerer/domain/access_credentials/exceptions.py +17 -0
- sourcerer/domain/access_credentials/repositories.py +84 -0
- sourcerer/domain/access_credentials/services.py +91 -0
- sourcerer/domain/file_system/__init__.py +7 -0
- sourcerer/domain/file_system/entities.py +70 -0
- sourcerer/domain/file_system/exceptions.py +17 -0
- sourcerer/domain/file_system/services.py +64 -0
- sourcerer/domain/shared/__init__.py +6 -0
- sourcerer/domain/shared/entities.py +18 -0
- sourcerer/domain/storage_provider/__init__.py +7 -0
- sourcerer/domain/storage_provider/entities.py +86 -0
- sourcerer/domain/storage_provider/exceptions.py +17 -0
- sourcerer/domain/storage_provider/services.py +130 -0
- sourcerer/infrastructure/__init__.py +13 -0
- sourcerer/infrastructure/access_credentials/__init__.py +7 -0
- sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
- sourcerer/infrastructure/access_credentials/registry.py +120 -0
- sourcerer/infrastructure/access_credentials/repositories.py +119 -0
- sourcerer/infrastructure/access_credentials/services.py +396 -0
- sourcerer/infrastructure/db/__init__.py +6 -0
- sourcerer/infrastructure/db/config.py +73 -0
- sourcerer/infrastructure/db/models.py +47 -0
- sourcerer/infrastructure/file_system/__init__.py +7 -0
- sourcerer/infrastructure/file_system/exceptions.py +89 -0
- sourcerer/infrastructure/file_system/services.py +147 -0
- sourcerer/infrastructure/storage_provider/__init__.py +7 -0
- sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
- sourcerer/infrastructure/storage_provider/registry.py +84 -0
- sourcerer/infrastructure/storage_provider/services.py +509 -0
- sourcerer/infrastructure/utils.py +106 -0
- sourcerer/presentation/__init__.py +12 -0
- sourcerer/presentation/app.py +36 -0
- sourcerer/presentation/di_container.py +46 -0
- sourcerer/presentation/screens/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/main.py +78 -0
- sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
- sourcerer/presentation/screens/file_system_finder/main.py +248 -0
- sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
- sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
- sourcerer/presentation/screens/main/__init__.py +0 -0
- sourcerer/presentation/screens/main/main.py +469 -0
- sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
- sourcerer/presentation/screens/main/messages/download_request.py +12 -0
- sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
- sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
- sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
- sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
- sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
- sourcerer/presentation/screens/main/styles.tcss +32 -0
- sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
- sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- sourcerer/presentation/screens/preview_content/main.py +59 -0
- sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
- sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
- sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
- sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
- sourcerer/presentation/screens/question/__init__.py +0 -0
- sourcerer/presentation/screens/question/main.py +31 -0
- sourcerer/presentation/screens/question/styles.tcss +33 -0
- sourcerer/presentation/screens/shared/__init__.py +0 -0
- sourcerer/presentation/screens/shared/containers.py +13 -0
- sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/shared/widgets/button.py +54 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
- sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
- sourcerer/presentation/settings.py +31 -0
- sourcerer/presentation/themes/__init__.py +0 -0
- sourcerer/presentation/themes/github_dark.py +21 -0
- sourcerer/presentation/utils.py +69 -0
- sourcerer/settings.py +72 -0
- sourcerer/utils.py +32 -0
@@ -0,0 +1,264 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from dependency_injector.wiring import Provide
|
6
|
+
from textual import on
|
7
|
+
from textual.app import ComposeResult
|
8
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
9
|
+
from textual.screen import ModalScreen
|
10
|
+
from textual.widgets import Select, Label
|
11
|
+
|
12
|
+
from sourcerer.domain.access_credentials.services import (
|
13
|
+
BaseAccessCredentialsService,
|
14
|
+
AuthField,
|
15
|
+
)
|
16
|
+
from sourcerer.presentation.di_container import DiContainer
|
17
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
18
|
+
from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
|
19
|
+
|
20
|
+
|
21
|
+
class ControlsEnum(Enum):
|
22
|
+
CANCEL = "Cancel"
|
23
|
+
CREATE = "Create"
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class ProviderCredentialsEntry:
|
28
|
+
name: str
|
29
|
+
cloud_storage_provider_credentials_service: type[BaseAccessCredentialsService]
|
30
|
+
fields: dict[str, str]
|
31
|
+
|
32
|
+
|
33
|
+
class ProviderCredsRegistrationScreen(ModalScreen):
|
34
|
+
CSS_PATH = "styles.tcss"
|
35
|
+
|
36
|
+
MAIN_CONTAINER_ID = "ProviderCredsRegistrationScreen"
|
37
|
+
SETTINGS_CONTAINER_ID = "settings"
|
38
|
+
PROVIDER_SELECTOR_ID = "provider_selector"
|
39
|
+
CREDENTIALS_TYPE_SELECTOR_ID = "credentials_type_select"
|
40
|
+
CREDENTIALS_FIELDS_CONTAINER_ID = "credentials_fields_container"
|
41
|
+
|
42
|
+
PROVIDERS_NAME = "providers"
|
43
|
+
AUTH_METHODS_NAME = "auth_methods"
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
credentials_type_registry=Provide[
|
48
|
+
DiContainer.config.access_credential_method_registry
|
49
|
+
],
|
50
|
+
*args,
|
51
|
+
**kwargs,
|
52
|
+
):
|
53
|
+
super().__init__(*args, **kwargs)
|
54
|
+
self.provider_credentials_settings = credentials_type_registry.get()
|
55
|
+
self.auth_method = None
|
56
|
+
|
57
|
+
def compose(self) -> ComposeResult:
|
58
|
+
with Container(id=self.MAIN_CONTAINER_ID):
|
59
|
+
with VerticalScroll(id=self.SETTINGS_CONTAINER_ID):
|
60
|
+
yield LabeledInput(
|
61
|
+
"Custom credentials label (suggest to set it unique)",
|
62
|
+
"Name:",
|
63
|
+
True,
|
64
|
+
multiline=False,
|
65
|
+
id="auth_name",
|
66
|
+
)
|
67
|
+
yield Label("* Provider:", classes="form_label")
|
68
|
+
yield Select(
|
69
|
+
options=(
|
70
|
+
(provider, provider)
|
71
|
+
for provider in self.provider_credentials_settings.keys()
|
72
|
+
),
|
73
|
+
name=self.PROVIDERS_NAME,
|
74
|
+
id=self.PROVIDER_SELECTOR_ID,
|
75
|
+
)
|
76
|
+
with Horizontal(id="controls"):
|
77
|
+
yield Button(ControlsEnum.CANCEL.value, name=ControlsEnum.CANCEL.name)
|
78
|
+
yield Button(ControlsEnum.CREATE.value, name=ControlsEnum.CREATE.name)
|
79
|
+
|
80
|
+
async def _process_selected_provider(self, provider: str) -> None:
|
81
|
+
"""
|
82
|
+
Processes the selected provider by removing any existing credential type selector
|
83
|
+
and retrieving the available authentication methods for the provider. If multiple
|
84
|
+
authentication methods are available, a selection dropdown is displayed. If only
|
85
|
+
one method is available, it is set and its fields are mounted.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
provider (str): The name of the selected provider.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
None
|
92
|
+
|
93
|
+
Flow:
|
94
|
+
1. Remove any existing credential type selector from the settings container.
|
95
|
+
2. Retrieve authentication methods for the given provider.
|
96
|
+
3. If multiple methods exist, display a dropdown for selection.
|
97
|
+
4. If only one method exists, set it and mount its fields.
|
98
|
+
"""
|
99
|
+
# Remove existing credential type selector
|
100
|
+
await self.query_one(f"#{self.SETTINGS_CONTAINER_ID}").remove_children(
|
101
|
+
f"#{self.CREDENTIALS_TYPE_SELECTOR_ID}"
|
102
|
+
)
|
103
|
+
|
104
|
+
# Retrieve authentication methods for the selected provider
|
105
|
+
auth_methods = self.provider_credentials_settings.get(provider)
|
106
|
+
if not auth_methods:
|
107
|
+
return
|
108
|
+
|
109
|
+
# If multiple authentication methods exist, display a selection dropdown
|
110
|
+
if len(auth_methods) > 1:
|
111
|
+
options = [(auth_type, auth_type) for auth_type in auth_methods.keys()]
|
112
|
+
await self.query_one(f"#{self.SETTINGS_CONTAINER_ID}").mount(
|
113
|
+
Container(
|
114
|
+
Label("Auth method:", classes="form_label"),
|
115
|
+
Select(options=options, name=self.AUTH_METHODS_NAME),
|
116
|
+
id=self.CREDENTIALS_TYPE_SELECTOR_ID,
|
117
|
+
)
|
118
|
+
)
|
119
|
+
return
|
120
|
+
|
121
|
+
# If only one authentication method exists, set it and mount its fields
|
122
|
+
self.auth_method = next(iter(auth_methods.values()))
|
123
|
+
cls: BaseAccessCredentialsService = self.auth_method
|
124
|
+
await self._mount_credentials_fields(cls.auth_fields())
|
125
|
+
|
126
|
+
@on(Select.Changed)
|
127
|
+
async def select_changed(self, event: Select.Changed) -> None:
|
128
|
+
"""
|
129
|
+
Handle changes in the selection of provider or authentication method.
|
130
|
+
|
131
|
+
This method is triggered when a selection change event occurs in the
|
132
|
+
provider or authentication method dropdowns. It clears existing credential
|
133
|
+
fields and processes the selection based on the control that triggered the
|
134
|
+
event.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
event (Select.Changed): The event object containing details about the
|
138
|
+
selection change.
|
139
|
+
Flow:
|
140
|
+
|
141
|
+
1. Clear existing credential fields.
|
142
|
+
2. Process based on the control that triggered the event:
|
143
|
+
- If the event is triggered by the provider dropdown, process the selected provider.
|
144
|
+
- If the event is triggered by the authentication method dropdown, process the selected provider
|
145
|
+
and authentication method
|
146
|
+
"""
|
147
|
+
# Clear existing credential fields
|
148
|
+
await self.query_one(f"#{self.SETTINGS_CONTAINER_ID}").remove_children(
|
149
|
+
f"#{self.CREDENTIALS_FIELDS_CONTAINER_ID}"
|
150
|
+
)
|
151
|
+
|
152
|
+
# Process based on the control that triggered the event
|
153
|
+
if event.control.name == self.PROVIDERS_NAME:
|
154
|
+
await self._process_selected_provider(str(event.value))
|
155
|
+
elif event.control.name == self.AUTH_METHODS_NAME:
|
156
|
+
provider = self.query_one(f"#{self.PROVIDER_SELECTOR_ID}").selection # type: ignore
|
157
|
+
await self._process_selected_provider_auth_method(
|
158
|
+
provider, str(event.value)
|
159
|
+
)
|
160
|
+
|
161
|
+
@on(Button.Click)
|
162
|
+
def on_control_button_click(self, event: Button.Click):
|
163
|
+
"""
|
164
|
+
Handle click events for control buttons on the registration screen.
|
165
|
+
|
166
|
+
Depending on the action associated with the button click event, either dismiss
|
167
|
+
the screen or gather authentication fields and then dismiss the screen with
|
168
|
+
the collected data.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
event (Button.Click): The click event containing the action to be performed.
|
172
|
+
|
173
|
+
Flow:
|
174
|
+
1. Check if the event.action is ControlsEnum.cancel.name. If true, dismiss the screen.
|
175
|
+
2. If event.action is ControlsEnum.create.name, gather authentication fields. Dismiss the screen with the
|
176
|
+
collected authentication fields.
|
177
|
+
"""
|
178
|
+
if event.action == ControlsEnum.CANCEL.name:
|
179
|
+
self.dismiss()
|
180
|
+
elif event.action == ControlsEnum.CREATE.name:
|
181
|
+
auth_fields = self._get_auth_fields()
|
182
|
+
if not auth_fields:
|
183
|
+
self.notify("Please select provider and auth method", severity="error")
|
184
|
+
else:
|
185
|
+
self.dismiss(auth_fields)
|
186
|
+
|
187
|
+
def _get_auth_fields(self) -> ProviderCredentialsEntry | None:
|
188
|
+
"""
|
189
|
+
Collects authentication fields from the UI and returns a ProviderCredentialsEntry.
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
ProviderCredentialsEntry: An object containing the authentication name, method, and fields.
|
193
|
+
"""
|
194
|
+
if not self.auth_method:
|
195
|
+
return
|
196
|
+
fields = {
|
197
|
+
input_field.get().name: input_field.get().value
|
198
|
+
for input_field in self.query_one(
|
199
|
+
f"#{self.CREDENTIALS_FIELDS_CONTAINER_ID}"
|
200
|
+
).children
|
201
|
+
if isinstance(input_field, LabeledInput)
|
202
|
+
}
|
203
|
+
auth_name = self.query_one("#auth_name").get().value or "default" # type: ignore
|
204
|
+
|
205
|
+
return ProviderCredentialsEntry(
|
206
|
+
name=auth_name,
|
207
|
+
cloud_storage_provider_credentials_service=self.auth_method,
|
208
|
+
fields=fields,
|
209
|
+
)
|
210
|
+
|
211
|
+
async def _process_selected_provider_auth_method(self, provider, method):
|
212
|
+
"""
|
213
|
+
Process the selected authentication method for a given provider.
|
214
|
+
|
215
|
+
This method retrieves the authentication class for the specified provider
|
216
|
+
and method, sets it as the current authentication method, and mounts its
|
217
|
+
credential fields if available.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
provider: The name of the provider.
|
221
|
+
method: The authentication method to be processed.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
None
|
225
|
+
|
226
|
+
Flow:
|
227
|
+
1. Retrieve the authentication class for the specified provider and method.
|
228
|
+
2. Set the authentication class as the current authentication method.
|
229
|
+
3. Mount the credential fields for the selected authentication method.
|
230
|
+
"""
|
231
|
+
provider_auth_class = self.provider_credentials_settings.get(provider, {}).get(
|
232
|
+
method
|
233
|
+
)
|
234
|
+
if provider_auth_class:
|
235
|
+
self.auth_method = provider_auth_class
|
236
|
+
await self._mount_credentials_fields(provider_auth_class.auth_fields())
|
237
|
+
|
238
|
+
async def _mount_credentials_fields(self, fields: List[AuthField]) -> None:
|
239
|
+
"""
|
240
|
+
Mounts a container of labeled input fields for credentials onto the settings container
|
241
|
+
and sets focus on the first input field.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
fields (List[AuthField]): A list of AuthField objects containing key, label, and required attributes.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
None
|
248
|
+
|
249
|
+
Flow:
|
250
|
+
1. Create a container of labeled input fields for the provided credentials.
|
251
|
+
2. Mount the container onto the settings container.
|
252
|
+
3. Set focus on the first input field in the container.
|
253
|
+
"""
|
254
|
+
container = Container(
|
255
|
+
*(
|
256
|
+
LabeledInput(field.key, field.label, field.required, field.multiline)
|
257
|
+
for field in fields
|
258
|
+
),
|
259
|
+
id=self.CREDENTIALS_FIELDS_CONTAINER_ID,
|
260
|
+
)
|
261
|
+
await self.query_one(f"#{self.SETTINGS_CONTAINER_ID}").mount(container)
|
262
|
+
self.query_one(f"#{self.CREDENTIALS_FIELDS_CONTAINER_ID}").query_one(
|
263
|
+
".form_input"
|
264
|
+
).focus()
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
Container {
|
3
|
+
height: auto;
|
4
|
+
}
|
5
|
+
|
6
|
+
Label {
|
7
|
+
padding-left: 1;
|
8
|
+
}
|
9
|
+
|
10
|
+
ProviderCredsRegistrationScreen {
|
11
|
+
align: center middle;
|
12
|
+
content-align: center top;
|
13
|
+
|
14
|
+
& > #ProviderCredsRegistrationScreen {
|
15
|
+
padding: 1 2 0 2;
|
16
|
+
margin: 0 0;
|
17
|
+
width: 70;
|
18
|
+
height: 25;
|
19
|
+
border: solid $secondary-background;
|
20
|
+
|
21
|
+
& > #settings {
|
22
|
+
width: 100%;
|
23
|
+
height: 18;
|
24
|
+
|
25
|
+
& > .form_label {
|
26
|
+
padding-left: 1;
|
27
|
+
margin-bottom: 1;
|
28
|
+
|
29
|
+
}
|
30
|
+
|
31
|
+
& > #credentials_fields_container {
|
32
|
+
margin-top: 2;
|
33
|
+
padding-left: 2;
|
34
|
+
}
|
35
|
+
|
36
|
+
& > Select {
|
37
|
+
margin-bottom: 1;
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
File without changes
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from textual import on
|
2
|
+
from textual.app import ComposeResult
|
3
|
+
from textual.containers import Horizontal, Container
|
4
|
+
from textual.screen import ModalScreen
|
5
|
+
from textual.widgets import Label
|
6
|
+
|
7
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
8
|
+
|
9
|
+
|
10
|
+
class QuestionScreen(ModalScreen[bool]):
|
11
|
+
"""Screen with a parameter."""
|
12
|
+
|
13
|
+
CSS_PATH = "styles.tcss"
|
14
|
+
|
15
|
+
def __init__(self, question: str) -> None:
|
16
|
+
self.question = question
|
17
|
+
super().__init__()
|
18
|
+
|
19
|
+
def compose(self) -> ComposeResult:
|
20
|
+
with Container():
|
21
|
+
yield Label(self.question)
|
22
|
+
with Horizontal():
|
23
|
+
yield Button("Yes", name="yes")
|
24
|
+
yield Button("No", name="no")
|
25
|
+
|
26
|
+
@on(Button.Click)
|
27
|
+
def on_button_click(self, event: Button.Click) -> None:
|
28
|
+
"""
|
29
|
+
Handle button click events.
|
30
|
+
"""
|
31
|
+
self.dismiss(event.action) # type: ignore
|
@@ -0,0 +1,33 @@
|
|
1
|
+
QuestionScreen {
|
2
|
+
align: center middle;
|
3
|
+
content-align: center top;
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
& > Container {
|
8
|
+
padding: 1 2 0 2;
|
9
|
+
margin: 0 0;
|
10
|
+
width: 50;
|
11
|
+
height: 8;
|
12
|
+
border: solid $warning-darken-1;
|
13
|
+
|
14
|
+
& > Label {
|
15
|
+
margin: 1;
|
16
|
+
text-align: center;
|
17
|
+
column-span: 2;
|
18
|
+
width: auto;
|
19
|
+
}
|
20
|
+
|
21
|
+
& > Horizontal {
|
22
|
+
align: center bottom;
|
23
|
+
& > Button {
|
24
|
+
width: 5;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
}
|
29
|
+
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from textual.containers import ScrollableContainer, HorizontalScroll, VerticalScroll
|
2
|
+
|
3
|
+
|
4
|
+
class ScrollableContainerWithNoBindings(ScrollableContainer, inherit_bindings=False):
|
5
|
+
pass
|
6
|
+
|
7
|
+
|
8
|
+
class ScrollHorizontalContainerWithNoBindings(HorizontalScroll, inherit_bindings=False):
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class ScrollVerticalContainerWithNoBindings(VerticalScroll, inherit_bindings=False):
|
13
|
+
pass
|
File without changes
|
@@ -0,0 +1,54 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
from textual import events
|
4
|
+
from textual.message import Message
|
5
|
+
from textual.widgets import Label
|
6
|
+
|
7
|
+
|
8
|
+
class Button(Label):
|
9
|
+
"""
|
10
|
+
A Button widget that extends the Label class to include click event handling.
|
11
|
+
|
12
|
+
Attributes:
|
13
|
+
Click (Message): A nested dataclass representing a click event with an action attribute.
|
14
|
+
|
15
|
+
Methods:
|
16
|
+
__init__(*args, **kwargs): Initializes the Button with optional arguments and ensures a 'name' attribute is
|
17
|
+
provided.
|
18
|
+
on_click(_: events.Click) -> None: Handles click events by posting a Click message with the button's name.
|
19
|
+
"""
|
20
|
+
|
21
|
+
DEFAULT_CSS = """
|
22
|
+
Button {
|
23
|
+
&:hover {
|
24
|
+
color: white;
|
25
|
+
}
|
26
|
+
&:focus {
|
27
|
+
color: white;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
"""
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class Click(Message):
|
34
|
+
action: str
|
35
|
+
|
36
|
+
def __init__(self, *args, **kwargs):
|
37
|
+
"""
|
38
|
+
Initialize the Button with optional arguments and ensure a 'name' attribute is provided.
|
39
|
+
|
40
|
+
Raises:
|
41
|
+
ValueError: If 'name' is not included in the keyword arguments.
|
42
|
+
"""
|
43
|
+
super().__init__(*args, **kwargs)
|
44
|
+
if "name" not in kwargs:
|
45
|
+
raise ValueError("Name is required attribute for button")
|
46
|
+
|
47
|
+
def on_click(self, _: events.Click) -> None:
|
48
|
+
"""
|
49
|
+
Handle a click event by posting a Click message with the button's name.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
_: An instance of events.Click representing the click event.
|
53
|
+
"""
|
54
|
+
self.post_message(self.Click(self.name)) # type: ignore
|
@@ -0,0 +1,80 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.containers import Container
|
5
|
+
from textual.widgets import Label, Input, TextArea
|
6
|
+
|
7
|
+
|
8
|
+
class LabeledInput(Container):
|
9
|
+
"""
|
10
|
+
A container widget that combines a label and an input field.
|
11
|
+
|
12
|
+
Attributes:
|
13
|
+
DEFAULT_CSS (str): Default CSS styling for the widget.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
key (str): The placeholder text for the input field.
|
17
|
+
label (str): The text to display as the label.
|
18
|
+
required (bool): Indicates if the input is required, adding an asterisk to the label if true.
|
19
|
+
*args: Additional positional arguments for the container.
|
20
|
+
**kwargs: Additional keyword arguments for the container.
|
21
|
+
|
22
|
+
Methods:
|
23
|
+
compose() -> ComposeResult: Yields a label and an input field for the widget.
|
24
|
+
"""
|
25
|
+
|
26
|
+
DEFAULT_CSS = """
|
27
|
+
LabeledInput {
|
28
|
+
height: auto;
|
29
|
+
margin-bottom: 1;
|
30
|
+
}
|
31
|
+
"""
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class Value:
|
35
|
+
name: str
|
36
|
+
value: str
|
37
|
+
|
38
|
+
def __init__(self, key, label, required, multiline, *args, **kwargs):
|
39
|
+
"""
|
40
|
+
Initializes a LabeledInput instance with a label and input field.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
key (str): The placeholder text for the input field.
|
44
|
+
label (str): The text to display as the label.
|
45
|
+
required (bool): Indicates if the input is required, adding an asterisk to the label if true.
|
46
|
+
multiline (bool): Indicates if input may contain multiple lines
|
47
|
+
*args: Additional positional arguments for the container.
|
48
|
+
**kwargs: Additional keyword arguments for the container.
|
49
|
+
"""
|
50
|
+
super().__init__(*args, **kwargs)
|
51
|
+
self.key = key
|
52
|
+
self.label = label
|
53
|
+
self.required = required
|
54
|
+
self.multiline = multiline
|
55
|
+
|
56
|
+
def compose(self) -> ComposeResult:
|
57
|
+
"""
|
58
|
+
Yields a label and an input field for the LabeledInput widget.
|
59
|
+
|
60
|
+
The label includes an asterisk if the input is required. The input field
|
61
|
+
uses the provided key as its placeholder text.
|
62
|
+
"""
|
63
|
+
label = f"* {self.label}" if self.required else self.label
|
64
|
+
yield Label(label)
|
65
|
+
if self.multiline:
|
66
|
+
yield TextArea(show_line_numbers=False, classes="form_input")
|
67
|
+
else:
|
68
|
+
yield Input(placeholder=self.key, classes="form_input")
|
69
|
+
|
70
|
+
def get(self):
|
71
|
+
"""
|
72
|
+
Retrieves the value from the input field.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Value: A dataclass containing the name and value of the input field.
|
76
|
+
|
77
|
+
"""
|
78
|
+
input_area = self.query_one(".form_input")
|
79
|
+
text = input_area.document.text if isinstance(input_area, TextArea) else input_area.value # type: ignore
|
80
|
+
return self.Value(name=self.key, value=text)
|
File without changes
|