falyx 0.1.40__tar.gz → 0.1.41__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {falyx-0.1.40 → falyx-0.1.41}/PKG-INFO +1 -1
  2. {falyx-0.1.40 → falyx-0.1.41}/falyx/__main__.py +27 -33
  3. {falyx-0.1.40 → falyx-0.1.41}/falyx/command.py +8 -0
  4. {falyx-0.1.40 → falyx-0.1.41}/falyx/config.py +12 -0
  5. {falyx-0.1.40 → falyx-0.1.41}/falyx/falyx.py +50 -4
  6. {falyx-0.1.40 → falyx-0.1.41}/falyx/init.py +1 -1
  7. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/argparse.py +38 -15
  8. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/parsers.py +29 -3
  9. falyx-0.1.41/falyx/version.py +1 -0
  10. {falyx-0.1.40 → falyx-0.1.41}/pyproject.toml +1 -1
  11. falyx-0.1.40/falyx/version.py +0 -1
  12. {falyx-0.1.40 → falyx-0.1.41}/LICENSE +0 -0
  13. {falyx-0.1.40 → falyx-0.1.41}/README.md +0 -0
  14. {falyx-0.1.40 → falyx-0.1.41}/falyx/.pytyped +0 -0
  15. {falyx-0.1.40 → falyx-0.1.41}/falyx/__init__.py +0 -0
  16. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/.pytyped +0 -0
  17. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/__init__.py +0 -0
  18. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/action.py +0 -0
  19. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/action_factory.py +0 -0
  20. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/action_group.py +0 -0
  21. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/base.py +0 -0
  22. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/chained_action.py +0 -0
  23. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/fallback_action.py +0 -0
  24. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/http_action.py +0 -0
  25. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/io_action.py +0 -0
  26. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/literal_input_action.py +0 -0
  27. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/menu_action.py +0 -0
  28. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/mixins.py +0 -0
  29. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/process_action.py +0 -0
  30. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/process_pool_action.py +0 -0
  31. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/prompt_menu_action.py +0 -0
  32. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/select_file_action.py +0 -0
  33. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/selection_action.py +0 -0
  34. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/signal_action.py +0 -0
  35. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/types.py +0 -0
  36. {falyx-0.1.40 → falyx-0.1.41}/falyx/action/user_input_action.py +0 -0
  37. {falyx-0.1.40 → falyx-0.1.41}/falyx/bottom_bar.py +0 -0
  38. {falyx-0.1.40 → falyx-0.1.41}/falyx/context.py +0 -0
  39. {falyx-0.1.40 → falyx-0.1.41}/falyx/debug.py +0 -0
  40. {falyx-0.1.40 → falyx-0.1.41}/falyx/exceptions.py +0 -0
  41. {falyx-0.1.40 → falyx-0.1.41}/falyx/execution_registry.py +0 -0
  42. {falyx-0.1.40 → falyx-0.1.41}/falyx/hook_manager.py +0 -0
  43. {falyx-0.1.40 → falyx-0.1.41}/falyx/hooks.py +0 -0
  44. {falyx-0.1.40 → falyx-0.1.41}/falyx/logger.py +0 -0
  45. {falyx-0.1.40 → falyx-0.1.41}/falyx/menu.py +0 -0
  46. {falyx-0.1.40 → falyx-0.1.41}/falyx/options_manager.py +0 -0
  47. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/.pytyped +0 -0
  48. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/__init__.py +0 -0
  49. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/signature.py +0 -0
  50. {falyx-0.1.40 → falyx-0.1.41}/falyx/parsers/utils.py +0 -0
  51. {falyx-0.1.40 → falyx-0.1.41}/falyx/prompt_utils.py +0 -0
  52. {falyx-0.1.40 → falyx-0.1.41}/falyx/protocols.py +0 -0
  53. {falyx-0.1.40 → falyx-0.1.41}/falyx/retry.py +0 -0
  54. {falyx-0.1.40 → falyx-0.1.41}/falyx/retry_utils.py +0 -0
  55. {falyx-0.1.40 → falyx-0.1.41}/falyx/selection.py +0 -0
  56. {falyx-0.1.40 → falyx-0.1.41}/falyx/signals.py +0 -0
  57. {falyx-0.1.40 → falyx-0.1.41}/falyx/tagged_table.py +0 -0
  58. {falyx-0.1.40 → falyx-0.1.41}/falyx/themes/__init__.py +0 -0
  59. {falyx-0.1.40 → falyx-0.1.41}/falyx/themes/colors.py +0 -0
  60. {falyx-0.1.40 → falyx-0.1.41}/falyx/utils.py +0 -0
  61. {falyx-0.1.40 → falyx-0.1.41}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.40
