falyx 0.1.58__py3-none-any.whl → 0.1.60__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.
@@ -1,5 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """types.py"""
2
+ """action_types.py"""
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum
@@ -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) -> list[Any]:
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)
@@ -56,13 +56,14 @@ class ConfirmAction(BaseAction):
56
56
  prompt_session (PromptSession | None): The session to use for input.
57
57
  confirm (bool): Whether to prompt the user for confirmation.
58
58
  word (str): The word to type for TYPE_WORD confirmation.
59
- return_last_result (bool): Whether to return the last result of the action.
59
+ return_last_result (bool): Whether to return the last result of the action
60
+ instead of a boolean.
60
61
  """
61
62
 
62
63
  def __init__(
63
64
  self,
64
65
  name: str,
65
- message: str = "Continue",
66
+ message: str = "Confirm?",
66
67
  confirm_type: ConfirmType | str = ConfirmType.YES_NO,
67
68
  prompt_session: PromptSession | None = None,
68
69
  confirm: bool = True,
@@ -114,16 +115,19 @@ class ConfirmAction(BaseAction):
114
115
  session=self.prompt_session,
115
116
  )
116
117
  case ConfirmType.YES_NO_CANCEL:
118
+ error_message = "Enter 'Y', 'y' to confirm, 'N', 'n' to decline, or 'C', 'c' to abort."
117
119
  answer = await self.prompt_session.prompt_async(
118
- f"❓ {self.message} ([Y]es, [N]o, or [C]ancel to abort): ",
119
- validator=words_validator(["Y", "N", "C"]),
120
+ f"❓ {self.message} [Y]es, [N]o, or [C]ancel to abort > ",
121
+ validator=words_validator(
122
+ ["Y", "N", "C"], error_message=error_message
123
+ ),
120
124
  )
121
125
  if answer.upper() == "C":
122
126
  raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
123
127
  return answer.upper() == "Y"
124
128
  case ConfirmType.TYPE_WORD:
125
129
  answer = await self.prompt_session.prompt_async(
126
- f"❓ {self.message} (type '{self.word}' to confirm or N/n): ",
130
+ f"❓ {self.message} [{self.word}] to confirm or [N/n] > ",
127
131
  validator=word_validator(self.word),
128
132
  )
129
133
  return answer.upper().strip() != "N"
@@ -138,9 +142,10 @@ class ConfirmAction(BaseAction):
138
142
  raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
139
143
  return answer
140
144
  case ConfirmType.OK_CANCEL:
145
+ error_message = "Enter 'O', 'o' to confirm or 'C', 'c' to abort."
141
146
  answer = await self.prompt_session.prompt_async(
142
- f"❓ {self.message} ([O]k to continue, [C]ancel to abort): ",
143
- validator=words_validator(["O", "C"]),
147
+ f"❓ {self.message} [O]k to confirm, [C]ancel to abort > ",
148
+ validator=words_validator(["O", "C"], error_message=error_message),
144
149
  )
145
150
  if answer.upper() == "C":
146
151
  raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
@@ -213,5 +218,5 @@ class ConfirmAction(BaseAction):
213
218
  def __str__(self) -> str:
214
219
  return (
215
220
  f"ConfirmAction(name={self.name}, message={self.message}, "
216
- f"confirm_type={self.confirm_type})"
221
+ f"confirm_type={self.confirm_type}, return_last_result={self.return_last_result})"
217
222
  )
@@ -14,7 +14,7 @@ class SignalAction(Action):
14
14
  Useful for exiting a menu, going back, or halting execution gracefully.
15
15
  """
16
16
 
17
- def __init__(self, name: str, signal: Exception):
17
+ def __init__(self, name: str, signal: FlowSignal):
18
18
  self.signal = signal
19
19
  super().__init__(name, action=self.raise_signal)
20
20
 
falyx/bottom_bar.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Callable
5
5
 
6
6
  from prompt_toolkit.formatted_text import HTML, merge_formatted_text
7
7
  from prompt_toolkit.key_binding import KeyBindings
8
+ from rich.console import Console
8
9
 
9
10
  from falyx.console import console
