falyx 0.1.23__tar.gz → 0.1.25__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.
- {falyx-0.1.23 → falyx-0.1.25}/PKG-INFO +2 -1
- {falyx-0.1.23 → falyx-0.1.25}/falyx/__init__.py +1 -1
- falyx-0.1.25/falyx/action/__init__.py +41 -0
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/action.py +41 -23
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/action_factory.py +17 -5
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/http_action.py +10 -9
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/io_action.py +19 -14
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/menu_action.py +9 -79
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/select_file_action.py +5 -36
- {falyx-0.1.23/falyx → falyx-0.1.25/falyx/action}/selection_action.py +22 -8
- falyx-0.1.25/falyx/action/signal_action.py +43 -0
- falyx-0.1.25/falyx/action/types.py +37 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/bottom_bar.py +3 -3
- {falyx-0.1.23 → falyx-0.1.25}/falyx/command.py +13 -10
- {falyx-0.1.23 → falyx-0.1.25}/falyx/config.py +17 -9
- {falyx-0.1.23 → falyx-0.1.25}/falyx/context.py +16 -8
- {falyx-0.1.23 → falyx-0.1.25}/falyx/debug.py +2 -1
- {falyx-0.1.23 → falyx-0.1.25}/falyx/exceptions.py +3 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/execution_registry.py +59 -13
- {falyx-0.1.23 → falyx-0.1.25}/falyx/falyx.py +67 -77
- {falyx-0.1.23 → falyx-0.1.25}/falyx/hook_manager.py +20 -3
- {falyx-0.1.23 → falyx-0.1.25}/falyx/hooks.py +13 -6
- {falyx-0.1.23 → falyx-0.1.25}/falyx/init.py +1 -0
- falyx-0.1.25/falyx/logger.py +5 -0
- falyx-0.1.25/falyx/menu.py +85 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/options_manager.py +7 -3
- {falyx-0.1.23 → falyx-0.1.25}/falyx/parsers.py +2 -2
- falyx-0.1.25/falyx/prompt_utils.py +48 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/protocols.py +2 -1
- {falyx-0.1.23 → falyx-0.1.25}/falyx/retry.py +23 -12
- {falyx-0.1.23 → falyx-0.1.25}/falyx/retry_utils.py +2 -1
- {falyx-0.1.23 → falyx-0.1.25}/falyx/selection.py +7 -3
- {falyx-0.1.23 → falyx-0.1.25}/falyx/signals.py +3 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/tagged_table.py +2 -1
- falyx-0.1.25/falyx/themes/__init__.py +15 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/utils.py +11 -39
- {falyx-0.1.23 → falyx-0.1.25}/falyx/validators.py +8 -7
- falyx-0.1.25/falyx/version.py +1 -0
- {falyx-0.1.23 → falyx-0.1.25}/pyproject.toml +4 -3
- falyx-0.1.23/falyx/prompt_utils.py +0 -19
- falyx-0.1.23/falyx/signal_action.py +0 -30
- falyx-0.1.23/falyx/version.py +0 -1
- {falyx-0.1.23 → falyx-0.1.25}/LICENSE +0 -0
- {falyx-0.1.23 → falyx-0.1.25}/README.md +0 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/.pytyped +0 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/__main__.py +0 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/config_schema.py +0 -0
- {falyx-0.1.23 → falyx-0.1.25}/falyx/themes/colors.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: falyx
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.25
|
4
4
|
Summary: Reliable and introspectable async CLI action framework.
|
5
5
|
License: MIT
|
6
6
|
Author: Roland Thomas Jr
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
15
|
+
Requires-Dist: aiohttp (>=3.11,<4.0)
|
15
16
|
Requires-Dist: prompt_toolkit (>=3.0,<4.0)
|
16
17
|
Requires-Dist: pydantic (>=2.0,<3.0)
|
17
18
|
Requires-Dist: python-json-logger (>=3.3.0,<4.0.0)
|
@@ -7,7 +7,7 @@ Licensed under the MIT License. See LICENSE file for details.
|
|
7
7
|
|
8
8
|
import logging
|
9
9
|
|
10
|
-
from .action import Action, ActionGroup, ChainedAction, ProcessAction
|
10
|
+
from .action.action import Action, ActionGroup, ChainedAction, ProcessAction
|
11
11
|
from .command import Command
|
12
12
|
from .context import ExecutionContext, SharedContext
|
13
13
|
from .execution_registry import ExecutionRegistry
|
@@ -0,0 +1,41 @@
|
|
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
|
+
from .action import (
|
9
|
+
Action,
|
10
|
+
ActionGroup,
|
11
|
+
BaseAction,
|
12
|
+
ChainedAction,
|
13
|
+
FallbackAction,
|
14
|
+
LiteralInputAction,
|
15
|
+
ProcessAction,
|
16
|
+
)
|
17
|
+
from .action_factory import ActionFactoryAction
|
18
|
+
from .http_action import HTTPAction
|
19
|
+
from .io_action import BaseIOAction, ShellAction
|
20
|
+
from .menu_action import MenuAction
|
21
|
+
from .select_file_action import SelectFileAction
|
22
|
+
from .selection_action import SelectionAction
|
23
|
+
from .signal_action import SignalAction
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
"Action",
|
27
|
+
"ActionGroup",
|
28
|
+
"BaseAction",
|
29
|
+
"ChainedAction",
|
30
|
+
"ProcessAction",
|
31
|
+
"ActionFactoryAction",
|
32
|
+
"HTTPAction",
|
33
|
+
"BaseIOAction",
|
34
|
+
"ShellAction",
|
35
|
+
"SelectionAction",
|
36
|
+
"SelectFileAction",
|
37
|
+
"MenuAction",
|
38
|
+
"SignalAction",
|
39
|
+
"FallbackAction",
|
40
|
+
"LiteralInputAction",
|
41
|
+
]
|
@@ -4,7 +4,8 @@
|
|
4
4
|
Core action system for Falyx.
|
5
5
|
|
6
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
|
7
|
+
providing a structured way to compose, execute, recover, and manage sequences of
|
8
|
+
operations.
|
8
9
|
|
9
10
|
All actions are callable and follow a unified signature:
|
10
11
|
result = action(*args, **kwargs)
|
@@ -14,7 +15,8 @@ Core guarantees:
|
|
14
15
|
- Consistent timing and execution context tracking for each run.
|
15
16
|
- Unified, predictable result handling and error propagation.
|
16
17
|
- Optional last_result injection to enable flexible, data-driven workflows.
|
17
|
-
- Built-in support for retries, rollbacks, parallel groups, chaining, and fallback
|
18
|
+
- Built-in support for retries, rollbacks, parallel groups, chaining, and fallback
|
19
|
+
recovery.
|
18
20
|
|
19
21
|
Key components:
|
20
22
|
- Action: wraps a function or coroutine into a standard executable unit.
|
@@ -43,10 +45,11 @@ from falyx.debug import register_debug_hooks
|
|
43
45
|
from falyx.exceptions import EmptyChainError
|
44
46
|
from falyx.execution_registry import ExecutionRegistry as er
|
45
47
|
from falyx.hook_manager import Hook, HookManager, HookType
|
48
|
+
from falyx.logger import logger
|
46
49
|
from falyx.options_manager import OptionsManager
|
47
50
|
from falyx.retry import RetryHandler, RetryPolicy
|
48
|
-
from falyx.themes
|
49
|
-
from falyx.utils import ensure_async
|
51
|
+
from falyx.themes import OneColors
|
52
|
+
from falyx.utils import ensure_async
|
50
53
|
|
51
54
|
|
52
55
|
class BaseAction(ABC):
|
@@ -55,7 +58,8 @@ class BaseAction(ABC):
|
|
55
58
|
complex actions like `ChainedAction` or `ActionGroup`. They can also
|
56
59
|
be run independently or as part of Falyx.
|
57
60
|
|
58
|
-
inject_last_result (bool): Whether to inject the previous action's result
|
61
|
+
inject_last_result (bool): Whether to inject the previous action's result
|
62
|
+
into kwargs.
|
59
63
|
inject_into (str): The name of the kwarg key to inject the result as
|
60
64
|
(default: 'last_result').
|
61
65
|
_requires_injection (bool): Whether the action requires input injection.
|
@@ -104,7 +108,9 @@ class BaseAction(ABC):
|
|
104
108
|
self.shared_context = shared_context
|
105
109
|
|
106
110
|
def get_option(self, option_name: str, default: Any = None) -> Any:
|
107
|
-
"""
|
111
|
+
"""
|
112
|
+
Resolve an option from the OptionsManager if present, otherwise use the fallback.
|
113
|
+
"""
|
108
114
|
if self.options_manager:
|
109
115
|
return self.options_manager.get(option_name, default)
|
110
116
|
return default
|
@@ -288,8 +294,10 @@ class Action(BaseAction):
|
|
288
294
|
|
289
295
|
def __str__(self):
|
290
296
|
return (
|
291
|
-
f"Action(name={self.name!r}, action=
|
292
|
-
f"
|
297
|
+
f"Action(name={self.name!r}, action="
|
298
|
+
f"{getattr(self._action, '__name__', repr(self._action))}, "
|
299
|
+
f"args={self.args!r}, kwargs={self.kwargs!r}, "
|
300
|
+
f"retry={self.retry_policy.enabled})"
|
293
301
|
)
|
294
302
|
|
295
303
|
|
@@ -309,7 +317,7 @@ class LiteralInputAction(Action):
|
|
309
317
|
def __init__(self, value: Any):
|
310
318
|
self._value = value
|
311
319
|
|
312
|
-
async def literal(*
|
320
|
+
async def literal(*_, **__):
|
313
321
|
return value
|
314
322
|
|
315
323
|
super().__init__("Input", literal)
|
@@ -333,14 +341,16 @@ class LiteralInputAction(Action):
|
|
333
341
|
|
334
342
|
class FallbackAction(Action):
|
335
343
|
"""
|
336
|
-
FallbackAction provides a default value if the previous action failed or
|
344
|
+
FallbackAction provides a default value if the previous action failed or
|
345
|
+
returned None.
|
337
346
|
|
338
347
|
It injects the last result and checks:
|
339
348
|
- If last_result is not None, it passes it through unchanged.
|
340
349
|
- If last_result is None (e.g., due to failure), it replaces it with a fallback value.
|
341
350
|
|
342
351
|
Used in ChainedAction pipelines to gracefully recover from errors or missing data.
|
343
|
-
When activated, it consumes the preceding error and allows the chain to continue
|
352
|
+
When activated, it consumes the preceding error and allows the chain to continue
|
353
|
+
normally.
|
344
354
|
|
345
355
|
Args:
|
346
356
|
fallback (Any): The fallback value to use if last_result is None.
|
@@ -413,16 +423,19 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
413
423
|
- Rolls back all previously executed actions if a failure occurs.
|
414
424
|
- Handles literal values with LiteralInputAction.
|
415
425
|
|
416
|
-
Best used for defining robust, ordered workflows where each step can depend on
|
426
|
+
Best used for defining robust, ordered workflows where each step can depend on
|
427
|
+
previous results.
|
417
428
|
|
418
429
|
Args:
|
419
430
|
name (str): Name of the chain.
|
420
431
|
actions (list): List of actions or literals to execute.
|
421
432
|
hooks (HookManager, optional): Hooks for lifecycle events.
|
422
|
-
inject_last_result (bool, optional): Whether to inject last results into kwargs
|
433
|
+
inject_last_result (bool, optional): Whether to inject last results into kwargs
|
434
|
+
by default.
|
423
435
|
inject_into (str, optional): Key name for injection.
|
424
436
|
auto_inject (bool, optional): Auto-enable injection for subsequent actions.
|
425
|
-
return_list (bool, optional): Whether to return a list of all results. False
|
437
|
+
return_list (bool, optional): Whether to return a list of all results. False
|
438
|
+
returns the last result.
|
426
439
|
"""
|
427
440
|
|
428
441
|
def __init__(
|
@@ -468,7 +481,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
468
481
|
if not self.actions:
|
469
482
|
raise EmptyChainError(f"[{self.name}] No actions to execute.")
|
470
483
|
|
471
|
-
shared_context = SharedContext(name=self.name)
|
484
|
+
shared_context = SharedContext(name=self.name, action=self)
|
472
485
|
if self.shared_context:
|
473
486
|
shared_context.add_result(self.shared_context.last_result())
|
474
487
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
@@ -503,7 +516,8 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
503
516
|
self.actions[index + 1], FallbackAction
|
504
517
|
):
|
505
518
|
logger.warning(
|
506
|
-
"[%s] ⚠️ Fallback triggered: %s, recovering with fallback
|
519
|
+
"[%s] ⚠️ Fallback triggered: %s, recovering with fallback "
|
520
|
+
"'%s'.",
|
507
521
|
self.name,
|
508
522
|
error,
|
509
523
|
self.actions[index + 1].name,
|
@@ -579,7 +593,8 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
579
593
|
|
580
594
|
def __str__(self):
|
581
595
|
return (
|
582
|
-
f"ChainedAction(name={self.name!r},
|
596
|
+
f"ChainedAction(name={self.name!r}, "
|
597
|
+
f"actions={[a.name for a in self.actions]!r}, "
|
583
598
|
f"auto_inject={self.auto_inject}, return_list={self.return_list})"
|
584
599
|
)
|
585
600
|
|
@@ -613,7 +628,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
613
628
|
name (str): Name of the chain.
|
614
629
|
actions (list): List of actions or literals to execute.
|
615
630
|
hooks (HookManager, optional): Hooks for lifecycle events.
|
616
|
-
inject_last_result (bool, optional): Whether to inject last results into kwargs
|
631
|
+
inject_last_result (bool, optional): Whether to inject last results into kwargs
|
632
|
+
by default.
|
617
633
|
inject_into (str, optional): Key name for injection.
|
618
634
|
"""
|
619
635
|
|
@@ -643,7 +659,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
643
659
|
return Action(name=action.__name__, action=action)
|
644
660
|
else:
|
645
661
|
raise TypeError(
|
646
|
-
|
662
|
+
"ActionGroup only accepts BaseAction or callable, got "
|
663
|
+
f"{type(action).__name__}"
|
647
664
|
)
|
648
665
|
|
649
666
|
def add_action(self, action: BaseAction | Any) -> None:
|
@@ -653,7 +670,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
653
670
|
action.register_teardown(self.hooks)
|
654
671
|
|
655
672
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
656
|
-
shared_context = SharedContext(name=self.name, is_parallel=True)
|
673
|
+
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
657
674
|
if self.shared_context:
|
658
675
|
shared_context.set_shared_result(self.shared_context.last_result())
|
659
676
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
@@ -721,8 +738,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
721
738
|
|
722
739
|
def __str__(self):
|
723
740
|
return (
|
724
|
-
f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},
|
725
|
-
f"inject_last_result={self.inject_last_result})"
|
741
|
+
f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},"
|
742
|
+
f" inject_last_result={self.inject_last_result})"
|
726
743
|
)
|
727
744
|
|
728
745
|
|
@@ -831,6 +848,7 @@ class ProcessAction(BaseAction):
|
|
831
848
|
|
832
849
|
def __str__(self) -> str:
|
833
850
|
return (
|
834
|
-
f"ProcessAction(name={self.name!r},
|
851
|
+
f"ProcessAction(name={self.name!r}, "
|
852
|
+
f"action={getattr(self.action, '__name__', repr(self.action))}, "
|
835
853
|
f"args={self.args!r}, kwargs={self.kwargs!r})"
|
836
854
|
)
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""action_factory.py"""
|
2
3
|
from typing import Any
|
3
4
|
|
4
5
|
from rich.tree import Tree
|
5
6
|
|
6
|
-
from falyx.action import BaseAction
|
7
|
+
from falyx.action.action import BaseAction
|
7
8
|
from falyx.context import ExecutionContext
|
8
9
|
from falyx.execution_registry import ExecutionRegistry as er
|
9
10
|
from falyx.hook_manager import HookType
|
11
|
+
from falyx.logger import logger
|
10
12
|
from falyx.protocols import ActionFactoryProtocol
|
11
|
-
from falyx.themes
|
13
|
+
from falyx.themes import OneColors
|
12
14
|
|
13
15
|
|
14
16
|
class ActionFactoryAction(BaseAction):
|
@@ -33,7 +35,7 @@ class ActionFactoryAction(BaseAction):
|
|
33
35
|
inject_last_result: bool = False,
|
34
36
|
inject_into: str = "last_result",
|
35
37
|
preview_args: tuple[Any, ...] = (),
|
36
|
-
preview_kwargs: dict[str, Any] =
|
38
|
+
preview_kwargs: dict[str, Any] | None = None,
|
37
39
|
):
|
38
40
|
super().__init__(
|
39
41
|
name=name,
|
@@ -42,7 +44,7 @@ class ActionFactoryAction(BaseAction):
|
|
42
44
|
)
|
43
45
|
self.factory = factory
|
44
46
|
self.preview_args = preview_args
|
45
|
-
self.preview_kwargs = preview_kwargs
|
47
|
+
self.preview_kwargs = preview_kwargs or {}
|
46
48
|
|
47
49
|
async def _run(self, *args, **kwargs) -> Any:
|
48
50
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
@@ -58,10 +60,20 @@ class ActionFactoryAction(BaseAction):
|
|
58
60
|
generated_action = self.factory(*args, **updated_kwargs)
|
59
61
|
if not isinstance(generated_action, BaseAction):
|
60
62
|
raise TypeError(
|
61
|
-
f"[{self.name}] Factory must return a BaseAction, got
|
63
|
+
f"[{self.name}] Factory must return a BaseAction, got "
|
64
|
+
f"{type(generated_action).__name__}"
|
62
65
|
)
|
63
66
|
if self.shared_context:
|
64
67
|
generated_action.set_shared_context(self.shared_context)
|
68
|
+
if hasattr(generated_action, "register_teardown") and callable(
|
69
|
+
generated_action.register_teardown
|
70
|
+
):
|
71
|
+
generated_action.register_teardown(self.shared_context.action.hooks)
|
72
|
+
logger.debug(
|
73
|
+
"[%s] Registered teardown for %s",
|
74
|
+
self.name,
|
75
|
+
generated_action.name,
|
76
|
+
)
|
65
77
|
if self.options_manager:
|
66
78
|
generated_action.set_options_manager(self.options_manager)
|
67
79
|
context.result = await generated_action(*args, **kwargs)
|
@@ -13,11 +13,11 @@ from typing import Any
|
|
13
13
|
import aiohttp
|
14
14
|
from rich.tree import Tree
|
15
15
|
|
16
|
-
from falyx.action import Action
|
16
|
+
from falyx.action.action import Action
|
17
17
|
from falyx.context import ExecutionContext, SharedContext
|
18
18
|
from falyx.hook_manager import HookManager, HookType
|
19
|
-
from falyx.
|
20
|
-
from falyx.
|
19
|
+
from falyx.logger import logger
|
20
|
+
from falyx.themes import OneColors
|
21
21
|
|
22
22
|
|
23
23
|
async def close_shared_http_session(context: ExecutionContext) -> None:
|
@@ -35,9 +35,9 @@ class HTTPAction(Action):
|
|
35
35
|
"""
|
36
36
|
An Action for executing HTTP requests using aiohttp with shared session reuse.
|
37
37
|
|
38
|
-
This action integrates seamlessly into Falyx pipelines, with automatic session
|
39
|
-
result injection, and lifecycle hook support. It is ideal for CLI-driven
|
40
|
-
where you need to call remote services and process their responses.
|
38
|
+
This action integrates seamlessly into Falyx pipelines, with automatic session
|
39
|
+
management, result injection, and lifecycle hook support. It is ideal for CLI-driven
|
40
|
+
API workflows where you need to call remote services and process their responses.
|
41
41
|
|
42
42
|
Features:
|
43
43
|
- Uses aiohttp for asynchronous HTTP requests
|
@@ -97,7 +97,7 @@ class HTTPAction(Action):
|
|
97
97
|
retry_policy=retry_policy,
|
98
98
|
)
|
99
99
|
|
100
|
-
async def _request(self, *
|
100
|
+
async def _request(self, *_, **__) -> dict[str, Any]:
|
101
101
|
if self.shared_context:
|
102
102
|
context: SharedContext = self.shared_context
|
103
103
|
session = context.get("http_session")
|
@@ -153,6 +153,7 @@ class HTTPAction(Action):
|
|
153
153
|
def __str__(self):
|
154
154
|
return (
|
155
155
|
f"HTTPAction(name={self.name!r}, method={self.method!r}, url={self.url!r}, "
|
156
|
-
f"headers={self.headers!r}, params={self.params!r}, json={self.json!r},
|
157
|
-
f"
|
156
|
+
f"headers={self.headers!r}, params={self.params!r}, json={self.json!r}, "
|
157
|
+
f"data={self.data!r}, retry={self.retry_policy.enabled}, "
|
158
|
+
f"inject_last_result={self.inject_last_result})"
|
158
159
|
)
|
@@ -23,13 +23,13 @@ from typing import Any
|
|
23
23
|
|
24
24
|
from rich.tree import Tree
|
25
25
|
|
26
|
-
from falyx.action import BaseAction
|
26
|
+
from falyx.action.action import BaseAction
|
27
27
|
from falyx.context import ExecutionContext
|
28
28
|
from falyx.exceptions import FalyxError
|
29
29
|
from falyx.execution_registry import ExecutionRegistry as er
|
30
30
|
from falyx.hook_manager import HookManager, HookType
|
31
|
-
from falyx.
|
32
|
-
from falyx.
|
31
|
+
from falyx.logger import logger
|
32
|
+
from falyx.themes import OneColors
|
33
33
|
|
34
34
|
|
35
35
|
class BaseIOAction(BaseAction):
|
@@ -78,7 +78,7 @@ class BaseIOAction(BaseAction):
|
|
78
78
|
def from_input(self, raw: str | bytes) -> Any:
|
79
79
|
raise NotImplementedError
|
80
80
|
|
81
|
-
def to_output(self,
|
81
|
+
def to_output(self, result: Any) -> str | bytes:
|
82
82
|
raise NotImplementedError
|
83
83
|
|
84
84
|
async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes:
|
@@ -113,7 +113,7 @@ class BaseIOAction(BaseAction):
|
|
113
113
|
try:
|
114
114
|
if self.mode == "stream":
|
115
115
|
line_gen = await self._read_stdin_stream()
|
116
|
-
async for
|
116
|
+
async for _ in self._stream_lines(line_gen, args, kwargs):
|
117
117
|
pass
|
118
118
|
result = getattr(self, "_last_result", None)
|
119
119
|
else:
|
@@ -185,8 +185,9 @@ class ShellAction(BaseIOAction):
|
|
185
185
|
Designed for quick integration with shell tools like `grep`, `ping`, `jq`, etc.
|
186
186
|
|
187
187
|
⚠️ Security Warning:
|
188
|
-
By default, ShellAction uses `shell=True`, which can be dangerous with
|
189
|
-
To mitigate this, set `safe_mode=True` to use `shell=False`
|
188
|
+
By default, ShellAction uses `shell=True`, which can be dangerous with
|
189
|
+
unsanitized input. To mitigate this, set `safe_mode=True` to use `shell=False`
|
190
|
+
with `shlex.split()`.
|
190
191
|
|
191
192
|
Features:
|
192
193
|
- Automatically handles input parsing (str/bytes)
|
@@ -198,9 +199,11 @@ class ShellAction(BaseIOAction):
|
|
198
199
|
|
199
200
|
Args:
|
200
201
|
name (str): Name of the action.
|
201
|
-
command_template (str): Shell command to execute. Must include `{}` to include
|
202
|
-
If no placeholder is present, the input is not
|
203
|
-
|
202
|
+
command_template (str): Shell command to execute. Must include `{}` to include
|
203
|
+
input. If no placeholder is present, the input is not
|
204
|
+
included.
|
205
|
+
safe_mode (bool): If True, runs with `shell=False` using shlex parsing
|
206
|
+
(default: False).
|
204
207
|
"""
|
205
208
|
|
206
209
|
def __init__(
|
@@ -222,9 +225,11 @@ class ShellAction(BaseIOAction):
|
|
222
225
|
command = self.command_template.format(parsed_input)
|
223
226
|
if self.safe_mode:
|
224
227
|
args = shlex.split(command)
|
225
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
228
|
+
result = subprocess.run(args, capture_output=True, text=True, check=True)
|
226
229
|
else:
|
227
|
-
result = subprocess.run(
|
230
|
+
result = subprocess.run(
|
231
|
+
command, shell=True, text=True, capture_output=True, check=True
|
232
|
+
)
|
228
233
|
if result.returncode != 0:
|
229
234
|
raise RuntimeError(result.stderr.strip())
|
230
235
|
return result.stdout.strip()
|
@@ -246,6 +251,6 @@ class ShellAction(BaseIOAction):
|
|
246
251
|
|
247
252
|
def __str__(self):
|
248
253
|
return (
|
249
|
-
f"ShellAction(name={self.name!r}, command_template={self.command_template!r},
|
250
|
-
f"safe_mode={self.safe_mode})"
|
254
|
+
f"ShellAction(name={self.name!r}, command_template={self.command_template!r},"
|
255
|
+
f" safe_mode={self.safe_mode})"
|
251
256
|
)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""menu_action.py"""
|
3
|
-
from dataclasses import dataclass
|
4
3
|
from typing import Any
|
5
4
|
|
6
5
|
from prompt_toolkit import PromptSession
|
@@ -8,91 +7,21 @@ from rich.console import Console
|
|
8
7
|
from rich.table import Table
|
9
8
|
from rich.tree import Tree
|
10
9
|
|
11
|
-
from falyx.action import BaseAction
|
10
|
+
from falyx.action.action import BaseAction
|
12
11
|
from falyx.context import ExecutionContext
|
13
12
|
from falyx.execution_registry import ExecutionRegistry as er
|
14
13
|
from falyx.hook_manager import HookType
|
14
|
+
from falyx.logger import logger
|
15
|
+
from falyx.menu import MenuOptionMap
|
15
16
|
from falyx.selection import prompt_for_selection, render_table_base
|
16
|
-
from falyx.signal_action import SignalAction
|
17
17
|
from falyx.signals import BackSignal, QuitSignal
|
18
|
-
from falyx.themes
|
19
|
-
from falyx.utils import
|
20
|
-
|
21
|
-
|
22
|
-
@dataclass
|
23
|
-
class MenuOption:
|
24
|
-
description: str
|
25
|
-
action: BaseAction
|
26
|
-
style: str = OneColors.WHITE
|
27
|
-
|
28
|
-
def __post_init__(self):
|
29
|
-
if not isinstance(self.description, str):
|
30
|
-
raise TypeError("MenuOption description must be a string.")
|
31
|
-
if not isinstance(self.action, BaseAction):
|
32
|
-
raise TypeError("MenuOption action must be a BaseAction instance.")
|
33
|
-
|
34
|
-
def render(self, key: str) -> str:
|
35
|
-
"""Render the menu option for display."""
|
36
|
-
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
37
|
-
|
38
|
-
|
39
|
-
class MenuOptionMap(CaseInsensitiveDict):
|
40
|
-
"""
|
41
|
-
Manages menu options including validation, reserved key protection,
|
42
|
-
and special signal entries like Quit and Back.
|
43
|
-
"""
|
44
|
-
|
45
|
-
RESERVED_KEYS = {"Q", "B"}
|
46
|
-
|
47
|
-
def __init__(
|
48
|
-
self,
|
49
|
-
options: dict[str, MenuOption] | None = None,
|
50
|
-
allow_reserved: bool = False,
|
51
|
-
):
|
52
|
-
super().__init__()
|
53
|
-
self.allow_reserved = allow_reserved
|
54
|
-
if options:
|
55
|
-
self.update(options)
|
56
|
-
self._inject_reserved_defaults()
|
57
|
-
|
58
|
-
def _inject_reserved_defaults(self):
|
59
|
-
self._add_reserved(
|
60
|
-
"Q",
|
61
|
-
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
62
|
-
)
|
63
|
-
self._add_reserved(
|
64
|
-
"B",
|
65
|
-
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
66
|
-
)
|
67
|
-
|
68
|
-
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
69
|
-
"""Add a reserved key, bypassing validation."""
|
70
|
-
norm_key = key.upper()
|
71
|
-
super().__setitem__(norm_key, option)
|
72
|
-
|
73
|
-
def __setitem__(self, key: str, option: MenuOption) -> None:
|
74
|
-
if not isinstance(option, MenuOption):
|
75
|
-
raise TypeError(f"Value for key '{key}' must be a MenuOption.")
|
76
|
-
norm_key = key.upper()
|
77
|
-
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
78
|
-
raise ValueError(
|
79
|
-
f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
|
80
|
-
)
|
81
|
-
super().__setitem__(norm_key, option)
|
82
|
-
|
83
|
-
def __delitem__(self, key: str) -> None:
|
84
|
-
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
85
|
-
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
86
|
-
super().__delitem__(key)
|
87
|
-
|
88
|
-
def items(self, include_reserved: bool = True):
|
89
|
-
for k, v in super().items():
|
90
|
-
if not include_reserved and k in self.RESERVED_KEYS:
|
91
|
-
continue
|
92
|
-
yield k, v
|
18
|
+
from falyx.themes import OneColors
|
19
|
+
from falyx.utils import chunks
|
93
20
|
|
94
21
|
|
95
22
|
class MenuAction(BaseAction):
|
23
|
+
"""MenuAction class for creating single use menu actions."""
|
24
|
+
|
96
25
|
def __init__(
|
97
26
|
self,
|
98
27
|
name: str,
|
@@ -162,7 +91,8 @@ class MenuAction(BaseAction):
|
|
162
91
|
|
163
92
|
if self.never_prompt and not effective_default:
|
164
93
|
raise ValueError(
|
165
|
-
f"[{self.name}] 'never_prompt' is True but no valid default_selection
|
94
|
+
f"[{self.name}] 'never_prompt' is True but no valid default_selection"
|
95
|
+
" was provided."
|
166
96
|
)
|
167
97
|
|
168
98
|
context.start_timer()
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""select_file_action.py"""
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
import csv
|
5
6
|
import json
|
6
7
|
import xml.etree.ElementTree as ET
|
7
|
-
from enum import Enum
|
8
8
|
from pathlib import Path
|
9
9
|
from typing import Any
|
10
10
|
|
@@ -14,49 +14,18 @@ from prompt_toolkit import PromptSession
|
|
14
14
|
from rich.console import Console
|
15
15
|
from rich.tree import Tree
|
16
16
|
|
17
|
-
from falyx.action import BaseAction
|
17
|
+
from falyx.action.action import BaseAction
|
18
|
+
from falyx.action.types import FileReturnType
|
18
19
|
from falyx.context import ExecutionContext
|
19
20
|
from falyx.execution_registry import ExecutionRegistry as er
|
20
21
|
from falyx.hook_manager import HookType
|
22
|
+
from falyx.logger import logger
|
21
23
|
from falyx.selection import (
|
22
24
|
SelectionOption,
|
23
25
|
prompt_for_selection,
|
24
26
|
render_selection_dict_table,
|
25
27
|
)
|
26
|
-
from falyx.themes
|
27
|
-
from falyx.utils import logger
|
28
|
-
|
29
|
-
|
30
|
-
class FileReturnType(Enum):
|
31
|
-
TEXT = "text"
|
32
|
-
PATH = "path"
|
33
|
-
JSON = "json"
|
34
|
-
TOML = "toml"
|
35
|
-
YAML = "yaml"
|
36
|
-
CSV = "csv"
|
37
|
-
TSV = "tsv"
|
38
|
-
XML = "xml"
|
39
|
-
|
40
|
-
@classmethod
|
41
|
-
def _get_alias(cls, value: str) -> str:
|
42
|
-
aliases = {
|
43
|
-
"yml": "yaml",
|
44
|
-
"txt": "text",
|
45
|
-
"file": "path",
|
46
|
-
"filepath": "path",
|
47
|
-
}
|
48
|
-
return aliases.get(value, value)
|
49
|
-
|
50
|
-
@classmethod
|
51
|
-
def _missing_(cls, value: object) -> FileReturnType:
|
52
|
-
if isinstance(value, str):
|
53
|
-
normalized = value.lower()
|
54
|
-
alias = cls._get_alias(normalized)
|
55
|
-
for member in cls:
|
56
|
-
if member.value == alias:
|
57
|
-
return member
|
58
|
-
valid = ", ".join(member.value for member in cls)
|
59
|
-
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
28
|
+
from falyx.themes import OneColors
|
60
29
|
|
61
30
|
|
62
31
|
class SelectFileAction(BaseAction):
|