falyx 0.1.37__tar.gz → 0.1.38__tar.gz

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.
Files changed (62) hide show
  1. {falyx-0.1.37 → falyx-0.1.38}/PKG-INFO +1 -1
  2. falyx-0.1.38/falyx/__init__.py +18 -0
  3. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/__init__.py +7 -9
  4. falyx-0.1.38/falyx/action/action.py +162 -0
  5. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/action_factory.py +1 -1
  6. falyx-0.1.38/falyx/action/action_group.py +169 -0
  7. falyx-0.1.38/falyx/action/base.py +156 -0
  8. falyx-0.1.38/falyx/action/chained_action.py +208 -0
  9. falyx-0.1.38/falyx/action/fallback_action.py +49 -0
  10. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/io_action.py +1 -1
  11. falyx-0.1.38/falyx/action/literal_input_action.py +47 -0
  12. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/menu_action.py +1 -1
  13. falyx-0.1.38/falyx/action/mixins.py +33 -0
  14. falyx-0.1.38/falyx/action/process_action.py +128 -0
  15. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/prompt_menu_action.py +1 -1
  16. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/select_file_action.py +1 -1
  17. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/selection_action.py +1 -2
  18. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/user_input_action.py +1 -1
  19. {falyx-0.1.37 → falyx-0.1.38}/falyx/command.py +2 -1
  20. {falyx-0.1.37 → falyx-0.1.38}/falyx/config.py +2 -1
  21. {falyx-0.1.37 → falyx-0.1.38}/falyx/falyx.py +21 -13
  22. {falyx-0.1.37 → falyx-0.1.38}/falyx/menu.py +1 -1
  23. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/argparse.py +139 -40
  24. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/utils.py +1 -1
  25. {falyx-0.1.37 → falyx-0.1.38}/falyx/protocols.py +1 -1
  26. {falyx-0.1.37 → falyx-0.1.38}/falyx/retry_utils.py +2 -1
  27. {falyx-0.1.37 → falyx-0.1.38}/falyx/utils.py +1 -1
  28. falyx-0.1.38/falyx/version.py +1 -0
  29. {falyx-0.1.37 → falyx-0.1.38}/pyproject.toml +1 -1
  30. falyx-0.1.37/falyx/__init__.py +0 -29
  31. falyx-0.1.37/falyx/action/action.py +0 -882
  32. falyx-0.1.37/falyx/version.py +0 -1
  33. {falyx-0.1.37 → falyx-0.1.38}/LICENSE +0 -0
  34. {falyx-0.1.37 → falyx-0.1.38}/README.md +0 -0
  35. {falyx-0.1.37 → falyx-0.1.38}/falyx/.pytyped +0 -0
  36. {falyx-0.1.37 → falyx-0.1.38}/falyx/__main__.py +0 -0
  37. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/.pytyped +0 -0
  38. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/http_action.py +0 -0
  39. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/signal_action.py +0 -0
  40. {falyx-0.1.37 → falyx-0.1.38}/falyx/action/types.py +0 -0
  41. {falyx-0.1.37 → falyx-0.1.38}/falyx/bottom_bar.py +0 -0
  42. {falyx-0.1.37 → falyx-0.1.38}/falyx/context.py +0 -0
  43. {falyx-0.1.37 → falyx-0.1.38}/falyx/debug.py +0 -0
  44. {falyx-0.1.37 → falyx-0.1.38}/falyx/exceptions.py +0 -0
  45. {falyx-0.1.37 → falyx-0.1.38}/falyx/execution_registry.py +0 -0
  46. {falyx-0.1.37 → falyx-0.1.38}/falyx/hook_manager.py +0 -0
  47. {falyx-0.1.37 → falyx-0.1.38}/falyx/hooks.py +0 -0
  48. {falyx-0.1.37 → falyx-0.1.38}/falyx/init.py +0 -0
  49. {falyx-0.1.37 → falyx-0.1.38}/falyx/logger.py +0 -0
  50. {falyx-0.1.37 → falyx-0.1.38}/falyx/options_manager.py +0 -0
  51. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/.pytyped +0 -0
  52. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/__init__.py +0 -0
  53. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/parsers.py +0 -0
  54. {falyx-0.1.37 → falyx-0.1.38}/falyx/parsers/signature.py +0 -0
  55. {falyx-0.1.37 → falyx-0.1.38}/falyx/prompt_utils.py +0 -0
  56. {falyx-0.1.37 → falyx-0.1.38}/falyx/retry.py +0 -0
  57. {falyx-0.1.37 → falyx-0.1.38}/falyx/selection.py +0 -0
  58. {falyx-0.1.37 → falyx-0.1.38}/falyx/signals.py +0 -0
  59. {falyx-0.1.37 → falyx-0.1.38}/falyx/tagged_table.py +0 -0
  60. {falyx-0.1.37 → falyx-0.1.38}/falyx/themes/__init__.py +0 -0
  61. {falyx-0.1.37 → falyx-0.1.38}/falyx/themes/colors.py +0 -0
  62. {falyx-0.1.37 → falyx-0.1.38}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.37
