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/action.py +40 -22
- falyx/action_factory.py +15 -3
- falyx/bottom_bar.py +2 -2
- falyx/command.py +10 -7
- falyx/config.py +15 -7
- falyx/context.py +16 -8
- falyx/debug.py +2 -1
- falyx/exceptions.py +3 -0
- falyx/execution_registry.py +58 -12
- falyx/falyx.py +65 -75
- falyx/hook_manager.py +20 -3
- falyx/hooks.py +12 -5
- falyx/http_action.py +8 -7
- falyx/init.py +1 -0
- falyx/io_action.py +17 -12
- falyx/logger.py +5 -0
- falyx/menu_action.py +8 -2
- falyx/options_manager.py +7 -3
- falyx/parsers.py +2 -2
- falyx/prompt_utils.py +30 -1
- falyx/protocols.py +1 -0
- falyx/retry.py +23 -12
- falyx/retry_utils.py +1 -0
- falyx/select_file_action.py +4 -1
- falyx/selection.py +6 -2
- falyx/selection_action.py +20 -6
- falyx/signal_action.py +1 -0
- falyx/signals.py +3 -0
- falyx/tagged_table.py +2 -1
- falyx/utils.py +11 -39
- falyx/validators.py +8 -7
- falyx/version.py +1 -1
- {falyx-0.1.23.dist-info → falyx-0.1.24.dist-info}/METADATA +1 -1
- falyx-0.1.24.dist-info/RECORD +42 -0
- falyx-0.1.23.dist-info/RECORD +0 -41
- {falyx-0.1.23.dist-info → falyx-0.1.24.dist-info}/LICENSE +0 -0
- {falyx-0.1.23.dist-info → falyx-0.1.24.dist-info}/WHEEL +0 -0
- {falyx-0.1.23.dist-info → falyx-0.1.24.dist-info}/entry_points.txt +0 -0
falyx/action.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
"""
|
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=
|
292
|
-
f"
|
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(*
|
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
|
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
|
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
|
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
|
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
|
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
|
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},
|
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
|
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
|
-
|
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},
|
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
|
)
|
falyx/action_factory.py
CHANGED
@@ -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
|
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)
|
falyx/bottom_bar.py
CHANGED
@@ -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 _(
|
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(
|
207
|
+
lines.extend(list(chunk))
|
208
208
|
lines.append(lambda: HTML("\n"))
|
209
209
|
return merge_formatted_text([fn() for fn in lines[:-1]])
|
falyx/command.py
CHANGED
@@ -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,
|
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,
|
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
|
-
|
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
|
-
|
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(
|
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}]⚠️
|
294
|
+
f"[{OneColors.DARK_RED}]⚠️ No preview available for this action.[/]"
|
292
295
|
)
|
293
296
|
|
294
297
|
def __str__(self) -> str:
|
falyx/config.py
CHANGED
@@ -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
|
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
|
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 =
|
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 =
|
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
|
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
|
falyx/context.py
CHANGED
@@ -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
|
33
|
-
metadata for each invocation of a Falyx `BaseAction`. It provides
|
34
|
-
Falyx hook system and execution registry, enabling lifecycle
|
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
|
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
|
-
|
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
|
194
|
-
|
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
|
falyx/debug.py
CHANGED
@@ -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.
|
5
|
+
from falyx.logger import logger
|
5
6
|
|
6
7
|
|
7
8
|
def log_before(context: ExecutionContext):
|
falyx/exceptions.py
CHANGED
falyx/execution_registry.py
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""
|
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)
|