falyx 0.1.23__py3-none-any.whl → 0.1.24__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/falyx.py CHANGED
@@ -51,12 +51,13 @@ from falyx.exceptions import (
51
51
  )
52
52
  from falyx.execution_registry import ExecutionRegistry as er
53
53
  from falyx.hook_manager import Hook, HookManager, HookType
54
+ from falyx.logger import logger
54
55
  from falyx.options_manager import OptionsManager
55
56
  from falyx.parsers import get_arg_parsers
56
57
  from falyx.retry import RetryPolicy
57
58
  from falyx.signals import BackSignal, QuitSignal
58
59
  from falyx.themes.colors import OneColors, get_nord_theme
59
- from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation, logger
60
+ from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
60
61
  from falyx.version import __version__
61
62
 
62
63
 
@@ -78,7 +79,8 @@ class Falyx:
78
79
  Key Features:
79
80
  - Interactive menu with Rich rendering and Prompt Toolkit input handling
80
81
  - Dynamic command management with alias and abbreviation matching
81
- - Full lifecycle hooks (before, success, error, after, teardown) at both menu and command levels
82
+ - Full lifecycle hooks (before, success, error, after, teardown) at both menu and
83
+ command levels
82
84
  - Built-in retry support, spinner visuals, and confirmation prompts
83
85
  - Submenu nesting and action chaining
84
86
  - History tracking, help generation, and run key execution modes
@@ -99,12 +101,14 @@ class Falyx:
99
101
  force_confirm (bool): Seed default for `OptionsManager["force_confirm"]`
100
102
  cli_args (Namespace | None): Parsed CLI arguments, usually from argparse.
101
103
  options (OptionsManager | None): Declarative option mappings.
102
- custom_table (Callable[[Falyx], Table] | Table | None): Custom menu table generator.
104
+ custom_table (Callable[[Falyx], Table] | Table | None): Custom menu table
105
+ generator.
103
106
 
104
107
  Methods:
105
- run(): Main entry point for CLI argument-based workflows. Most users will use this.
108
+ run(): Main entry point for CLI argument-based workflows. Suggested for
109
+ most use cases.
106
110
  menu(): Run the interactive menu loop.
107
- run_key(command_key, return_context): Run a command directly without showing the menu.
111
+ run_key(command_key, return_context): Run a command directly without the menu.
108
112
  add_command(): Add a single command to the menu.
109
113
  add_commands(): Add multiple commands at once.
110
114
  register_all_hooks(): Register hooks across all commands and submenus.
@@ -184,8 +188,10 @@ class Falyx:
184
188
 
185
189
  @property
186
190
  def _name_map(self) -> dict[str, Command]:
