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.
- falyx/__init__.py +1 -1
- falyx/action/__init__.py +41 -0
- falyx/{action.py → action/action.py} +41 -23
- falyx/{action_factory.py → action/action_factory.py} +17 -5
- falyx/{http_action.py → action/http_action.py} +10 -9
- falyx/{io_action.py → action/io_action.py} +19 -14
- falyx/{menu_action.py → action/menu_action.py} +9 -79
- falyx/{select_file_action.py → action/select_file_action.py} +5 -36
- falyx/{selection_action.py → action/selection_action.py} +22 -8
- falyx/action/signal_action.py +43 -0
- falyx/action/types.py +37 -0
- falyx/bottom_bar.py +3 -3
- falyx/command.py +13 -10
- falyx/config.py +17 -9
- falyx/context.py +16 -8
- falyx/debug.py +2 -1
- falyx/exceptions.py +3 -0
- falyx/execution_registry.py +59 -13
- falyx/falyx.py +67 -77
- falyx/hook_manager.py +20 -3
- falyx/hooks.py +13 -6
- falyx/init.py +1 -0
- falyx/logger.py +5 -0
- falyx/menu.py +85 -0
- falyx/options_manager.py +7 -3
- falyx/parsers.py +2 -2
- falyx/prompt_utils.py +30 -1
- falyx/protocols.py +2 -1
- falyx/retry.py +23 -12
- falyx/retry_utils.py +2 -1
- falyx/selection.py +7 -3
- falyx/signals.py +3 -0
- falyx/tagged_table.py +2 -1
- falyx/themes/__init__.py +15 -0
- falyx/utils.py +11 -39
- falyx/validators.py +8 -7
- falyx/version.py +1 -1
- {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/METADATA +2 -1
- falyx-0.1.25.dist-info/RECORD +46 -0
- falyx/signal_action.py +0 -30
- falyx-0.1.23.dist-info/RECORD +0 -41
- {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/LICENSE +0 -0
- {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/WHEEL +0 -0
- {falyx-0.1.23.dist-info → falyx-0.1.25.dist-info}/entry_points.txt +0 -0
@@ -6,10 +6,11 @@ from prompt_toolkit import PromptSession
|
|
6
6
|
from rich.console import Console
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
|
-
from falyx.action import BaseAction
|
9
|
+
from falyx.action.action import BaseAction
|
10
10
|
from falyx.context import ExecutionContext
|
11
11
|
from falyx.execution_registry import ExecutionRegistry as er
|
12
12
|
from falyx.hook_manager import HookType
|
13
|
+
from falyx.logger import logger
|
13
14
|
from falyx.selection import (
|
14
15
|
SelectionOption,
|
15
16
|
prompt_for_index,
|
@@ -17,11 +18,19 @@ from falyx.selection import (
|
|
17
18
|
render_selection_dict_table,
|
18
19
|
render_selection_indexed_table,
|
19
20
|
)
|
20
|
-
from falyx.themes
|
21
|
-
from falyx.utils import CaseInsensitiveDict
|
21
|
+
from falyx.themes import OneColors
|
22
|
+
from falyx.utils import CaseInsensitiveDict
|
22
23
|
|
23
24
|
|
24
25
|
class SelectionAction(BaseAction):
|
26
|
+
"""
|
27
|
+
A selection action that prompts the user to select an option from a list or
|
28
|
+
dictionary. The selected option is then returned as the result of the action.
|
29
|
+
|
30
|
+
If return_key is True, the key of the selected option is returned instead of
|
31
|
+
the value.
|
32
|
+
"""
|
33
|
+
|
25
34
|
def __init__(
|
26
35
|
self,
|
27
36
|
name: str,
|
@@ -45,7 +54,8 @@ class SelectionAction(BaseAction):
|
|
45
54
|
inject_into=inject_into,
|
46
55
|
never_prompt=never_prompt,
|
47
56
|
)
|
48
|
-
|
57
|
+
# Setter normalizes to correct type, mypy can't infer that
|
58
|
+
self.selections: list[str] | CaseInsensitiveDict = selections # type: ignore[assignment]
|
49
59
|
self.return_key = return_key
|
50
60
|
self.title = title
|
51
61
|
self.columns = columns
|
@@ -71,7 +81,8 @@ class SelectionAction(BaseAction):
|
|
71
81
|
self._selections = cid
|
72
82
|
else:
|
73
83
|
raise TypeError(
|
74
|
-
|
84
|
+
"'selections' must be a list[str] or dict[str, SelectionOption], "
|
85
|
+
f"got {type(value).__name__}"
|
75
86
|
)
|
76
87
|
|
77
88
|
async def _run(self, *args, **kwargs) -> Any:
|
@@ -108,7 +119,8 @@ class SelectionAction(BaseAction):
|
|
108
119
|
|
109
120
|
if self.never_prompt and not effective_default:
|
110
121
|
raise ValueError(
|
111
|
-
f"[{self.name}] 'never_prompt' is True but no valid default_selection
|
122
|
+
f"[{self.name}] 'never_prompt' is True but no valid default_selection "
|
123
|
+
"was provided."
|
112
124
|
)
|
113
125
|
|
114
126
|
context.start_timer()
|
@@ -152,7 +164,8 @@ class SelectionAction(BaseAction):
|
|
152
164
|
result = key if self.return_key else self.selections[key].value
|
153
165
|
else:
|
154
166
|
raise TypeError(
|
155
|
-
|
167
|
+
"'selections' must be a list[str] or dict[str, tuple[str, Any]], "
|
168
|
+
f"got {type(self.selections).__name__}"
|
156
169
|
)
|
157
170
|
context.result = result
|
158
171
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
@@ -205,5 +218,6 @@ class SelectionAction(BaseAction):
|
|
205
218
|
return (
|
206
219
|
f"SelectionAction(name={self.name!r}, type={selection_type}, "
|
207
220
|
f"default_selection={self.default_selection!r}, "
|
208
|
-
f"return_key={self.return_key},
|
221
|
+
f"return_key={self.return_key}, "
|
222
|
+
f"prompt={'off' if self.never_prompt else 'on'})"
|
209
223
|
)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""signal_action.py"""
|
3
|
+
from rich.tree import Tree
|
4
|
+
|
5
|
+
from falyx.action.action import Action
|
6
|
+
from falyx.signals import FlowSignal
|
7
|
+
from falyx.themes import OneColors
|
8
|
+
|
9
|
+
|
10
|
+
class SignalAction(Action):
|
11
|
+
"""
|
12
|
+
An action that raises a control flow signal when executed.
|
13
|
+
|
14
|
+
Useful for exiting a menu, going back, or halting execution gracefully.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, name: str, signal: Exception):
|
18
|
+
self.signal = signal
|
19
|
+
super().__init__(name, action=self.raise_signal)
|
20
|
+
|
21
|
+
async def raise_signal(self, *args, **kwargs):
|
22
|
+
raise self.signal
|
23
|
+
|
24
|
+
@property
|
25
|
+
def signal(self):
|
26
|
+
return self._signal
|
27
|
+
|
28
|
+
@signal.setter
|
29
|
+
def signal(self, value: FlowSignal):
|
30
|
+
if not isinstance(value, FlowSignal):
|
31
|
+
raise TypeError(
|
32
|
+
f"Signal must be an FlowSignal instance, got {type(value).__name__}"
|
33
|
+
)
|
34
|
+
self._signal = value
|
35
|
+
|
36
|
+
def __str__(self):
|
37
|
+
return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
|
38
|
+
|
39
|
+
async def preview(self, parent: Tree | None = None):
|
40
|
+
label = f"[{OneColors.LIGHT_RED}]⚡ SignalAction[/] '{self.signal.__class__.__name__}'"
|
41
|
+
tree = parent.add(label) if parent else Tree(label)
|
42
|
+
if not parent:
|
43
|
+
self.console.print(tree)
|
falyx/action/types.py
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
|
6
|
+
class FileReturnType(Enum):
|
7
|
+
"""Enum for file return types."""
|
8
|
+
|
9
|
+
TEXT = "text"
|
10
|
+
PATH = "path"
|
11
|
+
JSON = "json"
|
12
|
+
TOML = "toml"
|
13
|
+
YAML = "yaml"
|
14
|
+
CSV = "csv"
|
15
|
+
TSV = "tsv"
|
16
|
+
XML = "xml"
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def _get_alias(cls, value: str) -> str:
|
20
|
+
aliases = {
|
21
|
+
"yml": "yaml",
|
22
|
+
"txt": "text",
|
23
|
+
"file": "path",
|
24
|
+
"filepath": "path",
|
25
|
+
}
|
26
|
+
return aliases.get(value, value)
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def _missing_(cls, value: object) -> FileReturnType:
|
30
|
+
if isinstance(value, str):
|
31
|
+
normalized = value.lower()
|
32
|
+
alias = cls._get_alias(normalized)
|
33
|
+
for member in cls:
|
34
|
+
if member.value == alias:
|
35
|
+
return member
|
36
|
+
valid = ", ".join(member.value for member in cls)
|
37
|
+
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
falyx/bottom_bar.py
CHANGED
@@ -8,7 +8,7 @@ from prompt_toolkit.key_binding import KeyBindings
|
|
8
8
|
from rich.console import Console
|
9
9
|
|
10
10
|
from falyx.options_manager import OptionsManager
|
11
|
-
from falyx.themes
|
11
|
+
from falyx.themes import OneColors
|
12
12
|
from falyx.utils import CaseInsensitiveDict, chunks
|
13
13
|
|
14
14
|
|
@@ -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
@@ -26,19 +26,20 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
26
26
|
from rich.console import Console
|
27
27
|
from rich.tree import Tree
|
28
28
|
|
29
|
-
from falyx.action import Action, ActionGroup, BaseAction, ChainedAction
|
29
|
+
from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
|
30
|
+
from falyx.action.io_action import BaseIOAction
|
30
31
|
from falyx.context import ExecutionContext
|
31
32
|
from falyx.debug import register_debug_hooks
|
32
33
|
from falyx.exceptions import FalyxError
|
33
34
|
from falyx.execution_registry import ExecutionRegistry as er
|
34
35
|
from falyx.hook_manager import HookManager, HookType
|
35
|
-
from falyx.
|
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
|
-
from falyx.themes
|
41
|
-
from falyx.utils import _noop,
|
41
|
+
from falyx.themes import OneColors
|
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
@@ -13,12 +13,12 @@ import yaml
|
|
13
13
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
14
14
|
from rich.console import Console
|
15
15
|
|
16
|
-
from falyx.action import Action, BaseAction
|
16
|
+
from falyx.action.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
|
-
from falyx.themes
|
21
|
-
from falyx.utils import logger
|
21
|
+
from falyx.themes import OneColors
|
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
|
12
|
-
from falyx.
|
13
|
-
from falyx.
|
39
|
+
from falyx.logger import logger
|
40
|
+
from falyx.themes import OneColors
|
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)
|