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.
Files changed (95) hide show
  1. data_sourcerer-0.1.0.dist-info/METADATA +52 -0
  2. data_sourcerer-0.1.0.dist-info/RECORD +95 -0
  3. data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
  4. data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
  5. data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
  7. sourcerer/__init__.py +15 -0
  8. sourcerer/domain/__init__.py +13 -0
  9. sourcerer/domain/access_credentials/__init__.py +7 -0
  10. sourcerer/domain/access_credentials/entities.py +53 -0
  11. sourcerer/domain/access_credentials/exceptions.py +17 -0
  12. sourcerer/domain/access_credentials/repositories.py +84 -0
  13. sourcerer/domain/access_credentials/services.py +91 -0
  14. sourcerer/domain/file_system/__init__.py +7 -0
  15. sourcerer/domain/file_system/entities.py +70 -0
  16. sourcerer/domain/file_system/exceptions.py +17 -0
  17. sourcerer/domain/file_system/services.py +64 -0
  18. sourcerer/domain/shared/__init__.py +6 -0
  19. sourcerer/domain/shared/entities.py +18 -0
  20. sourcerer/domain/storage_provider/__init__.py +7 -0
  21. sourcerer/domain/storage_provider/entities.py +86 -0
  22. sourcerer/domain/storage_provider/exceptions.py +17 -0
  23. sourcerer/domain/storage_provider/services.py +130 -0
  24. sourcerer/infrastructure/__init__.py +13 -0
  25. sourcerer/infrastructure/access_credentials/__init__.py +7 -0
  26. sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
  27. sourcerer/infrastructure/access_credentials/registry.py +120 -0
  28. sourcerer/infrastructure/access_credentials/repositories.py +119 -0
  29. sourcerer/infrastructure/access_credentials/services.py +396 -0
  30. sourcerer/infrastructure/db/__init__.py +6 -0
  31. sourcerer/infrastructure/db/config.py +73 -0
  32. sourcerer/infrastructure/db/models.py +47 -0
  33. sourcerer/infrastructure/file_system/__init__.py +7 -0
  34. sourcerer/infrastructure/file_system/exceptions.py +89 -0
  35. sourcerer/infrastructure/file_system/services.py +147 -0
  36. sourcerer/infrastructure/storage_provider/__init__.py +7 -0
  37. sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
  38. sourcerer/infrastructure/storage_provider/registry.py +84 -0
  39. sourcerer/infrastructure/storage_provider/services.py +509 -0
  40. sourcerer/infrastructure/utils.py +106 -0
  41. sourcerer/presentation/__init__.py +12 -0
  42. sourcerer/presentation/app.py +36 -0
  43. sourcerer/presentation/di_container.py +46 -0
  44. sourcerer/presentation/screens/__init__.py +0 -0
  45. sourcerer/presentation/screens/critical_error/__init__.py +0 -0
  46. sourcerer/presentation/screens/critical_error/main.py +78 -0
  47. sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
  48. sourcerer/presentation/screens/file_system_finder/main.py +248 -0
  49. sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
  50. sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
  51. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
  52. sourcerer/presentation/screens/main/__init__.py +0 -0
  53. sourcerer/presentation/screens/main/main.py +469 -0
  54. sourcerer/presentation/screens/main/messages/__init__.py +0 -0
  55. sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
  56. sourcerer/presentation/screens/main/messages/download_request.py +12 -0
  57. sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
  58. sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
  59. sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
  60. sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
  61. sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
  62. sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
  63. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
  64. sourcerer/presentation/screens/main/styles.tcss +32 -0
  65. sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
  66. sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
  67. sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
  68. sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
  69. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
  70. sourcerer/presentation/screens/preview_content/__init__.py +0 -0
  71. sourcerer/presentation/screens/preview_content/main.py +59 -0
  72. sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
  73. sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
  74. sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
  75. sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
  76. sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
  77. sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
  78. sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
  79. sourcerer/presentation/screens/question/__init__.py +0 -0
  80. sourcerer/presentation/screens/question/main.py +31 -0
  81. sourcerer/presentation/screens/question/styles.tcss +33 -0
  82. sourcerer/presentation/screens/shared/__init__.py +0 -0
  83. sourcerer/presentation/screens/shared/containers.py +13 -0
  84. sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  85. sourcerer/presentation/screens/shared/widgets/button.py +54 -0
  86. sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
  87. sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  88. sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
  89. sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
  90. sourcerer/presentation/settings.py +31 -0
  91. sourcerer/presentation/themes/__init__.py +0 -0
  92. sourcerer/presentation/themes/github_dark.py +21 -0
  93. sourcerer/presentation/utils.py +69 -0
  94. sourcerer/settings.py +72 -0
  95. 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
@@ -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)