187
- """Builds a mapping of all valid input names (keys, aliases, normalized names) to Command objects.
188
- If a collision occurs, logs a warning and keeps the first registered command.
191
+ """
192
+ Builds a mapping of all valid input names (keys, aliases, normalized names) to
193
+ Command objects. If a collision occurs, logs a warning and keeps the first
194
+ registered command.
189
195
  """
190
196
  mapping: dict[str, Command] = {}
191
197
 
@@ -195,8 +201,11 @@ class Falyx:
195
201
  existing = mapping[norm]
196
202
  if existing is not cmd:
197
203
  logger.warning(
198
- f"[alias conflict] '{name}' already assigned to '{existing.description}'."
199
- f" Skipping for '{cmd.description}'."
204
+ "[alias conflict] '%s' already assigned to '%s'. "
205
+ "Skipping for '%s'.",
206
+ name,
207
+ existing.description,
208
+ cmd.description,
200
209
  )
201
210
  else:
202
211
  mapping[norm] = cmd
@@ -238,7 +247,7 @@ class Falyx:
238
247
  key="Y",
239
248
  description="History",
240
249
  aliases=["HISTORY"],
241
- action=er.get_history_action(),
250
+ action=Action(name="View Execution History", action=er.summary),
242
251
  style=OneColors.DARK_YELLOW,
243
252
  )
244
253
 
@@ -283,7 +292,8 @@ class Falyx:
283
292
  self.console.print(table, justify="center")
284
293
  if self.mode == FalyxMode.MENU:
285
294
  self.console.print(
286
- f"📦 Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command before running it.\n",
295
+ f"📦 Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command "
296
+ "before running it.\n",
287
297
  justify="center",
288
298
  )
289
299
 
@@ -346,7 +356,7 @@ class Falyx:
346
356
  is_preview, choice = self.get_command(text, from_validate=True)
347
357
  if is_preview and choice is None:
348
358
  return True
349
- return True if choice else False
359
+ return bool(choice)
350
360
 
351
361
  return Validator.from_callable(
352
362
  validator,
@@ -444,43 +454,10 @@ class Falyx:
444
454
 
445
455
  def debug_hooks(self) -> None:
446
456
  """Logs the names of all hooks registered for the menu and its commands."""
447
-
448
- def hook_names(hook_list):
449
- return [hook.__name__ for hook in hook_list]
450
-
451
- logger.debug(
452
- "Menu-level before hooks: "
453
- f"{hook_names(self.hooks._hooks[HookType.BEFORE])}"
454
- )
455
- logger.debug(
456
- f"Menu-level success hooks: {hook_names(self.hooks._hooks[HookType.ON_SUCCESS])}"
457
- )
458
- logger.debug(
459
- f"Menu-level error hooks: {hook_names(self.hooks._hooks[HookType.ON_ERROR])}"
460
- )
461
- logger.debug(
462
- f"Menu-level after hooks: {hook_names(self.hooks._hooks[HookType.AFTER])}"
463
- )
464
- logger.debug(
465
- f"Menu-level on_teardown hooks: {hook_names(self.hooks._hooks[HookType.ON_TEARDOWN])}"
466
- )
457
+ logger.debug("Menu-level hooks:\n%s", str(self.hooks))
467
458
 
468
459
  for key, command in self.commands.items():
469
- logger.debug(
470
- f"[Command '{key}'] before: {hook_names(command.hooks._hooks[HookType.BEFORE])}"
471
- )
472
- logger.debug(
473
- f"[Command '{key}'] success: {hook_names(command.hooks._hooks[HookType.ON_SUCCESS])}"
474
- )
475
- logger.debug(
476
- f"[Command '{key}'] error: {hook_names(command.hooks._hooks[HookType.ON_ERROR])}"
477
- )
478
- logger.debug(
479
- f"[Command '{key}'] after: {hook_names(command.hooks._hooks[HookType.AFTER])}"
480
- )
481
- logger.debug(
482
- f"[Command '{key}'] on_teardown: {hook_names(command.hooks._hooks[HookType.ON_TEARDOWN])}"
483
- )
460
+ logger.debug("[Command '%s'] hooks:\n%s", key, str(command.hooks))
484
461
 
485
462
  def is_key_available(self, key: str) -> bool:
486
463
  key = key.upper()
@@ -586,7 +563,7 @@ class Falyx:
586
563
  action: BaseAction | Callable[[], Any],
587
564
  *,
588
565
  args: tuple = (),
589
- kwargs: dict[str, Any] = {},
566
+ kwargs: dict[str, Any] | None = None,
590
567
  hidden: bool = False,
591
568
  aliases: list[str] | None = None,
592
569
  help_text: str = "",
@@ -619,7 +596,7 @@ class Falyx:
619
596
  description=description,
620
597
  action=action,
621
598
  args=args,
622
- kwargs=kwargs,
599
+ kwargs=kwargs if kwargs else {},
623
600
  hidden=hidden,
624
601
  aliases=aliases if aliases else [],
625
602
  help_text=help_text,
@@ -665,20 +642,26 @@ class Falyx:
665
642
  bottom_row = []
666
643
  if self.history_command:
667
644
  bottom_row.append(
668
- f"[{self.history_command.key}] [{self.history_command.style}]{self.history_command.description}"
645
+ f"[{self.history_command.key}] [{self.history_command.style}]"
646
+ f"{self.history_command.description}"
669
647
  )
670
648
  if self.help_command:
671
649
  bottom_row.append(
672
- f"[{self.help_command.key}] [{self.help_command.style}]{self.help_command.description}"
650
+ f"[{self.help_command.key}] [{self.help_command.style}]"
651
+ f"{self.help_command.description}"
673
652
  )
674
653
  bottom_row.append(
675
- f"[{self.exit_command.key}] [{self.exit_command.style}]{self.exit_command.description}"
654
+ f"[{self.exit_command.key}] [{self.exit_command.style}]"
655
+ f"{self.exit_command.description}"
676
656
  )
677
657
  return bottom_row
678
658
 
679
659
  def build_default_table(self) -> Table:
680
- """Build the standard table layout. Developers can subclass or call this in custom tables."""
681
- table = Table(title=self.title, show_header=False, box=box.SIMPLE, expand=True)
660
+ """
661
+ Build the standard table layout. Developers can subclass or call this
662
+ in custom tables.
663
+ """
664
+ table = Table(title=self.title, show_header=False, box=box.SIMPLE, expand=True) # type: ignore[arg-type]
682
665
  visible_commands = [item for item in self.commands.items() if not item[1].hidden]
683
666
  for chunk in chunks(visible_commands, self.columns):
684
667
  row = []
@@ -708,7 +691,10 @@ class Falyx:
708
691
  def get_command(
709
692
  self, choice: str, from_validate=False
710
693
  ) -> tuple[bool, Command | None]:
711
- """Returns the selected command based on user input. Supports keys, aliases, and abbreviations."""
694
+ """
695
+ Returns the selected command based on user input.
696
+ Supports keys, aliases, and abbreviations.
697
+ """
712
698
  is_preview, choice = self.parse_preview_command(choice)
713
699
  if is_preview and not choice and self.help_command:
714
700
  is_preview = False
@@ -716,7 +702,7 @@ class Falyx:
716
702
  elif is_preview and not choice:
717
703
  if not from_validate:
718
704
  self.console.print(
719
- f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode.[/]"
705
+ f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode."
720
706
  )
721
707
  return is_preview, None
722
708
 
@@ -734,7 +720,8 @@ class Falyx:
734
720
  if fuzzy_matches:
735
721
  if not from_validate:
736
722
  self.console.print(
737
- f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'. Did you mean:[/] "
723
+ f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'. "
724
+ "Did you mean:"
738
725
  )
739
726
  for match in fuzzy_matches:
740
727
  cmd = name_map[match]
@@ -759,7 +746,7 @@ class Falyx:
759
746
  self, selected_command: Command, error: Exception
760
747
  ) -> None:
761
748
  """Handles errors that occur during the action of the selected command."""
762
- logger.exception(f"Error executing '{selected_command.description}': {error}")
749
+ logger.exception("Error executing '%s': %s", selected_command.description, error)
763
750
  self.console.print(
764
751
  f"[{OneColors.DARK_RED}]An error occurred while executing "
765
752
  f"{selected_command.description}:[/] {error}"
@@ -770,27 +757,27 @@ class Falyx:
770
757
  choice = await self.prompt_session.prompt_async()
771
758
  is_preview, selected_command = self.get_command(choice)
772
759
  if not selected_command:
773
- logger.info(f"Invalid command '{choice}'.")
760
+ logger.info("Invalid command '%s'.", choice)
774
761
  return True
775
762
 
776
763
  if is_preview:
777
- logger.info(f"Preview command '{selected_command.key}' selected.")
764
+ logger.info("Preview command '%s' selected.", selected_command.key)
778
765
  await selected_command.preview()
779
766
  return True
780
767
 
781
768
  if selected_command.requires_input:
782
769
  program = get_program_invocation()
783
770
  self.console.print(
784
- f"[{OneColors.LIGHT_YELLOW}]⚠️ Command '{selected_command.key}' requires input "
785
- f"and must be run via [{OneColors.MAGENTA}]'{program} run'[{OneColors.LIGHT_YELLOW}] "
786
- "with proper piping or arguments.[/]"
771
+ f"[{OneColors.LIGHT_YELLOW}]⚠️ Command '{selected_command.key}' requires"
772
+ f" input and must be run via [{OneColors.MAGENTA}]'{program} run"
773
+ f"'[{OneColors.LIGHT_YELLOW}] with proper piping or arguments.[/]"
787
774
  )
788
775
  return True
789
776
 
790
777
  self.last_run_command = selected_command
791
778
 
792
779
  if selected_command == self.exit_command:
793
- logger.info(f"🔙 Back selected: exiting {self.get_title()}")
780
+ logger.info("🔙 Back selected: exiting %s", self.get_title())
794
781
  return False
795
782
 
796
783
  context = self._create_context(selected_command)
@@ -821,7 +808,7 @@ class Falyx:
821
808
  return None
822
809
 
823
810
  if is_preview:
824
- logger.info(f"Preview command '{selected_command.key}' selected.")
811
+ logger.info("Preview command '%s' selected.", selected_command.key)
825
812
  await selected_command.preview()
826
813
  return None
827
814
 
@@ -840,13 +827,13 @@ class Falyx:
840
827
 
841
828
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
842
829
  logger.info("[run_key] ✅ '%s' complete.", selected_command.description)
843
- except (KeyboardInterrupt, EOFError):
830
+ except (KeyboardInterrupt, EOFError) as error:
844
831
  logger.warning(
845
- "[run_key] ⚠️ Interrupted by user: ", selected_command.description
832
+ "[run_key] ⚠️ Interrupted by user: %s", selected_command.description
846
833
  )
847
834
  raise FalyxError(
848
835
  f"[run_key] ⚠️ '{selected_command.description}' interrupted by user."
849
- )
836
+ ) from error
850
837
  except Exception as error:
851
838
  context.exception = error
852
839
  await self.hooks.trigger(HookType.ON_ERROR, context)
@@ -885,7 +872,8 @@ class Falyx:
885
872
  selected_command.action.set_retry_policy(selected_command.retry_policy)
886
873
  else:
887
874
  logger.warning(
888
- f"[Command:{selected_command.key}] Retry requested, but action is not an Action instance."
875
+ "[Command:%s] Retry requested, but action is not an Action instance.",
876
+ selected_command.key,
889
877
  )
890
878
 
891
879
  def print_message(self, message: str | Markdown | dict[str, Any]) -> None:
@@ -904,7 +892,7 @@ class Falyx:
904
892
 
905
893
  async def menu(self) -> None:
906
894
  """Runs the menu and handles user input."""
907
- logger.info(f"Running menu: {self.get_title()}")
895
+ logger.info("Running menu: %s", self.get_title())
908
896
  self.debug_hooks()
909
897
  if self.welcome_message:
910
898
  self.print_message(self.welcome_message)
@@ -928,7 +916,7 @@ class Falyx:
928
916
  except BackSignal:
929
917
  logger.info("BackSignal received.")
930
918
  finally:
931
- logger.info(f"Exiting menu: {self.get_title()}")
919
+ logger.info("Exiting menu: %s", self.get_title())
932
920
  if self.exit_message:
933
921
  self.print_message(self.exit_message)
934
922
 
@@ -964,7 +952,7 @@ class Falyx:
964
952
  _, command = self.get_command(self.cli_args.name)
965
953
  if not command:
966
954
  self.console.print(
967
- f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
955
+ f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
968
956
  )
969
957
  sys.exit(1)
970
958
  self.console.print(
@@ -979,7 +967,7 @@ class Falyx:
979
967
  if is_preview:
980
968
  if command is None:
981
969
  sys.exit(1)
982
- logger.info(f"Preview command '{command.key}' selected.")
970
+ logger.info("Preview command '%s' selected.", command.key)
983
971
  await command.preview()
984
972
  sys.exit(0)
985
973
  if not command:
@@ -1004,12 +992,14 @@ class Falyx:
1004
992
  ]
1005
993
  if not matching:
1006
994
  self.console.print(
1007
- f"[{OneColors.LIGHT_YELLOW}]⚠️ No commands found with tag: '{self.cli_args.tag}'[/]"
995
+ f"[{OneColors.LIGHT_YELLOW}]⚠️ No commands found with tag: "
996
+ f"'{self.cli_args.tag}'"
1008
997
  )
1009
998
  sys.exit(1)
1010
999
 
1011
1000
  self.console.print(
1012
- f"[{OneColors.CYAN_b}]🚀 Running all commands with tag:[/] {self.cli_args.tag}"
1001
+ f"[{OneColors.CYAN_b}]🚀 Running all commands with tag:[/] "
1002
+ f"{self.cli_args.tag}"
1013
1003
  )
1014
1004
  for cmd in matching:
1015
1005
  self._set_retry_policy(cmd)
falyx/hook_manager.py CHANGED
@@ -7,7 +7,7 @@ from enum import Enum
7
7
  from typing import Awaitable, Callable, Dict, List, Optional, Union
8
8
 
9
9
  from falyx.context import ExecutionContext
10
- from falyx.utils import logger
10
+ from falyx.logger import logger
11
11
 
12
12
  Hook = Union[
13
13
  Callable[[ExecutionContext], None], Callable[[ExecutionContext], Awaitable[None]]
@@ -34,6 +34,8 @@ class HookType(Enum):
34
34
 
35
35
 
36
36
  class HookManager:
37
+ """HookManager"""
38
+
37
39
  def __init__(self) -> None:
38
40
  self._hooks: Dict[HookType, List[Hook]] = {
39
41
  hook_type: [] for hook_type in HookType
@@ -62,8 +64,11 @@ class HookManager:
62
64
  hook(context)
63
65
  except Exception as hook_error:
64
66
  logger.warning(
65
- f"⚠️ Hook '{hook.__name__}' raised an exception during '{hook_type}'"
66
- f" for '{context.name}': {hook_error}"
67
+ "⚠️ Hook '%s' raised an exception during '%s' for '%s': %s",
68
+ hook.__name__,
69
+ hook_type,
70
+ context.name,
71
+ hook_error,
67
72
  )
68
73
 
69
74
  if hook_type == HookType.ON_ERROR:
@@ -71,3 +76,15 @@ class HookManager:
71
76
  context.exception, Exception
72
77
  ), "Context exception should be set for ON_ERROR hook"
73
78
  raise context.exception from hook_error
79
+
80
+ def __str__(self) -> str:
81
+ """Return a formatted string of registered hooks grouped by hook type."""
82
+
83
+ def format_hook_list(hooks: list[Hook]) -> str:
84
+ return ", ".join(h.__name__ for h in hooks) if hooks else "—"
85
+
86
+ lines = ["<HookManager>"]
87
+ for hook_type in HookType:
88
+ hook_list = self._hooks.get(hook_type, [])
89
+ lines.append(f" {hook_type.value}: {format_hook_list(hook_list)}")
90
+ return "\n".join(lines)
falyx/hooks.py CHANGED
@@ -5,11 +5,13 @@ from typing import Any, Callable
5
5
 
6
6
  from falyx.context import ExecutionContext
7
7
  from falyx.exceptions import CircuitBreakerOpen
8
+ from falyx.logger import logger
8
9
  from falyx.themes.colors import OneColors
9
- from falyx.utils import logger
10
10
 
11
11
 
12
12
  class ResultReporter:
13
+ """Reports the success of an action."""
14
+
13
15
  def __init__(self, formatter: Callable[[Any], str] | None = None):
14
16
  """
