falyx 0.1.50__py3-none-any.whl → 0.1.51__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.
- falyx/__main__.py +1 -1
- falyx/action/action.py +2 -2
- falyx/action/action_group.py +1 -1
- falyx/action/io_action.py +1 -4
- falyx/action/menu_action.py +5 -1
- falyx/action/process_pool_action.py +1 -1
- falyx/action/select_file_action.py +32 -4
- falyx/action/selection_action.py +85 -23
- falyx/action/user_input_action.py +3 -0
- falyx/command.py +2 -2
- falyx/context.py +1 -1
- falyx/execution_registry.py +4 -4
- falyx/falyx.py +8 -2
- falyx/{parsers → parser}/argparse.py +1 -1
- falyx/{parsers → parser}/utils.py +1 -1
- falyx/selection.py +73 -11
- falyx/validators.py +89 -1
- falyx/version.py +1 -1
- {falyx-0.1.50.dist-info → falyx-0.1.51.dist-info}/METADATA +1 -1
- {falyx-0.1.50.dist-info → falyx-0.1.51.dist-info}/RECORD +27 -28
- falyx/.coverage +0 -0
- /falyx/{parsers → parser}/.pytyped +0 -0
- /falyx/{parsers → parser}/__init__.py +0 -0
- /falyx/{parsers → parser}/parsers.py +0 -0
- /falyx/{parsers → parser}/signature.py +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.51.dist-info}/LICENSE +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.51.dist-info}/WHEEL +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.51.dist-info}/entry_points.txt +0 -0
falyx/__main__.py
CHANGED
@@ -14,7 +14,7 @@ from typing import Any
|
|
14
14
|
|
15
15
|
from falyx.config import loader
|
16
16
|
from falyx.falyx import Falyx
|
17
|
-
from falyx.
|
17
|
+
from falyx.parser import CommandArgumentParser, get_root_parser, get_subparsers
|
18
18
|
|
19
19
|
|
20
20
|
def find_falyx_config() -> Path | None:
|
falyx/action/action.py
CHANGED
@@ -157,6 +157,6 @@ class Action(BaseAction):
|
|
157
157
|
return (
|
158
158
|
f"Action(name={self.name!r}, action="
|
159
159
|
f"{getattr(self._action, '__name__', repr(self._action))}, "
|
160
|
-
f"
|
161
|
-
f"
|
160
|
+
f"retry={self.retry_policy.enabled}, "
|
161
|
+
f"rollback={self.rollback is not None})"
|
162
162
|
)
|
falyx/action/action_group.py
CHANGED
@@ -11,7 +11,7 @@ from falyx.context import ExecutionContext, SharedContext
|
|
11
11
|
from falyx.execution_registry import ExecutionRegistry as er
|
12
12
|
from falyx.hook_manager import Hook, HookManager, HookType
|
13
13
|
from falyx.logger import logger
|
14
|
-
from falyx.
|
14
|
+
from falyx.parser.utils import same_argument_definitions
|
15
15
|
from falyx.themes.colors import OneColors
|
16
16
|
|
17
17
|
|
falyx/action/io_action.py
CHANGED
@@ -93,10 +93,7 @@ class BaseIOAction(BaseAction):
|
|
93
93
|
if self.inject_last_result and self.shared_context:
|
94
94
|
return self.shared_context.last_result()
|
95
95
|
|
96
|
-
|
97
|
-
"[%s] No input provided and no last result found for injection.", self.name
|
98
|
-
)
|
99
|
-
raise FalyxError("No input provided and no last result to inject.")
|
96
|
+
return ""
|
100
97
|
|
101
98
|
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
102
99
|
return None, None
|
falyx/action/menu_action.py
CHANGED
@@ -111,7 +111,7 @@ class MenuAction(BaseAction):
|
|
111
111
|
key = effective_default
|
112
112
|
if not self.never_prompt:
|
113
113
|
table = self._build_table()
|
114
|
-
|
114
|
+
key_ = await prompt_for_selection(
|
115
115
|
self.menu_options.keys(),
|
116
116
|
table,
|
117
117
|
default_selection=self.default_selection,
|
@@ -120,6 +120,10 @@ class MenuAction(BaseAction):
|
|
120
120
|
prompt_message=self.prompt_message,
|
121
121
|
show_table=self.show_table,
|
122
122
|
)
|
123
|
+
if isinstance(key_, str):
|
124
|
+
key = key_
|
125
|
+
else:
|
126
|
+
assert False, "Unreachable, MenuAction only supports single selection"
|
123
127
|
option = self.menu_options[key]
|
124
128
|
result = await option.action(*args, **kwargs)
|
125
129
|
context.result = result
|
@@ -14,7 +14,7 @@ from falyx.context import ExecutionContext, SharedContext
|
|
14
14
|
from falyx.execution_registry import ExecutionRegistry as er
|
15
15
|
from falyx.hook_manager import HookManager, HookType
|
16
16
|
from falyx.logger import logger
|
17
|
-
from falyx.
|
17
|
+
from falyx.parser.utils import same_argument_definitions
|
18
18
|
from falyx.themes import OneColors
|
19
19
|
|
20
20
|
|
@@ -66,6 +66,9 @@ class SelectFileAction(BaseAction):
|
|
66
66
|
style: str = OneColors.WHITE,
|
67
67
|
suffix_filter: str | None = None,
|
68
68
|
return_type: FileReturnType | str = FileReturnType.PATH,
|
69
|
+
number_selections: int | str = 1,
|
70
|
+
separator: str = ",",
|
71
|
+
allow_duplicates: bool = False,
|
69
72
|
console: Console | None = None,
|
70
73
|
prompt_session: PromptSession | None = None,
|
71
74
|
):
|
@@ -76,6 +79,9 @@ class SelectFileAction(BaseAction):
|
|
76
79
|
self.prompt_message = prompt_message
|
77
80
|
self.suffix_filter = suffix_filter
|
78
81
|
self.style = style
|
82
|
+
self.number_selections = number_selections
|
83
|
+
self.separator = separator
|
84
|
+
self.allow_duplicates = allow_duplicates
|
79
85
|
if isinstance(console, Console):
|
80
86
|
self.console = console
|
81
87
|
elif console:
|
@@ -83,6 +89,21 @@ class SelectFileAction(BaseAction):
|
|
83
89
|
self.prompt_session = prompt_session or PromptSession()
|
84
90
|
self.return_type = self._coerce_return_type(return_type)
|
85
91
|
|
92
|
+
@property
|
93
|
+
def number_selections(self) -> int | str:
|
94
|
+
return self._number_selections
|
95
|
+
|
96
|
+
@number_selections.setter
|
97
|
+
def number_selections(self, value: int | str):
|
98
|
+
if isinstance(value, int) and value > 0:
|
99
|
+
self._number_selections: int | str = value
|
100
|
+
elif isinstance(value, str):
|
101
|
+
if value not in ("*"):
|
102
|
+
raise ValueError("number_selections string must be one of '*'")
|
103
|
+
self._number_selections = value
|
104
|
+
else:
|
105
|
+
raise ValueError("number_selections must be a positive integer or one of '*'")
|
106
|
+
|
86
107
|
def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType:
|
87
108
|
if isinstance(return_type, FileReturnType):
|
88
109
|
return return_type
|
@@ -163,18 +184,25 @@ class SelectFileAction(BaseAction):
|
|
163
184
|
title=self.title, selections=options | cancel_option, columns=self.columns
|
164
185
|
)
|
165
186
|
|
166
|
-
|
187
|
+
keys = await prompt_for_selection(
|
167
188
|
(options | cancel_option).keys(),
|
168
189
|
table,
|
169
190
|
console=self.console,
|
170
191
|
prompt_session=self.prompt_session,
|
171
192
|
prompt_message=self.prompt_message,
|
193
|
+
number_selections=self.number_selections,
|
194
|
+
separator=self.separator,
|
195
|
+
allow_duplicates=self.allow_duplicates,
|
196
|
+
cancel_key=cancel_key,
|
172
197
|
)
|
173
198
|
|
174
|
-
if
|
175
|
-
|
199
|
+
if isinstance(keys, str):
|
200
|
+
if keys == cancel_key:
|
201
|
+
raise CancelSignal("User canceled the selection.")
|
202
|
+
result = options[keys].value
|
203
|
+
elif isinstance(keys, list):
|
204
|
+
result = [options[key].value for key in keys]
|
176
205
|
|
177
|
-
result = options[key].value
|
178
206
|
context.result = result
|
179
207
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
180
208
|
return result
|
falyx/action/selection_action.py
CHANGED
@@ -48,6 +48,9 @@ class SelectionAction(BaseAction):
|
|
48
48
|
columns: int = 5,
|
49
49
|
prompt_message: str = "Select > ",
|
50
50
|
default_selection: str = "",
|
51
|
+
number_selections: int | str = 1,
|
52
|
+
separator: str = ",",
|
53
|
+
allow_duplicates: bool = False,
|
51
54
|
inject_last_result: bool = False,
|
52
55
|
inject_into: str = "last_result",
|
53
56
|
return_type: SelectionReturnType | str = "value",
|
@@ -73,9 +76,26 @@ class SelectionAction(BaseAction):
|
|
73
76
|
raise ValueError("`console` must be an instance of `rich.console.Console`")
|
74
77
|
self.prompt_session = prompt_session or PromptSession()
|
75
78
|
self.default_selection = default_selection
|
79
|
+
self.number_selections = number_selections
|
80
|
+
self.separator = separator
|
81
|
+
self.allow_duplicates = allow_duplicates
|
76
82
|
self.prompt_message = prompt_message
|
77
83
|
self.show_table = show_table
|
78
|
-
|
84
|
+
|
85
|
+
@property
|
86
|
+
def number_selections(self) -> int | str:
|
87
|
+
return self._number_selections
|
88
|
+
|
89
|
+
@number_selections.setter
|
90
|
+
def number_selections(self, value: int | str):
|
91
|
+
if isinstance(value, int) and value > 0:
|
92
|
+
self._number_selections: int | str = value
|
93
|
+
elif isinstance(value, str):
|
94
|
+
if value not in ("*"):
|
95
|
+
raise ValueError("number_selections string must be '*'")
|
96
|
+
self._number_selections = value
|
97
|
+
else:
|
98
|
+
raise ValueError("number_selections must be a positive integer or '*'")
|
79
99
|
|
80
100
|
def _coerce_return_type(
|
81
101
|
self, return_type: SelectionReturnType | str
|
@@ -156,6 +176,38 @@ class SelectionAction(BaseAction):
|
|
156
176
|
def get_infer_target(self) -> tuple[None, None]:
|
157
177
|
return None, None
|
158
178
|
|
179
|
+
def _get_result_from_keys(self, keys: str | list[str]) -> Any:
|
180
|
+
if not isinstance(self.selections, dict):
|
181
|
+
raise TypeError("Selections must be a dictionary to get result by keys.")
|
182
|
+
if self.return_type == SelectionReturnType.KEY:
|
183
|
+
result: Any = keys
|
184
|
+
elif self.return_type == SelectionReturnType.VALUE:
|
185
|
+
if isinstance(keys, list):
|
186
|
+
result = [self.selections[key].value for key in keys]
|
187
|
+
elif isinstance(keys, str):
|
188
|
+
result = self.selections[keys].value
|
189
|
+
elif self.return_type == SelectionReturnType.ITEMS:
|
190
|
+
if isinstance(keys, list):
|
191
|
+
result = {key: self.selections[key] for key in keys}
|
192
|
+
elif isinstance(keys, str):
|
193
|
+
result = {keys: self.selections[keys]}
|
194
|
+
elif self.return_type == SelectionReturnType.DESCRIPTION:
|
195
|
+
if isinstance(keys, list):
|
196
|
+
result = [self.selections[key].description for key in keys]
|
197
|
+
elif isinstance(keys, str):
|
198
|
+
result = self.selections[keys].description
|
199
|
+
elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
|
200
|
+
if isinstance(keys, list):
|
201
|
+
result = {
|
202
|
+
self.selections[key].description: self.selections[key].value
|
203
|
+
for key in keys
|
204
|
+
}
|
205
|
+
elif isinstance(keys, str):
|
206
|
+
result = {self.selections[keys].description: self.selections[keys].value}
|
207
|
+
else:
|
208
|
+
raise ValueError(f"Unsupported return type: {self.return_type}")
|
209
|
+
return result
|
210
|
+
|
159
211
|
async def _run(self, *args, **kwargs) -> Any:
|
160
212
|
kwargs = self._maybe_inject_last_result(kwargs)
|
161
213
|
context = ExecutionContext(
|
@@ -191,7 +243,7 @@ class SelectionAction(BaseAction):
|
|
191
243
|
if self.never_prompt and not effective_default:
|
192
244
|
raise ValueError(
|
193
245
|
f"[{self.name}] 'never_prompt' is True but no valid default_selection "
|
194
|
-
"was
|
246
|
+
"or usable last_result was available."
|
195
247
|
)
|
196
248
|
|
197
249
|
context.start_timer()
|
@@ -206,7 +258,7 @@ class SelectionAction(BaseAction):
|
|
206
258
|
formatter=self.cancel_formatter,
|
207
259
|
)
|
208
260
|
if not self.never_prompt:
|
209
|
-
|
261
|
+
indices: int | list[int] = await prompt_for_index(
|
210
262
|
len(self.selections),
|
211
263
|
table,
|
212
264
|
default_selection=effective_default,
|
@@ -214,12 +266,30 @@ class SelectionAction(BaseAction):
|
|
214
266
|
prompt_session=self.prompt_session,
|
215
267
|
prompt_message=self.prompt_message,
|
216
268
|
show_table=self.show_table,
|
269
|
+
number_selections=self.number_selections,
|
270
|
+
separator=self.separator,
|
271
|
+
allow_duplicates=self.allow_duplicates,
|
272
|
+
cancel_key=self.cancel_key,
|
217
273
|
)
|
218
274
|
else:
|
219
|
-
|
220
|
-
|
275
|
+
if effective_default:
|
276
|
+
indices = int(effective_default)
|
277
|
+
else:
|
278
|
+
raise ValueError(
|
279
|
+
f"[{self.name}] 'never_prompt' is True but no valid "
|
280
|
+
"default_selection was provided."
|
281
|
+
)
|
282
|
+
|
283
|
+
if indices == int(self.cancel_key):
|
221
284
|
raise CancelSignal("User cancelled the selection.")
|
222
|
-
|
285
|
+
if isinstance(indices, list):
|
286
|
+
result: str | list[str] = [
|
287
|
+
self.selections[index] for index in indices
|
288
|
+
]
|
289
|
+
elif isinstance(indices, int):
|
290
|
+
result = self.selections[indices]
|
291
|
+
else:
|
292
|
+
assert False, "unreachable"
|
223
293
|
elif isinstance(self.selections, dict):
|
224
294
|
cancel_option = {
|
225
295
|
self.cancel_key: SelectionOption(
|
@@ -232,7 +302,7 @@ class SelectionAction(BaseAction):
|
|
232
302
|
columns=self.columns,
|
233
303
|
)
|
234
304
|
if not self.never_prompt:
|
235
|
-
|
305
|
+
keys = await prompt_for_selection(
|
236
306
|
(self.selections | cancel_option).keys(),
|
237
307
|
table,
|
238
308
|
default_selection=effective_default,
|
@@ -240,25 +310,17 @@ class SelectionAction(BaseAction):
|
|
240
310
|
prompt_session=self.prompt_session,
|
241
311
|
prompt_message=self.prompt_message,
|
242
312
|
show_table=self.show_table,
|
313
|
+
number_selections=self.number_selections,
|
314
|
+
separator=self.separator,
|
315
|
+
allow_duplicates=self.allow_duplicates,
|
316
|
+
cancel_key=self.cancel_key,
|
243
317
|
)
|
244
318
|
else:
|
245
|
-
|
246
|
-
if
|
319
|
+
keys = effective_default
|
320
|
+
if keys == self.cancel_key:
|
247
321
|
raise CancelSignal("User cancelled the selection.")
|
248
|
-
|
249
|
-
|
250
|
-
elif self.return_type == SelectionReturnType.VALUE:
|
251
|
-
result = self.selections[key].value
|
252
|
-
elif self.return_type == SelectionReturnType.ITEMS:
|
253
|
-
result = {key: self.selections[key]}
|
254
|
-
elif self.return_type == SelectionReturnType.DESCRIPTION:
|
255
|
-
result = self.selections[key].description
|
256
|
-
elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
|
257
|
-
result = {
|
258
|
-
self.selections[key].description: self.selections[key].value
|
259
|
-
}
|
260
|
-
else:
|
261
|
-
raise ValueError(f"Unsupported return type: {self.return_type}")
|
322
|
+
|
323
|
+
result = self._get_result_from_keys(keys)
|
262
324
|
else:
|
263
325
|
raise TypeError(
|
264
326
|
"'selections' must be a list[str] or dict[str, Any], "
|
@@ -29,6 +29,7 @@ class UserInputAction(BaseAction):
|
|
29
29
|
name: str,
|
30
30
|
*,
|
31
31
|
prompt_text: str = "Input > ",
|
32
|
+
default_text: str = "",
|
32
33
|
validator: Validator | None = None,
|
33
34
|
console: Console | None = None,
|
34
35
|
prompt_session: PromptSession | None = None,
|
@@ -45,6 +46,7 @@ class UserInputAction(BaseAction):
|
|
45
46
|
elif console:
|
46
47
|
raise ValueError("`console` must be an instance of `rich.console.Console`")
|
47
48
|
self.prompt_session = prompt_session or PromptSession()
|
49
|
+
self.default_text = default_text
|
48
50
|
|
49
51
|
def get_infer_target(self) -> tuple[None, None]:
|
50
52
|
return None, None
|
@@ -67,6 +69,7 @@ class UserInputAction(BaseAction):
|
|
67
69
|
answer = await self.prompt_session.prompt_async(
|
68
70
|
prompt_text,
|
69
71
|
validator=self.validator,
|
72
|
+
default=kwargs.get("default_text", self.default_text),
|
70
73
|
)
|
71
74
|
context.result = answer
|
72
75
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
falyx/command.py
CHANGED
@@ -34,8 +34,8 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
34
34
|
from falyx.hook_manager import HookManager, HookType
|
35
35
|
from falyx.logger import logger
|
36
36
|
from falyx.options_manager import OptionsManager
|
37
|
-
from falyx.
|
38
|
-
from falyx.
|
37
|
+
from falyx.parser.argparse import CommandArgumentParser
|
38
|
+
from falyx.parser.signature import infer_args_from_func
|
39
39
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
40
40
|
from falyx.protocols import ArgParserProtocol
|
41
41
|
from falyx.retry import RetryPolicy
|
falyx/context.py
CHANGED
@@ -129,7 +129,7 @@ class ExecutionContext(BaseModel):
|
|
129
129
|
args = ", ".join(map(repr, self.args))
|
130
130
|
kwargs = ", ".join(f"{key}={value!r}" for key, value in self.kwargs.items())
|
131
131
|
signature = ", ".join(filter(None, [args, kwargs]))
|
132
|
-
return f"{self.
|
132
|
+
return f"{self.action} ({signature})"
|
133
133
|
|
134
134
|
def as_dict(self) -> dict:
|
135
135
|
return {
|
falyx/execution_registry.py
CHANGED
@@ -112,7 +112,7 @@ class ExecutionRegistry:
|
|
112
112
|
cls,
|
113
113
|
name: str = "",
|
114
114
|
index: int | None = None,
|
115
|
-
|
115
|
+
result_index: int | None = None,
|
116
116
|
clear: bool = False,
|
117
117
|
last_result: bool = False,
|
118
118
|
status: Literal["all", "success", "error"] = "all",
|
@@ -138,12 +138,12 @@ class ExecutionRegistry:
|
|
138
138
|
)
|
139
139
|
return
|
140
140
|
|
141
|
-
if
|
141
|
+
if result_index is not None and result_index >= 0:
|
142
142
|
try:
|
143
|
-
result_context = cls._store_by_index[
|
143
|
+
result_context = cls._store_by_index[result_index]
|
144
144
|
except KeyError:
|
145
145
|
cls._console.print(
|
146
|
-
f"[{OneColors.DARK_RED}]❌ No execution found for index {
|
146
|
+
f"[{OneColors.DARK_RED}]❌ No execution found for index {result_index}."
|
147
147
|
)
|
148
148
|
return
|
149
149
|
cls._console.print(f"{result_context.signature}:")
|
falyx/falyx.py
CHANGED
@@ -59,7 +59,7 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
59
59
|
from falyx.hook_manager import Hook, HookManager, HookType
|
60
60
|
from falyx.logger import logger
|
61
61
|
from falyx.options_manager import OptionsManager
|
62
|
-
from falyx.
|
62
|
+
from falyx.parser import CommandArgumentParser, FalyxParsers, get_arg_parsers
|
63
63
|
from falyx.protocols import ArgParserProtocol
|
64
64
|
from falyx.retry import RetryPolicy
|
65
65
|
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
@@ -330,7 +330,13 @@ class Falyx:
|
|
330
330
|
action="store_true",
|
331
331
|
help="Clear the Execution History.",
|
332
332
|
)
|
333
|
-
parser.add_argument(
|
333
|
+
parser.add_argument(
|
334
|
+
"-r",
|
335
|
+
"--result",
|
336
|
+
type=int,
|
337
|
+
dest="result_index",
|
338
|
+
help="Get the result by index",
|
339
|
+
)
|
334
340
|
parser.add_argument(
|
335
341
|
"-l", "--last-result", action="store_true", help="Get the last result"
|
336
342
|
)
|
@@ -12,7 +12,7 @@ from rich.text import Text
|
|
12
12
|
|
13
13
|
from falyx.action.base import BaseAction
|
14
14
|
from falyx.exceptions import CommandArgumentError
|
15
|
-
from falyx.
|
15
|
+
from falyx.parser.utils import coerce_value
|
16
16
|
from falyx.signals import HelpSignal
|
17
17
|
|
18
18
|
|
@@ -7,7 +7,7 @@ from dateutil import parser as date_parser
|
|
7
7
|
|
8
8
|
from falyx.action.base import BaseAction
|
9
9
|
from falyx.logger import logger
|
10
|
-
from falyx.
|
10
|
+
from falyx.parser.signature import infer_args_from_func
|
11
11
|
|
12
12
|
|
13
13
|
def coerce_bool(value: str) -> bool:
|
falyx/selection.py
CHANGED
@@ -11,7 +11,7 @@ from rich.table import Table
|
|
11
11
|
|
12
12
|
from falyx.themes import OneColors
|
13
13
|
from falyx.utils import CaseInsensitiveDict, chunks
|
14
|
-
from falyx.validators import
|
14
|
+
from falyx.validators import MultiIndexValidator, MultiKeyValidator
|
15
15
|
|
16
16
|
|
17
17
|
@dataclass
|
@@ -271,7 +271,11 @@ async def prompt_for_index(
|
|
271
271
|
prompt_session: PromptSession | None = None,
|
272
272
|
prompt_message: str = "Select an option > ",
|
273
273
|
show_table: bool = True,
|
274
|
-
|
274
|
+
number_selections: int | str = 1,
|
275
|
+
separator: str = ",",
|
276
|
+
allow_duplicates: bool = False,
|
277
|
+
cancel_key: str = "",
|
278
|
+
) -> int | list[int]:
|
275
279
|
prompt_session = prompt_session or PromptSession()
|
276
280
|
console = console or Console(color_system="truecolor")
|
277
281
|
|
@@ -280,10 +284,22 @@ async def prompt_for_index(
|
|
280
284
|
|
281
285
|
selection = await prompt_session.prompt_async(
|
282
286
|
message=prompt_message,
|
283
|
-
validator=
|
287
|
+
validator=MultiIndexValidator(
|
288
|
+
min_index,
|
289
|
+
max_index,
|
290
|
+
number_selections,
|
291
|
+
separator,
|
292
|
+
allow_duplicates,
|
293
|
+
cancel_key,
|
294
|
+
),
|
284
295
|
default=default_selection,
|
285
296
|
)
|
286
|
-
|
297
|
+
|
298
|
+
if selection.strip() == cancel_key:
|
299
|
+
return int(cancel_key)
|
300
|
+
if isinstance(number_selections, int) and number_selections == 1:
|
301
|
+
return int(selection.strip())
|
302
|
+
return [int(index.strip()) for index in selection.strip().split(separator)]
|
287
303
|
|
288
304
|
|
289
305
|
async def prompt_for_selection(
|
@@ -295,7 +311,11 @@ async def prompt_for_selection(
|
|
295
311
|
prompt_session: PromptSession | None = None,
|
296
312
|
prompt_message: str = "Select an option > ",
|
297
313
|
show_table: bool = True,
|
298
|
-
|
314
|
+
number_selections: int | str = 1,
|
315
|
+
separator: str = ",",
|
316
|
+
allow_duplicates: bool = False,
|
317
|
+
cancel_key: str = "",
|
318
|
+
) -> str | list[str]:
|
299
319
|
"""Prompt the user to select a key from a set of options. Return the selected key."""
|
300
320
|
prompt_session = prompt_session or PromptSession()
|
301
321
|
console = console or Console(color_system="truecolor")
|
@@ -305,11 +325,17 @@ async def prompt_for_selection(
|
|
305
325
|
|
306
326
|
selected = await prompt_session.prompt_async(
|
307
327
|
message=prompt_message,
|
308
|
-
validator=
|
328
|
+
validator=MultiKeyValidator(
|
329
|
+
keys, number_selections, separator, allow_duplicates, cancel_key
|
330
|
+
),
|
309
331
|
default=default_selection,
|
310
332
|
)
|
311
333
|
|
312
|
-
|
334
|
+
if selected.strip() == cancel_key:
|
335
|
+
return cancel_key
|
336
|
+
if isinstance(number_selections, int) and number_selections == 1:
|
337
|
+
return selected.strip()
|
338
|
+
return [key.strip() for key in selected.strip().split(separator)]
|
313
339
|
|
314
340
|
|
315
341
|
async def select_value_from_list(
|
@@ -320,6 +346,10 @@ async def select_value_from_list(
|
|
320
346
|
prompt_session: PromptSession | None = None,
|
321
347
|
prompt_message: str = "Select an option > ",
|
322
348
|
default_selection: str = "",
|
349
|
+
number_selections: int | str = 1,
|
350
|
+
separator: str = ",",
|
351
|
+
allow_duplicates: bool = False,
|
352
|
+
cancel_key: str = "",
|
323
353
|
columns: int = 4,
|
324
354
|
caption: str = "",
|
325
355
|
box_style: box.Box = box.SIMPLE,
|
@@ -332,7 +362,7 @@ async def select_value_from_list(
|
|
332
362
|
title_style: str = "",
|
333
363
|
caption_style: str = "",
|
334
364
|
highlight: bool = False,
|
335
|
-
):
|
365
|
+
) -> str | list[str]:
|
336
366
|
"""Prompt for a selection. Return the selected item."""
|
337
367
|
table = render_selection_indexed_table(
|
338
368
|
title=title,
|
@@ -360,8 +390,14 @@ async def select_value_from_list(
|
|
360
390
|
console=console,
|
361
391
|
prompt_session=prompt_session,
|
362
392
|
prompt_message=prompt_message,
|
393
|
+
number_selections=number_selections,
|
394
|
+
separator=separator,
|
395
|
+
allow_duplicates=allow_duplicates,
|
396
|
+
cancel_key=cancel_key,
|
363
397
|
)
|
364
398
|
|
399
|
+
if isinstance(selection_index, list):
|
400
|
+
return [selections[i] for i in selection_index]
|
365
401
|
return selections[selection_index]
|
366
402
|
|
367
403
|
|
@@ -373,7 +409,11 @@ async def select_key_from_dict(
|
|
373
409
|
prompt_session: PromptSession | None = None,
|
374
410
|
prompt_message: str = "Select an option > ",
|
375
411
|
default_selection: str = "",
|
376
|
-
|
412
|
+
number_selections: int | str = 1,
|
413
|
+
separator: str = ",",
|
414
|
+
allow_duplicates: bool = False,
|
415
|
+
cancel_key: str = "",
|
416
|
+
) -> str | list[str]:
|
377
417
|
"""Prompt for a key from a dict, returns the key."""
|
378
418
|
prompt_session = prompt_session or PromptSession()
|
379
419
|
console = console or Console(color_system="truecolor")
|
@@ -387,6 +427,10 @@ async def select_key_from_dict(
|
|
387
427
|
console=console,
|
388
428
|
prompt_session=prompt_session,
|
389
429
|
prompt_message=prompt_message,
|
430
|
+
number_selections=number_selections,
|
431
|
+
separator=separator,
|
432
|
+
allow_duplicates=allow_duplicates,
|
433
|
+
cancel_key=cancel_key,
|
390
434
|
)
|
391
435
|
|
392
436
|
|
@@ -398,7 +442,11 @@ async def select_value_from_dict(
|
|
398
442
|
prompt_session: PromptSession | None = None,
|
399
443
|
prompt_message: str = "Select an option > ",
|
400
444
|
default_selection: str = "",
|
401
|
-
|
445
|
+
number_selections: int | str = 1,
|
446
|
+
separator: str = ",",
|
447
|
+
allow_duplicates: bool = False,
|
448
|
+
cancel_key: str = "",
|
449
|
+
) -> Any | list[Any]:
|
402
450
|
"""Prompt for a key from a dict, but return the value."""
|
403
451
|
prompt_session = prompt_session or PromptSession()
|
404
452
|
console = console or Console(color_system="truecolor")
|
@@ -412,8 +460,14 @@ async def select_value_from_dict(
|
|
412
460
|
console=console,
|
413
461
|
prompt_session=prompt_session,
|
414
462
|
prompt_message=prompt_message,
|
463
|
+
number_selections=number_selections,
|
464
|
+
separator=separator,
|
465
|
+
allow_duplicates=allow_duplicates,
|
466
|
+
cancel_key=cancel_key,
|
415
467
|
)
|
416
468
|
|
469
|
+
if isinstance(selection_key, list):
|
470
|
+
return [selections[key].value for key in selection_key]
|
417
471
|
return selections[selection_key].value
|
418
472
|
|
419
473
|
|
@@ -425,7 +479,11 @@ async def get_selection_from_dict_menu(
|
|
425
479
|
prompt_session: PromptSession | None = None,
|
426
480
|
prompt_message: str = "Select an option > ",
|
427
481
|
default_selection: str = "",
|
428
|
-
|
482
|
+
number_selections: int | str = 1,
|
483
|
+
separator: str = ",",
|
484
|
+
allow_duplicates: bool = False,
|
485
|
+
cancel_key: str = "",
|
486
|
+
) -> Any | list[Any]:
|
429
487
|
"""Prompt for a key from a dict, but return the value."""
|
430
488
|
table = render_selection_dict_table(
|
431
489
|
title,
|
@@ -439,4 +497,8 @@ async def get_selection_from_dict_menu(
|
|
439
497
|
prompt_session=prompt_session,
|
440
498
|
prompt_message=prompt_message,
|
441
499
|
default_selection=default_selection,
|
500
|
+
number_selections=number_selections,
|
501
|
+
separator=separator,
|
502
|
+
allow_duplicates=allow_duplicates,
|
503
|
+
cancel_key=cancel_key,
|
442
504
|
)
|
falyx/validators.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"""validators.py"""
|
3
3
|
from typing import KeysView, Sequence
|
4
4
|
|
5
|
-
from prompt_toolkit.validation import Validator
|
5
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
6
6
|
|
7
7
|
|
8
8
|
def int_range_validator(minimum: int, maximum: int) -> Validator:
|
@@ -45,3 +45,91 @@ def yes_no_validator() -> Validator:
|
|
45
45
|
return True
|
46
46
|
|
47
47
|
return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.")
|
48
|
+
|
49
|
+
|
50
|
+
class MultiIndexValidator(Validator):
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
minimum: int,
|
54
|
+
maximum: int,
|
55
|
+
number_selections: int | str,
|
56
|
+
separator: str,
|
57
|
+
allow_duplicates: bool,
|
58
|
+
cancel_key: str,
|
59
|
+
) -> None:
|
60
|
+
self.minimum = minimum
|
61
|
+
self.maximum = maximum
|
62
|
+
self.number_selections = number_selections
|
63
|
+
self.separator = separator
|
64
|
+
self.allow_duplicates = allow_duplicates
|
65
|
+
self.cancel_key = cancel_key
|
66
|
+
super().__init__()
|
67
|
+
|
68
|
+
def validate(self, document):
|
69
|
+
selections = [
|
70
|
+
index.strip() for index in document.text.strip().split(self.separator)
|
71
|
+
]
|
72
|
+
if not selections or selections == [""]:
|
73
|
+
raise ValidationError(message="Select at least 1 item.")
|
74
|
+
if self.cancel_key in selections and len(selections) == 1:
|
75
|
+
return
|
76
|
+
elif self.cancel_key in selections:
|
77
|
+
raise ValidationError(message="Cancel key must be selected alone.")
|
78
|
+
for selection in selections:
|
79
|
+
try:
|
80
|
+
index = int(selection)
|
81
|
+
if not self.minimum <= index <= self.maximum:
|
82
|
+
raise ValidationError(
|
83
|
+
message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
|
84
|
+
)
|
85
|
+
except ValueError:
|
86
|
+
raise ValidationError(
|
87
|
+
message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
|
88
|
+
)
|
89
|
+
if not self.allow_duplicates and selections.count(selection) > 1:
|
90
|
+
raise ValidationError(message=f"Duplicate selection: {selection}")
|
91
|
+
if isinstance(self.number_selections, int):
|
92
|
+
if self.number_selections == 1 and len(selections) > 1:
|
93
|
+
raise ValidationError(message="Invalid selection. Select only 1 item.")
|
94
|
+
if len(selections) != self.number_selections:
|
95
|
+
raise ValidationError(
|
96
|
+
message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
class MultiKeyValidator(Validator):
|
101
|
+
def __init__(
|
102
|
+
self,
|
103
|
+
keys: Sequence[str] | KeysView[str],
|
104
|
+
number_selections: int | str,
|
105
|
+
separator: str,
|
106
|
+
allow_duplicates: bool,
|
107
|
+
cancel_key: str,
|
108
|
+
) -> None:
|
109
|
+
self.keys = keys
|
110
|
+
self.separator = separator
|
111
|
+
self.number_selections = number_selections
|
112
|
+
self.allow_duplicates = allow_duplicates
|
113
|
+
self.cancel_key = cancel_key
|
114
|
+
super().__init__()
|
115
|
+
|
116
|
+
def validate(self, document):
|
117
|
+
selections = [key.strip() for key in document.text.strip().split(self.separator)]
|
118
|
+
if not selections or selections == [""]:
|
119
|
+
raise ValidationError(message="Select at least 1 item.")
|
120
|
+
if self.cancel_key in selections and len(selections) == 1:
|
121
|
+
return
|
122
|
+
elif self.cancel_key in selections:
|
123
|
+
raise ValidationError(message="Cancel key must be selected alone.")
|
124
|
+
for selection in selections:
|
125
|
+
if selection.upper() not in [key.upper() for key in self.keys]:
|
126
|
+
raise ValidationError(message=f"Invalid selection: {selection}")
|
127
|
+
if not self.allow_duplicates and selections.count(selection) > 1:
|
128
|
+
raise ValidationError(message=f"Duplicate selection: {selection}")
|
129
|
+
if isinstance(self.number_selections, int):
|
130
|
+
if self.number_selections == 1 and len(selections) > 1:
|
131
|
+
raise ValidationError(message="Invalid selection. Select only 1 item.")
|
132
|
+
if len(selections) != self.number_selections:
|
133
|
+
raise ValidationError(
|
134
|
+
message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
|
135
|
+
)
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.51"
|
@@ -1,62 +1,61 @@
|
|
1
|
-
falyx/.coverage,sha256=DNx1Ew1vSvuIcKko7httsyL62erJxVQ6CKtuJKxRVj4,53248
|
2
1
|
falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
2
|
falyx/__init__.py,sha256=Gh88lQ5pbD7xbGWrBgslE2kSTZKY9TkvKSa53rZ3l8U,305
|
4
|
-
falyx/__main__.py,sha256=
|
3
|
+
falyx/__main__.py,sha256=xHO4pB45rccixo-ougF84QJeB36ef8mEZXWVK_CJL9M,3420
|
5
4
|
falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
5
|
falyx/action/__init__.py,sha256=4E3Rb0GgGcmggrPJh0YFiwbVgN_PQjIzL06-Z3qMReo,1247
|
7
|
-
falyx/action/action.py,sha256=
|
6
|
+
falyx/action/action.py,sha256=j9ANO0xRfWoiNK6z-46T04EKAJ7GhjVrzb8_U8afEAA,5753
|
8
7
|
falyx/action/action_factory.py,sha256=br-P7Oip-4tZkO8qVT_ECwLe6idYjJa_GuBi5QR7vS4,4832
|
9
|
-
falyx/action/action_group.py,sha256=
|
8
|
+
falyx/action/action_group.py,sha256=_u64s81kgLAQFlTic3OJJoa8DwYJ2kqBh2U-_mpuPhs,6834
|
10
9
|
falyx/action/base.py,sha256=B7mt66oznmhv2qpSOwOuScgMckVXrxjRMU2buzZkRD8,5866
|
11
10
|
falyx/action/chained_action.py,sha256=aV_plUdDVdc1o-oU57anbWkw33jgRIh4W29QwEA_1Mw,8501
|
12
11
|
falyx/action/fallback_action.py,sha256=0z5l0s_LKnhIwgMdykm8lJqs246DKSpyYs-p7PnsKok,1619
|
13
12
|
falyx/action/http_action.py,sha256=DNeSBWh58UTFGlfFyTk2GnhS54hpLAJLC0QNbq2cYic,5799
|
14
|
-
falyx/action/io_action.py,sha256=
|
13
|
+
falyx/action/io_action.py,sha256=9vx3mX9lBUBmwl1Xtzr5-idEwlMKlK1ZQuZkzTmORJc,9881
|
15
14
|
falyx/action/literal_input_action.py,sha256=7H2VX_L5VaytVdV2uis-VTGi782kQtwKTB8T04c7J1k,1293
|
16
|
-
falyx/action/menu_action.py,sha256=
|
15
|
+
falyx/action/menu_action.py,sha256=SLqwmQ1TOt8kl_cgIWogBYfx8lYPLZa4E-Yy6M2cX_w,6069
|
17
16
|
falyx/action/mixins.py,sha256=eni8_PwzMnuwh0ZqOdzCdAyWlOphoiqL7z27xnFsg5s,1117
|
18
17
|
falyx/action/process_action.py,sha256=HsDqlKy1PkG3HHC6mHa4O6ayY_oKVY2qj5nDRJuSn24,4571
|
19
|
-
falyx/action/process_pool_action.py,sha256=
|
18
|
+
falyx/action/process_pool_action.py,sha256=8RQSjJaU0nGY_ObpcO31uI-HfNY7krqMN2wSzTnJ8jw,5939
|
20
19
|
falyx/action/prompt_menu_action.py,sha256=corzjpPNVMYKncfueeRUWwklnlZHN-Fc61psOzbZELg,5286
|
21
|
-
falyx/action/select_file_action.py,sha256=
|
22
|
-
falyx/action/selection_action.py,sha256=
|
20
|
+
falyx/action/select_file_action.py,sha256=2T4I1CLvHLAAqNUD2rFBIpdi74BP5amU4yTHUOGnd64,9911
|
21
|
+
falyx/action/selection_action.py,sha256=Mav39iTkVIJPDvmDek8R2bSF18f-mII56l5sSzZSPII,15735
|
23
22
|
falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
|
24
23
|
falyx/action/types.py,sha256=NfZz1ufZuvCgp-he2JIItbnjX7LjOUadjtKbjpRlSIY,1399
|
25
|
-
falyx/action/user_input_action.py,sha256=
|
24
|
+
falyx/action/user_input_action.py,sha256=EnwSk-ZW0bqSlEnWpUE9_0jmoFCoTGMQc5PqP49cSyg,3750
|
26
25
|
falyx/bottom_bar.py,sha256=KPACb9VC0I3dv_pYZLqy7e4uA_KT5dSfwnvuknyV0FI,7388
|
27
|
-
falyx/command.py,sha256=
|
26
|
+
falyx/command.py,sha256=7BM4JPK36dsWm2JPmRwarLJiDRk1GgfX5HESbeysuMY,16417
|
28
27
|
falyx/config.py,sha256=Cm1F9SfNSbugPALxaEz7NRqp1wrk-g2jYq35bQzN2uE,9658
|
29
|
-
falyx/context.py,sha256=
|
28
|
+
falyx/context.py,sha256=b9PGkIfhc1BbFUmaqmr4AojzONfKG1c9WP2uixzCJGQ,10806
|
30
29
|
falyx/debug.py,sha256=pguI0XQcZ-7jte5YUPexAufa1oxxalYO1JgmO6GU3rI,1557
|
31
30
|
falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
|
32
|
-
falyx/execution_registry.py,sha256=
|
33
|
-
falyx/falyx.py,sha256=
|
31
|
+
falyx/execution_registry.py,sha256=7t_96-Q7R7MAJBvWwAt5IAERp0TjbGZPGeeJ1s24ey8,7628
|
32
|
+
falyx/falyx.py,sha256=zOAah7OYHZgMRI60zwXjNgfeEHs8y87SDs3Meslood0,49805
|
34
33
|
falyx/hook_manager.py,sha256=TFuHQnAncS_rk6vuw-VSx8bnAppLuHfrZCrzLwqcO9o,2979
|
35
34
|
falyx/hooks.py,sha256=xMfQROib0BNsaQF4AXJpmCiGePoE1f1xpcdibgnVZWM,2913
|
36
35
|
falyx/init.py,sha256=F9jg7mLPoBWXdJnc_fyWG7zVQSnrAO8ueDiP8AJxDWE,3331
|
37
36
|
falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
|
38
37
|
falyx/menu.py,sha256=E580qZsx08bnWcqRVjJuD2Fy8Zh_1zIexp5f0lC7L2c,3745
|
39
38
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
40
|
-
falyx/
|
41
|
-
falyx/
|
42
|
-
falyx/
|
43
|
-
falyx/
|
44
|
-
falyx/
|
45
|
-
falyx/
|
39
|
+
falyx/parser/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
+
falyx/parser/__init__.py,sha256=ZfPmbtEUechDvgl99-lWhTXmFnXS_FMXJ_xb8KGEJLo,448
|
41
|
+
falyx/parser/argparse.py,sha256=izJDlhHxyP1y0-NjiuLcNpHYMIO6zS4nY_-v-Xg-O94,37503
|
42
|
+
falyx/parser/parsers.py,sha256=MXWC8OQ3apDaeKfY0O4J8NnkxofWVOCRnKatC00lGm0,8796
|
43
|
+
falyx/parser/signature.py,sha256=cCa-yKUcbbET0Ho45oFZWWHFGCX5a_LaAOWRP7b87po,2465
|
44
|
+
falyx/parser/utils.py,sha256=VX4C58pJdHQihkaLIrYmcwqHJrFjbNjb5blEU3IqSAE,2892
|
46
45
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
47
46
|
falyx/protocols.py,sha256=-9GbCBUzzsEgw2_KOCYqxxzWJuez0eHmwnZp_ShY0jc,493
|
48
47
|
falyx/retry.py,sha256=sGRE9QhdZK98M99G8F15WUsJ_fYLNyLlCgu3UANaSQs,3744
|
49
48
|
falyx/retry_utils.py,sha256=vwoZmFVCGVqZ13BX_xi3qZZVsmSxkp-jfaf6kJtBV9c,723
|
50
|
-
falyx/selection.py,sha256=
|
49
|
+
falyx/selection.py,sha256=TPSM_KKGHedJblWI0AzxTZR2haZjRF3k-gQoQeR3L28,15239
|
51
50
|
falyx/signals.py,sha256=Y_neFXpfHs7qY0syw9XcfR9WeAGRcRw1nG_2L1JJqKE,1083
|
52
51
|
falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
|
53
52
|
falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
54
53
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
55
54
|
falyx/utils.py,sha256=U45xnZFUdoFC4xiji_9S1jHS5V7MvxSDtufP8EgB0SM,6732
|
56
|
-
falyx/validators.py,sha256=
|
57
|
-
falyx/version.py,sha256=
|
58
|
-
falyx-0.1.
|
59
|
-
falyx-0.1.
|
60
|
-
falyx-0.1.
|
61
|
-
falyx-0.1.
|
62
|
-
falyx-0.1.
|
55
|
+
falyx/validators.py,sha256=Pbdxh5777Y03HxyArAh2ApeVSx23in4w4K38G43Vt98,5197
|
56
|
+
falyx/version.py,sha256=J5DqQAfJrmJsVwP_F1QEYvB7YXVaXR6y-BTblSWQs-k,23
|
57
|
+
falyx-0.1.51.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
58
|
+
falyx-0.1.51.dist-info/METADATA,sha256=1pOPjynlq0g3vfgJaV2VqedsHH4O-3Egjo1WEengCns,5561
|
59
|
+
falyx-0.1.51.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
60
|
+
falyx-0.1.51.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
61
|
+
falyx-0.1.51.dist-info/RECORD,,
|
falyx/.coverage
DELETED
Binary file
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|