Files
falyx/falyx/execution_registry.py

280 lines
9.8 KiB
Python

# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""
Provides the `ExecutionRegistry`, a centralized runtime store for capturing and inspecting
the execution history of Falyx actions.
The registry automatically records every `ExecutionContext` created during action
execution—including context metadata, results, exceptions, duration, and tracebacks.
It supports filtering, summarization, and visual inspection via a Rich-rendered table.
Designed for:
- Workflow debugging and CLI diagnostics
- Interactive history browsing or replaying previous runs
- Providing user-visible `history` or `last-result` commands inside CLI apps
Key Features:
- Global, in-memory store of all `ExecutionContext` objects (by name, index, or full list)
- Thread-safe indexing and summary display
- Traceback-aware result inspection and filtering by status (success/error)
- Used by built-in `History` command in Falyx CLI
Example:
from falyx.execution_registry import ExecutionRegistry as er
# Record a context
er.record(context)
# Display a rich table summary
er.summary()
# Print the last non-ignored result
er.summary(last_result=True)
# Clear execution history
er.summary(clear=True)
Note:
The registry is volatile and cleared on each process restart or when `clear()` is called.
All data is retained in memory only.
Public Interface:
- record(context): Log an ExecutionContext and assign index.
- get_all(): List all stored contexts.
- get_by_name(name): Retrieve all contexts by action name.
- get_latest(): Retrieve the most recent context.
- clear(): Reset the registry.
- summary(...): Rich console summary of stored execution results.
"""
from __future__ import annotations
from collections import defaultdict
from datetime import datetime
from threading import Lock
from typing import Literal
from rich import box
from rich.console import Console
from rich.table import Table
from falyx.console import console
from falyx.context import ExecutionContext
from falyx.logger import logger
from falyx.themes import OneColors
class ExecutionRegistry:
"""
Global registry for recording and inspecting Falyx action executions.
This class captures every `ExecutionContext` created by Falyx Actions,
tracking metadata, results, exceptions, and performance metrics. It enables
rich introspection, post-execution inspection, and formatted summaries
suitable for interactive and headless CLI use.
Data is retained in memory until cleared or process exit.
Use Cases:
- Auditing chained or dynamic workflows
- Rendering execution history in a help/debug menu
- Accessing previous results or errors for reuse
Attributes:
_store_by_name (dict): Maps action name → list of ExecutionContext objects.
_store_by_index (dict): Maps numeric index → ExecutionContext.
_store_all (list): Ordered list of all contexts.
_index (int): Global counter for assigning unique execution indices.
_lock (Lock): Thread lock for atomic writes to the registry.
_console (Console): Rich console used for rendering summaries.
"""
_store_by_name: dict[str, list[ExecutionContext]] = defaultdict(list)
_store_by_index: dict[int, ExecutionContext] = {}
_store_all: list[ExecutionContext] = []
_console: Console = console
_index = 0
_lock = Lock()
@classmethod
def record(cls, context: ExecutionContext):
"""
Record an execution context and assign a unique index.
This method logs the context, appends it to the registry,
and makes it available for future summary or filtering.
Args:
context (ExecutionContext): The context to be tracked.
"""
logger.debug(context.to_log_line())
with cls._lock:
context.index = cls._index
cls._store_by_index[cls._index] = context
cls._index += 1
cls._store_by_name[context.name].append(context)
cls._store_all.append(context)
@classmethod
def get_all(cls) -> list[ExecutionContext]:
"""
Return all recorded execution contexts in order of execution.
Returns:
list[ExecutionContext]: All stored action contexts.
"""
return cls._store_all
@classmethod
def get_by_name(cls, name: str) -> list[ExecutionContext]:
"""
Retrieve all executions recorded under a given action name.
Args:
name (str): The name of the action.
Returns:
list[ExecutionContext]: Matching contexts, or empty if none found.
"""
return cls._store_by_name.get(name, [])
@classmethod
def get_latest(cls) -> ExecutionContext:
"""
Return the most recent execution context.
Returns:
ExecutionContext: The last recorded context.
"""
return cls._store_all[-1]
@classmethod
def clear(cls):
"""
Clear all stored execution data and reset internal indices.
This operation is destructive and cannot be undone.
"""
cls._store_by_name.clear()
cls._store_all.clear()
cls._store_by_index.clear()
@classmethod
def summary(
cls,
name: str = "",
index: int | None = None,
result_index: int | None = None,
clear: bool = False,
last_result: bool = False,
status: Literal["all", "success", "error"] = "all",
):
"""
Display a formatted Rich table of recorded executions.
Supports filtering by action name, index, or execution status.
Can optionally show only the last result or a specific indexed result.
Also supports clearing the registry interactively.
Args:
name (str): Filter by action name.
index (int | None): Filter by specific execution index.
result_index (int | None): Print result (or traceback) of a specific index.
clear (bool): If True, clears the registry and exits.
last_result (bool): If True, prints only the most recent result.
status (Literal): One of "all", "success", or "error" to filter displayed rows.
"""
if clear:
cls.clear()
cls._console.print(f"[{OneColors.GREEN}]✅ Execution history cleared.")
return
if last_result:
for ctx in reversed(cls._store_all):
if not ctx.action.ignore_in_history:
cls._console.print(f"{ctx.signature}:")
if ctx.traceback:
cls._console.print(ctx.traceback)
else:
cls._console.print(ctx.result)
return
cls._console.print(
f"[{OneColors.DARK_RED}]❌ No valid executions found to display last result."
)
return
if result_index is not None and result_index >= 0:
try:
result_context = cls._store_by_index[result_index]
except KeyError:
cls._console.print(
f"[{OneColors.DARK_RED}]❌ No execution found for index {result_index}."
)
return
cls._console.print(f"{result_context.signature}:")
if result_context.traceback:
cls._console.print(result_context.traceback)
else:
cls._console.print(result_context.result)
return
if name:
contexts = cls.get_by_name(name)
if not contexts:
cls._console.print(
f"[{OneColors.DARK_RED}]❌ No executions found for action '{name}'."
)
return
title = f"📊 Execution History for '{contexts[0].name}'"
elif index is not None and index >= 0:
try:
contexts = [cls._store_by_index[index]]
print(contexts)
except KeyError:
cls._console.print(
f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."
)
return
title = f"📊 Execution History for Index {index}"
else:
contexts = cls.get_all()
title = "📊 Execution History"
table = Table(title=title, expand=True, box=box.SIMPLE)
table.add_column("Index", justify="right", style="dim")
table.add_column("Name", style="bold cyan")
table.add_column("Start", justify="right", style="dim")
table.add_column("End", justify="right", style="dim")
table.add_column("Duration", justify="right")
table.add_column("Status", style="bold")
table.add_column("Result / Exception", overflow="fold")
for ctx in contexts:
start = (
datetime.fromtimestamp(ctx.start_time).strftime("%H:%M:%S")
if ctx.start_time
else "n/a"
)
end = (
datetime.fromtimestamp(ctx.end_time).strftime("%H:%M:%S")
if ctx.end_time
else "n/a"
)
duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a"
if ctx.exception and status.lower() in ["all", "error"]:
final_status = f"[{OneColors.DARK_RED}]❌ Error"
final_result = repr(ctx.exception)
elif status.lower() in ["all", "success"]:
final_status = f"[{OneColors.GREEN}]✅ Success"
final_result = repr(ctx.result)
if len(final_result) > 50:
final_result = f"{final_result[:50]}..."
else:
continue
table.add_row(
str(ctx.index), ctx.name, start, end, duration, final_status, final_result
)
cls._console.print(table)