falyx/falyx/action_factory.py

109 lines
4.1 KiB
Python

# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""action_factory.py"""
from typing import Any
from rich.tree import Tree
from falyx.action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType
from falyx.logger import logger
from falyx.protocols import ActionFactoryProtocol
from falyx.themes.colors import OneColors
class ActionFactoryAction(BaseAction):
"""
Dynamically creates and runs another Action at runtime using a factory function.
This is useful for generating context-specific behavior (e.g., dynamic HTTPActions)
where the structure of the next action depends on runtime values.
Args:
name (str): Name of the action.
factory (Callable): A function that returns a BaseAction given args/kwargs.
inject_last_result (bool): Whether to inject last_result into the factory.
inject_into (str): The name of the kwarg to inject last_result as.
"""
def __init__(
self,
name: str,
factory: ActionFactoryProtocol,
*,
inject_last_result: bool = False,
inject_into: str = "last_result",
preview_args: tuple[Any, ...] = (),
preview_kwargs: dict[str, Any] | None = None,
):
super().__init__(
name=name,
inject_last_result=inject_last_result,
inject_into=inject_into,
)
self.factory = factory
self.preview_args = preview_args
self.preview_kwargs = preview_kwargs or {}
async def _run(self, *args, **kwargs) -> Any:
updated_kwargs = self._maybe_inject_last_result(kwargs)
context = ExecutionContext(
name=f"{self.name} (factory)",
args=args,
kwargs=updated_kwargs,
action=self,
)
context.start_timer()
try:
await self.hooks.trigger(HookType.BEFORE, context)
generated_action = self.factory(*args, **updated_kwargs)
if not isinstance(generated_action, BaseAction):
raise TypeError(
f"[{self.name}] Factory must return a BaseAction, got "
f"{type(generated_action).__name__}"
)
if self.shared_context:
generated_action.set_shared_context(self.shared_context)
if hasattr(generated_action, "register_teardown") and callable(
generated_action.register_teardown
):
generated_action.register_teardown(self.shared_context.action.hooks)
logger.debug(
"[%s] Registered teardown for %s",
self.name,
generated_action.name,
)
if self.options_manager:
generated_action.set_options_manager(self.options_manager)
context.result = await generated_action(*args, **kwargs)
await self.hooks.trigger(HookType.ON_SUCCESS, context)
return context.result
except Exception as error:
context.exception = error
await self.hooks.trigger(HookType.ON_ERROR, context)
raise
finally:
context.stop_timer()
await self.hooks.trigger(HookType.AFTER, context)
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
er.record(context)
async def preview(self, parent: Tree | None = None):
label = f"[{OneColors.CYAN_b}]🏗️ ActionFactory[/] '{self.name}'"
tree = parent.add(label) if parent else Tree(label)
try:
generated = self.factory(*self.preview_args, **self.preview_kwargs)
if isinstance(generated, BaseAction):
await generated.preview(parent=tree)
else:
tree.add(
f"[{OneColors.DARK_RED}]⚠️ Factory did not return a BaseAction[/]"
)
except Exception as error:
tree.add(f"[{OneColors.DARK_RED}]⚠️ Preview failed: {error}[/]")
if not parent:
self.console.print(tree)