3
+ Version: 0.1.41
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -8,13 +8,12 @@ Licensed under the MIT License. See LICENSE file for details.
8
8
  import asyncio
9
9
  import os
10
10
  import sys
11
- from argparse import Namespace
12
11
  from pathlib import Path
13
12
  from typing import Any
14
13
 
15
14
  from falyx.config import loader
16
15
  from falyx.falyx import Falyx
17
- from falyx.parsers import FalyxParsers, get_arg_parsers
16
+ from falyx.parsers import CommandArgumentParser
18
17
 
19
18
 
20
19
  def find_falyx_config() -> Path | None:
@@ -39,45 +38,40 @@ def bootstrap() -> Path | None:
39
38
  return config_path
40
39
 
41
40
 
42
- def get_falyx_parsers() -> FalyxParsers:
43
- falyx_parsers: FalyxParsers = get_arg_parsers()
44
- init_parser = falyx_parsers.subparsers.add_parser(
45
- "init", help="Create a new Falyx CLI project"
41
+ def init_config(parser: CommandArgumentParser) -> None:
42
+ parser.add_argument(
43
+ "name",
44
+ type=str,
45
+ help="Name of the new Falyx project",
46
+ default=".",
47
+ nargs="?",
46
48
  )
47
- init_parser.add_argument("name", nargs="?", default=".", help="Project directory")
48
- falyx_parsers.subparsers.add_parser(
49
- "init-global", help="Set up ~/.config/falyx with example tasks"
50
- )
51
- return falyx_parsers
52
-
53
-
54
- def run(args: Namespace) -> Any:
55
- if args.command == "init":
56
- from falyx.init import init_project
57
-
58
- init_project(args.name)
59
- return
60
49
 
61
- if args.command == "init-global":
62
- from falyx.init import init_global
63
-
64
- init_global()
65
- return
66
50
 
51
+ def main() -> Any:
67
52
  bootstrap_path = bootstrap()
68
53
  if not bootstrap_path:
69
- print("No Falyx config file found. Exiting.")
70
- return None
54
+ from falyx.init import init_global, init_project
55
+
56
+ flx: Falyx = Falyx()
57
+ flx.add_command(
58
+ "I",
59
+ "Initialize a new Falyx project",
60
+ init_project,
61
+ aliases=["init"],
62
+ argument_config=init_config,
63
+ )
64
+ flx.add_command(
65
+ "G",
66
+ "Initialize Falyx global configuration",
67
+ init_global,
68
+ aliases=["init-global"],
69
+ )
70
+ else:
71
+ flx = loader(bootstrap_path)
71
72
 
72
- flx: Falyx = loader(bootstrap_path)
73
73
  return asyncio.run(flx.run())
74
74
 
75
75
 
76
- def main():
77
- parsers = get_falyx_parsers()
78
- args = parsers.parse_args()
79
- run(args)
80
-
81
-
82
76
  if __name__ == "__main__":
83
77
  main()
@@ -307,6 +307,14 @@ class Command(BaseModel):
307
307
 
308
308
  return FormattedText(prompt)
309
309
 
310
+ @property
311
+ def usage(self) -> str:
312
+ """Generate a help string for the command arguments."""
313
+ if not self.arg_parser:
314
+ return "No arguments defined."
315
+
316
+ return self.arg_parser.get_usage(plain_text=True)
317
+
310
318
  def log_summary(self) -> None:
311
319
  if self._context:
312
320
  self._context.log_summary()
@@ -18,6 +18,7 @@ from falyx.action.base import BaseAction
18
18
  from falyx.command import Command
19
19
  from falyx.falyx import Falyx
20
20
  from falyx.logger import logger
21
+ from falyx.parsers import CommandArgumentParser
21
22
  from falyx.retry import RetryPolicy
22
23
  from falyx.themes import OneColors
23
24
 
@@ -101,6 +102,7 @@ class RawCommand(BaseModel):
101
102
  retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
102
103
  hidden: bool = False
103
104
  help_text: str = ""
105
+ help_epilogue: str = ""
104
106
 
105
107
  @field_validator("retry_policy")
106
108
  @classmethod
@@ -116,6 +118,14 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
116
118
  commands = []
117
119
  for entry in raw_commands:
118
120
  raw_command = RawCommand(**entry)
121
+ parser = CommandArgumentParser(
122
+ command_key=raw_command.key,
123
+ command_description=raw_command.description,
124
+ command_style=raw_command.style,
125
+ help_text=raw_command.help_text,
126
+ help_epilogue=raw_command.help_epilogue,
127
+ aliases=raw_command.aliases,
128
+ )
119
129
  commands.append(
120
130
  Command.model_validate(
121
131
  {
@@ -123,9 +133,11 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
123
133
  "action": wrap_if_needed(
124
134
  import_action(raw_command.action), name=raw_command.description
125
135
  ),
136
+ "arg_parser": parser,
126
137
  }
127
138
  )
128
139
  )
140
+
129
141
  return commands
130
142
 
131
143
 
@@ -59,7 +59,7 @@ from falyx.execution_registry import ExecutionRegistry as er
59
59
  from falyx.hook_manager import Hook, HookManager, HookType
60
60
  from falyx.logger import logger
61
61
  from falyx.options_manager import OptionsManager
62
- from falyx.parsers import CommandArgumentParser, get_arg_parsers
62
+ from falyx.parsers import CommandArgumentParser, FalyxParsers, get_arg_parsers
63
63
  from falyx.protocols import ArgParserProtocol
64
64
  from falyx.retry import RetryPolicy
65
65
  from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
@@ -152,6 +152,11 @@ class Falyx:
152
152
  self,
153
153
  title: str | Markdown = "Menu",
154
154
  *,
155
+ program: str | None = "falyx",
156
+ usage: str | None = None,
157
+ description: str | None = "Falyx CLI - Run structured async command workflows.",
158
+ epilog: str | None = None,
159
+ version: str = __version__,
155
160
  prompt: str | AnyFormattedText = "> ",
156
161
  columns: int = 3,
157
162
  bottom_bar: BottomBar | str | Callable[[], Any] | None = None,
@@ -170,6 +175,11 @@ class Falyx:
170
175
  ) -> None:
