falyx 0.1.50__py3-none-any.whl → 0.1.52__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/__init__.py +2 -1
- falyx/action/action.py +2 -2
- falyx/action/action_group.py +3 -1
- falyx/action/chained_action.py +2 -0
- falyx/action/fallback_action.py +2 -0
- falyx/action/io_action.py +1 -97
- falyx/action/literal_input_action.py +2 -0
- falyx/action/menu_action.py +5 -1
- falyx/action/mixins.py +2 -0
- falyx/action/process_action.py +2 -0
- falyx/action/process_pool_action.py +3 -1
- falyx/action/select_file_action.py +32 -4
- falyx/action/selection_action.py +85 -23
- falyx/action/shell_action.py +105 -0
- falyx/action/types.py +2 -0
- falyx/action/user_input_action.py +5 -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}/__init__.py +3 -1
- falyx/parser/argument.py +98 -0
- falyx/parser/argument_action.py +27 -0
- falyx/{parsers/argparse.py → parser/command_argument_parser.py} +4 -116
- falyx/{parsers → parser}/signature.py +1 -0
- falyx/{parsers → parser}/utils.py +2 -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.52.dist-info}/METADATA +1 -1
- falyx-0.1.52.dist-info/RECORD +64 -0
- falyx/.coverage +0 -0
- falyx-0.1.50.dist-info/RECORD +0 -62
- /falyx/{parsers → parser}/.pytyped +0 -0
- /falyx/{parsers → parser}/parsers.py +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.52.dist-info}/LICENSE +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.52.dist-info}/WHEEL +0 -0
- {falyx-0.1.50.dist-info → falyx-0.1.52.dist-info}/entry_points.txt +0 -0
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
|
)
|
@@ -5,7 +5,9 @@ Copyright (c) 2025 rtj.dev LLC.
|
|
5
5
|
Licensed under the MIT License. See LICENSE file for details.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from .
|
8
|
+
from .argument import Argument
|
9
|
+
from .argument_action import ArgumentAction
|
10
|
+
from .command_argument_parser import CommandArgumentParser
|
9
11
|
from .parsers import FalyxParsers, get_arg_parsers, get_root_parser, get_subparsers
|
10
12
|
|
11
13
|
__all__ = [
|
falyx/parser/argument.py
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""argument.py"""
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from falyx.action.base import BaseAction
|
7
|
+
from falyx.parser.argument_action import ArgumentAction
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class Argument:
|
12
|
+
"""Represents a command-line argument."""
|
13
|
+
|
14
|
+
flags: tuple[str, ...]
|
15
|
+
dest: str # Destination name for the argument
|
16
|
+
action: ArgumentAction = (
|
17
|
+
ArgumentAction.STORE
|
18
|
+
) # Action to be taken when the argument is encountered
|
19
|
+
type: Any = str # Type of the argument (e.g., str, int, float) or callable
|
20
|
+
default: Any = None # Default value if the argument is not provided
|
21
|
+
choices: list[str] | None = None # List of valid choices for the argument
|
22
|
+
required: bool = False # True if the argument is required
|
23
|
+
help: str = "" # Help text for the argument
|
24
|
+
nargs: int | str | None = None # int, '?', '*', '+', None
|
25
|
+
positional: bool = False # True if no leading - or -- in flags
|
26
|
+
resolver: BaseAction | None = None # Action object for the argument
|
27
|
+
|
28
|
+
def get_positional_text(self) -> str:
|
29
|
+
"""Get the positional text for the argument."""
|
30
|
+
text = ""
|
31
|
+
if self.positional:
|
32
|
+
if self.choices:
|
33
|
+
text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
34
|
+
else:
|
35
|
+
text = self.dest
|
36
|
+
return text
|
37
|
+
|
38
|
+
def get_choice_text(self) -> str:
|
39
|
+
"""Get the choice text for the argument."""
|
40
|
+
choice_text = ""
|
41
|
+
if self.choices:
|
42
|
+
choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
43
|
+
elif (
|
44
|
+
self.action
|
45
|
+
in (
|
46
|
+
ArgumentAction.STORE,
|
47
|
+
ArgumentAction.APPEND,
|
48
|
+
ArgumentAction.EXTEND,
|
49
|
+
)
|
50
|
+
and not self.positional
|
51
|
+
):
|
52
|
+
choice_text = self.dest.upper()
|
53
|
+
elif self.action in (
|
54
|
+
ArgumentAction.STORE,
|
55
|
+
ArgumentAction.APPEND,
|
56
|
+
ArgumentAction.EXTEND,
|
57
|
+
) or isinstance(self.nargs, str):
|
58
|
+
choice_text = self.dest
|
59
|
+
|
60
|
+
if self.nargs == "?":
|
61
|
+
choice_text = f"[{choice_text}]"
|
62
|
+
elif self.nargs == "*":
|
63
|
+
choice_text = f"[{choice_text} ...]"
|
64
|
+
elif self.nargs == "+":
|
65
|
+
choice_text = f"{choice_text} [{choice_text} ...]"
|
66
|
+
return choice_text
|
67
|
+
|
68
|
+
def __eq__(self, other: object) -> bool:
|
69
|
+
if not isinstance(other, Argument):
|
70
|
+
return False
|
71
|
+
return (
|
72
|
+
self.flags == other.flags
|
73
|
+
and self.dest == other.dest
|
74
|
+
and self.action == other.action
|
75
|
+
and self.type == other.type
|
76
|
+
and self.choices == other.choices
|
77
|
+
and self.required == other.required
|
78
|
+
and self.nargs == other.nargs
|
79
|
+
and self.positional == other.positional
|
80
|
+
and self.default == other.default
|
81
|
+
and self.help == other.help
|
82
|
+
)
|
83
|
+
|
84
|
+
def __hash__(self) -> int:
|
85
|
+
return hash(
|
86
|
+
(
|
87
|
+
tuple(self.flags),
|
88
|
+
self.dest,
|
89
|
+
self.action,
|
90
|
+
self.type,
|
91
|
+
tuple(self.choices or []),
|
92
|
+
self.required,
|
93
|
+
self.nargs,
|
94
|
+
self.positional,
|
95
|
+
self.default,
|
96
|
+
self.help,
|
97
|
+
)
|
98
|
+
)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""argument_action.py"""
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
|
8
|
+
class ArgumentAction(Enum):
|
9
|
+
"""Defines the action to be taken when the argument is encountered."""
|
10
|
+
|
11
|
+
ACTION = "action"
|
12
|
+
STORE = "store"
|
13
|
+
STORE_TRUE = "store_true"
|
14
|
+
STORE_FALSE = "store_false"
|
15
|
+
APPEND = "append"
|
16
|
+
EXTEND = "extend"
|
17
|
+
COUNT = "count"
|
18
|
+
HELP = "help"
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def choices(cls) -> list[ArgumentAction]:
|
22
|
+
"""Return a list of all argument actions."""
|
23
|
+
return list(cls)
|
24
|
+
|
25
|
+
def __str__(self) -> str:
|
26
|
+
"""Return the string representation of the argument action."""
|
27
|
+
return self.value
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""command_argument_parser.py"""
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
from copy import deepcopy
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from enum import Enum
|
7
6
|
from typing import Any, Iterable
|
8
7
|
|
9
8
|
from rich.console import Console
|
@@ -12,123 +11,12 @@ from rich.text import Text
|
|
12
11
|
|
13
12
|
from falyx.action.base import BaseAction
|
14
13
|
from falyx.exceptions import CommandArgumentError
|
15
|
-
from falyx.
|
14
|
+
from falyx.parser.argument import Argument
|
15
|
+
from falyx.parser.argument_action import ArgumentAction
|
16
|
+
from falyx.parser.utils import coerce_value
|
16
17
|
from falyx.signals import HelpSignal
|
17
18
|
|
18
19
|
|
19
|
-
class ArgumentAction(Enum):
|
20
|
-
"""Defines the action to be taken when the argument is encountered."""
|
21
|
-
|
22
|
-
ACTION = "action"
|
23
|
-
STORE = "store"
|
24
|
-
STORE_TRUE = "store_true"
|
25
|
-
STORE_FALSE = "store_false"
|
26
|
-
APPEND = "append"
|
27
|
-
EXTEND = "extend"
|
28
|
-
COUNT = "count"
|
29
|
-
HELP = "help"
|
30
|
-
|
31
|
-
@classmethod
|
32
|
-
def choices(cls) -> list[ArgumentAction]:
|
33
|
-
"""Return a list of all argument actions."""
|
34
|
-
return list(cls)
|
35
|
-
|
36
|
-
def __str__(self) -> str:
|
37
|
-
"""Return the string representation of the argument action."""
|
38
|
-
return self.value
|
39
|
-
|
40
|
-
|
41
|
-
@dataclass
|
42
|
-
class Argument:
|
43
|
-
"""Represents a command-line argument."""
|
44
|
-
|
45
|
-
flags: tuple[str, ...]
|
46
|
-
dest: str # Destination name for the argument
|
47
|
-
action: ArgumentAction = (
|
48
|
-
ArgumentAction.STORE
|
49
|
-
) # Action to be taken when the argument is encountered
|
50
|
-
type: Any = str # Type of the argument (e.g., str, int, float) or callable
|
51
|
-
default: Any = None # Default value if the argument is not provided
|
52
|
-
choices: list[str] | None = None # List of valid choices for the argument
|
53
|
-
required: bool = False # True if the argument is required
|
54
|
-
help: str = "" # Help text for the argument
|
55
|
-
nargs: int | str | None = None # int, '?', '*', '+', None
|
56
|
-
positional: bool = False # True if no leading - or -- in flags
|
57
|
-
resolver: BaseAction | None = None # Action object for the argument
|
58
|
-
|
59
|
-
def get_positional_text(self) -> str:
|
60
|
-
"""Get the positional text for the argument."""
|
61
|
-
text = ""
|
62
|
-
if self.positional:
|
63
|
-
if self.choices:
|
64
|
-
text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
65
|
-
else:
|
66
|
-
text = self.dest
|
67
|
-
return text
|
68
|
-
|
69
|
-
def get_choice_text(self) -> str:
|
70
|
-
"""Get the choice text for the argument."""
|
71
|
-
choice_text = ""
|
72
|
-
if self.choices:
|
73
|
-
choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
74
|
-
elif (
|
75
|
-
self.action
|
76
|
-
in (
|
77
|
-
ArgumentAction.STORE,
|
78
|
-
ArgumentAction.APPEND,
|
79
|
-
ArgumentAction.EXTEND,
|
80
|
-
)
|
81
|
-
and not self.positional
|
82
|
-
):
|
83
|
-
choice_text = self.dest.upper()
|
84
|
-
elif self.action in (
|
85
|
-
ArgumentAction.STORE,
|
86
|
-
ArgumentAction.APPEND,
|
87
|
-
ArgumentAction.EXTEND,
|
88
|
-
) or isinstance(self.nargs, str):
|
89
|
-
choice_text = self.dest
|
90
|
-
|
91
|
-
if self.nargs == "?":
|
92
|
-
choice_text = f"[{choice_text}]"
|
93
|
-
elif self.nargs == "*":
|
94
|
-
choice_text = f"[{choice_text} ...]"
|
95
|
-
elif self.nargs == "+":
|
96
|
-
choice_text = f"{choice_text} [{choice_text} ...]"
|
97
|
-
return choice_text
|
98
|
-
|
99
|
-
def __eq__(self, other: object) -> bool:
|
100
|
-
if not isinstance(other, Argument):
|
101
|
-
return False
|
102
|
-
return (
|
103
|
-
self.flags == other.flags
|
104
|
-
and self.dest == other.dest
|
105
|
-
and self.action == other.action
|
106
|
-
and self.type == other.type
|
107
|
-
and self.choices == other.choices
|
108
|
-
and self.required == other.required
|
109
|
-
and self.nargs == other.nargs
|
110
|
-
and self.positional == other.positional
|
111
|
-
and self.default == other.default
|
112
|
-
and self.help == other.help
|
113
|
-
)
|
114
|
-
|
115
|
-
def __hash__(self) -> int:
|
116
|
-
return hash(
|
117
|
-
(
|
118
|
-
tuple(self.flags),
|
119
|
-
self.dest,
|
120
|
-
self.action,
|
121
|
-
self.type,
|
122
|
-
tuple(self.choices or []),
|
123
|
-
self.required,
|
124
|
-
self.nargs,
|
125
|
-
self.positional,
|
126
|
-
self.default,
|
127
|
-
self.help,
|
128
|
-
)
|
129
|
-
)
|
130
|
-
|
131
|
-
|
132
20
|
class CommandArgumentParser:
|
133
21
|
"""
|
134
22
|
Custom argument parser for Falyx Commands.
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
1
2
|
import types
|
2
3
|
from datetime import datetime
|
3
4
|
from enum import EnumMeta
|
@@ -7,7 +8,7 @@ from dateutil import parser as date_parser
|
|
7
8
|
|
8
9
|
from falyx.action.base import BaseAction
|
9
10
|
from falyx.logger import logger
|
10
|
-
from falyx.
|
11
|
+
from falyx.parser.signature import infer_args_from_func
|
11
12
|
|
12
13
|
|
13
14
|
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.52"
|