69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
|
"""hooks.py"""
|
|
import time
|
|
from typing import Callable
|
|
|
|
from falyx.context import ExecutionContext
|
|
from falyx.exceptions import CircuitBreakerOpen
|
|
from falyx.themes.colors import OneColors
|
|
from falyx.utils import logger
|
|
|
|
|
|
|
|
class ResultReporter:
|
|
def __init__(self, formatter: Callable[[], str] | None = None):
|
|
"""
|
|
Optional result formatter. If not provided, uses repr(result).
|
|
"""
|
|
self.formatter = formatter or (lambda r: repr(r))
|
|
|
|
@property
|
|
def __name__(self):
|
|
return "ResultReporter"
|
|
|
|
async def report(self, context: ExecutionContext):
|
|
if not callable(self.formatter):
|
|
raise TypeError("formatter must be callable")
|
|
if context.result is not None:
|
|
result_text = self.formatter(context.result)
|
|
duration = f"{context.duration:.3f}s" if context.duration is not None else "n/a"
|
|
context.console.print(f"[{OneColors.GREEN}]✅ '{context.name}' "
|
|
f"completed:[/] {result_text} in {duration}.")
|
|
|
|
|
|
class CircuitBreaker:
|
|
def __init__(self, max_failures=3, reset_timeout=10):
|
|
self.max_failures = max_failures
|
|
self.reset_timeout = reset_timeout
|
|
self.failures = 0
|
|
self.open_until = None
|
|
|
|
def before_hook(self, context: ExecutionContext):
|
|
name = context.name
|
|
if self.open_until:
|
|
if time.time() < self.open_until:
|
|
raise CircuitBreakerOpen(f"🔴 Circuit open for '{name}' until {time.ctime(self.open_until)}.")
|
|
else:
|
|
logger.info(f"🟢 Circuit closed again for '{name}'.")
|
|
self.failures = 0
|
|
self.open_until = None
|
|
|
|
def error_hook(self, context: ExecutionContext):
|
|
name = context.name
|
|
self.failures += 1
|
|
logger.warning(f"⚠️ CircuitBreaker: '{name}' failure {self.failures}/{self.max_failures}.")
|
|
if self.failures >= self.max_failures:
|
|
self.open_until = time.time() + self.reset_timeout
|
|
logger.error(f"🔴 Circuit opened for '{name}' until {time.ctime(self.open_until)}.")
|
|
|
|
def after_hook(self, context: ExecutionContext):
|
|
self.failures = 0
|
|
|
|
def is_open(self):
|
|
return self.open_until is not None and time.time() < self.open_until
|
|
|
|
def reset(self):
|
|
self.failures = 0
|
|
self.open_until = None
|
|
logger.info("🔄 Circuit reset.")
|