15
17
  Optional result formatter. If not provided, uses repr(result).
@@ -41,6 +43,8 @@ class ResultReporter:
41
43
 
42
44
 
43
45
  class CircuitBreaker:
46
+ """Circuit Breaker pattern to prevent repeated failures."""
47
+
44
48
  def __init__(self, max_failures=3, reset_timeout=10):
45
49
  self.max_failures = max_failures
46
50
  self.reset_timeout = reset_timeout
@@ -55,7 +59,7 @@ class CircuitBreaker:
55
59
  f"🔴 Circuit open for '{name}' until {time.ctime(self.open_until)}."
56
60
  )
57
61
  else:
58
- logger.info(f"🟢 Circuit closed again for '{name}'.")
62
+ logger.info("🟢 Circuit closed again for '%s'.")
59
63
  self.failures = 0
60
64
  self.open_until = None
61
65
 
@@ -63,15 +67,18 @@ class CircuitBreaker:
63
67
  name = context.name
64
68
  self.failures += 1
65
69
  logger.warning(
66
- f"⚠️ CircuitBreaker: '{name}' failure {self.failures}/{self.max_failures}."
70
+ "⚠️ CircuitBreaker: '%s' failure %s/%s.",
71
+ name,
72
+ self.failures,
73
+ self.max_failures,
67
74
  )