3
+ Version: 0.1.38
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -0,0 +1,18 @@
1
+ """
2
+ Falyx CLI Framework
3
+
4
+ Copyright (c) 2025 rtj.dev LLC.
5
+ Licensed under the MIT License. See LICENSE file for details.
6
+ """
7
+
8
+ import logging
9
+
10
+ from .execution_registry import ExecutionRegistry
11
+ from .falyx import Falyx
12
+
13
+ logger = logging.getLogger("falyx")
14
+
15
+ __all__ = [
16
+ "Falyx",
17
+ "ExecutionRegistry",
18
+ ]
@@ -5,19 +5,17 @@ Copyright (c) 2025 rtj.dev LLC.
5
5
  Licensed under the MIT License. See LICENSE file for details.
6
6
  """
7
7
 
8
- from .action import (
9
- Action,
10
- ActionGroup,
11
- BaseAction,
12
- ChainedAction,
13
- FallbackAction,
14
- LiteralInputAction,
15
- ProcessAction,
16
- )
8
+ from .action import Action
17
9
  from .action_factory import ActionFactoryAction
10
+ from .action_group import ActionGroup
11
+ from .base import BaseAction
12
+ from .chained_action import ChainedAction
13
+ from .fallback_action import FallbackAction
18
14
  from .http_action import HTTPAction
19
15
  from .io_action import BaseIOAction, ShellAction
16
+ from .literal_input_action import LiteralInputAction
20
17
  from .menu_action import MenuAction
18
+ from .process_action import ProcessAction
21
19
  from .prompt_menu_action import PromptMenuAction
22
20
  from .select_file_action import SelectFileAction
23
21
  from .selection_action import SelectionAction
@@ -0,0 +1,162 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """action.py"""
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ from rich.tree import Tree
8
+
9
+ from falyx.action.base import BaseAction
10
+ from falyx.context import ExecutionContext
11
+ from falyx.execution_registry import ExecutionRegistry as er
12
+ from falyx.hook_manager import HookManager, HookType
13
+ from falyx.logger import logger
14
+ from falyx.retry import RetryHandler, RetryPolicy
15
+ from falyx.themes import OneColors
16
+ from falyx.utils import ensure_async
17
+
18
+
19
+ class Action(BaseAction):
20
+ """
21
+ Action wraps a simple function or coroutine into a standard executable unit.
22
+
23
+ It supports:
24
+ - Optional retry logic.
25
+ - Hook lifecycle (before, success, error, after, teardown).
26
+ - Last result injection for chaining.
27
+ - Optional rollback handlers for undo logic.
28
+
29
+ Args:
30
+ name (str): Name of the action.
31
+ action (Callable): The function or coroutine to execute.
32
+ rollback (Callable, optional): Rollback function to undo the action.
33
+ args (tuple, optional): Static positional arguments.
34
+ kwargs (dict, optional): Static keyword arguments.
35
+ hooks (HookManager, optional): Hook manager for lifecycle events.
36
+ inject_last_result (bool, optional): Enable last_result injection.
37
+ inject_into (str, optional): Name of injected key.
38
+ retry (bool, optional): Enable retry logic.
39
+ retry_policy (RetryPolicy, optional): Retry settings.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ name: str,
45
+ action: Callable[..., Any],
46
+ *,
47
+ rollback: Callable[..., Any] | None = None,
48
+ args: tuple[Any, ...] = (),
49
+ kwargs: dict[str, Any] | None = None,
50
+ hooks: HookManager | None = None,
51
+ inject_last_result: bool = False,
52
+ inject_into: str = "last_result",
53
+ retry: bool = False,
54
+ retry_policy: RetryPolicy | None = None,
55
+ ) -> None:
56
+ super().__init__(
57
+ name,
58
+ hooks=hooks,
59
+ inject_last_result=inject_last_result,
60
+ inject_into=inject_into,
61
+ )
62
+ self.action = action
63
+ self.rollback = rollback
64
+ self.args = args
65
+ self.kwargs = kwargs or {}
66
+ self.is_retryable = True
67
+ self.retry_policy = retry_policy or RetryPolicy()
68
+ if retry or (retry_policy and retry_policy.enabled):
69
+ self.enable_retry()
70
+
71
+ @property
72
+ def action(self) -> Callable[..., Any]:
73
+ return self._action
74
+
75
+ @action.setter
76
+ def action(self, value: Callable[..., Any]):
77
+ self._action = ensure_async(value)
78
+
79
+ @property
80
+ def rollback(self) -> Callable[..., Any] | None:
81
+ return self._rollback
82
+
83
+ @rollback.setter
84
+ def rollback(self, value: Callable[..., Any] | None):
85
+ if value is None:
86
+ self._rollback = None
87
+ else:
88
+ self._rollback = ensure_async(value)
89
+
90
+ def enable_retry(self):
91
+ """Enable retry with the existing retry policy."""
92
+ self.retry_policy.enable_policy()
93
+ logger.debug("[%s] Registering retry handler", self.name)
94
+ handler = RetryHandler(self.retry_policy)
95
+ self.hooks.register(HookType.ON_ERROR, handler.retry_on_error)
96
+
97
+ def set_retry_policy(self, policy: RetryPolicy):
98
+ """Set a new retry policy and re-register the handler."""
99
+ self.retry_policy = policy
100
+ if policy.enabled:
101
+ self.enable_retry()
102
+
103
+ def get_infer_target(self) -> tuple[Callable[..., Any], None]:
104
+ """
105
+ Returns the callable to be used for argument inference.
106
+ By default, it returns the action itself.
107
+ """
108
+ return self.action, None
109
+
110
+ async def _run(self, *args, **kwargs) -> Any:
111
+ combined_args = args + self.args
112
+ combined_kwargs = self._maybe_inject_last_result({**self.kwargs, **kwargs})
113
+
114
+ context = ExecutionContext(
115
+ name=self.name,
116
+ args=combined_args,
117
+ kwargs=combined_kwargs,
118
+ action=self,
119
+ )
120
+
121
+ context.start_timer()
122
+ try:
123
+ await self.hooks.trigger(HookType.BEFORE, context)
124
+ result = await self.action(*combined_args, **combined_kwargs)
125
+ context.result = result
126
+ await self.hooks.trigger(HookType.ON_SUCCESS, context)
127
+ return context.result
128
+ except Exception as error:
129
+ context.exception = error
130
+ await self.hooks.trigger(HookType.ON_ERROR, context)
131
+ if context.result is not None:
132
+ logger.info("[%s] Recovered: %s", self.name, self.name)
133
+ return context.result
134
+ raise
135
+ finally:
136
+ context.stop_timer()
137
+ await self.hooks.trigger(HookType.AFTER, context)
138
+ await self.hooks.trigger(HookType.ON_TEARDOWN, context)
139
+ er.record(context)
140
+
141
+ async def preview(self, parent: Tree | None = None):
142
+ label = [f"[{OneColors.GREEN_b}]⚙ Action[/] '{self.name}'"]
143
+ if self.inject_last_result:
144
+ label.append(f" [dim](injects '{self.inject_into}')[/dim]")
145
+ if self.retry_policy.enabled:
146
+ label.append(
147
+ f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, "
148
+ f"delay {self.retry_policy.delay}s, backoff {self.retry_policy.backoff}x"
149
+ )
150
+
151
+ if parent:
152
+ parent.add("".join(label))
153
+ else:
154
+ self.console.print(Tree("".join(label)))
155
+
156
+ def __str__(self):
157
+ return (
158
+ f"Action(name={self.name!r}, action="
159
+ f"{getattr(self._action, '__name__', repr(self._action))}, "
160
+ f"args={self.args!r}, kwargs={self.kwargs!r}, "
161
+ f"retry={self.retry_policy.enabled})"
162
+ )
@@ -4,7 +4,7 @@ from typing import Any, Callable
4
4
 
