falyx 0.1.44__tar.gz → 0.1.45__tar.gz
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-0.1.44 → falyx-0.1.45}/PKG-INFO +2 -1
- {falyx-0.1.44 → falyx-0.1.45}/falyx/command.py +21 -4
- {falyx-0.1.44 → falyx-0.1.45}/falyx/config.py +0 -9
- {falyx-0.1.44 → falyx-0.1.45}/falyx/falyx.py +0 -10
- {falyx-0.1.44 → falyx-0.1.45}/falyx/parsers/argparse.py +17 -8
- falyx-0.1.45/falyx/parsers/utils.py +97 -0
- falyx-0.1.45/falyx/version.py +1 -0
- {falyx-0.1.44 → falyx-0.1.45}/pyproject.toml +2 -1
- falyx-0.1.44/falyx/parsers/utils.py +0 -28
- falyx-0.1.44/falyx/version.py +0 -1
- {falyx-0.1.44 → falyx-0.1.45}/LICENSE +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/README.md +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/.pytyped +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/__init__.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/__main__.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/.pytyped +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/__init__.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/action_factory.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/action_group.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/base.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/chained_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/fallback_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/http_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/io_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/literal_input_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/menu_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/mixins.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/process_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/process_pool_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/prompt_menu_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/select_file_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/selection_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/signal_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/types.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/action/user_input_action.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/bottom_bar.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/context.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/debug.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/exceptions.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/execution_registry.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/hook_manager.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/hooks.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/init.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/logger.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/menu.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/options_manager.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/parsers/.pytyped +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/parsers/__init__.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/parsers/parsers.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/parsers/signature.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/prompt_utils.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/protocols.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/retry.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/retry_utils.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/selection.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/signals.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/tagged_table.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/themes/__init__.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/themes/colors.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/utils.py +0 -0
- {falyx-0.1.44 → falyx-0.1.45}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: falyx
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.45
|
4
4
|
Summary: Reliable and introspectable async CLI action framework.
|
5
5
|
License: MIT
|
6
6
|
Author: Roland Thomas Jr
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: aiohttp (>=3.11,<4.0)
|
16
16
|
Requires-Dist: prompt_toolkit (>=3.0,<4.0)
|
17
17
|
Requires-Dist: pydantic (>=2.0,<3.0)
|
18
|
+
Requires-Dist: python-dateutil (>=2.8,<3.0)
|
18
19
|
Requires-Dist: python-json-logger (>=3.3.0,<4.0.0)
|
19
20
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
20
21
|
Requires-Dist: rich (>=13.0,<14.0)
|
@@ -128,7 +128,7 @@ class Command(BaseModel):
|
|
128
128
|
tags: list[str] = Field(default_factory=list)
|
129
129
|
logging_hooks: bool = False
|
130
130
|
options_manager: OptionsManager = Field(default_factory=OptionsManager)
|
131
|
-
arg_parser: CommandArgumentParser =
|
131
|
+
arg_parser: CommandArgumentParser | None = None
|
132
132
|
arguments: list[dict[str, Any]] = Field(default_factory=list)
|
133
133
|
argument_config: Callable[[CommandArgumentParser], None] | None = None
|
134
134
|
custom_parser: ArgParserProtocol | None = None
|
@@ -167,6 +167,12 @@ class Command(BaseModel):
|
|
167
167
|
raw_args,
|
168
168
|
)
|
169
169
|
return ((), {})
|
170
|
+
if not isinstance(self.arg_parser, CommandArgumentParser):
|
171
|
+
logger.warning(
|
172
|
+
"[Command:%s] No argument parser configured, using default parsing.",
|
173
|
+
self.key,
|
174
|
+
)
|
175
|
+
return ((), {})
|
170
176
|
return await self.arg_parser.parse_args_split(
|
171
177
|
raw_args, from_validate=from_validate
|
172
178
|
)
|
@@ -183,7 +189,9 @@ class Command(BaseModel):
|
|
183
189
|
def get_argument_definitions(self) -> list[dict[str, Any]]:
|
184
190
|
if self.arguments:
|
185
191
|
return self.arguments
|
186
|
-
elif callable(self.argument_config)
|
192
|
+
elif callable(self.argument_config) and isinstance(
|
193
|
+
self.arg_parser, CommandArgumentParser
|
194
|
+
):
|
187
195
|
self.argument_config(self.arg_parser)
|
188
196
|
elif self.auto_args:
|
189
197
|
if isinstance(self.action, BaseAction):
|
@@ -219,8 +227,17 @@ class Command(BaseModel):
|
|
219
227
|
if self.logging_hooks and isinstance(self.action, BaseAction):
|
220
228
|
register_debug_hooks(self.action.hooks)
|
221
229
|
|
222
|
-
|
223
|
-
self.arg_parser
|
230
|
+
if self.arg_parser is None:
|
231
|
+
self.arg_parser = CommandArgumentParser(
|
232
|
+
command_key=self.key,
|
233
|
+
command_description=self.description,
|
234
|
+
command_style=self.style,
|
235
|
+
help_text=self.help_text,
|
236
|
+
help_epilogue=self.help_epilogue,
|
237
|
+
aliases=self.aliases,
|
238
|
+
)
|
239
|
+
for arg_def in self.get_argument_definitions():
|
240
|
+
self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
|
224
241
|
|
225
242
|
def _inject_options_manager(self) -> None:
|
226
243
|
"""Inject the options manager into the action if applicable."""
|
@@ -118,14 +118,6 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
|
|
118
118
|
commands = []
|
119
119
|
for entry in raw_commands:
|
120
120
|
raw_command = RawCommand(**entry)
|
121
|
-
parser = CommandArgumentParser(
|
122
|
-
command_key=raw_command.key,
|
123
|
-
command_description=raw_command.description,
|
124
|
-
command_style=raw_command.style,
|
125
|
-
help_text=raw_command.help_text,
|
126
|
-
help_epilogue=raw_command.help_epilogue,
|
127
|
-
aliases=raw_command.aliases,
|
128
|
-
)
|
129
121
|
commands.append(
|
130
122
|
Command.model_validate(
|
131
123
|
{
|
@@ -133,7 +125,6 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
|
|
133
125
|
"action": wrap_if_needed(
|
134
126
|
import_action(raw_command.action), name=raw_command.description
|
135
127
|
),
|
136
|
-
"arg_parser": parser,
|
137
128
|
}
|
138
129
|
)
|
139
130
|
)
|
@@ -359,7 +359,6 @@ class Falyx:
|
|
359
359
|
action=Action("Help", self._show_help),
|
360
360
|
style=OneColors.LIGHT_YELLOW,
|
361
361
|
arg_parser=parser,
|
362
|
-
auto_args=False,
|
363
362
|
)
|
364
363
|
|
365
364
|
def _get_completer(self) -> WordCompleter:
|
@@ -655,15 +654,6 @@ class Falyx:
|
|
655
654
|
"arg_parser must be an instance of CommandArgumentParser."
|
656
655
|
)
|
657
656
|
arg_parser = arg_parser
|
658
|
-
else:
|
659
|
-
arg_parser = CommandArgumentParser(
|
660
|
-
command_key=key,
|
661
|
-
command_description=description,
|
662
|
-
command_style=style,
|
663
|
-
help_text=help_text,
|
664
|
-
help_epilogue=help_epilogue,
|
665
|
-
aliases=aliases,
|
666
|
-
)
|
667
657
|
|
668
658
|
command = Command(
|
669
659
|
key=key,
|
@@ -12,6 +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.parsers.utils import coerce_value
|
15
16
|
from falyx.signals import HelpSignal
|
16
17
|
|
17
18
|
|
@@ -290,7 +291,7 @@ class CommandArgumentParser:
|
|
290
291
|
for choice in choices:
|
291
292
|
if not isinstance(choice, expected_type):
|
292
293
|
try:
|
293
|
-
|
294
|
+
coerce_value(choice, expected_type)
|
294
295
|
except Exception:
|
295
296
|
raise CommandArgumentError(
|
296
297
|
f"Invalid choice {choice!r}: not coercible to {expected_type.__name__}"
|
@@ -303,7 +304,7 @@ class CommandArgumentParser:
|
|
303
304
|
"""Validate the default value type."""
|
304
305
|
if default is not None and not isinstance(default, expected_type):
|
305
306
|
try:
|
306
|
-
|
307
|
+
coerce_value(default, expected_type)
|
307
308
|
except Exception:
|
308
309
|
raise CommandArgumentError(
|
309
310
|
f"Default value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
|
@@ -316,7 +317,7 @@ class CommandArgumentParser:
|
|
316
317
|
for item in default:
|
317
318
|
if not isinstance(item, expected_type):
|
318
319
|
try:
|
319
|
-
|
320
|
+
coerce_value(item, expected_type)
|
320
321
|
except Exception:
|
321
322
|
raise CommandArgumentError(
|
322
323
|
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
|
@@ -595,7 +596,7 @@ class CommandArgumentParser:
|
|
595
596
|
i += new_i
|
596
597
|
|
597
598
|
try:
|
598
|
-
typed = [spec.type
|
599
|
+
typed = [coerce_value(value, spec.type) for value in values]
|
599
600
|
except Exception:
|
600
601
|
raise CommandArgumentError(
|
601
602
|
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
@@ -680,7 +681,9 @@ class CommandArgumentParser:
|
|
680
681
|
), "resolver should be an instance of BaseAction"
|
681
682
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
682
683
|
try:
|
683
|
-
typed_values = [
|
684
|
+
typed_values = [
|
685
|
+
coerce_value(value, spec.type) for value in values
|
686
|
+
]
|
684
687
|
except ValueError:
|
685
688
|
raise CommandArgumentError(
|
686
689
|
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
@@ -709,7 +712,9 @@ class CommandArgumentParser:
|
|
709
712
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
710
713
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
711
714
|
try:
|
712
|
-
typed_values = [
|
715
|
+
typed_values = [
|
716
|
+
coerce_value(value, spec.type) for value in values
|
717
|
+
]
|
713
718
|
except ValueError:
|
714
719
|
raise CommandArgumentError(
|
715
720
|
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
@@ -724,7 +729,9 @@ class CommandArgumentParser:
|
|
724
729
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
725
730
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
726
731
|
try:
|
727
|
-
typed_values = [
|
732
|
+
typed_values = [
|
733
|
+
coerce_value(value, spec.type) for value in values
|
734
|
+
]
|
728
735
|
except ValueError:
|
729
736
|
raise CommandArgumentError(
|
730
737
|
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
@@ -735,7 +742,9 @@ class CommandArgumentParser:
|
|
735
742
|
else:
|
736
743
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
737
744
|
try:
|
738
|
-
typed_values = [
|
745
|
+
typed_values = [
|
746
|
+
coerce_value(value, spec.type) for value in values
|
747
|
+
]
|
739
748
|
except ValueError:
|
740
749
|
raise CommandArgumentError(
|
741
750
|
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import types
|
2
|
+
from datetime import datetime
|
3
|
+
from enum import EnumMeta
|
4
|
+
from typing import Any, Literal, Union, get_args, get_origin
|
5
|
+
|
6
|
+
from dateutil import parser as date_parser
|
7
|
+
|
8
|
+
from falyx.action.base import BaseAction
|
9
|
+
from falyx.logger import logger
|
10
|
+
from falyx.parsers.signature import infer_args_from_func
|
11
|
+
|
12
|
+
|
13
|
+
def coerce_bool(value: str) -> bool:
|
14
|
+
if isinstance(value, bool):
|
15
|
+
return value
|
16
|
+
value = value.strip().lower()
|
17
|
+
if value in {"true", "1", "yes", "on"}:
|
18
|
+
return True
|
19
|
+
elif value in {"false", "0", "no", "off"}:
|
20
|
+
return False
|
21
|
+
return bool(value)
|
22
|
+
|
23
|
+
|
24
|
+
def coerce_enum(value: Any, enum_type: EnumMeta) -> Any:
|
25
|
+
if isinstance(value, enum_type):
|
26
|
+
return value
|
27
|
+
|
28
|
+
if isinstance(value, str):
|
29
|
+
try:
|
30
|
+
return enum_type[value]
|
31
|
+
except KeyError:
|
32
|
+
pass
|
33
|
+
|
34
|
+
base_type = type(next(iter(enum_type)).value)
|
35
|
+
print(base_type)
|
36
|
+
try:
|
37
|
+
coerced_value = base_type(value)
|
38
|
+
return enum_type(coerced_value)
|
39
|
+
except (ValueError, TypeError):
|
40
|
+
raise ValueError(f"Value '{value}' could not be coerced to enum type {enum_type}")
|
41
|
+
|
42
|
+
|
43
|
+
def coerce_value(value: str, target_type: type) -> Any:
|
44
|
+
origin = get_origin(target_type)
|
45
|
+
args = get_args(target_type)
|
46
|
+
|
47
|
+
if origin is Literal:
|
48
|
+
if value not in args:
|
49
|
+
raise ValueError(
|
50
|
+
f"Value '{value}' is not a valid literal for type {target_type}"
|
51
|
+
)
|
52
|
+
return value
|
53
|
+
|
54
|
+
if isinstance(target_type, types.UnionType) or get_origin(target_type) is Union:
|
55
|
+
for arg in args:
|
56
|
+
try:
|
57
|
+
return coerce_value(value, arg)
|
58
|
+
except Exception:
|
59
|
+
continue
|
60
|
+
raise ValueError(f"Value '{value}' could not be coerced to any of {args!r}")
|
61
|
+
|
62
|
+
if isinstance(target_type, EnumMeta):
|
63
|
+
return coerce_enum(value, target_type)
|
64
|
+
|
65
|
+
if target_type is bool:
|
66
|
+
return coerce_bool(value)
|
67
|
+
|
68
|
+
if target_type is datetime:
|
69
|
+
try:
|
70
|
+
return date_parser.parse(value)
|
71
|
+
except ValueError as e:
|
72
|
+
raise ValueError(f"Value '{value}' could not be parsed as a datetime") from e
|
73
|
+
|
74
|
+
return target_type(value)
|
75
|
+
|
76
|
+
|
77
|
+
def same_argument_definitions(
|
78
|
+
actions: list[Any],
|
79
|
+
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
80
|
+
) -> list[dict[str, Any]] | None:
|
81
|
+
|
82
|
+
arg_sets = []
|
83
|
+
for action in actions:
|
84
|
+
if isinstance(action, BaseAction):
|
85
|
+
infer_target, _ = action.get_infer_target()
|
86
|
+
arg_defs = infer_args_from_func(infer_target, arg_metadata)
|
87
|
+
elif callable(action):
|
88
|
+
arg_defs = infer_args_from_func(action, arg_metadata)
|
89
|
+
else:
|
90
|
+
logger.debug("Auto args unsupported for action: %s", action)
|
91
|
+
return None
|
92
|
+
arg_sets.append(arg_defs)
|
93
|
+
|
94
|
+
first = arg_sets[0]
|
95
|
+
if all(arg_set == first for arg_set in arg_sets[1:]):
|
96
|
+
return first
|
97
|
+
return None
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.45"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "falyx"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.45"
|
4
4
|
description = "Reliable and introspectable async CLI action framework."
|
5
5
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
6
6
|
license = "MIT"
|
@@ -16,6 +16,7 @@ python-json-logger = "^3.3.0"
|
|
16
16
|
toml = "^0.10"
|
17
17
|
pyyaml = "^6.0"
|
18
18
|
aiohttp = "^3.11"
|
19
|
+
python-dateutil = "^2.8"
|
19
20
|
|
20
21
|
[tool.poetry.group.dev.dependencies]
|
21
22
|
pytest = "^8.3.5"
|
@@ -1,28 +0,0 @@
|
|
1
|
-
from typing import Any
|
2
|
-
|
3
|
-
from falyx.action.base import BaseAction
|
4
|
-
from falyx.logger import logger
|
5
|
-
from falyx.parsers.signature import infer_args_from_func
|
6
|
-
|
7
|
-
|
8
|
-
def same_argument_definitions(
|
9
|
-
actions: list[Any],
|
10
|
-
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
11
|
-
) -> list[dict[str, Any]] | None:
|
12
|
-
|
13
|
-
arg_sets = []
|
14
|
-
for action in actions:
|
15
|
-
if isinstance(action, BaseAction):
|
16
|
-
infer_target, _ = action.get_infer_target()
|
17
|
-
arg_defs = infer_args_from_func(infer_target, arg_metadata)
|
18
|
-
elif callable(action):
|
19
|
-
arg_defs = infer_args_from_func(action, arg_metadata)
|
20
|
-
else:
|
21
|
-
logger.debug("Auto args unsupported for action: %s", action)
|
22
|
-
return None
|
23
|
-
arg_sets.append(arg_defs)
|
24
|
-
|
25
|
-
first = arg_sets[0]
|
26
|
-
if all(arg_set == first for arg_set in arg_sets[1:]):
|
27
|
-
return first
|
28
|
-
return None
|
falyx-0.1.44/falyx/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.44"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|