falyx 0.1.23__tar.gz → 0.1.24__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 (43) hide show
  1. {falyx-0.1.23 → falyx-0.1.24}/PKG-INFO +1 -1
  2. {falyx-0.1.23 → falyx-0.1.24}/falyx/action.py +40 -22
  3. {falyx-0.1.23 → falyx-0.1.24}/falyx/action_factory.py +15 -3
  4. {falyx-0.1.23 → falyx-0.1.24}/falyx/bottom_bar.py +2 -2
  5. {falyx-0.1.23 → falyx-0.1.24}/falyx/command.py +10 -7
  6. {falyx-0.1.23 → falyx-0.1.24}/falyx/config.py +15 -7
  7. {falyx-0.1.23 → falyx-0.1.24}/falyx/context.py +16 -8
  8. {falyx-0.1.23 → falyx-0.1.24}/falyx/debug.py +2 -1
  9. {falyx-0.1.23 → falyx-0.1.24}/falyx/exceptions.py +3 -0
  10. {falyx-0.1.23 → falyx-0.1.24}/falyx/execution_registry.py +58 -12
  11. {falyx-0.1.23 → falyx-0.1.24}/falyx/falyx.py +65 -75
  12. {falyx-0.1.23 → falyx-0.1.24}/falyx/hook_manager.py +20 -3
  13. {falyx-0.1.23 → falyx-0.1.24}/falyx/hooks.py +12 -5
  14. {falyx-0.1.23 → falyx-0.1.24}/falyx/http_action.py +8 -7
  15. {falyx-0.1.23 → falyx-0.1.24}/falyx/init.py +1 -0
  16. {falyx-0.1.23 → falyx-0.1.24}/falyx/io_action.py +17 -12
  17. falyx-0.1.24/falyx/logger.py +5 -0
  18. {falyx-0.1.23 → falyx-0.1.24}/falyx/menu_action.py +8 -2
  19. {falyx-0.1.23 → falyx-0.1.24}/falyx/options_manager.py +7 -3
  20. {falyx-0.1.23 → falyx-0.1.24}/falyx/parsers.py +2 -2
  21. falyx-0.1.24/falyx/prompt_utils.py +48 -0
  22. {falyx-0.1.23 → falyx-0.1.24}/falyx/protocols.py +1 -0
  23. {falyx-0.1.23 → falyx-0.1.24}/falyx/retry.py +23 -12
  24. {falyx-0.1.23 → falyx-0.1.24}/falyx/retry_utils.py +1 -0
  25. {falyx-0.1.23 → falyx-0.1.24}/falyx/select_file_action.py +4 -1
  26. {falyx-0.1.23 → falyx-0.1.24}/falyx/selection.py +6 -2
  27. {falyx-0.1.23 → falyx-0.1.24}/falyx/selection_action.py +20 -6
  28. {falyx-0.1.23 → falyx-0.1.24}/falyx/signal_action.py +1 -0
  29. {falyx-0.1.23 → falyx-0.1.24}/falyx/signals.py +3 -0
  30. {falyx-0.1.23 → falyx-0.1.24}/falyx/tagged_table.py +2 -1
  31. {falyx-0.1.23 → falyx-0.1.24}/falyx/utils.py +11 -39
  32. {falyx-0.1.23 → falyx-0.1.24}/falyx/validators.py +8 -7
  33. falyx-0.1.24/falyx/version.py +1 -0
  34. {falyx-0.1.23 → falyx-0.1.24}/pyproject.toml +3 -3
  35. falyx-0.1.23/falyx/prompt_utils.py +0 -19
  36. falyx-0.1.23/falyx/version.py +0 -1
  37. {falyx-0.1.23 → falyx-0.1.24}/LICENSE +0 -0
  38. {falyx-0.1.23 → falyx-0.1.24}/README.md +0 -0
  39. {falyx-0.1.23 → falyx-0.1.24}/falyx/.pytyped +0 -0
  40. {falyx-0.1.23 → falyx-0.1.24}/falyx/__init__.py +0 -0
  41. {falyx-0.1.23 → falyx-0.1.24}/falyx/__main__.py +0 -0
  42. {falyx-0.1.23 → falyx-0.1.24}/falyx/config_schema.py +0 -0
  43. {falyx-0.1.23 → falyx-0.1.24}/falyx/themes/colors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.23