171
176
  """Initializes the Falyx object."""
172
177
  self.title: str | Markdown = title
178
+ self.program: str | None = program
179
+ self.usage: str | None = usage
180
+ self.description: str | None = description
181
+ self.epilog: str | None = epilog
182
+ self.version: str = version
173
183
  self.prompt: str | AnyFormattedText = prompt
174
184
  self.columns: int = columns
175
185
  self.commands: dict[str, Command] = CaseInsensitiveDict()
@@ -1015,12 +1025,35 @@ class Falyx:
1015
1025
  if self.exit_message:
1016
1026
  self.print_message(self.exit_message)
1017
1027
 
1018
- async def run(self) -> None:
1028
+ async def run(
1029
+ self,
1030
+ falyx_parsers: FalyxParsers | None = None,
1031
+ callback: Callable[..., Any] | None = None,
1032
+ ) -> None:
1019
1033
  """Run Falyx CLI with structured subcommands."""
1020
- if not self.cli_args:
1021
- self.cli_args = get_arg_parsers().root.parse_args()
1034
+ if self.cli_args:
1035
+ raise FalyxError(
1036
+ "Run is incompatible with CLI arguments. Use 'run_key' instead."
1037
+ )
1038
+ if falyx_parsers:
1039
+ if not isinstance(falyx_parsers, FalyxParsers):
1040
+ raise FalyxError("falyx_parsers must be an instance of FalyxParsers.")
1041
+ else:
1042
+ falyx_parsers = get_arg_parsers(
1043
+ self.program,
1044
+ self.usage,
1045
+ self.description,
1046
+ self.epilog,
1047
+ commands=self.commands,
1048
+ )
1049
+ self.cli_args = falyx_parsers.parse_args()
1022
1050
  self.options.from_namespace(self.cli_args, "cli_args")
1023
1051
 
