falyx 0.1.23__py3-none-any.whl → 0.1.25__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.
Files changed (44) hide show
  1. falyx/__init__.py +1 -1
  2. falyx/action/__init__.py +41 -0
  3. falyx/{action.py → action/action.py} +41 -23
  4. falyx/{action_factory.py → action/action_factory.py} +17 -5
  5. falyx/{http_action.py → action/http_action.py} +10 -9
  6. falyx/{io_action.py → action/io_action.py} +19 -14
  7. falyx/{menu_action.py → action/menu_action.py} +9 -79
  8. falyx/{select_file_action.py → action/select_file_action.py} +5 -36
  9. falyx/{selection_action.py → action/selection_action.py} +22 -8
  10. falyx/action/signal_action.py +43 -0
  11. falyx/action/types.py +37 -0
  12. falyx/bottom_bar.py +3 -3
  13. falyx/command.py +13 -10
  14. falyx/config.py +17 -9
  15. falyx/context.py +16 -8
  16. falyx/debug.py +2 -1
  17. falyx/exceptions.py +3 -0
  18. falyx/execution_registry.py +59 -13
  19. falyx/falyx.py +67 -77
  20. falyx/hook_manager.py +20 -3
  21. falyx/hooks.py +13 -6
  22. falyx/init.py +1 -0
  23. falyx/logger.py +5 -0
  24. falyx/menu.py +85 -0
  25. falyx/options_manager.py +7 -3
  26. falyx/parsers.py +2 -2
  27. falyx/prompt_utils.py +30 -1
  28. falyx/protocols.py +2 -1
  29. falyx/retry.py +23 -12
  30. falyx/retry_utils.py +2 -1
  31. falyx/selection.py +7 -3
  32. falyx/signals.py +3 -0
  33. falyx/tagged_table.py +2 -1
  34. falyx/themes/__init__.py +15 -0
  35. falyx/utils.py +11 -39
  36. falyx/validators.py +8 -7
  37. falyx/version.py +1 -1
  38. {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/METADATA +2 -1
  39. falyx-0.1.25.dist-info/RECORD +46 -0
  40. falyx/signal_action.py +0 -30
  41. falyx-0.1.23.dist-info/RECORD +0 -41
  42. {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/LICENSE +0 -0
  43. {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/WHEEL +0 -0
  44. {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/entry_points.txt +0 -0
falyx/__init__.py CHANGED
@@ -7,7 +7,7 @@ Licensed under the MIT License. See LICENSE file for details.
7
7
 
8
8
  import logging
9
9
 
10
- from .action import Action, ActionGroup, ChainedAction, ProcessAction
10
+ from .action.action import Action, ActionGroup, ChainedAction, ProcessAction
11
11
  from .command import Command
12
12
  from .context import ExecutionContext, SharedContext
13
13
  from .execution_registry import ExecutionRegistry
@@ -0,0 +1,41 @@
1
+ """
2
+ Falyx CLI Framework
3
+
4
+ Copyright (c) 2025 rtj.dev LLC.
5
+ Licensed under the MIT License. See LICENSE file for details.
6
+ """
7
+
8
+ from .action import (
9
+ Action,
10
+ ActionGroup,
11
+ BaseAction,
12
+ ChainedAction,
13
+ FallbackAction,
14
+ LiteralInputAction,
15
+ ProcessAction,
16
+ )
17
+ from .action_factory import ActionFactoryAction
18
+ from .http_action import HTTPAction
19
+ from .io_action import BaseIOAction, ShellAction
20
+ from .menu_action import MenuAction
21
+ from .select_file_action import SelectFileAction
22
+ from .selection_action import SelectionAction
23
+ from .signal_action import SignalAction
24
+
25
+ __all__ = [
26
+ "Action",
27
+ "ActionGroup",
28
+ "BaseAction",
29
+ "ChainedAction",
30
+ "ProcessAction",
31
+ "ActionFactoryAction",
32
+ "HTTPAction",
33
+ "BaseIOAction",
34
+ "ShellAction",
35
+ "SelectionAction",
36
+ "SelectFileAction",
37
+ "MenuAction",
38
+ "SignalAction",
39
+ "FallbackAction",
40
+ "LiteralInputAction",
41
+ ]
@@ -4,7 +4,8 @@
4
4
  Core action system for Falyx.
5
5
 
6
6
  This module defines the building blocks for executable actions and workflows,
7
- providing a structured way to compose, execute, recover, and manage sequences of operations.
7
+ providing a structured way to compose, execute, recover, and manage sequences of
8
+ operations.
8
9
 
9
10
  All actions are callable and follow a unified signature:
10
11
  result = action(*args, **kwargs)
@@ -14,7 +15,8 @@ Core guarantees:
14
15
  - Consistent timing and execution context tracking for each run.
15
16
  - Unified, predictable result handling and error propagation.
16
17
  - Optional last_result injection to enable flexible, data-driven workflows.
17
- - Built-in support for retries, rollbacks, parallel groups, chaining, and fallback recovery.
18
+ - Built-in support for retries, rollbacks, parallel groups, chaining, and fallback
19
+ recovery.
18
20
 
19
21
  Key components:
20
22
  - Action: wraps a function or coroutine into a standard executable unit.
@@ -43,10 +45,11 @@ from falyx.debug import register_debug_hooks
43
45
  from falyx.exceptions import EmptyChainError
44
46
  from falyx.execution_registry import ExecutionRegistry as er
45
47
  from falyx.hook_manager import Hook, HookManager, HookType
48
+ from falyx.logger import logger
46
49
  from falyx.options_manager import OptionsManager
47
50
  from falyx.retry import RetryHandler, RetryPolicy
48
- from falyx.themes.colors import OneColors
49
- from falyx.utils import ensure_async, logger
51
+ from falyx.themes import OneColors
52
+ from falyx.utils import ensure_async
50
53
 
51
54
 
52
55
  class BaseAction(ABC):
@@ -55,7 +58,8 @@ class BaseAction(ABC):
55
58
  complex actions like `ChainedAction` or `ActionGroup`. They can also
56
59
  be run independently or as part of Falyx.
57
60
 
58
- inject_last_result (bool): Whether to inject the previous action's result into kwargs.
61
+ inject_last_result (bool): Whether to inject the previous action's result
62
+ into kwargs.
59
63
  inject_into (str): The name of the kwarg key to inject the result as
60
64
  (default: 'last_result').
61
65
  _requires_injection (bool): Whether the action requires input injection.
@@ -104,7 +108,9 @@ class BaseAction(ABC):
104
108
  self.shared_context = shared_context
105
109
 
106
110
  def get_option(self, option_name: str, default: Any = None) -> Any:
107
- """Resolve an option from the OptionsManager if present, otherwise use the fallback."""
111
+ """
112
+ Resolve an option from the OptionsManager if present, otherwise use the fallback.
113
+ """
108
114
  if self.options_manager:
109
115
  return self.options_manager.get(option_name, default)
110
116
  return default
@@ -288,8 +294,10 @@ class Action(BaseAction):
288
294
 
289
295
  def __str__(self):
290
296
  return (
291
- f"Action(name={self.name!r}, action={getattr(self._action, '__name__', repr(self._action))}, "
292
- f"args={self.args!r}, kwargs={self.kwargs!r}, retry={self.retry_policy.enabled})"
297
+ f"Action(name={self.name!r}, action="
298
+ f"{getattr(self._action, '__name__', repr(self._action))}, "
299
+ f"args={self.args!r}, kwargs={self.kwargs!r}, "
300
+ f"retry={self.retry_policy.enabled})"
293
301
  )
294
302
 
295
303
 
@@ -309,7 +317,7 @@ class LiteralInputAction(Action):
309
317
  def __init__(self, value: Any):
310
318
  self._value = value
311
319
 
312
- async def literal(*args, **kwargs):
320
+ async def literal(*_, **__):
313
321
  return value
314
322
 
315
323
  super().__init__("Input", literal)
@@ -333,14 +341,16 @@ class LiteralInputAction(Action):
333
341
 
334
342
  class FallbackAction(Action):
335
343
  """
336
- FallbackAction provides a default value if the previous action failed or returned None.
344
+ FallbackAction provides a default value if the previous action failed or
345
+ returned None.
337
346
 
338
347
  It injects the last result and checks:
339
348
  - If last_result is not None, it passes it through unchanged.
340
349
  - If last_result is None (e.g., due to failure), it replaces it with a fallback value.
341
350
 
342
351
  Used in ChainedAction pipelines to gracefully recover from errors or missing data.
343
- When activated, it consumes the preceding error and allows the chain to continue normally.
352
+ When activated, it consumes the preceding error and allows the chain to continue
353
+ normally.
344
354
 
345
355
  Args:
346
356
  fallback (Any): The fallback value to use if last_result is None.
@@ -413,16 +423,19 @@ class ChainedAction(BaseAction, ActionListMixin):
413
423
  - Rolls back all previously executed actions if a failure occurs.
414
424
  - Handles literal values with LiteralInputAction.
415
425
 
416
- Best used for defining robust, ordered workflows where each step can depend on previous results.
426
+ Best used for defining robust, ordered workflows where each step can depend on
427
+ previous results.
417
428
 
418
429
  Args:
419
430
  name (str): Name of the chain.
420
431
  actions (list): List of actions or literals to execute.
421
432
  hooks (HookManager, optional): Hooks for lifecycle events.
422
- inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
433
+ inject_last_result (bool, optional): Whether to inject last results into kwargs
434
+ by default.
423
435
  inject_into (str, optional): Key name for injection.
424
436
  auto_inject (bool, optional): Auto-enable injection for subsequent actions.
425
- return_list (bool, optional): Whether to return a list of all results. False returns the last result.
437
+ return_list (bool, optional): Whether to return a list of all results. False
438
+ returns the last result.
426
439
  """
427
440
 
428
441
  def __init__(
@@ -468,7 +481,7 @@ class ChainedAction(BaseAction, ActionListMixin):
468
481
  if not self.actions:
469
482
  raise EmptyChainError(f"[{self.name}] No actions to execute.")
470
483
 
471
- shared_context = SharedContext(name=self.name)
484
+ shared_context = SharedContext(name=self.name, action=self)
472
485
  if self.shared_context:
473
486
  shared_context.add_result(self.shared_context.last_result())
474
487
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -503,7 +516,8 @@ class ChainedAction(BaseAction, ActionListMixin):
503
516
  self.actions[index + 1], FallbackAction
504
517
  ):
505
518
  logger.warning(
506
- "[%s] ⚠️ Fallback triggered: %s, recovering with fallback '%s'.",
519
+ "[%s] ⚠️ Fallback triggered: %s, recovering with fallback "
520
+ "'%s'.",
507
521
  self.name,
508
522
  error,
509
523
  self.actions[index + 1].name,
@@ -579,7 +593,8 @@ class ChainedAction(BaseAction, ActionListMixin):
579
593
 
580
594
  def __str__(self):
581
595
  return (
582
- f"ChainedAction(name={self.name!r}, actions={[a.name for a in self.actions]!r}, "
596
+ f"ChainedAction(name={self.name!r}, "
597
+ f"actions={[a.name for a in self.actions]!r}, "
583
598
  f"auto_inject={self.auto_inject}, return_list={self.return_list})"
584
599
  )
585
600
 
@@ -613,7 +628,8 @@ class ActionGroup(BaseAction, ActionListMixin):
613
628
  name (str): Name of the chain.
614
629
  actions (list): List of actions or literals to execute.
615
630
  hooks (HookManager, optional): Hooks for lifecycle events.
616
- inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
631
+ inject_last_result (bool, optional): Whether to inject last results into kwargs
632
+ by default.
617
633
  inject_into (str, optional): Key name for injection.
618
634
  """
619
635
 
@@ -643,7 +659,8 @@ class ActionGroup(BaseAction, ActionListMixin):
643
659
  return Action(name=action.__name__, action=action)
644
660
  else:
645
661
  raise TypeError(
646
- f"ActionGroup only accepts BaseAction or callable, got {type(action).__name__}"
662
+ "ActionGroup only accepts BaseAction or callable, got "
663
+ f"{type(action).__name__}"
647
664
  )
648
665
 
649
666
  def add_action(self, action: BaseAction | Any) -> None:
@@ -653,7 +670,7 @@ class ActionGroup(BaseAction, ActionListMixin):
653
670
  action.register_teardown(self.hooks)
654
671
 
655
672
  async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
656
- shared_context = SharedContext(name=self.name, is_parallel=True)
673
+ shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
657
674
  if self.shared_context:
658
675
  shared_context.set_shared_result(self.shared_context.last_result())
659
676
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -721,8 +738,8 @@ class ActionGroup(BaseAction, ActionListMixin):
721
738
 
722
739
  def __str__(self):
723
740
  return (
724
- f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r}, "
725
- f"inject_last_result={self.inject_last_result})"
741
+ f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},"
742
+ f" inject_last_result={self.inject_last_result})"
726
743
  )
727
744
 
728
745
 
@@ -831,6 +848,7 @@ class ProcessAction(BaseAction):
831
848
 
832
849
  def __str__(self) -> str:
833
850
  return (
834
- f"ProcessAction(name={self.name!r}, action={getattr(self.action, '__name__', repr(self.action))}, "
851
+ f"ProcessAction(name={self.name!r}, "
852
+ f"action={getattr(self.action, '__name__', repr(self.action))}, "
835
853
  f"args={self.args!r}, kwargs={self.kwargs!r})"
836
854
  )
@@ -1,14 +1,16 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """action_factory.py"""
2
3
  from typing import Any
3
4
 
4
5
  from rich.tree import Tree
5
6
 
6
- from falyx.action import BaseAction
7
+ from falyx.action.action import BaseAction
7
8
  from falyx.context import ExecutionContext
8
9
  from falyx.execution_registry import ExecutionRegistry as er
9
10
  from falyx.hook_manager import HookType
11
+ from falyx.logger import logger
10
12
  from falyx.protocols import ActionFactoryProtocol
11
- from falyx.themes.colors import OneColors
13
+ from falyx.themes import OneColors
12
14
 
13
15
 
14
16
  class ActionFactoryAction(BaseAction):
@@ -33,7 +35,7 @@ class ActionFactoryAction(BaseAction):
33
35
  inject_last_result: bool = False,
34
36
  inject_into: str = "last_result",
35
37
  preview_args: tuple[Any, ...] = (),
36
- preview_kwargs: dict[str, Any] = {},
38
+ preview_kwargs: dict[str, Any] | None = None,
37
39
  ):
38
40
  super().__init__(
39
41
  name=name,
@@ -42,7 +44,7 @@ class ActionFactoryAction(BaseAction):
42
44
  )
43
45
  self.factory = factory
44
46
  self.preview_args = preview_args
45
- self.preview_kwargs = preview_kwargs
47
+ self.preview_kwargs = preview_kwargs or {}
46
48
 
47
49
  async def _run(self, *args, **kwargs) -> Any:
48
50
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -58,10 +60,20 @@ class ActionFactoryAction(BaseAction):
58
60
  generated_action = self.factory(*args, **updated_kwargs)
59
61
  if not isinstance(generated_action, BaseAction):
60
62
  raise TypeError(
61
- f"[{self.name}] Factory must return a BaseAction, got {type(generated_action).__name__}"
63
+ f"[{self.name}] Factory must return a BaseAction, got "
64
+ f"{type(generated_action).__name__}"
62
65
  )
63
66
  if self.shared_context:
64
67
  generated_action.set_shared_context(self.shared_context)
68
+ if hasattr(generated_action, "register_teardown") and callable(
69
+ generated_action.register_teardown
70
+ ):
71
+ generated_action.register_teardown(self.shared_context.action.hooks)
72
+ logger.debug(
73
+ "[%s] Registered teardown for %s",
74
+ self.name,
75
+ generated_action.name,
76
+ )
65
77
  if self.options_manager:
66
78
  generated_action.set_options_manager(self.options_manager)
67
79
  context.result = await generated_action(*args, **kwargs)
@@ -13,11 +13,11 @@ from typing import Any
13
13
  import aiohttp
14
14
  from rich.tree import Tree
15
15
 
16
- from falyx.action import Action
16
+ from falyx.action.action import Action
17
17
  from falyx.context import ExecutionContext, SharedContext
18
18
  from falyx.hook_manager import HookManager, HookType
19
- from falyx.themes.colors import OneColors
20
- from falyx.utils import logger
19
+ from falyx.logger import logger
20
+ from falyx.themes import OneColors
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
  )
@@ -23,13 +23,13 @@ from typing import Any
23
23
 
24
24
  from rich.tree import Tree
25
25
 
26
- from falyx.action import BaseAction
26
+ from falyx.action.action import BaseAction
27
27
  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.themes.colors import OneColors
32
- from falyx.utils import logger
31
+ from falyx.logger import logger
32
+ from falyx.themes import OneColors
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
  )
@@ -1,6 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """menu_action.py"""
3
- from dataclasses import dataclass
4
3
  from typing import Any
5
4
 
6
5
  from prompt_toolkit import PromptSession
@@ -8,91 +7,21 @@ from rich.console import Console
8
7
  from rich.table import Table
9
8
  from rich.tree import Tree
10
9
 
11
- from falyx.action import BaseAction
10
+ from falyx.action.action import BaseAction
12
11
  from falyx.context import ExecutionContext
13
12
  from falyx.execution_registry import ExecutionRegistry as er
14
13
  from falyx.hook_manager import HookType
14
+ from falyx.logger import logger
15
+ from falyx.menu import MenuOptionMap
15
16
  from falyx.selection import prompt_for_selection, render_table_base
16
- from falyx.signal_action import SignalAction
17
17
  from falyx.signals import BackSignal, QuitSignal
18
- from falyx.themes.colors import OneColors
19
- from falyx.utils import CaseInsensitiveDict, chunks, logger
20
-
21
-
22
- @dataclass
23
- class MenuOption:
24
- description: str
25
- action: BaseAction
26
- style: str = OneColors.WHITE
27
-
28
- def __post_init__(self):
29
- if not isinstance(self.description, str):
30
- raise TypeError("MenuOption description must be a string.")
31
- if not isinstance(self.action, BaseAction):
32
- raise TypeError("MenuOption action must be a BaseAction instance.")
33
-
34
- def render(self, key: str) -> str:
35
- """Render the menu option for display."""
36
- return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
37
-
38
-
39
- class MenuOptionMap(CaseInsensitiveDict):
40
- """
41
- Manages menu options including validation, reserved key protection,
42
- and special signal entries like Quit and Back.
43
- """
44
-
45
- RESERVED_KEYS = {"Q", "B"}
46
-
47
- def __init__(
48
- self,
49
- options: dict[str, MenuOption] | None = None,
50
- allow_reserved: bool = False,
51
- ):
52
- super().__init__()
53
- self.allow_reserved = allow_reserved
54
- if options:
55
- self.update(options)
56
- self._inject_reserved_defaults()
57
-
58
- def _inject_reserved_defaults(self):
59
- self._add_reserved(
60
- "Q",
61
- MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
62
- )
63
- self._add_reserved(
64
- "B",
65
- MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
66
- )
67
-
68
- def _add_reserved(self, key: str, option: MenuOption) -> None:
69
- """Add a reserved key, bypassing validation."""
70
- norm_key = key.upper()
71
- super().__setitem__(norm_key, option)
72
-
73
- def __setitem__(self, key: str, option: MenuOption) -> None:
74
- if not isinstance(option, MenuOption):
75
- raise TypeError(f"Value for key '{key}' must be a MenuOption.")
76
- norm_key = key.upper()
77
- if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
78
- raise ValueError(
79
- f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
80
- )
81
- super().__setitem__(norm_key, option)
82
-
83
- def __delitem__(self, key: str) -> None:
84
- if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
85
- raise ValueError(f"Cannot delete reserved option '{key}'.")
86
- super().__delitem__(key)
87
-
88
- def items(self, include_reserved: bool = True):
89
- for k, v in super().items():
90
- if not include_reserved and k in self.RESERVED_KEYS:
91
- continue
92
- yield k, v
18
+ from falyx.themes import OneColors
19
+ from falyx.utils import chunks
93
20
 
94
21
 
95
22
  class MenuAction(BaseAction):
23
+ """MenuAction class for creating single use menu actions."""
24
+
96
25
  def __init__(
97
26
  self,
98
27
  name: str,
@@ -162,7 +91,8 @@ class MenuAction(BaseAction):
162
91
 
163
92
  if self.never_prompt and not effective_default:
164
93
  raise ValueError(
165
- f"[{self.name}] 'never_prompt' is True but no valid default_selection was provided."
94
+ f"[{self.name}] 'never_prompt' is True but no valid default_selection"
95
+ " was provided."
166
96
  )
167
97
 
168
98
  context.start_timer()
@@ -1,10 +1,10 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """select_file_action.py"""
2
3
  from __future__ import annotations
3
4
 
4
5
  import csv
5
6
  import json
6
7
  import xml.etree.ElementTree as ET
7
- from enum import Enum
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
@@ -14,49 +14,18 @@ from prompt_toolkit import PromptSession
14
14
  from rich.console import Console
15
15
  from rich.tree import Tree
16
16
 
17
- from falyx.action import BaseAction
17
+ from falyx.action.action import BaseAction
18
+ from falyx.action.types import FileReturnType
18
19
  from falyx.context import ExecutionContext
19
20
  from falyx.execution_registry import ExecutionRegistry as er
20
21
  from falyx.hook_manager import HookType
22
+ from falyx.logger import logger
21
23
  from falyx.selection import (
22
24
  SelectionOption,
23
25
  prompt_for_selection,
24
26
  render_selection_dict_table,
25
27
  )
26
- from falyx.themes.colors import OneColors
27
- from falyx.utils import logger
28
-
29
-
30
- class FileReturnType(Enum):
31
- TEXT = "text"
32
- PATH = "path"
33
- JSON = "json"
34
- TOML = "toml"
35
- YAML = "yaml"
36
- CSV = "csv"
37
- TSV = "tsv"
38
- XML = "xml"
39
-
40
- @classmethod
41
- def _get_alias(cls, value: str) -> str:
42
- aliases = {
43
- "yml": "yaml",
44
- "txt": "text",
45
- "file": "path",
46
- "filepath": "path",
47
- }
48
- return aliases.get(value, value)
49
-
50
- @classmethod
51
- def _missing_(cls, value: object) -> FileReturnType:
52
- if isinstance(value, str):
53
- normalized = value.lower()
54
- alias = cls._get_alias(normalized)
55
- for member in cls:
56
- if member.value == alias:
57
- return member
58
- valid = ", ".join(member.value for member in cls)
59
- raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
28
+ from falyx.themes import OneColors
60
29
 
61
30
 
62
31
  class SelectFileAction(BaseAction):