68
75
  if self.failures >= self.max_failures:
69
76
  self.open_until = time.time() + self.reset_timeout
70
77
  logger.error(
71
- f"🔴 Circuit opened for '{name}' until {time.ctime(self.open_until)}."
78
+ "🔴 Circuit opened for '%s' until %s.", name, time.ctime(self.open_until)
72
79
  )
73
80
 
74
- def after_hook(self, context: ExecutionContext):
81
+ def after_hook(self, _: ExecutionContext):
75
82
  self.failures = 0
76
83
 
77
84
  def is_open(self):
falyx/http_action.py CHANGED
@@ -16,8 +16,8 @@ from rich.tree import Tree
16
16
  from falyx.action import Action
17
17
  from falyx.context import ExecutionContext, SharedContext
18
18
  from falyx.hook_manager import HookManager, HookType
19
+ from falyx.logger import logger
19
20
  from falyx.themes.colors import OneColors
20
- from falyx.utils import logger
21
21
 
22
22
 
23
23
  async def close_shared_http_session(context: ExecutionContext) -> None:
@@ -35,9 +35,9 @@ class HTTPAction(Action):
35
35
  """
36
36
  An Action for executing HTTP requests using aiohttp with shared session reuse.
37
37
 
38
- This action integrates seamlessly into Falyx pipelines, with automatic session management,
39
- result injection, and lifecycle hook support. It is ideal for CLI-driven API workflows
40
- where you need to call remote services and process their responses.
38
+ This action integrates seamlessly into Falyx pipelines, with automatic session
39
+ management, result injection, and lifecycle hook support. It is ideal for CLI-driven
40
+ API workflows where you need to call remote services and process their responses.
41
41
 
42
42
  Features:
43
43
  - Uses aiohttp for asynchronous HTTP requests
@@ -97,7 +97,7 @@ class HTTPAction(Action):
97
97
  retry_policy=retry_policy,
98
98
  )
99
99
 
100
- async def _request(self, *args, **kwargs) -> dict[str, Any]:
100
+ async def _request(self, *_, **__) -> dict[str, Any]:
101
101
  if self.shared_context:
102
102
  context: SharedContext = self.shared_context
103
103
  session = context.get("http_session")
@@ -153,6 +153,7 @@ class HTTPAction(Action):
153
153
  def __str__(self):
154
154
  return (
155
155
  f"HTTPAction(name={self.name!r}, method={self.method!r}, url={self.url!r}, "
156
- f"headers={self.headers!r}, params={self.params!r}, json={self.json!r}, data={self.data!r}, "
157
- f"retry={self.retry_policy.enabled}, inject_last_result={self.inject_last_result})"
156
+ f"headers={self.headers!r}, params={self.params!r}, json={self.json!r}, "
157
+ f"data={self.data!r}, retry={self.retry_policy.enabled}, "
158
+ f"inject_last_result={self.inject_last_result})"
158
159
  )
falyx/init.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """init.py"""
2
3
  from pathlib import Path
3
4
 
4
5
  from rich.console import Console
falyx/io_action.py CHANGED
@@ -28,8 +28,8 @@ from falyx.context import ExecutionContext
28
28
  from falyx.exceptions import FalyxError
29
29
  from falyx.execution_registry import ExecutionRegistry as er
30
30
  from falyx.hook_manager import HookManager, HookType
31
+ from falyx.logger import logger
31
32
  from falyx.themes.colors import OneColors
32
- from falyx.utils import logger
33
33
 
34
34
 
35
35
  class BaseIOAction(BaseAction):
@@ -78,7 +78,7 @@ class BaseIOAction(BaseAction):
78
78
  def from_input(self, raw: str | bytes) -> Any:
79
79
  raise NotImplementedError
80
80
 
81
- def to_output(self, data: Any) -> str | bytes:
81
+ def to_output(self, result: Any) -> str | bytes:
82
82
  raise NotImplementedError
83
83
 
84
84
  async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes:
@@ -113,7 +113,7 @@ class BaseIOAction(BaseAction):
113
113
  try:
114
114
  if self.mode == "stream":
115
115
  line_gen = await self._read_stdin_stream()
116
- async for line in self._stream_lines(line_gen, args, kwargs):
116
+ async for _ in self._stream_lines(line_gen, args, kwargs):
117
117
  pass
118
118
  result = getattr(self, "_last_result", None)
119
119
  else:
@@ -185,8 +185,9 @@ class ShellAction(BaseIOAction):
185
185
  Designed for quick integration with shell tools like `grep`, `ping`, `jq`, etc.
186
186
 
187
187
  ⚠️ Security Warning:
188
- By default, ShellAction uses `shell=True`, which can be dangerous with unsanitized input.
189
- To mitigate this, set `safe_mode=True` to use `shell=False` with `shlex.split()`.
188
+ By default, ShellAction uses `shell=True`, which can be dangerous with
189
+ unsanitized input. To mitigate this, set `safe_mode=True` to use `shell=False`
190
+ with `shlex.split()`.
190
191
 
191
192
  Features:
192
193
  - Automatically handles input parsing (str/bytes)
@@ -198,9 +199,11 @@ class ShellAction(BaseIOAction):
198
199
 
199
200
  Args:
200
201
  name (str): Name of the action.
201
- command_template (str): Shell command to execute. Must include `{}` to include input.
202
- If no placeholder is present, the input is not included.
203
- safe_mode (bool): If True, runs with `shell=False` using shlex parsing (default: False).
202
+ command_template (str): Shell command to execute. Must include `{}` to include
203
+ input. If no placeholder is present, the input is not
204
+ included.
205
+ safe_mode (bool): If True, runs with `shell=False` using shlex parsing
206
+ (default: False).
204
207
  """