1052
+ if callback:
1053
+ if not callable(callback):
1054
+ raise FalyxError("Callback must be a callable function.")
1055
+ callback(self.cli_args)
1056
+
1024
1057
  if not self.options.get("never_prompt"):
1025
1058
  self.options.set("never_prompt", self._never_prompt)
1026
1059
 
@@ -1075,11 +1108,24 @@ class Falyx:
1075
1108
  args, kwargs = await command.parse_args(self.cli_args.command_args)
1076
1109
  except HelpSignal:
1077
1110
  sys.exit(0)
1111
+ except CommandArgumentError as error:
1112
+ self.console.print(f"[{OneColors.DARK_RED}]❌ ['{command.key}'] {error}")
1113
+ command.show_help()
1114
+ sys.exit(1)
1078
1115
  try:
1079
1116
  await self.run_key(self.cli_args.name, args=args, kwargs=kwargs)
1080
1117
  except FalyxError as error:
1081
1118
  self.console.print(f"[{OneColors.DARK_RED}]❌ Error: {error}[/]")
1082
1119
  sys.exit(1)
1120
+ except QuitSignal:
1121
+ logger.info("[QuitSignal]. <- Exiting run.")
1122
+ sys.exit(0)
1123
+ except BackSignal:
1124
+ logger.info("[BackSignal]. <- Exiting run.")
1125
+ sys.exit(0)
1126
+ except CancelSignal:
1127
+ logger.info("[CancelSignal]. <- Exiting run.")
1128
+ sys.exit(0)
1083
1129
 
1084
1130
  if self.cli_args.summary:
1085
1131
  er.summary()
@@ -101,7 +101,7 @@ commands:
101
101
  console = Console(color_system="auto")
102
102
 
103
103
 
104
- def init_project(name: str = ".") -> None:
104
+ def init_project(name: str) -> None:
105
105
  target = Path(name).resolve()
106
106
  target.mkdir(parents=True, exist_ok=True)
107
107
 
@@ -168,6 +168,7 @@ class CommandArgumentParser:
168
168
  self._arguments: list[Argument] = []
169
169
  self._positional: dict[str, Argument] = {}
170
170
  self._keyword: dict[str, Argument] = {}
171
+ self._keyword_list: list[Argument] = []
171
172
  self._flag_map: dict[str, Argument] = {}
172
173
  self._dest_set: set[str] = set()
173
174
  self._add_help()
@@ -488,6 +489,8 @@ class CommandArgumentParser:
488
489
  self._arguments.append(argument)
489
490
  if positional:
490
491
  self._positional[dest] = argument
492
+ else:
493
+ self._keyword_list.append(argument)
491
494
 
492
495
  def get_argument(self, dest: str) -> Argument | None:
493
496
  return next((a for a in self._arguments if a.dest == dest), None)
@@ -832,11 +835,11 @@ class CommandArgumentParser:
832
835
  kwargs_dict[arg.dest] = parsed[arg.dest]
833
836
  return tuple(args_list), kwargs_dict
834
837
 
835
- def render_help(self) -> None:
838
+ def get_options_text(self, plain_text=False) -> str:
836
839
  # Options
837
840
  # Add all keyword arguments to the options list
838
841
  options_list = []
839
- for arg in self._keyword.values():
842
+ for arg in self._keyword_list:
840
843
  choice_text = arg.get_choice_text()
841
844
  if choice_text:
842
845
  options_list.extend([f"[{arg.flags[0]} {choice_text}]"])
@@ -848,19 +851,39 @@ class CommandArgumentParser:
848
851
  choice_text = arg.get_choice_text()
849
852
  if isinstance(arg.nargs, int):
850
853
  choice_text = " ".join([choice_text] * arg.nargs)
