falyx 0.1.59__py3-none-any.whl → 0.1.61__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/action_types.py +29 -1
- falyx/action/chained_action.py +7 -2
- falyx/action/confirm_action.py +6 -27
- falyx/context.py +5 -5
- falyx/parser/argument_action.py +1 -0
- falyx/parser/command_argument_parser.py +145 -37
- falyx/parser/parser_types.py +15 -0
- falyx/parser/utils.py +2 -2
- falyx/signals.py +7 -0
- falyx/version.py +1 -1
- {falyx-0.1.59.dist-info → falyx-0.1.61.dist-info}/METADATA +1 -1
- {falyx-0.1.59.dist-info → falyx-0.1.61.dist-info}/RECORD +15 -14
- {falyx-0.1.59.dist-info → falyx-0.1.61.dist-info}/LICENSE +0 -0
- {falyx-0.1.59.dist-info → falyx-0.1.61.dist-info}/WHEEL +0 -0
- {falyx-0.1.59.dist-info → falyx-0.1.61.dist-info}/entry_points.txt +0 -0
falyx/action/action_types.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""
|
2
|
+
"""action_types.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from enum import Enum
|
@@ -52,3 +52,31 @@ class SelectionReturnType(Enum):
|
|
52
52
|
def _missing_(cls, value: object) -> SelectionReturnType:
|
53
53
|
valid = ", ".join(member.value for member in cls)
|
54
54
|
raise ValueError(f"Invalid DictReturnType: '{value}'. Must be one of: {valid}")
|
55
|
+
|
56
|
+
|
57
|
+
class ConfirmType(Enum):
|
58
|
+
"""Enum for different confirmation types."""
|
59
|
+
|
60
|
+
YES_NO = "yes_no"
|
61
|
+
YES_CANCEL = "yes_cancel"
|
62
|
+
YES_NO_CANCEL = "yes_no_cancel"
|
63
|
+
TYPE_WORD = "type_word"
|
64
|
+
OK_CANCEL = "ok_cancel"
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def choices(cls) -> list[ConfirmType]:
|
68
|
+
"""Return a list of all hook type choices."""
|
69
|
+
return list(cls)
|
70
|
+
|
71
|
+
def __str__(self) -> str:
|
72
|
+
"""Return the string representation of the confirm type."""
|
73
|
+
return self.value
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def _missing_(cls, value: object) -> ConfirmType:
|
77
|
+
if isinstance(value, str):
|
78
|
+
for member in cls:
|
79
|
+
if member.value == value.lower():
|
80
|
+
return member
|
81
|
+
valid = ", ".join(member.value for member in cls)
|
82
|
+
raise ValueError(f"Invalid ConfirmType: '{value}'. Must be one of: {valid}")
|
falyx/action/chained_action.py
CHANGED
@@ -17,6 +17,7 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
17
17
|
from falyx.hook_manager import Hook, HookManager, HookType
|
18
18
|
from falyx.logger import logger
|
19
19
|
from falyx.options_manager import OptionsManager
|
20
|
+
from falyx.signals import BreakChainSignal
|
20
21
|
from falyx.themes import OneColors
|
21
22
|
|
22
23
|
|
@@ -106,7 +107,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
106
107
|
def _clear_args(self):
|
107
108
|
return (), {}
|
108
109
|
|
109
|
-
async def _run(self, *args, **kwargs) ->
|
110
|
+
async def _run(self, *args, **kwargs) -> Any:
|
110
111
|
if not self.actions:
|
111
112
|
raise EmptyChainError(f"[{self.name}] No actions to execute.")
|
112
113
|
|
@@ -166,7 +167,11 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
166
167
|
context.result = all_results if self.return_list else all_results[-1]
|
167
168
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
168
169
|
return context.result
|
169
|
-
|
170
|
+
except BreakChainSignal as error:
|
171
|
+
logger.info("[%s] Chain broken: %s", self.name, error)
|
172
|
+
context.exception = error
|
173
|
+
shared_context.add_error(shared_context.current_index, error)
|
174
|
+
await self._rollback(context.extra["rollback_stack"], *args, **kwargs)
|
170
175
|
except Exception as error:
|
171
176
|
context.exception = error
|
172
177
|
shared_context.add_error(shared_context.current_index, error)
|
falyx/action/confirm_action.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from enum import Enum
|
4
3
|
from typing import Any
|
5
4
|
|
6
5
|
from prompt_toolkit import PromptSession
|
7
6
|
from rich.tree import Tree
|
8
7
|
|
8
|
+
from falyx.action.action_types import ConfirmType
|
9
9
|
from falyx.action.base_action import BaseAction
|
10
10
|
from falyx.context import ExecutionContext
|
11
11
|
from falyx.execution_registry import ExecutionRegistry as er
|
@@ -17,25 +17,6 @@ from falyx.themes import OneColors
|
|
17
17
|
from falyx.validators import word_validator, words_validator
|
18
18
|
|
19
19
|
|
20
|
-
class ConfirmType(Enum):
|
21
|
-
"""Enum for different confirmation types."""
|
22
|
-
|
23
|
-
YES_NO = "yes_no"
|
24
|
-
YES_CANCEL = "yes_cancel"
|
25
|
-
YES_NO_CANCEL = "yes_no_cancel"
|
26
|
-
TYPE_WORD = "type_word"
|
27
|
-
OK_CANCEL = "ok_cancel"
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def choices(cls) -> list[ConfirmType]:
|
31
|
-
"""Return a list of all hook type choices."""
|
32
|
-
return list(cls)
|
33
|
-
|
34
|
-
def __str__(self) -> str:
|
35
|
-
"""Return the string representation of the confirm type."""
|
36
|
-
return self.value
|
37
|
-
|
38
|
-
|
39
20
|
class ConfirmAction(BaseAction):
|
40
21
|
"""
|
41
22
|
Action to confirm an operation with the user.
|
@@ -66,7 +47,7 @@ class ConfirmAction(BaseAction):
|
|
66
47
|
message: str = "Confirm?",
|
67
48
|
confirm_type: ConfirmType | str = ConfirmType.YES_NO,
|
68
49
|
prompt_session: PromptSession | None = None,
|
69
|
-
|
50
|
+
never_prompt: bool = False,
|
70
51
|
word: str = "CONFIRM",
|
71
52
|
return_last_result: bool = False,
|
72
53
|
inject_last_result: bool = True,
|
@@ -88,11 +69,11 @@ class ConfirmAction(BaseAction):
|
|
88
69
|
name=name,
|
89
70
|
inject_last_result=inject_last_result,
|
90
71
|
inject_into=inject_into,
|
72
|
+
never_prompt=never_prompt,
|
91
73
|
)
|
92
74
|
self.message = message
|
93
75
|
self.confirm_type = self._coerce_confirm_type(confirm_type)
|
94
76
|
self.prompt_session = prompt_session or PromptSession()
|
95
|
-
self.confirm = confirm
|
96
77
|
self.word = word
|
97
78
|
self.return_last_result = return_last_result
|
98
79
|
|
@@ -165,11 +146,9 @@ class ConfirmAction(BaseAction):
|
|
165
146
|
try:
|
166
147
|
await self.hooks.trigger(HookType.BEFORE, context)
|
167
148
|
if (
|
168
|
-
|
149
|
+
self.never_prompt
|
169
150
|
or self.options_manager
|
170
|
-
and not should_prompt_user(
|
171
|
-
confirm=self.confirm, options=self.options_manager
|
172
|
-
)
|
151
|
+
and not should_prompt_user(confirm=True, options=self.options_manager)
|
173
152
|
):
|
174
153
|
logger.debug(
|
175
154
|
"Skipping confirmation for action '%s' as 'confirm' is False or options manager indicates no prompt.",
|
@@ -209,7 +188,7 @@ class ConfirmAction(BaseAction):
|
|
209
188
|
)
|
210
189
|
tree.add(f"[bold]Message:[/] {self.message}")
|
211
190
|
tree.add(f"[bold]Type:[/] {self.confirm_type.value}")
|
212
|
-
tree.add(f"[bold]Prompt Required:[/] {'
|
191
|
+
tree.add(f"[bold]Prompt Required:[/] {'No' if self.never_prompt else 'Yes'}")
|
213
192
|
if self.confirm_type == ConfirmType.TYPE_WORD:
|
214
193
|
tree.add(f"[bold]Confirmation Word:[/] {self.word}")
|
215
194
|
if parent is None:
|
falyx/context.py
CHANGED
@@ -42,7 +42,7 @@ class ExecutionContext(BaseModel):
|
|
42
42
|
kwargs (dict): Keyword arguments passed to the action.
|
43
43
|
action (BaseAction | Callable): The action instance being executed.
|
44
44
|
result (Any | None): The result of the action, if successful.
|
45
|
-
exception (
|
45
|
+
exception (BaseException | None): The exception raised, if execution failed.
|
46
46
|
start_time (float | None): High-resolution performance start time.
|
47
47
|
end_time (float | None): High-resolution performance end time.
|
48
48
|
start_wall (datetime | None): Wall-clock timestamp when execution began.
|
@@ -75,7 +75,7 @@ class ExecutionContext(BaseModel):
|
|
75
75
|
kwargs: dict = Field(default_factory=dict)
|
76
76
|
action: Any
|
77
77
|
result: Any | None = None
|
78
|
-
exception:
|
78
|
+
exception: BaseException | None = None
|
79
79
|
|
80
80
|
start_time: float | None = None
|
81
81
|
end_time: float | None = None
|
@@ -207,7 +207,7 @@ class SharedContext(BaseModel):
|
|
207
207
|
Attributes:
|
208
208
|
name (str): Identifier for the context (usually the parent action name).
|
209
209
|
results (list[Any]): Captures results from each action, in order of execution.
|
210
|
-
errors (list[tuple[int,
|
210
|
+
errors (list[tuple[int, BaseException]]): Indexed list of errors from failed actions.
|
211
211
|
current_index (int): Index of the currently executing action (used in chains).
|
212
212
|
is_parallel (bool): Whether the context is used in parallel mode (ActionGroup).
|
213
213
|
shared_result (Any | None): Optional shared value available to all actions in
|
@@ -232,7 +232,7 @@ class SharedContext(BaseModel):
|
|
232
232
|
name: str
|
233
233
|
action: Any
|
234
234
|
results: list[Any] = Field(default_factory=list)
|
235
|
-
errors: list[tuple[int,
|
235
|
+
errors: list[tuple[int, BaseException]] = Field(default_factory=list)
|
236
236
|
current_index: int = -1
|
237
237
|
is_parallel: bool = False
|
238
238
|
shared_result: Any | None = None
|
@@ -244,7 +244,7 @@ class SharedContext(BaseModel):
|
|
244
244
|
def add_result(self, result: Any) -> None:
|
245
245
|
self.results.append(result)
|
246
246
|
|
247
|
-
def add_error(self, index: int, error:
|
247
|
+
def add_error(self, index: int, error: BaseException) -> None:
|
248
248
|
self.errors.append((index, error))
|
249
249
|
|
250
250
|
def set_shared_result(self, result: Any) -> None:
|
falyx/parser/argument_action.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
"""command_argument_parser.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
from collections import defaultdict
|
5
6
|
from copy import deepcopy
|
6
7
|
from typing import Any, Iterable
|
7
8
|
|
@@ -13,6 +14,7 @@ from falyx.console import console
|
|
13
14
|
from falyx.exceptions import CommandArgumentError
|
14
15
|
from falyx.parser.argument import Argument
|
15
16
|
from falyx.parser.argument_action import ArgumentAction
|
17
|
+
from falyx.parser.parser_types import false_none, true_none
|
16
18
|
from falyx.parser.utils import coerce_value
|
17
19
|
from falyx.signals import HelpSignal
|
18
20
|
|
@@ -33,6 +35,7 @@ class CommandArgumentParser:
|
|
33
35
|
- Support for positional and keyword arguments.
|
34
36
|
- Support for default values.
|
35
37
|
- Support for boolean flags.
|
38
|
+
- Support for optional boolean flags.
|
36
39
|
- Exception handling for invalid arguments.
|
37
40
|
- Render Help using Rich library.
|
38
41
|
"""
|
@@ -111,10 +114,23 @@ class CommandArgumentParser:
|
|
111
114
|
return dest
|
112
115
|
|
113
116
|
def _determine_required(
|
114
|
-
self,
|
117
|
+
self,
|
118
|
+
required: bool,
|
119
|
+
positional: bool,
|
120
|
+
nargs: int | str | None,
|
121
|
+
action: ArgumentAction,
|
115
122
|
) -> bool:
|
116
123
|
"""Determine if the argument is required."""
|
117
124
|
if required:
|
125
|
+
if action in (
|
126
|
+
ArgumentAction.STORE_TRUE,
|
127
|
+
ArgumentAction.STORE_FALSE,
|
128
|
+
ArgumentAction.STORE_BOOL_OPTIONAL,
|
129
|
+
ArgumentAction.HELP,
|
130
|
+
):
|
131
|
+
raise CommandArgumentError(
|
132
|
+
f"Argument with action {action} cannot be required"
|
133
|
+
)
|
118
134
|
return True
|
119
135
|
if positional:
|
120
136
|
assert (
|
@@ -143,6 +159,7 @@ class CommandArgumentParser:
|
|
143
159
|
ArgumentAction.STORE_TRUE,
|
144
160
|
ArgumentAction.COUNT,
|
145
161
|
ArgumentAction.HELP,
|
162
|
+
ArgumentAction.STORE_BOOL_OPTIONAL,
|
146
163
|
):
|
147
164
|
if nargs is not None:
|
148
165
|
raise CommandArgumentError(
|
@@ -163,9 +180,17 @@ class CommandArgumentParser:
|
|
163
180
|
return nargs
|
164
181
|
|
165
182
|
def _normalize_choices(
|
166
|
-
self, choices: Iterable | None, expected_type: Any
|
183
|
+
self, choices: Iterable | None, expected_type: Any, action: ArgumentAction
|
167
184
|
) -> list[Any]:
|
168
185
|
if choices is not None:
|
186
|
+
if action in (
|
187
|
+
ArgumentAction.STORE_TRUE,
|
188
|
+
ArgumentAction.STORE_FALSE,
|
189
|
+
ArgumentAction.STORE_BOOL_OPTIONAL,
|
190
|
+
):
|
191
|
+
raise CommandArgumentError(
|
192
|
+
f"choices cannot be specified for {action} actions"
|
193
|
+
)
|
169
194
|
if isinstance(choices, dict):
|
170
195
|
raise CommandArgumentError("choices cannot be a dict")
|
171
196
|
try:
|
@@ -239,6 +264,7 @@ class CommandArgumentParser:
|
|
239
264
|
if action in (
|
240
265
|
ArgumentAction.STORE_TRUE,
|
241
266
|
ArgumentAction.STORE_FALSE,
|
267
|
+
ArgumentAction.STORE_BOOL_OPTIONAL,
|
242
268
|
ArgumentAction.COUNT,
|
243
269
|
ArgumentAction.HELP,
|
244
270
|
):
|
@@ -271,6 +297,14 @@ class CommandArgumentParser:
|
|
271
297
|
return []
|
272
298
|
else:
|
273
299
|
return None
|
300
|
+
elif action in (
|
301
|
+
ArgumentAction.STORE_TRUE,
|
302
|
+
ArgumentAction.STORE_FALSE,
|
303
|
+
ArgumentAction.STORE_BOOL_OPTIONAL,
|
304
|
+
):
|
305
|
+
raise CommandArgumentError(
|
306
|
+
f"Default value cannot be set for action {action}. It is a boolean flag."
|
307
|
+
)
|
274
308
|
return default
|
275
309
|
|
276
310
|
def _validate_flags(self, flags: tuple[str, ...]) -> None:
|
@@ -289,6 +323,66 @@ class CommandArgumentParser:
|
|
289
323
|
f"Flag '{flag}' must be a single character or start with '--'"
|
290
324
|
)
|
291
325
|
|
326
|
+
def _register_store_bool_optional(
|
327
|
+
self,
|
328
|
+
flags: tuple[str, ...],
|
329
|
+
dest: str,
|
330
|
+
help: str,
|
331
|
+
) -> None:
|
332
|
+
if len(flags) != 1:
|
333
|
+
raise CommandArgumentError(
|
334
|
+
"store_bool_optional action can only have a single flag"
|
335
|
+
)
|
336
|
+
if not flags[0].startswith("--"):
|
337
|
+
raise CommandArgumentError(
|
338
|
+
"store_bool_optional action must use a long flag (e.g. --flag)"
|
339
|
+
)
|
340
|
+
base_flag = flags[0]
|
341
|
+
negated_flag = f"--no-{base_flag.lstrip('-')}"
|
342
|
+
|
343
|
+
argument = Argument(
|
344
|
+
flags=flags,
|
345
|
+
dest=dest,
|
346
|
+
action=ArgumentAction.STORE_BOOL_OPTIONAL,
|
347
|
+
type=true_none,
|
348
|
+
default=None,
|
349
|
+
help=help,
|
350
|
+
)
|
351
|
+
|
352
|
+
negated_argument = Argument(
|
353
|
+
flags=(negated_flag,),
|
354
|
+
dest=dest,
|
355
|
+
action=ArgumentAction.STORE_BOOL_OPTIONAL,
|
356
|
+
type=false_none,
|
357
|
+
default=None,
|
358
|
+
help=help,
|
359
|
+
)
|
360
|
+
|
361
|
+
self._register_argument(argument)
|
362
|
+
self._register_argument(negated_argument)
|
363
|
+
|
364
|
+
def _register_argument(self, argument: Argument):
|
365
|
+
|
366
|
+
for flag in argument.flags:
|
367
|
+
if (
|
368
|
+
flag in self._flag_map
|
369
|
+
and not argument.action == ArgumentAction.STORE_BOOL_OPTIONAL
|
370
|
+
):
|
371
|
+
existing = self._flag_map[flag]
|
372
|
+
raise CommandArgumentError(
|
373
|
+
f"Flag '{flag}' is already used by argument '{existing.dest}'"
|
374
|
+
)
|
375
|
+
for flag in argument.flags:
|
376
|
+
self._flag_map[flag] = argument
|
377
|
+
if not argument.positional:
|
378
|
+
self._keyword[flag] = argument
|
379
|
+
self._dest_set.add(argument.dest)
|
380
|
+
self._arguments.append(argument)
|
381
|
+
if argument.positional:
|
382
|
+
self._positional[argument.dest] = argument
|
383
|
+
else:
|
384
|
+
self._keyword_list.append(argument)
|
385
|
+
|
292
386
|
def add_argument(
|
293
387
|
self,
|
294
388
|
*flags,
|
@@ -301,7 +395,7 @@ class CommandArgumentParser:
|
|
301
395
|
help: str = "",
|
302
396
|
dest: str | None = None,
|
303
397
|
resolver: BaseAction | None = None,
|
304
|
-
lazy_resolver: bool =
|
398
|
+
lazy_resolver: bool = True,
|
305
399
|
) -> None:
|
306
400
|
"""Add an argument to the parser.
|
307
401
|
For `ArgumentAction.ACTION`, `nargs` and `type` determine how many and what kind
|
@@ -334,6 +428,7 @@ class CommandArgumentParser:
|
|
334
428
|
)
|
335
429
|
action = self._validate_action(action, positional)
|
336
430
|
resolver = self._validate_resolver(action, resolver)
|
431
|
+
|
337
432
|
nargs = self._validate_nargs(nargs, action)
|
338
433
|
default = self._resolve_default(default, action, nargs)
|
339
434
|
if (
|
@@ -344,46 +439,34 @@ class CommandArgumentParser:
|
|
344
439
|
self._validate_default_list_type(default, expected_type, dest)
|
345
440
|
else:
|
346
441
|
self._validate_default_type(default, expected_type, dest)
|
347
|
-
choices = self._normalize_choices(choices, expected_type)
|
442
|
+
choices = self._normalize_choices(choices, expected_type, action)
|
348
443
|
if default is not None and choices and default not in choices:
|
349
444
|
raise CommandArgumentError(
|
350
445
|
f"Default value '{default}' not in allowed choices: {choices}"
|
351
446
|
)
|
352
|
-
required = self._determine_required(required, positional, nargs)
|
447
|
+
required = self._determine_required(required, positional, nargs, action)
|
353
448
|
if not isinstance(lazy_resolver, bool):
|
354
449
|
raise CommandArgumentError(
|
355
450
|
f"lazy_resolver must be a boolean, got {type(lazy_resolver)}"
|
356
451
|
)
|
357
|
-
|
358
|
-
flags
|
359
|
-
dest=dest,
|
360
|
-
action=action,
|
361
|
-
type=expected_type,
|
362
|
-
default=default,
|
363
|
-
choices=choices,
|
364
|
-
required=required,
|
365
|
-
help=help,
|
366
|
-
nargs=nargs,
|
367
|
-
positional=positional,
|
368
|
-
resolver=resolver,
|
369
|
-
lazy_resolver=lazy_resolver,
|
370
|
-
)
|
371
|
-
for flag in flags:
|
372
|
-
if flag in self._flag_map:
|
373
|
-
existing = self._flag_map[flag]
|
374
|
-
raise CommandArgumentError(
|
375
|
-
f"Flag '{flag}' is already used by argument '{existing.dest}'"
|
376
|
-
)
|
377
|
-
for flag in flags:
|
378
|
-
self._flag_map[flag] = argument
|
379
|
-
if not positional:
|
380
|
-
self._keyword[flag] = argument
|
381
|
-
self._dest_set.add(dest)
|
382
|
-
self._arguments.append(argument)
|
383
|
-
if positional:
|
384
|
-
self._positional[dest] = argument
|
452
|
+
if action == ArgumentAction.STORE_BOOL_OPTIONAL:
|
453
|
+
self._register_store_bool_optional(flags, dest, help)
|
385
454
|
else:
|
386
|
-
|
455
|
+
argument = Argument(
|
456
|
+
flags=flags,
|
457
|
+
dest=dest,
|
458
|
+
action=action,
|
459
|
+
type=expected_type,
|
460
|
+
default=default,
|
461
|
+
choices=choices,
|
462
|
+
required=required,
|
463
|
+
help=help,
|
464
|
+
nargs=nargs,
|
465
|
+
positional=positional,
|
466
|
+
resolver=resolver,
|
467
|
+
lazy_resolver=lazy_resolver,
|
468
|
+
)
|
469
|
+
self._register_argument(argument)
|
387
470
|
|
388
471
|
def get_argument(self, dest: str) -> Argument | None:
|
389
472
|
return next((a for a in self._arguments if a.dest == dest), None)
|
@@ -624,6 +707,10 @@ class CommandArgumentParser:
|
|
624
707
|
result[spec.dest] = False
|
625
708
|
consumed_indices.add(i)
|
626
709
|
i += 1
|
710
|
+
elif action == ArgumentAction.STORE_BOOL_OPTIONAL:
|
711
|
+
result[spec.dest] = spec.type(True)
|
712
|
+
consumed_indices.add(i)
|
713
|
+
i += 1
|
627
714
|
elif action == ArgumentAction.COUNT:
|
628
715
|
result[spec.dest] = result.get(spec.dest, 0) + 1
|
629
716
|
consumed_indices.add(i)
|
@@ -765,6 +852,10 @@ class CommandArgumentParser:
|
|
765
852
|
and spec.lazy_resolver
|
766
853
|
and from_validate
|
767
854
|
):
|
855
|
+
if not args:
|
856
|
+
raise CommandArgumentError(
|
857
|
+
f"Missing required argument '{spec.dest}': {spec.get_choice_text()}{help_text}"
|
858
|
+
)
|
768
859
|
continue # Lazy resolvers are not validated here
|
769
860
|
raise CommandArgumentError(
|
770
861
|
f"Missing required argument '{spec.dest}': {spec.get_choice_text()}{help_text}"
|
@@ -889,11 +980,28 @@ class CommandArgumentParser:
|
|
889
980
|
help_text = f"\n{'':<33}{help_text}"
|
890
981
|
self.console.print(f"{arg_line}{help_text}")
|
891
982
|
self.console.print("[bold]options:[/bold]")
|
983
|
+
arg_groups = defaultdict(list)
|
892
984
|
for arg in self._keyword_list:
|
893
|
-
|
894
|
-
|
985
|
+
arg_groups[arg.dest].append(arg)
|
986
|
+
|
987
|
+
for group in arg_groups.values():
|
988
|
+
if len(group) == 2 and all(
|
989
|
+
arg.action == ArgumentAction.STORE_BOOL_OPTIONAL for arg in group
|
990
|
+
):
|
991
|
+
# Merge --flag / --no-flag pair into single help line for STORE_BOOL_OPTIONAL
|
992
|
+
all_flags = tuple(
|
993
|
+
sorted(
|
994
|
+
(arg.flags[0] for arg in group),
|
995
|
+
key=lambda f: f.startswith("--no-"),
|
996
|
+
)
|
997
|
+
)
|
998
|
+
else:
|
999
|
+
all_flags = group[0].flags
|
1000
|
+
|
1001
|
+
flags = ", ".join(all_flags)
|
1002
|
+
flags_choice = f"{flags} {group[0].get_choice_text()}"
|
895
1003
|
arg_line = f" {flags_choice:<30} "
|
896
|
-
help_text =
|
1004
|
+
help_text = group[0].help or ""
|
897
1005
|
if help_text and len(flags_choice) > 30:
|
898
1006
|
help_text = f"\n{'':<33}{help_text}"
|
899
1007
|
self.console.print(f"{arg_line}{help_text}")
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""parser_types.py"""
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
|
6
|
+
def true_none(value: Any) -> bool | None:
|
7
|
+
if value is None:
|
8
|
+
return None
|
9
|
+
return True
|
10
|
+
|
11
|
+
|
12
|
+
def false_none(value: Any) -> bool | None:
|
13
|
+
if value is None:
|
14
|
+
return None
|
15
|
+
return False
|
falyx/parser/utils.py
CHANGED
@@ -15,9 +15,9 @@ def coerce_bool(value: str) -> bool:
|
|
15
15
|
if isinstance(value, bool):
|
16
16
|
return value
|
17
17
|
value = value.strip().lower()
|
18
|
-
if value in {"true", "1", "yes", "on"}:
|
18
|
+
if value in {"true", "t", "1", "yes", "on"}:
|
19
19
|
return True
|
20
|
-
elif value in {"false", "0", "no", "off"}:
|
20
|
+
elif value in {"false", "f", "0", "no", "off"}:
|
21
21
|
return False
|
22
22
|
return bool(value)
|
23
23
|
|
falyx/signals.py
CHANGED
@@ -10,6 +10,13 @@ class FlowSignal(BaseException):
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
|
13
|
+
class BreakChainSignal(FlowSignal):
|
14
|
+
"""Raised to break the current action chain and return to the previous context."""
|
15
|
+
|
16
|
+
def __init__(self, message: str = "Break chain signal received."):
|
17
|
+
super().__init__(message)
|
18
|
+
|
19
|
+
|
13
20
|
class QuitSignal(FlowSignal):
|
14
21
|
"""Raised to signal an immediate exit from the CLI framework."""
|
15
22
|
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.61"
|
@@ -7,10 +7,10 @@ falyx/action/action.py,sha256=Pm9lnxKhJoI1vs-fadAJrXkcDGYcBCMwvQ81HHxd0HA,5879
|
|
7
7
|
falyx/action/action_factory.py,sha256=WosLeaYf79e83XHAAkxKi62zi8jJEiVlzvgOC84Z7t0,4840
|
8
8
|
falyx/action/action_group.py,sha256=RQdHOWCa8XRUme3S5YGJTICLozIApAlIpRUgEeFaiyw,7728
|
9
9
|
falyx/action/action_mixins.py,sha256=oUrjbweCeateshg3tqtbQiGuV8u4GvlioIZCUr9D1m4,1244
|
10
|
-
falyx/action/action_types.py,sha256=
|
10
|
+
falyx/action/action_types.py,sha256=TjUdwbnWVNzp5B5pFjgwRdA-P-MiY4bwe1dRSz-Ur3s,2318
|
11
11
|
falyx/action/base_action.py,sha256=o9Nml70-SEVTXnu9J0-VYnO-t5wZZM0o59lYDth24Po,5829
|
12
|
-
falyx/action/chained_action.py,sha256=
|
13
|
-
falyx/action/confirm_action.py,sha256=
|
12
|
+
falyx/action/chained_action.py,sha256=TNJz25jnzQQAIwXM6uaK8mzhq4bgRks5n7IZ2nDQM6Q,9614
|
13
|
+
falyx/action/confirm_action.py,sha256=rBtkaMuMYJEXcLu5VeWA0YPO6Yvj0gJBiGDAfoAkCEI,8601
|
14
14
|
falyx/action/fallback_action.py,sha256=3FGWfoR1MIgY0ZkDNOpKu8p3JqPWzh5ON3943mfgDGs,1708
|
15
15
|
falyx/action/http_action.py,sha256=DNeSBWh58UTFGlfFyTk2GnhS54hpLAJLC0QNbq2cYic,5799
|
16
16
|
falyx/action/io_action.py,sha256=V888tQgAynqsVvkhICnEeE4wRs2vvdTcdlEpDSEbHqo,6128
|
@@ -31,7 +31,7 @@ falyx/command.py,sha256=QdcwLEFIaq3a4Lfot4cV3zHbVJNQxwSpShprBgLBkh8,16891
|
|
31
31
|
falyx/completer.py,sha256=EODbakx5PFAwjNcfuUZPFuSx2Q9MXBlWRZJ2LejF6DI,1686
|
32
32
|
falyx/config.py,sha256=OFEq9pFhV39o6_D7dP_QUDjqEusSQNpgomRsh5AAZYY,9621
|
33
33
|
falyx/console.py,sha256=WIZ004R9x6DJp2g3onBQ4DOJ7iDeTOr8HqJCwRt30Rc,143
|
34
|
-
falyx/context.py,sha256=
|
34
|
+
falyx/context.py,sha256=M7iWEKto_NwI3GM-VCDPwXT0dBpFPf1Y_RvHQKodZgI,10804
|
35
35
|
falyx/debug.py,sha256=pguI0XQcZ-7jte5YUPexAufa1oxxalYO1JgmO6GU3rI,1557
|
36
36
|
falyx/exceptions.py,sha256=58D4BYkyJ7PlpZoNk37GsUsFThm_gIlb2Ex2XXhLklI,1099
|
37
37
|
falyx/execution_registry.py,sha256=RLRMOEmfDElFy4tuC8L9tRyNToX7GJ4GoEBh2Iri8zo,7662
|
@@ -46,25 +46,26 @@ falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
|
46
46
|
falyx/parser/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
47
|
falyx/parser/__init__.py,sha256=NbxAovKIY-duFTs6DAsdM_OzL7s3VIu19KMOmltX9ts,512
|
48
48
|
falyx/parser/argument.py,sha256=MIKUj-hrdLDUK8xuW84_l9ms_t5CoNFpVDmxMZIbW-I,4105
|
49
|
-
falyx/parser/argument_action.py,sha256=
|
50
|
-
falyx/parser/command_argument_parser.py,sha256=
|
49
|
+
falyx/parser/argument_action.py,sha256=Lcpb9siYr_q2T8qU-jXVtqFb11bFPPKEGH3gurJv2NM,757
|
50
|
+
falyx/parser/command_argument_parser.py,sha256=W6vfj5acUx9wiA5TDjDO3CYlzsgGHrMM86-Gs19q-Eo,41482
|
51
|
+
falyx/parser/parser_types.py,sha256=DLLuIXE8cAVLS41trfsNy-XJmtqSa1HfnJVAYIIc42w,315
|
51
52
|
falyx/parser/parsers.py,sha256=vb-l_NNh5O9L98Lcafhz91flRLxC1BnW6U8JdeabRCw,14118
|
52
53
|
falyx/parser/signature.py,sha256=fSltLEr8ctj1qpbU-OvTMnREjlb8OTG5t-guJFR7j4E,2529
|
53
|
-
falyx/parser/utils.py,sha256=
|
54
|
+
falyx/parser/utils.py,sha256=GlxB1WORwoJ5XUtmmAVBUPaDV2nF9Hio7TbvNJvd8oY,3006
|
54
55
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
55
56
|
falyx/protocols.py,sha256=vd9JL-TXdLEiAQXLw2UKLd3MUMivoG7iMLo08ZggwYQ,539
|
56
57
|
falyx/retry.py,sha256=sGRE9QhdZK98M99G8F15WUsJ_fYLNyLlCgu3UANaSQs,3744
|
57
58
|
falyx/retry_utils.py,sha256=IqvEy_F0dXG8Yl2UoEJVLX-6OXk-dh-D72_SWv4w-p0,730
|
58
59
|
falyx/selection.py,sha256=Q2GrpiRyuV8KwVrZGuK2WZvihZ0VIylyvckJKYwrC-A,14629
|
59
|
-
falyx/signals.py,sha256=
|
60
|
+
falyx/signals.py,sha256=at_COqTdDQIqzDY05nc5BChU1R_lJ28ghR5lNLYaDtI,1314
|
60
61
|
falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
|
61
62
|
falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
62
63
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
63
64
|
falyx/utils.py,sha256=U45xnZFUdoFC4xiji_9S1jHS5V7MvxSDtufP8EgB0SM,6732
|
64
65
|
falyx/validators.py,sha256=AXpMGnk1_7J7MAbbol6pkMAiSIdNHoF5pwtA2-xS6H8,6029
|
65
|
-
falyx/version.py,sha256=
|
66
|
-
falyx-0.1.
|
67
|
-
falyx-0.1.
|
68
|
-
falyx-0.1.
|
69
|
-
falyx-0.1.
|
70
|
-
falyx-0.1.
|
66
|
+
falyx/version.py,sha256=CjOPJsj7rCFM2zpom_253GmmHGb2RQ8NuZwsEg0ZmF0,23
|
67
|
+
falyx-0.1.61.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
68
|
+
falyx-0.1.61.dist-info/METADATA,sha256=JdPjGhW2VQKmq-EPF5IfR5m7lRVx8Xlq6Yb4hb11WR4,5561
|
69
|
+
falyx-0.1.61.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
70
|
+
falyx-0.1.61.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
71
|
+
falyx-0.1.61.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|