10
11
  from falyx.options_manager import OptionsManager
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 (Exception | None): The exception raised, if execution failed.
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: Exception | None = None
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, Exception]]): Indexed list of errors from failed actions.
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, Exception]] = Field(default_factory=list)
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: Exception) -> None:
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/falyx.py CHANGED
@@ -1,7 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """falyx.py
3
-
4
- Main class for constructing and running Falyx CLI menus.
2
+ """Main class for constructing and running Falyx CLI menus.
5
3
 
6
4
  Falyx provides a structured, customizable interactive menu system
7
5
  for running commands, actions, and workflows. It supports:
@@ -12,6 +12,7 @@ class ArgumentAction(Enum):
12
12
  STORE = "store"
13
13
  STORE_TRUE = "store_true"
14
14
  STORE_FALSE = "store_false"
15
+ STORE_BOOL_OPTIONAL = "store_bool_optional"
15
16
  APPEND = "append"
16
17
  EXTEND = "extend"
17
18
  COUNT = "count"
@@ -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, required: bool, positional: bool, nargs: int | str | None
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,
@@ -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
- argument = Argument(
358
- flags=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
- self._keyword_list.append(argument)
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)
@@ -889,11 +976,28 @@ class CommandArgumentParser:
889
976
  help_text = f"\n{'':<33}{help_text}"
890
977
  self.console.print(f"{arg_line}{help_text}")
891
978
  self.console.print("[bold]options:[/bold]")
979
+ arg_groups = defaultdict(list)
892
980
  for arg in self._keyword_list:
893
- flags = ", ".join(arg.flags)
894
- flags_choice = f"{flags} {arg.get_choice_text()}"
981
+ arg_groups[arg.dest].append(arg)
982
+
983
+ for group in arg_groups.values():
984
+ if len(group) == 2 and all(
985
+ arg.action == ArgumentAction.STORE_BOOL_OPTIONAL for arg in group
986
+ ):
987
+ # Merge --flag / --no-flag pair into single help line for STORE_BOOL_OPTIONAL
988
+ all_flags = tuple(
989
+ sorted(
990
+ (arg.flags[0] for arg in group),
991
+ key=lambda f: f.startswith("--no-"),
992
+ )
993
+ )
994
+ else:
995
+ all_flags = group[0].flags
996
+
997
+ flags = ", ".join(all_flags)
998
+ flags_choice = f"{flags} {group[0].get_choice_text()}"
895
999
  arg_line = f" {flags_choice:<30} "
896
- help_text = arg.help or ""
1000
+ help_text = group[0].help or ""
897
1001
  if help_text and len(flags_choice) > 30:
898
1002
  help_text = f"\n{'':<33}{help_text}"
899
1003
  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/parsers.py CHANGED
@@ -56,6 +56,39 @@ def get_root_parser(
56
56
  allow_abbrev: bool = True,
57
57
  exit_on_error: bool = True,
58
58
  ) -> ArgumentParser:
59
+ """
60
+ Construct the root-level ArgumentParser for the Falyx CLI.
61
+
62
+ This parser handles global arguments shared across subcommands and can serve
63
+ as the base parser for the Falyx CLI or standalone applications. It includes
64
+ options for verbosity, debug logging, and version output.
65
+
66
+ Args:
67
+ prog (str | None): Name of the program (e.g., 'falyx').
68
+ usage (str | None): Optional custom usage string.
69
+ description (str | None): Description shown in the CLI help.
70
+ epilog (str | None): Message displayed at the end of help output.
71
+ parents (Sequence[ArgumentParser] | None): Optional parent parsers.
72
+ prefix_chars (str): Characters to denote optional arguments (default: "-").
73
+ fromfile_prefix_chars (str | None): Prefix to indicate argument file input.
74
+ argument_default (Any): Global default value for arguments.
75
+ conflict_handler (str): Strategy to resolve conflicting argument names.
76
+ add_help (bool): Whether to include help (`-h/--help`) in this parser.
77
+ allow_abbrev (bool): Allow abbreviated long options.
78
+ exit_on_error (bool): Exit immediately on error or raise an exception.
79
+
80
+ Returns:
81
+ ArgumentParser: The root parser with global options attached.
82
+
83
+ Notes:
84
+ ```
85
+ Includes the following arguments:
86
+ --never-prompt : Run in non-interactive mode.
87
+ -v / --verbose : Enable debug logging.
88
+ --debug-hooks : Enable hook lifecycle debug logs.
89
+ --version : Print the Falyx version.
90
+ ```
91
+ """
59
92
  parser = ArgumentParser(
60
93
  prog=prog,
61
94
  usage=usage,
@@ -92,7 +125,30 @@ def get_subparsers(
92
125
  title: str = "Falyx Commands",
93
126
  description: str | None = "Available commands for the Falyx CLI.",
94
127
  ) -> _SubParsersAction:
95
- """Create and return a subparsers action for the given parser."""
128
+ """
129
+ Create and return a subparsers object for registering Falyx CLI subcommands.
130
+
131
+ This function adds a `subparsers` block to the given root parser, enabling
132
+ structured subcommands such as `run`, `run-all`, `preview`, etc.
133
+
134
+ Args:
135
+ parser (ArgumentParser): The root parser to attach the subparsers to.
136
+ title (str): Title used in help output to group subcommands.
137
+ description (str | None): Optional text describing the group of subcommands.
138
+
139
+ Returns:
140
+ _SubParsersAction: The subparsers object that can be used to add new CLI subcommands.
141
+
142
+ Raises:
143
+ TypeError: If `parser` is not an instance of `ArgumentParser`.
144
+
145
+ Example:
146
+ ```python
147
+ >>> parser = get_root_parser()
148
+ >>> subparsers = get_subparsers(parser, title="Available Commands")
149
+ >>> subparsers.add_parser("run", help="Run a Falyx command")
150
+ ```
151
+ """
96
152
  if not isinstance(parser, ArgumentParser):
97
153
  raise TypeError("parser must be an instance of ArgumentParser")
98
154
  subparsers = parser.add_subparsers(
@@ -122,7 +178,54 @@ def get_arg_parsers(
122
178
  root_parser: ArgumentParser | None = None,
123
179
  subparsers: _SubParsersAction | None = None,
124
180
  ) -> FalyxParsers:
125
- """Returns the argument parser for the CLI."""
181
+ """
182
+ Create and return the full suite of argument parsers used by the Falyx CLI.
183
+
184
+ This function builds the root parser and all subcommand parsers used for structured
185
+ CLI workflows in Falyx. It supports standard subcommands including `run`, `run-all`,
186
+ `preview`, `list`, and `version`, and integrates with registered `Command` objects
187
+ to populate dynamic help and usage documentation.
188
+
189
+ Args:
190
+ prog (str | None): Program name to display in help and usage messages.
191
+ usage (str | None): Optional usage message to override the default.
192
+ description (str | None): Description for the CLI root parser.
193
+ epilog (str | None): Epilog message shown after the help text.
194
+ parents (Sequence[ArgumentParser] | None): Optional parent parsers.
195
+ prefix_chars (str): Characters that prefix optional arguments.
196
+ fromfile_prefix_chars (str | None): Prefix character for reading args from file.
197
+ argument_default (Any): Default value for arguments if not specified.
198
+ conflict_handler (str): Strategy for resolving conflicting arguments.
199
+ add_help (bool): Whether to add the `-h/--help` option to the root parser.
200
+ allow_abbrev (bool): Whether to allow abbreviated long options.
201
+ exit_on_error (bool): Whether the parser exits on error or raises.
202
+ commands (dict[str, Command] | None): Optional dictionary of registered commands
203
+ to populate help and subcommand descriptions dynamically.
204
+ root_parser (ArgumentParser | None): Custom root parser to use instead of building one.
205
+ subparsers (_SubParsersAction | None): Optional existing subparser object to extend.
206
+
207
+ Returns:
208
+ FalyxParsers: A structured container of all parsers, including `run`, `run-all`,
209
+ `preview`, `list`, `version`, and the root parser.
210
+
211
+ Raises:
212
+ TypeError: If `root_parser` is not an instance of ArgumentParser or
213
+ `subparsers` is not an instance of _SubParsersAction.
214
+
215
+ Example:
216
+ ```python
217
+ >>> parsers = get_arg_parsers(commands=my_command_dict)
218
+ >>> args = parsers.root.parse_args()
219
+ ```
220
+
221
+ Notes:
222
+ - This function integrates dynamic command usage and descriptions if the
223
+ `commands` argument is provided.
224
+ - The `run` parser supports additional options for retry logic and confirmation
225
+ prompts.
226
+ - The `run-all` parser executes all commands matching a tag.
227
+ - Use `falyx run ?[COMMAND]` from the CLI to preview a command.
228
+ """
126
229
  if epilog is None:
127
230
  epilog = f"Tip: Use '{prog} run ?[COMMAND]' to preview any command from the CLI."
128
231
  if root_parser is None:
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/validators.py CHANGED
@@ -44,10 +44,12 @@ def yes_no_validator() -> Validator:
44
44
  return False
45
45
  return True
46
46
 
47
- return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.")
47
+ return Validator.from_callable(validate, error_message="Enter 'Y', 'y' or 'N', 'n'.")
48
48
 
49
49
 
50
- def words_validator(keys: Sequence[str] | KeysView[str]) -> Validator:
50
+ def words_validator(
51
+ keys: Sequence[str] | KeysView[str], error_message: str | None = None
52
+ ) -> Validator:
51
53
  """Validator for specific word inputs."""
52
54
 
53
55
  def validate(text: str) -> bool:
@@ -55,9 +57,10 @@ def words_validator(keys: Sequence[str] | KeysView[str]) -> Validator:
55
57
  return False
56
58
  return True
57
59
 
58
- return Validator.from_callable(
59
- validate, error_message=f"Invalid input. Choices: {{{', '.join(keys)}}}."
60
- )
60
+ if error_message is None:
61
+ error_message = f"Invalid input. Choices: {{{', '.join(keys)}}}."
62
+
63
+ return Validator.from_callable(validate, error_message=error_message)
61
64
 
62
65
 
63
66
  def word_validator(word: str) -> Validator:
@@ -68,7 +71,7 @@ def word_validator(word: str) -> Validator:
68
71
  return True
69
72
  return text.upper().strip() == word.upper()
70
73
 
71
- return Validator.from_callable(validate, error_message=f"Enter '{word}' or 'N'.")
74
+ return Validator.from_callable(validate, error_message=f"Enter '{word}' or 'N', 'n'.")
72
75
 
73
76
 
74
77
  class MultiIndexValidator(Validator):
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.58"
1
+ __version__ = "0.1.60"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.58
3
+ Version: 0.1.60
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -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=oG8qIFObZdiU1VZQlnWzR8uBDq8rcxzkCli9A31_tMI,1460
10
+ falyx/action/action_types.py,sha256=TfzZ8y560h9RWu8Sh1HAbQVRlSAWJZcTdBMbQ-STbQY,1467
11
11
  falyx/action/base_action.py,sha256=o9Nml70-SEVTXnu9J0-VYnO-t5wZZM0o59lYDth24Po,5829
12
- falyx/action/chained_action.py,sha256=oknG4ACr79Yf7jb4jvpp6Au7VJLz3PRqABzUKJLn_eY,9274
13
- falyx/action/confirm_action.py,sha256=HCzDBQMX1I_cquV_26ppKPfB0GJjr24v12kaZ4Q6bSU,8700
12
+ falyx/action/chained_action.py,sha256=TNJz25jnzQQAIwXM6uaK8mzhq4bgRks5n7IZ2nDQM6Q,9614
13
+ falyx/action/confirm_action.py,sha256=oXPnZswkQFJ-F-s73vddpF-BRg1wJeVnHjR4K2yN7ow,9085
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
@@ -24,18 +24,18 @@ falyx/action/save_file_action.py,sha256=Pe_j0hZjDNsO14bykzVYM0gkWB3zmpB1cExSN01I
24
24
  falyx/action/select_file_action.py,sha256=PcV22_wiPeDoJLIhHRiEUmW8N3pYeqQZMVTscQKXuas,9867
25
25
  falyx/action/selection_action.py,sha256=L1evMm7oAQFGMviZ8nMwFKhWKWe8X7wW6dJPHGxpqAE,15398
26
26
  falyx/action/shell_action.py,sha256=0A_kvZLsYmeLHInMM_4Jpe8GCSnXzGBm7H9PnXPvbAs,4055
27
- falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
27
+ falyx/action/signal_action.py,sha256=GxV-0zqYqODOQUa3-tvFTZ2AS1W1QpW6ExonxmWNWbs,1351
28
28
  falyx/action/user_input_action.py,sha256=Up47lumscxnhORMvaft0X-NWpxTXc2GmMZMua__pGhA,3524
29
- falyx/bottom_bar.py,sha256=QYAuWWOM_FVObuRrGrZWOYvuvL45cnYDtV34s5B1qFU,7372
29
+ falyx/bottom_bar.py,sha256=B62N3YCQF_h2Rw_hpc2_FUuLNARI-XIGbQkg-1XvaYE,7405
30
30
  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=SuQxxYuv1cb__EieJyRHp7opO-9T5KsMPxruEjVN36Q,10784
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
38
- falyx/falyx.py,sha256=5pOpYHuw6VZqjC9tszA6wgvahOfxmmIPDtzphcVOVi4,49342
38
+ falyx/falyx.py,sha256=6hKcD1RC-jTPA0WiF9JJ31QkacNJAJwqoqO1zhegwoM,49332
39
39
  falyx/falyx_completer.py,sha256=MsfuZXpfGwbsGG-4Zp-j-vNsNnaote-UAJkJh0s2NZI,5236
40
40
  falyx/hook_manager.py,sha256=TFuHQnAncS_rk6vuw-VSx8bnAppLuHfrZCrzLwqcO9o,2979
41
41
  falyx/hooks.py,sha256=xMfQROib0BNsaQF4AXJpmCiGePoE1f1xpcdibgnVZWM,2913
@@ -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=rNVeth0eMpkZRU_eT1RPVxOGzD4pbdAMx9Kq07T4mG4,709
50
- falyx/parser/command_argument_parser.py,sha256=Ej4gYOxSUOYfLr5b0KkZkC0OkuSUrj54JpuI0D0C6h4,37504
51
- falyx/parser/parsers.py,sha256=X3eEltxBbwRwWG5Q1A1GqSdQCJZAYN5Eub0_U6dlBN4,9159
49
+ falyx/parser/argument_action.py,sha256=Lcpb9siYr_q2T8qU-jXVtqFb11bFPPKEGH3gurJv2NM,757
50
+ falyx/parser/command_argument_parser.py,sha256=lWPeoYj0NUMbBsXrf-Z3KUo4osYrRERoXA_Uu3HmF9w,41264
51
+ falyx/parser/parser_types.py,sha256=DLLuIXE8cAVLS41trfsNy-XJmtqSa1HfnJVAYIIc42w,315
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=km6WvoPUgVdoIpPwDfr6axyjyy8iZy8xndwBNprI2Lk,2996
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=Y_neFXpfHs7qY0syw9XcfR9WeAGRcRw1nG_2L1JJqKE,1083
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
- falyx/validators.py,sha256=DHq-ELpeYVHvRg9daY4Te6ufAf4ryUAM7i_uF7B15Sc,5919
65
- falyx/version.py,sha256=ujgZXX_cw13M2UkyXWVg5j1fgdE25Qb3rUzLZVokSKg,23
66
- falyx-0.1.58.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
67
- falyx-0.1.58.dist-info/METADATA,sha256=Kz5boaWHPPVBRUvQEJFrOOnQZdwUcGGTnwwQ5lsTaTg,5561
68
- falyx-0.1.58.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
69
- falyx-0.1.58.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
70
- falyx-0.1.58.dist-info/RECORD,,
65
+ falyx/validators.py,sha256=AXpMGnk1_7J7MAbbol6pkMAiSIdNHoF5pwtA2-xS6H8,6029
66
+ falyx/version.py,sha256=SH6dBtwKkjChke7DXHi0Y5HbtzV16-5wAfhv91d_D0A,23
67
+ falyx-0.1.60.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
68
+ falyx-0.1.60.dist-info/METADATA,sha256=8GLh3rrDHvaz6XtiNJCau_CDE9iJGeHVYnlrDlFMXc4,5561
69
+ falyx-0.1.60.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
70
+ falyx-0.1.60.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
71
+ falyx-0.1.60.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any