205
208
 
206
209
  def __init__(
@@ -222,9 +225,11 @@ class ShellAction(BaseIOAction):
222
225
  command = self.command_template.format(parsed_input)
223
226
  if self.safe_mode:
224
227
  args = shlex.split(command)
225
- result = subprocess.run(args, capture_output=True, text=True)
228
+ result = subprocess.run(args, capture_output=True, text=True, check=True)
226
229
  else:
227
- result = subprocess.run(command, shell=True, text=True, capture_output=True)
230
+ result = subprocess.run(
231
+ command, shell=True, text=True, capture_output=True, check=True
232
+ )
228
233
  if result.returncode != 0:
229
234
  raise RuntimeError(result.stderr.strip())
230
235
  return result.stdout.strip()
@@ -246,6 +251,6 @@ class ShellAction(BaseIOAction):
246
251
 
247
252
  def __str__(self):
248
253
  return (
249
- f"ShellAction(name={self.name!r}, command_template={self.command_template!r}, "
250
- f"safe_mode={self.safe_mode})"
254
+ f"ShellAction(name={self.name!r}, command_template={self.command_template!r},"
255
+ f" safe_mode={self.safe_mode})"
251
256
  )
falyx/logger.py ADDED
@@ -0,0 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """logger.py"""
3
+ import logging
4
+
5
+ logger = logging.getLogger("falyx")
falyx/menu_action.py CHANGED
@@ -12,15 +12,18 @@ from falyx.action import BaseAction
12
12
  from falyx.context import ExecutionContext
13
13
  from falyx.execution_registry import ExecutionRegistry as er
14
14
  from falyx.hook_manager import HookType
15
+ from falyx.logger import logger
15
16
  from falyx.selection import prompt_for_selection, render_table_base
16
17
  from falyx.signal_action import SignalAction
17
18
  from falyx.signals import BackSignal, QuitSignal
18
19
  from falyx.themes.colors import OneColors
19
- from falyx.utils import CaseInsensitiveDict, chunks, logger
20
+ from falyx.utils import CaseInsensitiveDict, chunks
20
21
 
21
22
 
22
23
  @dataclass
23
24
  class MenuOption:
25
+ """Represents a single menu option with a description and an action to execute."""
26
+
24
27
  description: str
25
28
  action: BaseAction
26
29
  style: str = OneColors.WHITE
@@ -93,6 +96,8 @@ class MenuOptionMap(CaseInsensitiveDict):
93
96
 
94
97
 
95
98
  class MenuAction(BaseAction):
99
+ """MenuAction class for creating single use menu actions."""
100
+
96
101
  def __init__(
97
102
  self,
98
103
  name: str,
@@ -162,7 +167,8 @@ class MenuAction(BaseAction):
162
167
 
163
168
  if self.never_prompt and not effective_default:
164
169
  raise ValueError(
165
- f"[{self.name}] 'never_prompt' is True but no valid default_selection was provided."
170
+ f"[{self.name}] 'never_prompt' is True but no valid default_selection"
171
+ " was provided."
166
172
  )
167
173
 
168
174
  context.start_timer()
falyx/options_manager.py CHANGED
@@ -5,12 +5,14 @@ from argparse import Namespace
5
5
  from collections import defaultdict
6
6
  from typing import Any, Callable
7
7
 
8
- from falyx.utils import logger
8
+ from falyx.logger import logger
9
9
 
10
10
 
11
11
  class OptionsManager:
12
+ """OptionsManager"""
13
+
12
14
  def __init__(self, namespaces: list[tuple[str, Namespace]] | None = None) -> None:
13
- self.options: defaultdict = defaultdict(lambda: Namespace())
15
+ self.options: defaultdict = defaultdict(Namespace)
14
16
  if namespaces:
15
17
  for namespace_name, namespace in namespaces:
16
18
  self.from_namespace(namespace, namespace_name)
@@ -42,7 +44,9 @@ class OptionsManager:
42
44
  f"Cannot toggle non-boolean option: '{option_name}' in '{namespace_name}'"
43
45
  )
44
46
  self.set(option_name, not current, namespace_name=namespace_name)
45
- logger.debug(f"Toggled '{option_name}' in '{namespace_name}' to {not current}")
47
+ logger.debug(
48
+ "Toggled '%s' in '%s' to %s", option_name, namespace_name, not current
49
+ )
46
50
 
47
51
  def get_value_getter(
48
52
  self, option_name: str, namespace_name: str = "cli_args"
falyx/parsers.py CHANGED
@@ -39,7 +39,7 @@ def get_arg_parsers(
39
39
  epilog: (
40
40
  str | None
41
41
  ) = "Tip: Use 'falyx run ?[COMMAND]' to preview any command from the CLI.",
42
- parents: Sequence[ArgumentParser] = [],
42
+ parents: Sequence[ArgumentParser] | None = None,
43
43
  prefix_chars: str = "-",
44
44
  fromfile_prefix_chars: str | None = None,
45
45
  argument_default: Any = None,
@@ -54,7 +54,7 @@ def get_arg_parsers(
54
54
  usage=usage,
55
55
  description=description,
56
56
  epilog=epilog,
57
- parents=parents,
57
+ parents=parents if parents else [],
58
58
  prefix_chars=prefix_chars,
59
59
  fromfile_prefix_chars=fromfile_prefix_chars,
60
60
  argument_default=argument_default,