3
+ Version: 0.1.24
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -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
51
  from falyx.themes.colors import OneColors
49
- from falyx.utils import ensure_async, logger
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,4 +1,5 @@
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
@@ -7,6 +8,7 @@ from falyx.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
13
  from falyx.themes.colors import OneColors
12
14
 
@@ -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)
@@ -146,7 +146,7 @@ class BottomBar:
146
146
  for k in (key.upper(), key.lower()):
147
147
 
148
148
  @self.key_bindings.add(k)
149
- def _(event):
149
+ def _(_):
150
150
  toggle_state()
151
151
 
152
152
  def add_toggle_from_option(
@@ -204,6 +204,6 @@ class BottomBar:
204
204
  """Render the bottom bar."""
205
205
  lines = []
206
206
  for chunk in chunks(self._named_items.values(), self.columns):
207
- lines.extend([fn for fn in chunk])
207
+ lines.extend(list(chunk))
208
208
  lines.append(lambda: HTML("\n"))
209
209
  return merge_formatted_text([fn() for fn in lines[:-1]])
@@ -33,12 +33,13 @@ from falyx.exceptions import FalyxError
33
33
  from falyx.execution_registry import ExecutionRegistry as er
34
34
  from falyx.hook_manager import HookManager, HookType
35
35
  from falyx.io_action import BaseIOAction
36
+ from falyx.logger import logger
36
37
  from falyx.options_manager import OptionsManager
37
- from falyx.prompt_utils import should_prompt_user
38
+ from falyx.prompt_utils import confirm_async, should_prompt_user
38
39
  from falyx.retry import RetryPolicy
39
40
  from falyx.retry_utils import enable_retries_recursively
40
41
  from falyx.themes.colors import OneColors
41
- from falyx.utils import _noop, confirm_async, ensure_async, logger
42
+ from falyx.utils import _noop, ensure_async
42
43
 
43
44
  console = Console(color_system="auto")
44
45
 
@@ -134,7 +135,7 @@ class Command(BaseModel):
134
135
  return ensure_async(action)
135
136
  raise TypeError("Action must be a callable or an instance of BaseAction")
136
137
 
137
- def model_post_init(self, __context: Any) -> None:
138
+ def model_post_init(self, _: Any) -> None:
138
139
  """Post-initialization to set up the action and hooks."""
139
140
  if self.retry and isinstance(self.action, Action):
140
141
  self.action.enable_retry()
@@ -142,14 +143,16 @@ class Command(BaseModel):
142
143
  self.action.set_retry_policy(self.retry_policy)
143
144
  elif self.retry:
144
145
  logger.warning(
145
- f"[Command:{self.key}] Retry requested, but action is not an Action instance."
146
+ "[Command:%s] Retry requested, but action is not an Action instance.",
147
+ self.key,
146
148
  )
147
149
  if self.retry_all and isinstance(self.action, BaseAction):
148
150
  self.retry_policy.enabled = True
149
151
  enable_retries_recursively(self.action, self.retry_policy)
150
152
  elif self.retry_all:
151
153
  logger.warning(
152
- f"[Command:{self.key}] Retry all requested, but action is not a BaseAction instance."
154
+ "[Command:%s] Retry all requested, but action is not a BaseAction.",
155
+ self.key,
153
156
  )
154
157
 
155
158
  if self.logging_hooks and isinstance(self.action, BaseAction):
@@ -201,7 +204,7 @@ class Command(BaseModel):
201
204
  if self.preview_before_confirm:
202
205
  await self.preview()
203
206
  if not await confirm_async(self.confirmation_prompt):
204
- logger.info(f"[Command:{self.key}] ❌ Cancelled by user.")
207
+ logger.info("[Command:%s] ❌ Cancelled by user.", self.key)
205
208
  raise FalyxError(f"[Command:{self.key}] Cancelled by confirmation.")
206
209
 
207
210
  context.start_timer()
@@ -288,7 +291,7 @@ class Command(BaseModel):
288
291
  if self.help_text:
289
292
  console.print(f"[dim]💡 {self.help_text}[/dim]")
290
293
  console.print(
291
- f"[{OneColors.DARK_RED}]⚠️ Action is not callable or lacks a preview method.[/]"
294
+ f"[{OneColors.DARK_RED}]⚠️ No preview available for this action.[/]"
292
295
  )
293
296
 
294
297
  def __str__(self) -> str:
@@ -16,9 +16,9 @@ from rich.console import Console
16
16
  from falyx.action import Action, BaseAction
17
17
  from falyx.command import Command
18
18
  from falyx.falyx import Falyx
19
+ from falyx.logger import logger
19
20
  from falyx.retry import RetryPolicy
20
21
  from falyx.themes.colors import OneColors
21
- from falyx.utils import logger
22
22
 
23
23
  console = Console(color_system="auto")
24
24
 
@@ -47,7 +47,8 @@ def import_action(dotted_path: str) -> Any:
47
47
  logger.error("Failed to import module '%s': %s", module_path, error)
48
48
  console.print(
49
49
  f"[{OneColors.DARK_RED}]❌ Could not import '{dotted_path}': {error}[/]\n"
50
- f"[{OneColors.COMMENT_GREY}]Ensure the module is installed and discoverable via PYTHONPATH."
50
+ f"[{OneColors.COMMENT_GREY}]Ensure the module is installed and discoverable "
51
+ "via PYTHONPATH."
51
52
  )
52
53
  sys.exit(1)
53
54
  try:
@@ -57,13 +58,16 @@ def import_action(dotted_path: str) -> Any:
57
58
  "Module '%s' does not have attribute '%s': %s", module_path, attr, error
58
59
  )
59
60
  console.print(
60
- f"[{OneColors.DARK_RED}]❌ Module '{module_path}' has no attribute '{attr}': {error}[/]"
61
+ f"[{OneColors.DARK_RED}]❌ Module '{module_path}' has no attribute "
62
+ f"'{attr}': {error}[/]"
61
63
  )
62
64
  sys.exit(1)
63
65
  return action
64
66
 
65
67
 
66
68
  class RawCommand(BaseModel):
69
+ """Raw command model for Falyx CLI configuration."""
70
+
67
71
  key: str
68
72
  description: str
69
73
  action: str
@@ -72,7 +76,7 @@ class RawCommand(BaseModel):
72
76
  kwargs: dict[str, Any] = {}
73
77
  aliases: list[str] = []
74
78
  tags: list[str] = []
75
- style: str = "white"
79
+ style: str = OneColors.WHITE
76
80
 
77
81
  confirm: bool = False
78
82
  confirm_message: str = "Are you sure?"
@@ -81,7 +85,7 @@ class RawCommand(BaseModel):
81
85
  spinner: bool = False
82
86
  spinner_message: str = "Processing..."
83
87
  spinner_type: str = "dots"
84
- spinner_style: str = "cyan"
88
+ spinner_style: str = OneColors.CYAN
85
89
  spinner_kwargs: dict[str, Any] = {}
86
90
 
87
91
  before_hooks: list[Callable] = []
@@ -126,6 +130,8 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
126
130
 
127
131
 
128
132
  class FalyxConfig(BaseModel):
133
+ """Falyx CLI configuration model."""
134
+
129
135
  title: str = "Falyx CLI"
130
136
  prompt: str | list[tuple[str, str]] | list[list[str]] = [
131
137
  (OneColors.BLUE_b, "FALYX > ")
@@ -148,7 +154,7 @@ class FalyxConfig(BaseModel):
148
154
  def to_falyx(self) -> Falyx:
149
155
  flx = Falyx(
150
156
  title=self.title,
151
- prompt=self.prompt,
157
+ prompt=self.prompt, # type: ignore[arg-type]
152
158
  columns=self.columns,
153
159
  welcome_message=self.welcome_message,
154
160
  exit_message=self.exit_message,
@@ -159,7 +165,9 @@ class FalyxConfig(BaseModel):
159
165
 
160
166
  def loader(file_path: Path | str) -> Falyx:
161
167
  """
162
- Load command definitions from a YAML or TOML file.
168
+ Load Falyx CLI configuration from a YAML or TOML file.
169
+
170
+ The file should contain a dictionary with a list of commands.
163
171
 
164
172
  Each command should be defined as a dictionary with at least:
165
173
  - key: a unique single-character key
@@ -29,10 +29,10 @@ class ExecutionContext(BaseModel):
29
29
  """
30
30
  Represents the runtime metadata and state for a single action execution.
31
31
 
32
- The `ExecutionContext` tracks arguments, results, exceptions, timing, and additional
33
- metadata for each invocation of a Falyx `BaseAction`. It provides integration with the
34
- Falyx hook system and execution registry, enabling lifecycle management, diagnostics,
35
- and structured logging.
32
+ The `ExecutionContext` tracks arguments, results, exceptions, timing, and
33
+ additional metadata for each invocation of a Falyx `BaseAction`. It provides
34
+ integration with the Falyx hook system and execution registry, enabling lifecycle
35
+ management, diagnostics, and structured logging.
36
36
 
37
37
  Attributes:
38
38
  name (str): The name of the action being executed.
@@ -47,7 +47,8 @@ class ExecutionContext(BaseModel):
47
47
  end_wall (datetime | None): Wall-clock timestamp when execution ended.
48
48
  extra (dict): Metadata for custom introspection or special use by Actions.
49
49
  console (Console): Rich console instance for logging or UI output.
50
- shared_context (SharedContext | None): Optional shared context when running in a chain or group.
50
+ shared_context (SharedContext | None): Optional shared context when running in
51
+ a chain or group.
51
52
 
52
53
  Properties:
53
54
  duration (float | None): The execution duration in seconds.
@@ -95,7 +96,11 @@ class ExecutionContext(BaseModel):
95
96
  self.end_wall = datetime.now()
96
97
 
97
98
  def get_shared_context(self) -> SharedContext:
98
- return self.shared_context or SharedContext(name="default")
99
+ if not self.shared_context:
100
+ raise ValueError(
101
+ "SharedContext is not set. This context is not part of a chain or group."
102
+ )
103
+ return self.shared_context
99
104
 
100
105
  @property
101
106
  def duration(self) -> float | None:
@@ -190,8 +195,10 @@ class SharedContext(BaseModel):
190
195
  errors (list[tuple[int, Exception]]): Indexed list of errors from failed actions.
191
196
  current_index (int): Index of the currently executing action (used in chains).
192
197
  is_parallel (bool): Whether the context is used in parallel mode (ActionGroup).
193
- shared_result (Any | None): Optional shared value available to all actions in parallel mode.
194
- share (dict[str, Any]): Custom shared key-value store for user-defined communication
198
+ shared_result (Any | None): Optional shared value available to all actions in
199
+ parallel mode.
200
+ share (dict[str, Any]): Custom shared key-value store for user-defined
201
+ communication
195
202
  between actions (e.g., flags, intermediate data, settings).
196
203
 
197
204
  Note:
@@ -208,6 +215,7 @@ class SharedContext(BaseModel):
208
215
  """
209
216
 
210
217
  name: str
218
+ action: Any
211
219
  results: list[Any] = Field(default_factory=list)
212
220
  errors: list[tuple[int, Exception]] = Field(default_factory=list)
213
221
  current_index: int = -1
@@ -1,7 +1,8 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """debug.py"""
2
3
  from falyx.context import ExecutionContext
3
4
  from falyx.hook_manager import HookManager, HookType
4
- from falyx.utils import logger
5
+ from falyx.logger import logger
5
6
 
6
7
 
7
8
  def log_before(context: ExecutionContext):
@@ -1,4 +1,7 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """exceptions.py"""
3
+
4
+
2
5
  class FalyxError(Exception):
3
6
  """Custom exception for the Menu class."""
4
7
 
@@ -1,5 +1,32 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """execution_registry.py"""
2
+ """
3
+ execution_registry.py
4
+
5
+ This module provides the `ExecutionRegistry`, a global class for tracking and
6
+ introspecting the execution history of Falyx actions.
7
+
8
+ The registry captures `ExecutionContext` instances from all executed actions, making it
9
+ easy to debug, audit, and visualize workflow behavior over time. It supports retrieval,
10
+ filtering, clearing, and formatted summary display.
11
+
12
+ Core Features:
13
+ - Stores all action execution contexts globally (with access by name).
14
+ - Provides live execution summaries in a rich table format.
15
+ - Enables creation of a built-in Falyx Action to print history on demand.
16
+ - Integrates with Falyx's introspectable and hook-driven execution model.
17
+
18
+ Intended for:
19
+ - Debugging and diagnostics
20
+ - Post-run inspection of CLI workflows
21
+ - Interactive tools built with Falyx
22
+
23
+ Example:
24
+ from falyx.execution_registry import ExecutionRegistry as er
25
+ er.record(context)
26
+ er.summary()
27
+ """
28
+ from __future__ import annotations
29
+
3
30
  from collections import defaultdict
4
31
  from datetime import datetime
5
32
  from typing import Dict, List
@@ -9,11 +36,40 @@ from rich.console import Console
9
36
  from rich.table import Table
10
37
 
11
38
  from falyx.context import ExecutionContext
39
+ from falyx.logger import logger
12
40
  from falyx.themes.colors import OneColors
13
- from falyx.utils import logger
14
41
 
15
42
 
16
43
  class ExecutionRegistry:
44
+ """
45
+ Global registry for recording and inspecting Falyx action executions.
46
+
47
+ This class captures every `ExecutionContext` generated by a Falyx `Action`,
48
+ `ChainedAction`, or `ActionGroup`, maintaining both full history and
49
+ name-indexed access for filtered analysis.
50
+
51
+ Methods:
52
+ - record(context): Stores an ExecutionContext, logging a summary line.
53
+ - get_all(): Returns the list of all recorded executions.
54
+ - get_by_name(name): Returns all executions with the given action name.
55
+ - get_latest(): Returns the most recent execution.
56
+ - clear(): Wipes the registry for a fresh run.
57
+ - summary(): Renders a formatted Rich table of all execution results.
58
+
59
+ Use Cases:
60
+ - Debugging chained or factory-generated workflows
61
+ - Viewing results and exceptions from multiple runs
62
+ - Embedding a diagnostic command into your CLI for user support
63
+
64
+ Note:
65
+ This registry is in-memory and not persistent. It's reset each time the process
66
+ restarts or `clear()` is called.
67
+
68
+ Example:
69
+ ExecutionRegistry.record(context)
70
+ ExecutionRegistry.summary()
71
+ """
72
+
17
73
  _store_by_name: Dict[str, List[ExecutionContext]] = defaultdict(list)
18
74
  _store_all: List[ExecutionContext] = []
19
75
  _console = Console(color_system="auto")
@@ -78,13 +134,3 @@ class ExecutionRegistry:
78
134
  table.add_row(ctx.name, start, end, duration, status, result)
79
135
 
80
136
  cls._console.print(table)
81
-
82
- @classmethod
83
- def get_history_action(cls) -> "Action":
84
- """Return an Action that prints the execution summary."""
85
- from falyx.action import Action
86
-
87
- async def show_history():
88
- cls.summary()
89
-
90
- return Action(name="View Execution History", action=show_history)