5
5
  from rich.tree import Tree
6
6
 
7
- from falyx.action.action import BaseAction
7
+ from falyx.action.base import BaseAction
8
8
  from falyx.context import ExecutionContext
9
9
  from falyx.execution_registry import ExecutionRegistry as er
10
10
  from falyx.hook_manager import HookType
@@ -0,0 +1,169 @@
1
+ import asyncio
2
+ import random
3
+ from typing import Any, Callable
4
+
5
+ from rich.tree import Tree
6
+
7
+ from falyx.action.action import Action
8
+ from falyx.action.base import BaseAction
9
+ from falyx.action.mixins import ActionListMixin
10
+ from falyx.context import ExecutionContext, SharedContext
11
+ from falyx.execution_registry import ExecutionRegistry as er
12
+ from falyx.hook_manager import Hook, HookManager, HookType
13
+ from falyx.logger import logger
14
+ from falyx.parsers.utils import same_argument_definitions
15
+ from falyx.themes.colors import OneColors
16
+
17
+
18
+ class ActionGroup(BaseAction, ActionListMixin):
19
+ """
20
+ ActionGroup executes multiple actions concurrently in parallel.
21
+
22
+ It is ideal for independent tasks that can be safely run simultaneously,
23
+ improving overall throughput and responsiveness of workflows.
24
+
25
+ Core features:
26
+ - Parallel execution of all contained actions.
27
+ - Shared last_result injection across all actions if configured.
28
+ - Aggregated collection of individual results as (name, result) pairs.
29
+ - Hook lifecycle support (before, on_success, on_error, after, on_teardown).
30
+ - Error aggregation: captures all action errors and reports them together.
31
+
32
+ Behavior:
33
+ - If any action fails, the group collects the errors but continues executing
34
+ other actions without interruption.
35
+ - After all actions complete, ActionGroup raises a single exception summarizing
36
+ all failures, or returns all results if successful.
37
+
38
+ Best used for:
39
+ - Batch processing multiple independent tasks.
40
+ - Reducing latency for workflows with parallelizable steps.
41
+ - Isolating errors while maximizing successful execution.
42
+
43
+ Args:
44
+ name (str): Name of the chain.
45
+ actions (list): List of actions or literals to execute.
46
+ hooks (HookManager, optional): Hooks for lifecycle events.
47
+ inject_last_result (bool, optional): Whether to inject last results into kwargs
48
+ by default.
49
+ inject_into (str, optional): Key name for injection.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ name: str,
55
+ actions: list[BaseAction] | None = None,
56
+ *,
57
+ hooks: HookManager | None = None,
58
+ inject_last_result: bool = False,
59
+ inject_into: str = "last_result",
60
+ ):
61
+ super().__init__(
62
+ name,
63
+ hooks=hooks,
64
+ inject_last_result=inject_last_result,
65
+ inject_into=inject_into,
66
+ )
67
+ ActionListMixin.__init__(self)
68
+ if actions:
69
+ self.set_actions(actions)
70
+
71
+ def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
72
+ if isinstance(action, BaseAction):
73
+ return action
74
+ elif callable(action):
75
+ return Action(name=action.__name__, action=action)
76
+ else:
77
+ raise TypeError(
78
+ "ActionGroup only accepts BaseAction or callable, got "
79
+ f"{type(action).__name__}"
80
+ )
81
+
82
+ def add_action(self, action: BaseAction | Any) -> None:
83
+ action = self._wrap_if_needed(action)
84
+ super().add_action(action)
85
+ if hasattr(action, "register_teardown") and callable(action.register_teardown):
86
+ action.register_teardown(self.hooks)
87
+
88
+ def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
89
+ arg_defs = same_argument_definitions(self.actions)
90
+ if arg_defs:
91
+ return self.actions[0].get_infer_target()
92
+ logger.debug(
93
+ "[%s] auto_args disabled: mismatched ActionGroup arguments",
94
+ self.name,
95
+ )
96
+ return None, None
97
+
98
+ async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
99
+ shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
100
+ if self.shared_context:
101
+ shared_context.set_shared_result(self.shared_context.last_result())
102
+ updated_kwargs = self._maybe_inject_last_result(kwargs)
103
+ context = ExecutionContext(
104
+ name=self.name,
105
+ args=args,
106
+ kwargs=updated_kwargs,
107
+ action=self,
108
+ extra={"results": [], "errors": []},
109
+ shared_context=shared_context,
110
+ )
111
+
112
+ async def run_one(action: BaseAction):
113
+ try:
114
+ prepared = action.prepare(shared_context, self.options_manager)
115
+ result = await prepared(*args, **updated_kwargs)
116
+ shared_context.add_result((action.name, result))
117
+ context.extra["results"].append((action.name, result))
118
+ except Exception as error:
119
+ shared_context.add_error(shared_context.current_index, error)
120
+ context.extra["errors"].append((action.name, error))
121
+
122
+ context.start_timer()
123
+ try:
124
+ await self.hooks.trigger(HookType.BEFORE, context)
125
+ await asyncio.gather(*[run_one(a) for a in self.actions])
126
+
127
+ if context.extra["errors"]:
128
+ context.exception = Exception(
129
+ f"{len(context.extra['errors'])} action(s) failed: "
130
+ f"{' ,'.join(name for name, _ in context.extra['errors'])}"
131
+ )
132
+ await self.hooks.trigger(HookType.ON_ERROR, context)
133
+ raise context.exception
134
+
135
+ context.result = context.extra["results"]
136
+ await self.hooks.trigger(HookType.ON_SUCCESS, context)
137
+ return context.result
138
+
139
+ except Exception as error:
140
+ context.exception = error
141
+ raise
142
+ finally:
143
+ context.stop_timer()
144
+ await self.hooks.trigger(HookType.AFTER, context)
145
+ await self.hooks.trigger(HookType.ON_TEARDOWN, context)
146
+ er.record(context)
147
+
148
+ def register_hooks_recursively(self, hook_type: HookType, hook: Hook):
149
+ """Register a hook for all actions and sub-actions."""
150
+ super().register_hooks_recursively(hook_type, hook)
151
+ for action in self.actions:
152
+ action.register_hooks_recursively(hook_type, hook)
153
+
154
+ async def preview(self, parent: Tree | None = None):
155
+ label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (parallel)[/] '{self.name}'"]
156
+ if self.inject_last_result:
157
+ label.append(f" [dim](receives '{self.inject_into}')[/dim]")
158
+ tree = parent.add("".join(label)) if parent else Tree("".join(label))
159
+ actions = self.actions.copy()
160
+ random.shuffle(actions)
161
+ await asyncio.gather(*(action.preview(parent=tree) for action in actions))
162
+ if not parent:
163
+ self.console.print(tree)
164
+
165
+ def __str__(self):
166
+ return (
167
+ f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},"
168
+ f" inject_last_result={self.inject_last_result})"
169
+ )
@@ -0,0 +1,156 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """base.py
3
+
4
+ Core action system for Falyx.
5
+
6
+ This module defines the building blocks for executable actions and workflows,
7
+ providing a structured way to compose, execute, recover, and manage sequences of
8
+ operations.
9
+
10
+ All actions are callable and follow a unified signature:
11
+ result = action(*args, **kwargs)
12
+
13
+ Core guarantees:
14
+ - Full hook lifecycle support (before, on_success, on_error, after, on_teardown).
15
+ - Consistent timing and execution context tracking for each run.
16
+ - Unified, predictable result handling and error propagation.
17
+ - Optional last_result injection to enable flexible, data-driven workflows.
18
+ - Built-in support for retries, rollbacks, parallel groups, chaining, and fallback
19
+ recovery.
20
+
21
+ Key components:
22
+ - Action: wraps a function or coroutine into a standard executable unit.
23
+ - ChainedAction: runs actions sequentially, optionally injecting last results.
24
+ - ActionGroup: runs actions in parallel and gathers results.
25
+ - ProcessAction: executes CPU-bound functions in a separate process.
26
+ - LiteralInputAction: injects static values into workflows.
27
+ - FallbackAction: gracefully recovers from failures or missing data.
28
+
29
+ This design promotes clean, fault-tolerant, modular CLI and automation systems.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ from abc import ABC, abstractmethod
34
+ from typing import Any, Callable
35
+
36
+ from rich.console import Console
37
+ from rich.tree import Tree
38
+
39
+ from falyx.context import SharedContext
40
+ from falyx.debug import register_debug_hooks
41
+ from falyx.execution_registry import ExecutionRegistry as er
42
+ from falyx.hook_manager import Hook, HookManager, HookType
43
+ from falyx.logger import logger
44
+ from falyx.options_manager import OptionsManager
45
+
46
+
47
+ class BaseAction(ABC):
48
+ """
49
+ Base class for actions. Actions can be simple functions or more
50
+ complex actions like `ChainedAction` or `ActionGroup`. They can also
51
+ be run independently or as part of Falyx.
52
+
53
+ inject_last_result (bool): Whether to inject the previous action's result
54
+ into kwargs.
55
+ inject_into (str): The name of the kwarg key to inject the result as
56
+ (default: 'last_result').
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ name: str,
62
+ *,
63
+ hooks: HookManager | None = None,
64
+ inject_last_result: bool = False,
65
+ inject_into: str = "last_result",
66
+ never_prompt: bool = False,
67
+ logging_hooks: bool = False,
68
+ ) -> None:
69
+ self.name = name
70
+ self.hooks = hooks or HookManager()
71
+ self.is_retryable: bool = False
72
+ self.shared_context: SharedContext | None = None
73
+ self.inject_last_result: bool = inject_last_result
74
+ self.inject_into: str = inject_into
75
+ self._never_prompt: bool = never_prompt
76
+ self._skip_in_chain: bool = False
77
+ self.console = Console(color_system="auto")
78
+ self.options_manager: OptionsManager | None = None
79
+
80
+ if logging_hooks:
81
+ register_debug_hooks(self.hooks)
82
+
83
+ async def __call__(self, *args, **kwargs) -> Any:
84
+ return await self._run(*args, **kwargs)
85
+
86
+ @abstractmethod
87
+ async def _run(self, *args, **kwargs) -> Any:
88
+ raise NotImplementedError("_run must be implemented by subclasses")
89
+
90
+ @abstractmethod
91
+ async def preview(self, parent: Tree | None = None):
92
+ raise NotImplementedError("preview must be implemented by subclasses")
93
+
94
+ @abstractmethod
95
+ def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
96
+ """
97
+ Returns the callable to be used for argument inference.
98
+ By default, it returns None.
99
+ """
100
+ raise NotImplementedError("get_infer_target must be implemented by subclasses")
101
+
102
+ def set_options_manager(self, options_manager: OptionsManager) -> None:
103
+ self.options_manager = options_manager
104
+
105
+ def set_shared_context(self, shared_context: SharedContext) -> None:
106
+ self.shared_context = shared_context
107
+
108
+ def get_option(self, option_name: str, default: Any = None) -> Any:
109
+ """
110
+ Resolve an option from the OptionsManager if present, otherwise use the fallback.
111
+ """
112
+ if self.options_manager:
113
+ return self.options_manager.get(option_name, default)
114
+ return default
115
+
116
+ @property
117
+ def last_result(self) -> Any:
118
+ """Return the last result from the shared context."""
119
+ if self.shared_context:
120
+ return self.shared_context.last_result()
121
+ return None
122
+
123
+ @property
124
+ def never_prompt(self) -> bool:
125
+ return self.get_option("never_prompt", self._never_prompt)
126
+
127
+ def prepare(
128
+ self, shared_context: SharedContext, options_manager: OptionsManager | None = None
129
+ ) -> BaseAction:
130
+ """
131
+ Prepare the action specifically for sequential (ChainedAction) execution.
132
+ Can be overridden for chain-specific logic.
133
+ """
134
+ self.set_shared_context(shared_context)
135
+ if options_manager:
136
+ self.set_options_manager(options_manager)
137
+ return self
138
+
139
+ def _maybe_inject_last_result(self, kwargs: dict[str, Any]) -> dict[str, Any]:
140
+ if self.inject_last_result and self.shared_context:
141
+ key = self.inject_into
142
+ if key in kwargs:
143
+ logger.warning("[%s] Overriding '%s' with last_result", self.name, key)
144
+ kwargs = dict(kwargs)
145
+ kwargs[key] = self.shared_context.last_result()
146
+ return kwargs
147
+
148
+ def register_hooks_recursively(self, hook_type: HookType, hook: Hook):
149
+ """Register a hook for all actions and sub-actions."""
150
+ self.hooks.register(hook_type, hook)
151
+
152
+ async def _write_stdout(self, data: str) -> None:
153
+ """Override in subclasses that produce terminal output."""
154
+
155
+ def __repr__(self) -> str:
156
+ return str(self)