851
- options_list.append(escape(choice_text))
852
-
853
- options_text = " ".join(options_list)
854
- command_keys = " | ".join(
855
- [f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
856
- + [
857
- f"[{self.command_style}]{alias}[/{self.command_style}]"
858
- for alias in self.aliases
859
- ]
860
- )
854
+ if plain_text:
855
+ options_list.append(choice_text)
856
+ else:
857
+ options_list.append(escape(choice_text))
858
+
859
+ return " ".join(options_list)
861
860
 
862
- usage = f"usage: {command_keys} {options_text}"
863
- self.console.print(f"[bold]{usage}[/bold]\n")
861
+ def get_command_keys_text(self, plain_text=False) -> str:
862
+ if plain_text:
863
+ command_keys = " | ".join(
864
+ [f"{self.command_key}"] + [f"{alias}" for alias in self.aliases]
865
+ )
866
+ else:
867
+ command_keys = " | ".join(
868
+ [f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
869
+ + [
870
+ f"[{self.command_style}]{alias}[/{self.command_style}]"
871
+ for alias in self.aliases
872
+ ]
873
+ )
874
+ return command_keys
875
+
876
+ def get_usage(self, plain_text=False) -> str:
877
+ """Get the usage text for the command."""
878
+ command_keys = self.get_command_keys_text(plain_text)
879
+ options_text = self.get_options_text(plain_text)
880
+ if options_text:
881
+ return f"{command_keys} {options_text}"
882
+ return command_keys
883
+
884
+ def render_help(self) -> None:
885
+ usage = self.get_usage()
886
+ self.console.print(f"[bold]usage: {usage}[/bold]\n")
864
887
 
865
888
  # Description
866
889
  if self.help_text:
@@ -877,7 +900,7 @@ class CommandArgumentParser:
877
900
  arg_line.append(help_text)
878
901
  self.console.print(arg_line)
879
902
  self.console.print("[bold]options:[/bold]")
880
- for arg in self._keyword.values():
903
+ for arg in self._keyword_list:
881
904
  flags = ", ".join(arg.flags)
882
905
  flags_choice = f"{flags} {arg.get_choice_text()}"
883
906
  arg_line = Text(f" {flags_choice:<30} ")
@@ -2,10 +2,18 @@
2
2
  """parsers.py
3
3
  This module contains the argument parsers used for the Falyx CLI.
4
4
  """
5
- from argparse import REMAINDER, ArgumentParser, Namespace, _SubParsersAction
5
+ from argparse import (
6
+ REMAINDER,
7
+ ArgumentParser,
8
+ Namespace,
9
+ RawDescriptionHelpFormatter,
10
+ _SubParsersAction,
11
+ )
6
12
  from dataclasses import asdict, dataclass
7
13
  from typing import Any, Sequence
8
14
 
15
+ from falyx.command import Command
16
+
9
17
 
10
18
  @dataclass
11
19
  class FalyxParsers:
@@ -47,6 +55,7 @@ def get_arg_parsers(
47
55
  add_help: bool = True,
48
56
  allow_abbrev: bool = True,
49
57
  exit_on_error: bool = True,
58
+ commands: dict[str, Command] | None = None,
50
59
  ) -> FalyxParsers:
51
60
  """Returns the argument parser for the CLI."""
52
61
  parser = ArgumentParser(
@@ -79,8 +88,25 @@ def get_arg_parsers(
79
88
  parser.add_argument("--version", action="store_true", help="Show Falyx version")
80
89
  subparsers = parser.add_subparsers(dest="command")
81
90
 
82
- run_parser = subparsers.add_parser("run", help="Run a specific command")
83
- run_parser.add_argument("name", help="Key, alias, or description of the command")
91
+ run_description = "Run a command by its key or alias."
92
+ run_epilog = ["commands:"]
93
+ if isinstance(commands, dict):
94
+ for command in commands.values():
95
+ run_epilog.append(command.usage)
96
+ command_description = command.description or command.help_text
97
+ run_epilog.append(f" {command_description}")
98
+ run_epilog.append(" ")
99
+ run_epilog.append(
100
+ "Tip: Use 'falyx run ?[COMMAND]' to preview commands by their key or alias."
101
+ )
102
+ run_parser = subparsers.add_parser(
103
+ "run",
104
+ help="Run a specific command",
105
+ description=run_description,
106
+ epilog="\n".join(run_epilog),
107
+ formatter_class=RawDescriptionHelpFormatter,
108
+ )
109
+ run_parser.add_argument("name", help="Run a command by its key or alias")
84
110
  run_parser.add_argument(
85
111
  "--summary",
86
112
  action="store_true",
@@ -0,0 +1 @@
1
+ __version__ = "0.1.41"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.40"
3
+ version = "0.1.41"
4
4
  description = "Reliable and introspectable async CLI action framework."
5
5
  authors = ["Roland Thomas Jr <roland@rtj.dev>"]
6
6
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.1.40"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes