218 lines
7.5 KiB
Python
218 lines
7.5 KiB
Python
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
|
"""
|
|
execution_registry.py
|
|
|
|
This module provides the `ExecutionRegistry`, a global class for tracking and
|
|
introspecting the execution history of Falyx actions.
|
|
|
|
The registry captures `ExecutionContext` instances from all executed actions, making it
|
|
easy to debug, audit, and visualize workflow behavior over time. It supports retrieval,
|
|
filtering, clearing, and formatted summary display.
|
|
|
|
Core Features:
|
|
- Stores all action execution contexts globally (with access by name).
|
|
- Provides live execution summaries in a rich table format.
|
|
- Enables creation of a built-in Falyx Action to print history on demand.
|
|
- Integrates with Falyx's introspectable and hook-driven execution model.
|
|
|
|
Intended for:
|
|
- Debugging and diagnostics
|
|
- Post-run inspection of CLI workflows
|
|
- Interactive tools built with Falyx
|
|
|
|
Example:
|
|
from falyx.execution_registry import ExecutionRegistry as er
|
|
er.record(context)
|
|
er.summary()
|
|
"""
|
|
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` generated by a Falyx `Action`,
|
|
`ChainedAction`, or `ActionGroup`, maintaining both full history and
|
|
name-indexed access for filtered analysis.
|
|
|
|
Methods:
|
|
- record(context): Stores an ExecutionContext, logging a summary line.
|
|
- get_all(): Returns the list of all recorded executions.
|
|
- get_by_name(name): Returns all executions with the given action name.
|
|
- get_latest(): Returns the most recent execution.
|
|
- clear(): Wipes the registry for a fresh run.
|
|
- summary(): Renders a formatted Rich table of all execution results.
|
|
|
|
Use Cases:
|
|
- Debugging chained or factory-generated workflows
|
|
- Viewing results and exceptions from multiple runs
|
|
- Embedding a diagnostic command into your CLI for user support
|
|
|
|
Note:
|
|
This registry is in-memory and not persistent. It's reset each time the process
|
|
restarts or `clear()` is called.
|
|
|
|
Example:
|
|
ExecutionRegistry.record(context)
|
|
ExecutionRegistry.summary()
|
|
"""
|
|
|
|
_store_by_name: dict[str, list[ExecutionContext]] = defaultdict(list)
|
|
_store_by_index: dict[int, ExecutionContext] = {}
|
|
_store_all: list[ExecutionContext] = []
|
|
_console = Console(color_system="truecolor")
|
|
_index = 0
|
|
_lock = Lock()
|
|
|
|
@classmethod
|
|
def record(cls, context: ExecutionContext):
|
|
"""Record an execution context."""
|
|
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 cls._store_all
|
|
|
|
@classmethod
|
|
def get_by_name(cls, name: str) -> list[ExecutionContext]:
|
|
return cls._store_by_name.get(name, [])
|
|
|
|
@classmethod
|
|
def get_latest(cls) -> ExecutionContext:
|
|
return cls._store_all[-1]
|
|
|
|
@classmethod
|
|
def clear(cls):
|
|
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",
|
|
):
|
|
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 ctx.name.upper() not in [
|
|
"HISTORY",
|
|
"HELP",
|
|
"EXIT",
|
|
"VIEW EXECUTION HISTORY",
|
|
"BACK",
|
|
]:
|
|
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.exception:
|
|
cls._console.print(result_context.exception)
|
|
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) > 1000:
|
|
final_result = f"{final_result[:1000]}..."
|
|
else:
|
|
continue
|
|
|
|
table.add_row(
|
|
str(ctx.index), ctx.name, start, end, duration, final_status, final_result
|
|
)
|
|
|
|
cls._console.print(table)
|