falyx 0.1.28__py3-none-any.whl → 0.1.30__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/action/.pytyped +0 -0
- falyx/action/action.py +41 -7
- falyx/action/action_factory.py +4 -1
- falyx/action/io_action.py +20 -9
- falyx/action/menu_action.py +3 -0
- falyx/action/select_file_action.py +3 -0
- falyx/action/selection_action.py +3 -0
- falyx/action/user_input_action.py +3 -0
- falyx/command.py +52 -9
- falyx/falyx.py +52 -24
- falyx/parsers/.pytyped +0 -0
- falyx/parsers/__init__.py +17 -0
- falyx/{argparse.py → parsers/argparse.py} +183 -23
- falyx/parsers/signature.py +74 -0
- falyx/parsers/utils.py +27 -0
- falyx/version.py +1 -1
- {falyx-0.1.28.dist-info → falyx-0.1.30.dist-info}/METADATA +1 -1
- {falyx-0.1.28.dist-info → falyx-0.1.30.dist-info}/RECORD +22 -18
- falyx/config_schema.py +0 -76
- /falyx/{parsers.py → parsers/parsers.py} +0 -0
- {falyx-0.1.28.dist-info → falyx-0.1.30.dist-info}/LICENSE +0 -0
- {falyx-0.1.28.dist-info → falyx-0.1.30.dist-info}/WHEEL +0 -0
- {falyx-0.1.28.dist-info → falyx-0.1.30.dist-info}/entry_points.txt +0 -0
falyx/action/.pytyped
ADDED
File without changes
|
falyx/action/action.py
CHANGED
@@ -47,6 +47,7 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
47
47
|
from falyx.hook_manager import Hook, HookManager, HookType
|
48
48
|
from falyx.logger import logger
|
49
49
|
from falyx.options_manager import OptionsManager
|
50
|
+
from falyx.parsers.utils import same_argument_definitions
|
50
51
|
from falyx.retry import RetryHandler, RetryPolicy
|
51
52
|
from falyx.themes import OneColors
|
52
53
|
from falyx.utils import ensure_async
|
@@ -101,6 +102,14 @@ class BaseAction(ABC):
|
|
101
102
|
async def preview(self, parent: Tree | None = None):
|
102
103
|
raise NotImplementedError("preview must be implemented by subclasses")
|
103
104
|
|
105
|
+
@abstractmethod
|
106
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
107
|
+
"""
|
108
|
+
Returns the callable to be used for argument inference.
|
109
|
+
By default, it returns None.
|
110
|
+
"""
|
111
|
+
raise NotImplementedError("get_infer_target must be implemented by subclasses")
|
112
|
+
|
104
113
|
def set_options_manager(self, options_manager: OptionsManager) -> None:
|
105
114
|
self.options_manager = options_manager
|
106
115
|
|
@@ -246,6 +255,13 @@ class Action(BaseAction):
|
|
246
255
|
if policy.enabled:
|
247
256
|
self.enable_retry()
|
248
257
|
|
258
|
+
def get_infer_target(self) -> Callable[..., Any]:
|
259
|
+
"""
|
260
|
+
Returns the callable to be used for argument inference.
|
261
|
+
By default, it returns the action itself.
|
262
|
+
"""
|
263
|
+
return self.action
|
264
|
+
|
249
265
|
async def _run(self, *args, **kwargs) -> Any:
|
250
266
|
combined_args = args + self.args
|
251
267
|
combined_kwargs = self._maybe_inject_last_result({**self.kwargs, **kwargs})
|
@@ -477,6 +493,14 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
477
493
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
478
494
|
action.register_teardown(self.hooks)
|
479
495
|
|
496
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
497
|
+
if self.actions:
|
498
|
+
return self.actions[0].get_infer_target()
|
499
|
+
return None
|
500
|
+
|
501
|
+
def _clear_args(self):
|
502
|
+
return (), {}
|
503
|
+
|
480
504
|
async def _run(self, *args, **kwargs) -> list[Any]:
|
481
505
|
if not self.actions:
|
482
506
|
raise EmptyChainError(f"[{self.name}] No actions to execute.")
|
@@ -505,12 +529,8 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
505
529
|
continue
|
506
530
|
shared_context.current_index = index
|
507
531
|
prepared = action.prepare(shared_context, self.options_manager)
|
508
|
-
last_result = shared_context.last_result()
|
509
532
|
try:
|
510
|
-
|
511
|
-
result = await prepared(**{prepared.inject_into: last_result})
|
512
|
-
else:
|
513
|
-
result = await prepared(*args, **updated_kwargs)
|
533
|
+
result = await prepared(*args, **updated_kwargs)
|
514
534
|
except Exception as error:
|
515
535
|
if index + 1 < len(self.actions) and isinstance(
|
516
536
|
self.actions[index + 1], FallbackAction
|
@@ -529,6 +549,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
529
549
|
fallback._skip_in_chain = True
|
530
550
|
else:
|
531
551
|
raise
|
552
|
+
args, updated_kwargs = self._clear_args()
|
532
553
|
shared_context.add_result(result)
|
533
554
|
context.extra["results"].append(result)
|
534
555
|
context.extra["rollback_stack"].append(prepared)
|
@@ -669,6 +690,16 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
669
690
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
670
691
|
action.register_teardown(self.hooks)
|
671
692
|
|
693
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
694
|
+
arg_defs = same_argument_definitions(self.actions)
|
695
|
+
if arg_defs:
|
696
|
+
return self.actions[0].get_infer_target()
|
697
|
+
logger.debug(
|
698
|
+
"[%s] auto_args disabled: mismatched ActionGroup arguments",
|
699
|
+
self.name,
|
700
|
+
)
|
701
|
+
return None
|
702
|
+
|
672
703
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
673
704
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
674
705
|
if self.shared_context:
|
@@ -787,8 +818,11 @@ class ProcessAction(BaseAction):
|
|
787
818
|
self.executor = executor or ProcessPoolExecutor()
|
788
819
|
self.is_retryable = True
|
789
820
|
|
790
|
-
|
791
|
-
|
821
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
822
|
+
return self.action
|
823
|
+
|
824
|
+
async def _run(self, *args, **kwargs) -> Any:
|
825
|
+
if self.inject_last_result and self.shared_context:
|
792
826
|
last_result = self.shared_context.last_result()
|
793
827
|
if not self._validate_pickleable(last_result):
|
794
828
|
raise ValueError(
|
falyx/action/action_factory.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""action_factory.py"""
|
3
|
-
from typing import Any
|
3
|
+
from typing import Any, Callable
|
4
4
|
|
5
5
|
from rich.tree import Tree
|
6
6
|
|
@@ -55,6 +55,9 @@ class ActionFactoryAction(BaseAction):
|
|
55
55
|
def factory(self, value: ActionFactoryProtocol):
|
56
56
|
self._factory = ensure_async(value)
|
57
57
|
|
58
|
+
def get_infer_target(self) -> Callable[..., Any]:
|
59
|
+
return self.factory
|
60
|
+
|
58
61
|
async def _run(self, *args, **kwargs) -> Any:
|
59
62
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
60
63
|
context = ExecutionContext(
|
falyx/action/io_action.py
CHANGED
@@ -19,7 +19,7 @@ import asyncio
|
|
19
19
|
import shlex
|
20
20
|
import subprocess
|
21
21
|
import sys
|
22
|
-
from typing import Any
|
22
|
+
from typing import Any, Callable
|
23
23
|
|
24
24
|
from rich.tree import Tree
|
25
25
|
|
@@ -81,15 +81,15 @@ class BaseIOAction(BaseAction):
|
|
81
81
|
def to_output(self, result: Any) -> str | bytes:
|
82
82
|
raise NotImplementedError
|
83
83
|
|
84
|
-
async def _resolve_input(
|
85
|
-
|
86
|
-
|
84
|
+
async def _resolve_input(
|
85
|
+
self, args: tuple[Any], kwargs: dict[str, Any]
|
86
|
+
) -> str | bytes:
|
87
87
|
data = await self._read_stdin()
|
88
88
|
if data:
|
89
89
|
return self.from_input(data)
|
90
90
|
|
91
|
-
if
|
92
|
-
return
|
91
|
+
if len(args) == 1:
|
92
|
+
return self.from_input(args[0])
|
93
93
|
|
94
94
|
if self.inject_last_result and self.shared_context:
|
95
95
|
return self.shared_context.last_result()
|
@@ -99,6 +99,9 @@ class BaseIOAction(BaseAction):
|
|
99
99
|
)
|
100
100
|
raise FalyxError("No input provided and no last result to inject.")
|
101
101
|
|
102
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
103
|
+
return None
|
104
|
+
|
102
105
|
async def __call__(self, *args, **kwargs):
|
103
106
|
context = ExecutionContext(
|
104
107
|
name=self.name,
|
@@ -117,8 +120,8 @@ class BaseIOAction(BaseAction):
|
|
117
120
|
pass
|
118
121
|
result = getattr(self, "_last_result", None)
|
119
122
|
else:
|
120
|
-
parsed_input = await self._resolve_input(kwargs)
|
121
|
-
result = await self._run(parsed_input
|
123
|
+
parsed_input = await self._resolve_input(args, kwargs)
|
124
|
+
result = await self._run(parsed_input)
|
122
125
|
output = self.to_output(result)
|
123
126
|
await self._write_stdout(output)
|
124
127
|
context.result = result
|
@@ -220,11 +223,19 @@ class ShellAction(BaseIOAction):
|
|
220
223
|
)
|
221
224
|
return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
|
222
225
|
|
226
|
+
def get_infer_target(self) -> Callable[..., Any] | None:
|
227
|
+
if sys.stdin.isatty():
|
228
|
+
return self._run
|
229
|
+
return None
|
230
|
+
|
223
231
|
async def _run(self, parsed_input: str) -> str:
|
224
232
|
# Replace placeholder in template, or use raw input as full command
|
225
233
|
command = self.command_template.format(parsed_input)
|
226
234
|
if self.safe_mode:
|
227
|
-
|
235
|
+
try:
|
236
|
+
args = shlex.split(command)
|
237
|
+
except ValueError as error:
|
238
|
+
raise FalyxError(f"Invalid command template: {error}")
|
228
239
|
result = subprocess.run(args, capture_output=True, text=True, check=True)
|
229
240
|
else:
|
230
241
|
result = subprocess.run(
|
falyx/action/menu_action.py
CHANGED
@@ -73,6 +73,9 @@ class MenuAction(BaseAction):
|
|
73
73
|
table.add_row(*row)
|
74
74
|
return table
|
75
75
|
|
76
|
+
def get_infer_target(self) -> None:
|
77
|
+
return None
|
78
|
+
|
76
79
|
async def _run(self, *args, **kwargs) -> Any:
|
77
80
|
kwargs = self._maybe_inject_last_result(kwargs)
|
78
81
|
context = ExecutionContext(
|
@@ -121,6 +121,9 @@ class SelectFileAction(BaseAction):
|
|
121
121
|
logger.warning("[ERROR] Failed to parse %s: %s", file.name, error)
|
122
122
|
return options
|
123
123
|
|
124
|
+
def get_infer_target(self) -> None:
|
125
|
+
return None
|
126
|
+
|
124
127
|
async def _run(self, *args, **kwargs) -> Any:
|
125
128
|
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
126
129
|
context.start_timer()
|
falyx/action/selection_action.py
CHANGED
@@ -85,6 +85,9 @@ class SelectionAction(BaseAction):
|
|
85
85
|
f"got {type(value).__name__}"
|
86
86
|
)
|
87
87
|
|
88
|
+
def get_infer_target(self) -> None:
|
89
|
+
return None
|
90
|
+
|
88
91
|
async def _run(self, *args, **kwargs) -> Any:
|
89
92
|
kwargs = self._maybe_inject_last_result(kwargs)
|
90
93
|
context = ExecutionContext(
|
@@ -43,6 +43,9 @@ class UserInputAction(BaseAction):
|
|
43
43
|
self.console = console or Console(color_system="auto")
|
44
44
|
self.prompt_session = prompt_session or PromptSession()
|
45
45
|
|
46
|
+
def get_infer_target(self) -> None:
|
47
|
+
return None
|
48
|
+
|
46
49
|
async def _run(self, *args, **kwargs) -> str:
|
47
50
|
context = ExecutionContext(
|
48
51
|
name=self.name,
|
falyx/command.py
CHANGED
@@ -29,13 +29,14 @@ from rich.tree import Tree
|
|
29
29
|
|
30
30
|
from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
|
31
31
|
from falyx.action.io_action import BaseIOAction
|
32
|
-
from falyx.argparse import CommandArgumentParser
|
33
32
|
from falyx.context import ExecutionContext
|
34
33
|
from falyx.debug import register_debug_hooks
|
35
34
|
from falyx.execution_registry import ExecutionRegistry as er
|
36
35
|
from falyx.hook_manager import HookManager, HookType
|
37
36
|
from falyx.logger import logger
|
38
37
|
from falyx.options_manager import OptionsManager
|
38
|
+
from falyx.parsers.argparse import CommandArgumentParser
|
39
|
+
from falyx.parsers.signature import infer_args_from_func
|
39
40
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
40
41
|
from falyx.protocols import ArgParserProtocol
|
41
42
|
from falyx.retry import RetryPolicy
|
@@ -90,6 +91,11 @@ class Command(BaseModel):
|
|
90
91
|
tags (list[str]): Organizational tags for the command.
|
91
92
|
logging_hooks (bool): Whether to attach logging hooks automatically.
|
92
93
|
requires_input (bool | None): Indicates if the action needs input.
|
94
|
+
options_manager (OptionsManager): Manages global command-line options.
|
95
|
+
arg_parser (CommandArgumentParser): Parses command arguments.
|
96
|
+
custom_parser (ArgParserProtocol | None): Custom argument parser.
|
97
|
+
custom_help (Callable[[], str | None] | None): Custom help message generator.
|
98
|
+
auto_args (bool): Automatically infer arguments from the action.
|
93
99
|
|
94
100
|
Methods:
|
95
101
|
__call__(): Executes the command, respecting hooks and retries.
|
@@ -101,12 +107,13 @@ class Command(BaseModel):
|
|
101
107
|
|
102
108
|
key: str
|
103
109
|
description: str
|
104
|
-
action: BaseAction | Callable[
|
110
|
+
action: BaseAction | Callable[..., Any]
|
105
111
|
args: tuple = ()
|
106
112
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
107
113
|
hidden: bool = False
|
108
114
|
aliases: list[str] = Field(default_factory=list)
|
109
115
|
help_text: str = ""
|
116
|
+
help_epilogue: str = ""
|
110
117
|
style: str = OneColors.WHITE
|
111
118
|
confirm: bool = False
|
112
119
|
confirm_message: str = "Are you sure?"
|
@@ -125,22 +132,44 @@ class Command(BaseModel):
|
|
125
132
|
requires_input: bool | None = None
|
126
133
|
options_manager: OptionsManager = Field(default_factory=OptionsManager)
|
127
134
|
arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
|
135
|
+
arguments: list[dict[str, Any]] = Field(default_factory=list)
|
136
|
+
argument_config: Callable[[CommandArgumentParser], None] | None = None
|
128
137
|
custom_parser: ArgParserProtocol | None = None
|
129
138
|
custom_help: Callable[[], str | None] | None = None
|
139
|
+
auto_args: bool = True
|
140
|
+
arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
|
130
141
|
|
131
142
|
_context: ExecutionContext | None = PrivateAttr(default=None)
|
132
143
|
|
133
144
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
134
145
|
|
135
|
-
def parse_args(
|
146
|
+
def parse_args(
|
147
|
+
self, raw_args: list[str] | str, from_validate: bool = False
|
148
|
+
) -> tuple[tuple, dict]:
|
136
149
|
if self.custom_parser:
|
137
150
|
if isinstance(raw_args, str):
|
138
|
-
|
151
|
+
try:
|
152
|
+
raw_args = shlex.split(raw_args)
|
153
|
+
except ValueError:
|
154
|
+
logger.warning(
|
155
|
+
"[Command:%s] Failed to split arguments: %s",
|
156
|
+
self.key,
|
157
|
+
raw_args,
|
158
|
+
)
|
159
|
+
return ((), {})
|
139
160
|
return self.custom_parser(raw_args)
|
140
161
|
|
141
162
|
if isinstance(raw_args, str):
|
142
|
-
|
143
|
-
|
163
|
+
try:
|
164
|
+
raw_args = shlex.split(raw_args)
|
165
|
+
except ValueError:
|
166
|
+
logger.warning(
|
167
|
+
"[Command:%s] Failed to split arguments: %s",
|
168
|
+
self.key,
|
169
|
+
raw_args,
|
170
|
+
)
|
171
|
+
return ((), {})
|
172
|
+
return self.arg_parser.parse_args_split(raw_args, from_validate=from_validate)
|
144
173
|
|
145
174
|
@field_validator("action", mode="before")
|
146
175
|
@classmethod
|
@@ -151,11 +180,22 @@ class Command(BaseModel):
|
|
151
180
|
return ensure_async(action)
|
152
181
|
raise TypeError("Action must be a callable or an instance of BaseAction")
|
153
182
|
|
183
|
+
def get_argument_definitions(self) -> list[dict[str, Any]]:
|
184
|
+
if self.arguments:
|
185
|
+
return self.arguments
|
186
|
+
elif self.argument_config:
|
187
|
+
self.argument_config(self.arg_parser)
|
188
|
+
elif self.auto_args:
|
189
|
+
if isinstance(self.action, BaseAction):
|
190
|
+
return infer_args_from_func(
|
191
|
+
self.action.get_infer_target(), self.arg_metadata
|
192
|
+
)
|
193
|
+
elif callable(self.action):
|
194
|
+
return infer_args_from_func(self.action, self.arg_metadata)
|
195
|
+
return []
|
196
|
+
|
154
197
|
def model_post_init(self, _: Any) -> None:
|
155
198
|
"""Post-initialization to set up the action and hooks."""
|
156
|
-
if isinstance(self.arg_parser, CommandArgumentParser):
|
157
|
-
self.arg_parser.command_description = self.description
|
158
|
-
|
159
199
|
if self.retry and isinstance(self.action, Action):
|
160
200
|
self.action.enable_retry()
|
161
201
|
elif self.retry_policy and isinstance(self.action, Action):
|
@@ -183,6 +223,9 @@ class Command(BaseModel):
|
|
183
223
|
elif self.requires_input is None:
|
184
224
|
self.requires_input = False
|
185
225
|
|
226
|
+
for arg_def in self.get_argument_definitions():
|
227
|
+
self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
|
228
|
+
|
186
229
|
@cached_property
|
187
230
|
def detect_requires_input(self) -> bool:
|
188
231
|
"""Detect if the action requires input based on its type."""
|
falyx/falyx.py
CHANGED
@@ -58,11 +58,12 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
58
58
|
from falyx.hook_manager import Hook, HookManager, HookType
|
59
59
|
from falyx.logger import logger
|
60
60
|
from falyx.options_manager import OptionsManager
|
61
|
-
from falyx.parsers import get_arg_parsers
|
61
|
+
from falyx.parsers import CommandArgumentParser, get_arg_parsers
|
62
|
+
from falyx.protocols import ArgParserProtocol
|
62
63
|
from falyx.retry import RetryPolicy
|
63
|
-
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
64
|
+
from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
|
64
65
|
from falyx.themes import OneColors, get_nord_theme
|
65
|
-
from falyx.utils import CaseInsensitiveDict, _noop, chunks
|
66
|
+
from falyx.utils import CaseInsensitiveDict, _noop, chunks
|
66
67
|
from falyx.version import __version__
|
67
68
|
|
68
69
|
|
@@ -157,8 +158,8 @@ class Falyx:
|
|
157
158
|
force_confirm: bool = False,
|
158
159
|
cli_args: Namespace | None = None,
|
159
160
|
options: OptionsManager | None = None,
|
160
|
-
render_menu: Callable[[
|
161
|
-
custom_table: Callable[[
|
161
|
+
render_menu: Callable[[Falyx], None] | None = None,
|
162
|
+
custom_table: Callable[[Falyx], Table] | Table | None = None,
|
162
163
|
) -> None:
|
163
164
|
"""Initializes the Falyx object."""
|
164
165
|
self.title: str | Markdown = title
|
@@ -182,8 +183,8 @@ class Falyx:
|
|
182
183
|
self._never_prompt: bool = never_prompt
|
183
184
|
self._force_confirm: bool = force_confirm
|
184
185
|
self.cli_args: Namespace | None = cli_args
|
185
|
-
self.render_menu: Callable[[
|
186
|
-
self.custom_table: Callable[[
|
186
|
+
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
187
|
+
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
187
188
|
self.validate_options(cli_args, options)
|
188
189
|
self._prompt_session: PromptSession | None = None
|
189
190
|
self.mode = FalyxMode.MENU
|
@@ -444,6 +445,7 @@ class Falyx:
|
|
444
445
|
bottom_toolbar=self._get_bottom_bar_render(),
|
445
446
|
key_bindings=self.key_bindings,
|
446
447
|
validate_while_typing=False,
|
448
|
+
interrupt_exception=FlowSignal,
|
447
449
|
)
|
448
450
|
return self._prompt_session
|
449
451
|
|
@@ -524,7 +526,7 @@ class Falyx:
|
|
524
526
|
key: str = "X",
|
525
527
|
description: str = "Exit",
|
526
528
|
aliases: list[str] | None = None,
|
527
|
-
action: Callable[
|
529
|
+
action: Callable[..., Any] | None = None,
|
528
530
|
style: str = OneColors.DARK_RED,
|
529
531
|
confirm: bool = False,
|
530
532
|
confirm_message: str = "Are you sure?",
|
@@ -578,13 +580,14 @@ class Falyx:
|
|
578
580
|
self,
|
579
581
|
key: str,
|
580
582
|
description: str,
|
581
|
-
action: BaseAction | Callable[
|
583
|
+
action: BaseAction | Callable[..., Any],
|
582
584
|
*,
|
583
585
|
args: tuple = (),
|
584
586
|
kwargs: dict[str, Any] | None = None,
|
585
587
|
hidden: bool = False,
|
586
588
|
aliases: list[str] | None = None,
|
587
589
|
help_text: str = "",
|
590
|
+
help_epilogue: str = "",
|
588
591
|
style: str = OneColors.WHITE,
|
589
592
|
confirm: bool = False,
|
590
593
|
confirm_message: str = "Are you sure?",
|
@@ -606,9 +609,33 @@ class Falyx:
|
|
606
609
|
retry_all: bool = False,
|
607
610
|
retry_policy: RetryPolicy | None = None,
|
608
611
|
requires_input: bool | None = None,
|
612
|
+
arg_parser: CommandArgumentParser | None = None,
|
613
|
+
arguments: list[dict[str, Any]] | None = None,
|
614
|
+
argument_config: Callable[[CommandArgumentParser], None] | None = None,
|
615
|
+
custom_parser: ArgParserProtocol | None = None,
|
616
|
+
custom_help: Callable[[], str | None] | None = None,
|
617
|
+
auto_args: bool = True,
|
618
|
+
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
609
619
|
) -> Command:
|
610
620
|
"""Adds an command to the menu, preventing duplicates."""
|
611
621
|
self._validate_command_key(key)
|
622
|
+
|
623
|
+
if arg_parser:
|
624
|
+
if not isinstance(arg_parser, CommandArgumentParser):
|
625
|
+
raise NotAFalyxError(
|
626
|
+
"arg_parser must be an instance of CommandArgumentParser."
|
627
|
+
)
|
628
|
+
arg_parser = arg_parser
|
629
|
+
else:
|
630
|
+
arg_parser = CommandArgumentParser(
|
631
|
+
command_key=key,
|
632
|
+
command_description=description,
|
633
|
+
command_style=style,
|
634
|
+
help_text=help_text,
|
635
|
+
help_epilogue=help_epilogue,
|
636
|
+
aliases=aliases,
|
637
|
+
)
|
638
|
+
|
612
639
|
command = Command(
|
613
640
|
key=key,
|
614
641
|
description=description,
|
@@ -618,6 +645,7 @@ class Falyx:
|
|
618
645
|
hidden=hidden,
|
619
646
|
aliases=aliases if aliases else [],
|
620
647
|
help_text=help_text,
|
648
|
+
help_epilogue=help_epilogue,
|
621
649
|
style=style,
|
622
650
|
confirm=confirm,
|
623
651
|
confirm_message=confirm_message,
|
@@ -634,6 +662,13 @@ class Falyx:
|
|
634
662
|
retry_policy=retry_policy or RetryPolicy(),
|
635
663
|
requires_input=requires_input,
|
636
664
|
options_manager=self.options,
|
665
|
+
arg_parser=arg_parser,
|
666
|
+
arguments=arguments or [],
|
667
|
+
argument_config=argument_config,
|
668
|
+
custom_parser=custom_parser,
|
669
|
+
custom_help=custom_help,
|
670
|
+
auto_args=auto_args,
|
671
|
+
arg_metadata=arg_metadata or {},
|
637
672
|
)
|
638
673
|
|
639
674
|
if hooks:
|
@@ -715,7 +750,10 @@ class Falyx:
|
|
715
750
|
"""
|
716
751
|
args = ()
|
717
752
|
kwargs: dict[str, Any] = {}
|
718
|
-
|
753
|
+
try:
|
754
|
+
choice, *input_args = shlex.split(raw_choices)
|
755
|
+
except ValueError:
|
756
|
+
return False, None, args, kwargs
|
719
757
|
is_preview, choice = self.parse_preview_command(choice)
|
720
758
|
if is_preview and not choice and self.help_command:
|
721
759
|
is_preview = False
|
@@ -735,7 +773,7 @@ class Falyx:
|
|
735
773
|
logger.info("Command '%s' selected.", choice)
|
736
774
|
if input_args and name_map[choice].arg_parser:
|
737
775
|
try:
|
738
|
-
args, kwargs = name_map[choice].parse_args(input_args)
|
776
|
+
args, kwargs = name_map[choice].parse_args(input_args, from_validate)
|
739
777
|
except CommandArgumentError as error:
|
740
778
|
if not from_validate:
|
741
779
|
if not name_map[choice].show_help():
|
@@ -748,6 +786,8 @@ class Falyx:
|
|
748
786
|
message=str(error), cursor_position=len(raw_choices)
|
749
787
|
)
|
750
788
|
return is_preview, None, args, kwargs
|
789
|
+
except HelpSignal:
|
790
|
+
return True, None, args, kwargs
|
751
791
|
return is_preview, name_map[choice], args, kwargs
|
752
792
|
|
753
793
|
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
|
@@ -804,15 +844,6 @@ class Falyx:
|
|
804
844
|
await selected_command.preview()
|
805
845
|
return True
|
806
846
|
|
807
|
-
if selected_command.requires_input:
|
808
|
-
program = get_program_invocation()
|
809
|
-
self.console.print(
|
810
|
-
f"[{OneColors.LIGHT_YELLOW}]⚠️ Command '{selected_command.key}' requires"
|
811
|
-
f" input and must be run via [{OneColors.MAGENTA}]'{program} run"
|
812
|
-
f"'[{OneColors.LIGHT_YELLOW}] with proper piping or arguments.[/]"
|
813
|
-
)
|
814
|
-
return True
|
815
|
-
|
816
847
|
self.last_run_command = selected_command
|
817
848
|
|
818
849
|
if selected_command == self.exit_command:
|
@@ -823,7 +854,6 @@ class Falyx:
|
|
823
854
|
context.start_timer()
|
824
855
|
try:
|
825
856
|
await self.hooks.trigger(HookType.BEFORE, context)
|
826
|
-
print(args, kwargs)
|
827
857
|
result = await selected_command(*args, **kwargs)
|
828
858
|
context.result = result
|
829
859
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
@@ -964,8 +994,6 @@ class Falyx:
|
|
964
994
|
logger.info("BackSignal received.")
|
965
995
|
except CancelSignal:
|
966
996
|
logger.info("CancelSignal received.")
|
967
|
-
except HelpSignal:
|
968
|
-
logger.info("HelpSignal received.")
|
969
997
|
finally:
|
970
998
|
logger.info("Exiting menu: %s", self.get_title())
|
971
999
|
if self.exit_message:
|
@@ -995,7 +1023,7 @@ class Falyx:
|
|
995
1023
|
sys.exit(0)
|
996
1024
|
|
997
1025
|
if self.cli_args.command == "version" or self.cli_args.version:
|
998
|
-
self.console.print(f"[{OneColors.
|
1026
|
+
self.console.print(f"[{OneColors.BLUE_b}]Falyx CLI v{__version__}[/]")
|
999
1027
|
sys.exit(0)
|
1000
1028
|
|
1001
1029
|
if self.cli_args.command == "preview":
|
falyx/parsers/.pytyped
ADDED
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
Falyx CLI Framework
|
3
|
+
|
4
|
+
Copyright (c) 2025 rtj.dev LLC.
|
5
|
+
Licensed under the MIT License. See LICENSE file for details.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .argparse import Argument, ArgumentAction, CommandArgumentParser
|
9
|
+
from .parsers import FalyxParsers, get_arg_parsers
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"Argument",
|
13
|
+
"ArgumentAction",
|
14
|
+
"CommandArgumentParser",
|
15
|
+
"get_arg_parsers",
|
16
|
+
"FalyxParsers",
|
17
|
+
]
|
@@ -5,7 +5,8 @@ from enum import Enum
|
|
5
5
|
from typing import Any, Iterable
|
6
6
|
|
7
7
|
from rich.console import Console
|
8
|
-
from rich.
|
8
|
+
from rich.markup import escape
|
9
|
+
from rich.text import Text
|
9
10
|
|
10
11
|
from falyx.exceptions import CommandArgumentError
|
11
12
|
from falyx.signals import HelpSignal
|
@@ -40,6 +41,70 @@ class Argument:
|
|
40
41
|
nargs: int | str = 1 # int, '?', '*', '+'
|
41
42
|
positional: bool = False # True if no leading - or -- in flags
|
42
43
|
|
44
|
+
def get_positional_text(self) -> str:
|
45
|
+
"""Get the positional text for the argument."""
|
46
|
+
text = ""
|
47
|
+
if self.positional:
|
48
|
+
if self.choices:
|
49
|
+
text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
50
|
+
else:
|
51
|
+
text = self.dest
|
52
|
+
return text
|
53
|
+
|
54
|
+
def get_choice_text(self) -> str:
|
55
|
+
"""Get the choice text for the argument."""
|
56
|
+
choice_text = ""
|
57
|
+
if self.choices:
|
58
|
+
choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
|
59
|
+
elif (
|
60
|
+
self.action
|
61
|
+
in (
|
62
|
+
ArgumentAction.STORE,
|
63
|
+
ArgumentAction.APPEND,
|
64
|
+
ArgumentAction.EXTEND,
|
65
|
+
)
|
66
|
+
and not self.positional
|
67
|
+
):
|
68
|
+
choice_text = self.dest.upper()
|
69
|
+
elif isinstance(self.nargs, str):
|
70
|
+
choice_text = self.dest
|
71
|
+
|
72
|
+
if self.nargs == "?":
|
73
|
+
choice_text = f"[{choice_text}]"
|
74
|
+
elif self.nargs == "*":
|
75
|
+
choice_text = f"[{choice_text} ...]"
|
76
|
+
elif self.nargs == "+":
|
77
|
+
choice_text = f"{choice_text} [{choice_text} ...]"
|
78
|
+
return choice_text
|
79
|
+
|
80
|
+
def __eq__(self, other: object) -> bool:
|
81
|
+
if not isinstance(other, Argument):
|
82
|
+
return False
|
83
|
+
return (
|
84
|
+
self.flags == other.flags
|
85
|
+
and self.dest == other.dest
|
86
|
+
and self.action == other.action
|
87
|
+
and self.type == other.type
|
88
|
+
and self.choices == other.choices
|
89
|
+
and self.required == other.required
|
90
|
+
and self.nargs == other.nargs
|
91
|
+
and self.positional == other.positional
|
92
|
+
)
|
93
|
+
|
94
|
+
def __hash__(self) -> int:
|
95
|
+
return hash(
|
96
|
+
(
|
97
|
+
tuple(self.flags),
|
98
|
+
self.dest,
|
99
|
+
self.action,
|
100
|
+
self.type,
|
101
|
+
tuple(self.choices or []),
|
102
|
+
self.required,
|
103
|
+
self.nargs,
|
104
|
+
self.positional,
|
105
|
+
)
|
106
|
+
)
|
107
|
+
|
43
108
|
|
44
109
|
class CommandArgumentParser:
|
45
110
|
"""
|
@@ -61,10 +126,25 @@ class CommandArgumentParser:
|
|
61
126
|
- Render Help using Rich library.
|
62
127
|
"""
|
63
128
|
|
64
|
-
def __init__(
|
129
|
+
def __init__(
|
130
|
+
self,
|
131
|
+
command_key: str = "",
|
132
|
+
command_description: str = "",
|
133
|
+
command_style: str = "bold",
|
134
|
+
help_text: str = "",
|
135
|
+
help_epilogue: str = "",
|
136
|
+
aliases: list[str] | None = None,
|
137
|
+
) -> None:
|
65
138
|
"""Initialize the CommandArgumentParser."""
|
66
|
-
self.
|
139
|
+
self.command_key: str = command_key
|
140
|
+
self.command_description: str = command_description
|
141
|
+
self.command_style: str = command_style
|
142
|
+
self.help_text: str = help_text
|
143
|
+
self.help_epilogue: str = help_epilogue
|
144
|
+
self.aliases: list[str] = aliases or []
|
67
145
|
self._arguments: list[Argument] = []
|
146
|
+
self._positional: list[Argument] = []
|
147
|
+
self._keyword: list[Argument] = []
|
68
148
|
self._flag_map: dict[str, Argument] = {}
|
69
149
|
self._dest_set: set[str] = set()
|
70
150
|
self._add_help()
|
@@ -73,10 +153,10 @@ class CommandArgumentParser:
|
|
73
153
|
def _add_help(self):
|
74
154
|
"""Add help argument to the parser."""
|
75
155
|
self.add_argument(
|
76
|
-
"--help",
|
77
156
|
"-h",
|
157
|
+
"--help",
|
78
158
|
action=ArgumentAction.HELP,
|
79
|
-
help="Show this help message
|
159
|
+
help="Show this help message.",
|
80
160
|
dest="help",
|
81
161
|
)
|
82
162
|
|
@@ -304,10 +384,31 @@ class CommandArgumentParser:
|
|
304
384
|
)
|
305
385
|
self._flag_map[flag] = argument
|
306
386
|
self._arguments.append(argument)
|
387
|
+
if positional:
|
388
|
+
self._positional.append(argument)
|
389
|
+
else:
|
390
|
+
self._keyword.append(argument)
|
307
391
|
|
308
392
|
def get_argument(self, dest: str) -> Argument | None:
|
309
393
|
return next((a for a in self._arguments if a.dest == dest), None)
|
310
394
|
|
395
|
+
def to_definition_list(self) -> list[dict[str, Any]]:
|
396
|
+
defs = []
|
397
|
+
for arg in self._arguments:
|
398
|
+
defs.append(
|
399
|
+
{
|
400
|
+
"flags": arg.flags,
|
401
|
+
"dest": arg.dest,
|
402
|
+
"action": arg.action,
|
403
|
+
"type": arg.type,
|
404
|
+
"choices": arg.choices,
|
405
|
+
"required": arg.required,
|
406
|
+
"nargs": arg.nargs,
|
407
|
+
"positional": arg.positional,
|
408
|
+
}
|
409
|
+
)
|
410
|
+
return defs
|
411
|
+
|
311
412
|
def _consume_nargs(
|
312
413
|
self, args: list[str], start: int, spec: Argument
|
313
414
|
) -> tuple[list[str], int]:
|
@@ -405,7 +506,9 @@ class CommandArgumentParser:
|
|
405
506
|
|
406
507
|
return i
|
407
508
|
|
408
|
-
def parse_args(
|
509
|
+
def parse_args(
|
510
|
+
self, args: list[str] | None = None, from_validate: bool = False
|
511
|
+
) -> dict[str, Any]:
|
409
512
|
"""Parse Falyx Command arguments."""
|
410
513
|
if args is None:
|
411
514
|
args = []
|
@@ -423,7 +526,8 @@ class CommandArgumentParser:
|
|
423
526
|
action = spec.action
|
424
527
|
|
425
528
|
if action == ArgumentAction.HELP:
|
426
|
-
|
529
|
+
if not from_validate:
|
530
|
+
self.render_help()
|
427
531
|
raise HelpSignal()
|
428
532
|
elif action == ArgumentAction.STORE_TRUE:
|
429
533
|
result[spec.dest] = True
|
@@ -550,13 +654,15 @@ class CommandArgumentParser:
|
|
550
654
|
result.pop("help", None)
|
551
655
|
return result
|
552
656
|
|
553
|
-
def parse_args_split(
|
657
|
+
def parse_args_split(
|
658
|
+
self, args: list[str], from_validate: bool = False
|
659
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
554
660
|
"""
|
555
661
|
Returns:
|
556
662
|
tuple[args, kwargs] - Positional arguments in defined order,
|
557
663
|
followed by keyword argument mapping.
|
558
664
|
"""
|
559
|
-
parsed = self.parse_args(args)
|
665
|
+
parsed = self.parse_args(args, from_validate)
|
560
666
|
args_list = []
|
561
667
|
kwargs_dict = {}
|
562
668
|
for arg in self._arguments:
|
@@ -568,20 +674,74 @@ class CommandArgumentParser:
|
|
568
674
|
kwargs_dict[arg.dest] = parsed[arg.dest]
|
569
675
|
return tuple(args_list), kwargs_dict
|
570
676
|
|
571
|
-
def render_help(self):
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
for arg in self.
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
677
|
+
def render_help(self) -> None:
|
678
|
+
# Options
|
679
|
+
# Add all keyword arguments to the options list
|
680
|
+
options_list = []
|
681
|
+
for arg in self._keyword:
|
682
|
+
choice_text = arg.get_choice_text()
|
683
|
+
if choice_text:
|
684
|
+
options_list.extend([f"[{arg.flags[0]} {choice_text}]"])
|
685
|
+
else:
|
686
|
+
options_list.extend([f"[{arg.flags[0]}]"])
|
687
|
+
|
688
|
+
# Add positional arguments to the options list
|
689
|
+
for arg in self._positional:
|
690
|
+
choice_text = arg.get_choice_text()
|
691
|
+
if isinstance(arg.nargs, int):
|
692
|
+
choice_text = " ".join([choice_text] * arg.nargs)
|
693
|
+
options_list.append(escape(choice_text))
|
694
|
+
|
695
|
+
options_text = " ".join(options_list)
|
696
|
+
command_keys = " | ".join(
|
697
|
+
[f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
|
698
|
+
+ [
|
699
|
+
f"[{self.command_style}]{alias}[/{self.command_style}]"
|
700
|
+
for alias in self.aliases
|
701
|
+
]
|
702
|
+
)
|
703
|
+
|
704
|
+
usage = f"usage: {command_keys} {options_text}"
|
705
|
+
self.console.print(f"[bold]{usage}[/bold]\n")
|
706
|
+
|
707
|
+
# Description
|
708
|
+
if self.help_text:
|
709
|
+
self.console.print(self.help_text + "\n")
|
710
|
+
|
711
|
+
# Arguments
|
712
|
+
if self._arguments:
|
713
|
+
if self._positional:
|
714
|
+
self.console.print("[bold]positional:[/bold]")
|
715
|
+
for arg in self._positional:
|
716
|
+
flags = arg.get_positional_text()
|
717
|
+
arg_line = Text(f" {flags:<30} ")
|
718
|
+
help_text = arg.help or ""
|
719
|
+
arg_line.append(help_text)
|
720
|
+
self.console.print(arg_line)
|
721
|
+
self.console.print("[bold]options:[/bold]")
|
722
|
+
for arg in self._keyword:
|
723
|
+
flags = ", ".join(arg.flags)
|
724
|
+
flags_choice = f"{flags} {arg.get_choice_text()}"
|
725
|
+
arg_line = Text(f" {flags_choice:<30} ")
|
726
|
+
help_text = arg.help or ""
|
727
|
+
arg_line.append(help_text)
|
728
|
+
self.console.print(arg_line)
|
729
|
+
|
730
|
+
# Epilogue
|
731
|
+
if self.help_epilogue:
|
732
|
+
self.console.print("\n" + self.help_epilogue, style="dim")
|
733
|
+
|
734
|
+
def __eq__(self, other: object) -> bool:
|
735
|
+
if not isinstance(other, CommandArgumentParser):
|
736
|
+
return False
|
737
|
+
|
738
|
+
def sorted_args(parser):
|
739
|
+
return sorted(parser._arguments, key=lambda a: a.dest)
|
740
|
+
|
741
|
+
return sorted_args(self) == sorted_args(other)
|
742
|
+
|
743
|
+
def __hash__(self) -> int:
|
744
|
+
return hash(tuple(sorted(self._arguments, key=lambda a: a.dest)))
|
585
745
|
|
586
746
|
def __str__(self) -> str:
|
587
747
|
positional = sum(arg.positional for arg in self._arguments)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import Any, Callable
|
3
|
+
|
4
|
+
from falyx.logger import logger
|
5
|
+
|
6
|
+
|
7
|
+
def infer_args_from_func(
|
8
|
+
func: Callable[[Any], Any] | None,
|
9
|
+
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
10
|
+
) -> list[dict[str, Any]]:
|
11
|
+
"""
|
12
|
+
Infer argument definitions from a callable's signature.
|
13
|
+
Returns a list of kwargs suitable for CommandArgumentParser.add_argument.
|
14
|
+
"""
|
15
|
+
if not callable(func):
|
16
|
+
logger.debug("Provided argument is not callable: %s", func)
|
17
|
+
return []
|
18
|
+
arg_metadata = arg_metadata or {}
|
19
|
+
signature = inspect.signature(func)
|
20
|
+
arg_defs = []
|
21
|
+
|
22
|
+
for name, param in signature.parameters.items():
|
23
|
+
raw_metadata = arg_metadata.get(name, {})
|
24
|
+
metadata = (
|
25
|
+
{"help": raw_metadata} if isinstance(raw_metadata, str) else raw_metadata
|
26
|
+
)
|
27
|
+
|
28
|
+
if param.kind not in (
|
29
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
30
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
31
|
+
inspect.Parameter.KEYWORD_ONLY,
|
32
|
+
):
|
33
|
+
continue
|
34
|
+
|
35
|
+
arg_type = (
|
36
|
+
param.annotation if param.annotation is not inspect.Parameter.empty else str
|
37
|
+
)
|
38
|
+
default = param.default if param.default is not inspect.Parameter.empty else None
|
39
|
+
is_required = param.default is inspect.Parameter.empty
|
40
|
+
if is_required:
|
41
|
+
flags = [f"{name.replace('_', '-')}"]
|
42
|
+
else:
|
43
|
+
flags = [f"--{name.replace('_', '-')}"]
|
44
|
+
action = "store"
|
45
|
+
nargs: int | str = 1
|
46
|
+
|
47
|
+
if arg_type is bool:
|
48
|
+
if param.default is False:
|
49
|
+
action = "store_true"
|
50
|
+
else:
|
51
|
+
action = "store_false"
|
52
|
+
|
53
|
+
if arg_type is list:
|
54
|
+
action = "append"
|
55
|
+
if is_required:
|
56
|
+
nargs = "+"
|
57
|
+
else:
|
58
|
+
nargs = "*"
|
59
|
+
|
60
|
+
arg_defs.append(
|
61
|
+
{
|
62
|
+
"flags": flags,
|
63
|
+
"dest": name,
|
64
|
+
"type": arg_type,
|
65
|
+
"default": default,
|
66
|
+
"required": is_required,
|
67
|
+
"nargs": nargs,
|
68
|
+
"action": action,
|
69
|
+
"help": metadata.get("help", ""),
|
70
|
+
"choices": metadata.get("choices"),
|
71
|
+
}
|
72
|
+
)
|
73
|
+
|
74
|
+
return arg_defs
|
falyx/parsers/utils.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from falyx import logger
|
4
|
+
from falyx.parsers.signature import infer_args_from_func
|
5
|
+
|
6
|
+
|
7
|
+
def same_argument_definitions(
|
8
|
+
actions: list[Any],
|
9
|
+
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
10
|
+
) -> list[dict[str, Any]] | None:
|
11
|
+
from falyx.action.action import BaseAction
|
12
|
+
|
13
|
+
arg_sets = []
|
14
|
+
for action in actions:
|
15
|
+
if isinstance(action, BaseAction):
|
16
|
+
arg_defs = infer_args_from_func(action.get_infer_target(), arg_metadata)
|
17
|
+
elif callable(action):
|
18
|
+
arg_defs = infer_args_from_func(action, arg_metadata)
|
19
|
+
else:
|
20
|
+
logger.debug("Auto args unsupported for action: %s", action)
|
21
|
+
return None
|
22
|
+
arg_sets.append(arg_defs)
|
23
|
+
|
24
|
+
first = arg_sets[0]
|
25
|
+
if all(arg_set == first for arg_set in arg_sets[1:]):
|
26
|
+
return first
|
27
|
+
return None
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.30"
|
@@ -1,34 +1,38 @@
|
|
1
1
|
falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
falyx/__init__.py,sha256=L40665QyjAqHQxHdxxY2_yPeDa4p0LE7Nu_2dkm08Ls,650
|
3
3
|
falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
|
4
|
+
falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
5
|
falyx/action/__init__.py,sha256=zpOK5g4DybydV8d3QI0Zq52aWaKFPYi-J6szAQTsQ2c,974
|
5
|
-
falyx/action/action.py,sha256=
|
6
|
-
falyx/action/action_factory.py,sha256=
|
6
|
+
falyx/action/action.py,sha256=f1u7emC3Qa5JUaUsqOFTXMgc5JCv72-MZzckk1N_VO8,33537
|
7
|
+
falyx/action/action_factory.py,sha256=ex2Wxpe5mW23FnHuTChzbbw4hmXYuDHKJ330iLBQKIQ,4610
|
7
8
|
falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
|
8
|
-
falyx/action/io_action.py,sha256=
|
9
|
-
falyx/action/menu_action.py,sha256=
|
10
|
-
falyx/action/select_file_action.py,sha256=
|
11
|
-
falyx/action/selection_action.py,sha256=
|
9
|
+
falyx/action/io_action.py,sha256=TAa3keVfhCCDgXsp0hWGPx2-B6TKxlvkUGaAD7K-25Q,10051
|
10
|
+
falyx/action/menu_action.py,sha256=S_Y_hmyALHrZkfkPXtBKY2qwczfNugAA4M0xOAgOTrM,5744
|
11
|
+
falyx/action/select_file_action.py,sha256=5kzN2Kx6wN86Sl5tSLdUs1bAlbSG9AGAi3x1p5HlP6A,7860
|
12
|
+
falyx/action/selection_action.py,sha256=aEoCdinMfiKL7tnVOPJ8b56oZ1X51mGfV9rkn_DJKtc,8872
|
12
13
|
falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
|
13
14
|
falyx/action/types.py,sha256=iVD-bHm1GRXOTIlHOeT_KcDBRZm4Hz5Xzl_BOalvEf4,961
|
14
|
-
falyx/action/user_input_action.py,sha256=
|
15
|
-
falyx/argparse.py,sha256=kI_tLYD6KOiWvXEHEvOi8bprx5sQm9KTrgens2_nWKU,24498
|
15
|
+
falyx/action/user_input_action.py,sha256=ne9ytu96QVnuM4jOjjAzB_dd6XS162961uNURtJIRXU,3459
|
16
16
|
falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
|
17
|
-
falyx/command.py,sha256=
|
17
|
+
falyx/command.py,sha256=BT_zl_R0nYPYKjD_n-ww73zLBmKQB9MmDuWk8ndR4VU,15345
|
18
18
|
falyx/config.py,sha256=8dkQfL-Ro-vWw1AcO2fD1PGZ92Cyfnwl885ZlpLkp4Y,9636
|
19
|
-
falyx/config_schema.py,sha256=j5GQuHVlaU-VLxLF9t8idZRjqOP9MIKp1hyd9NhpAGU,3124
|
20
19
|
falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
|
21
20
|
falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
|
22
21
|
falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
|
23
22
|
falyx/execution_registry.py,sha256=rctsz0mrIHPToLZqylblVjDdKWdq1x_JBc8GwMP5sJ8,4710
|
24
|
-
falyx/falyx.py,sha256=
|
23
|
+
falyx/falyx.py,sha256=7luGMkM692puSAQ0j0NE3Pox9QbM3Lc2IFvI0D-3UWc,44137
|
25
24
|
falyx/hook_manager.py,sha256=GuGxVVz9FXrU39Tk220QcsLsMXeut7ZDkGW3hU9GcwQ,2952
|
26
25
|
falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
|
27
26
|
falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
|
28
27
|
falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
|
29
28
|
falyx/menu.py,sha256=faxGgocqQYY6HtzVbenHaFj8YqsmycBEyziC8Ahzqjo,2870
|
30
29
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
31
|
-
falyx/parsers
|
30
|
+
falyx/parsers/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
|
+
falyx/parsers/__init__.py,sha256=l0QMf89uJHhTpOqQfiV3tx7aAHvELqDFWAyjCbwEgBQ,370
|
32
|
+
falyx/parsers/argparse.py,sha256=ElWg4nl1ygkDY9Q4lTfjj4KMXI0FKDLVdc08eiGw4AQ,29889
|
33
|
+
falyx/parsers/parsers.py,sha256=KsDFEmJLM86d2X4Kh4SHA9mBbUk351NjLhhFYzQkaPk,5762
|
34
|
+
falyx/parsers/signature.py,sha256=kniazHBDFIY-cb4JC-gxPL4fviAsoYf8wX0AmWKetGM,2252
|
35
|
+
falyx/parsers/utils.py,sha256=g9C_8eJm-2A52SfAL-qyow0VmJXwI-YeRve0phoFiFk,845
|
32
36
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
33
37
|
falyx/protocols.py,sha256=mesdq5CjPF_5Kyu7Evwr6qMT71tUHlw0SjjtmnggTZw,495
|
34
38
|
falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
|
@@ -40,9 +44,9 @@ falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
|
40
44
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
41
45
|
falyx/utils.py,sha256=u3puR4Bh-unNBw9a0V9sw7PDTIzRaNLolap0oz5bVIk,6718
|
42
46
|
falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
|
43
|
-
falyx/version.py,sha256=
|
44
|
-
falyx-0.1.
|
45
|
-
falyx-0.1.
|
46
|
-
falyx-0.1.
|
47
|
-
falyx-0.1.
|
48
|
-
falyx-0.1.
|
47
|
+
falyx/version.py,sha256=2GUJJyX8g8EAXKUqyj7DGVzG-jNXOGaqVSWilvGYuX8,23
|
48
|
+
falyx-0.1.30.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
49
|
+
falyx-0.1.30.dist-info/METADATA,sha256=U_0NoWJB6xbD0oxYzA8wwVEuIFJvgIB6D4UrgOyt9pc,5521
|
50
|
+
falyx-0.1.30.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
51
|
+
falyx-0.1.30.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
52
|
+
falyx-0.1.30.dist-info/RECORD,,
|
falyx/config_schema.py
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
FALYX_CONFIG_SCHEMA = {
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
3
|
-
"title": "Falyx CLI Config",
|
4
|
-
"type": "object",
|
5
|
-
"properties": {
|
6
|
-
"title": {"type": "string", "description": "Title shown at top of menu"},
|
7
|
-
"prompt": {
|
8
|
-
"oneOf": [
|
9
|
-
{"type": "string"},
|
10
|
-
{
|
11
|
-
"type": "array",
|
12
|
-
"items": {
|
13
|
-
"type": "array",
|
14
|
-
"prefixItems": [
|
15
|
-
{
|
16
|
-
"type": "string",
|
17
|
-
"description": "Style string (e.g., 'bold #ff0000 italic')",
|
18
|
-
},
|
19
|
-
{"type": "string", "description": "Text content"},
|
20
|
-
],
|
21
|
-
"minItems": 2,
|
22
|
-
"maxItems": 2,
|
23
|
-
},
|
24
|
-
},
|
25
|
-
]
|
26
|
-
},
|
27
|
-
"columns": {
|
28
|
-
"type": "integer",
|
29
|
-
"minimum": 1,
|
30
|
-
"description": "Number of menu columns",
|
31
|
-
},
|
32
|
-
"welcome_message": {"type": "string"},
|
33
|
-
"exit_message": {"type": "string"},
|
34
|
-
"commands": {
|
35
|
-
"type": "array",
|
36
|
-
"items": {
|
37
|
-
"type": "object",
|
38
|
-
"required": ["key", "description", "action"],
|
39
|
-
"properties": {
|
40
|
-
"key": {"type": "string", "minLength": 1},
|
41
|
-
"description": {"type": "string"},
|
42
|
-
"action": {
|
43
|
-
"type": "string",
|
44
|
-
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)+$",
|
45
|
-
"description": "Dotted import path (e.g., mymodule.task)",
|
46
|
-
},
|
47
|
-
"args": {"type": "array"},
|
48
|
-
"kwargs": {"type": "object"},
|
49
|
-
"aliases": {"type": "array", "items": {"type": "string"}},
|
50
|
-
"tags": {"type": "array", "items": {"type": "string"}},
|
51
|
-
"style": {"type": "string"},
|
52
|
-
"confirm": {"type": "boolean"},
|
53
|
-
"confirm_message": {"type": "string"},
|
54
|
-
"preview_before_confirm": {"type": "boolean"},
|
55
|
-
"spinner": {"type": "boolean"},
|
56
|
-
"spinner_message": {"type": "string"},
|
57
|
-
"spinner_type": {"type": "string"},
|
58
|
-
"spinner_style": {"type": "string"},
|
59
|
-
"logging_hooks": {"type": "boolean"},
|
60
|
-
"retry": {"type": "boolean"},
|
61
|
-
"retry_all": {"type": "boolean"},
|
62
|
-
"retry_policy": {
|
63
|
-
"type": "object",
|
64
|
-
"properties": {
|
65
|
-
"enabled": {"type": "boolean"},
|
66
|
-
"max_retries": {"type": "integer"},
|
67
|
-
"delay": {"type": "number"},
|
68
|
-
"backoff": {"type": "number"},
|
69
|
-
},
|
70
|
-
},
|
71
|
-
},
|
72
|
-
},
|
73
|
-
},
|
74
|
-
},
|
75
|
-
"required": ["commands"],
|
76
|
-
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|