falyx 0.1.52__py3-none-any.whl → 0.1.54__py3-none-any.whl
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/action/__init__.py +5 -3
- falyx/action/action.py +8 -8
- falyx/action/action_factory.py +3 -3
- falyx/action/action_group.py +17 -6
- falyx/action/{mixins.py → action_mixins.py} +5 -3
- falyx/action/{types.py → action_types.py} +3 -3
- falyx/action/{base.py → base_action.py} +1 -2
- falyx/action/chained_action.py +15 -6
- falyx/action/io_action.py +1 -1
- falyx/action/load_file_action.py +196 -0
- falyx/action/menu_action.py +1 -1
- falyx/action/process_action.py +1 -1
- falyx/action/process_pool_action.py +7 -4
- falyx/action/prompt_menu_action.py +1 -1
- falyx/action/save_file_action.py +44 -0
- falyx/action/select_file_action.py +18 -15
- falyx/action/selection_action.py +2 -2
- falyx/action/user_input_action.py +1 -1
- falyx/command.py +3 -3
- falyx/config.py +1 -1
- falyx/exceptions.py +8 -0
- falyx/falyx.py +2 -3
- falyx/logger.py +1 -1
- falyx/menu.py +1 -1
- falyx/parser/argument.py +3 -1
- falyx/parser/command_argument_parser.py +171 -153
- falyx/parser/parsers.py +15 -7
- falyx/parser/utils.py +1 -2
- falyx/protocols.py +5 -3
- falyx/retry_utils.py +1 -1
- falyx/version.py +1 -1
- {falyx-0.1.52.dist-info → falyx-0.1.54.dist-info}/METADATA +1 -1
- falyx-0.1.54.dist-info/RECORD +66 -0
- falyx-0.1.52.dist-info/RECORD +0 -64
- {falyx-0.1.52.dist-info → falyx-0.1.54.dist-info}/LICENSE +0 -0
- {falyx-0.1.52.dist-info → falyx-0.1.54.dist-info}/WHEEL +0 -0
- {falyx-0.1.52.dist-info → falyx-0.1.54.dist-info}/entry_points.txt +0 -0
falyx/action/__init__.py
CHANGED
@@ -6,14 +6,15 @@ Licensed under the MIT License. See LICENSE file for details.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from .action import Action
|
9
|
-
from .action_factory import
|
9
|
+
from .action_factory import ActionFactory
|
10
10
|
from .action_group import ActionGroup
|
11
|
-
from .
|
11
|
+
from .base_action import BaseAction
|
12
12
|
from .chained_action import ChainedAction
|
13
13
|
from .fallback_action import FallbackAction
|
14
14
|
from .http_action import HTTPAction
|
15
15
|
from .io_action import BaseIOAction
|
16
16
|
from .literal_input_action import LiteralInputAction
|
17
|
+
from .load_file_action import LoadFileAction
|
17
18
|
from .menu_action import MenuAction
|
18
19
|
from .process_action import ProcessAction
|
19
20
|
from .process_pool_action import ProcessPoolAction
|
@@ -30,7 +31,7 @@ __all__ = [
|
|
30
31
|
"BaseAction",
|
31
32
|
"ChainedAction",
|
32
33
|
"ProcessAction",
|
33
|
-
"
|
34
|
+
"ActionFactory",
|
34
35
|
"HTTPAction",
|
35
36
|
"BaseIOAction",
|
36
37
|
"ShellAction",
|
@@ -43,4 +44,5 @@ __all__ = [
|
|
43
44
|
"UserInputAction",
|
44
45
|
"PromptMenuAction",
|
45
46
|
"ProcessPoolAction",
|
47
|
+
"LoadFileAction",
|
46
48
|
]
|
falyx/action/action.py
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
"""action.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any, Callable
|
5
|
+
from typing import Any, Awaitable, Callable
|
6
6
|
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
|
-
from falyx.action.
|
9
|
+
from falyx.action.base_action import BaseAction
|
10
10
|
from falyx.context import ExecutionContext
|
11
11
|
from falyx.execution_registry import ExecutionRegistry as er
|
12
12
|
from falyx.hook_manager import HookManager, HookType
|
@@ -42,9 +42,9 @@ class Action(BaseAction):
|
|
42
42
|
def __init__(
|
43
43
|
self,
|
44
44
|
name: str,
|
45
|
-
action: Callable[..., Any],
|
45
|
+
action: Callable[..., Any] | Callable[..., Awaitable[Any]],
|
46
46
|
*,
|
47
|
-
rollback: Callable[..., Any] | None = None,
|
47
|
+
rollback: Callable[..., Any] | Callable[..., Awaitable[Any]] | None = None,
|
48
48
|
args: tuple[Any, ...] = (),
|
49
49
|
kwargs: dict[str, Any] | None = None,
|
50
50
|
hooks: HookManager | None = None,
|
@@ -69,19 +69,19 @@ class Action(BaseAction):
|
|
69
69
|
self.enable_retry()
|
70
70
|
|
71
71
|
@property
|
72
|
-
def action(self) -> Callable[..., Any]:
|
72
|
+
def action(self) -> Callable[..., Awaitable[Any]]:
|
73
73
|
return self._action
|
74
74
|
|
75
75
|
@action.setter
|
76
|
-
def action(self, value: Callable[..., Any]):
|
76
|
+
def action(self, value: Callable[..., Awaitable[Any]]):
|
77
77
|
self._action = ensure_async(value)
|
78
78
|
|
79
79
|
@property
|
80
|
-
def rollback(self) -> Callable[..., Any] | None:
|
80
|
+
def rollback(self) -> Callable[..., Awaitable[Any]] | None:
|
81
81
|
return self._rollback
|
82
82
|
|
83
83
|
@rollback.setter
|
84
|
-
def rollback(self, value: Callable[..., Any] | None):
|
84
|
+
def rollback(self, value: Callable[..., Awaitable[Any]] | None):
|
85
85
|
if value is None:
|
86
86
|
self._rollback = None
|
87
87
|
else:
|
falyx/action/action_factory.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""
|
2
|
+
"""action_factory_action.py"""
|
3
3
|
from typing import Any, Callable
|
4
4
|
|
5
5
|
from rich.tree import Tree
|
6
6
|
|
7
|
-
from falyx.action.
|
7
|
+
from falyx.action.base_action 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
|
@@ -14,7 +14,7 @@ from falyx.themes import OneColors
|
|
14
14
|
from falyx.utils import ensure_async
|
15
15
|
|
16
16
|
|
17
|
-
class
|
17
|
+
class ActionFactory(BaseAction):
|
18
18
|
"""
|
19
19
|
Dynamically creates and runs another Action at runtime using a factory function.
|
20
20
|
|
falyx/action/action_group.py
CHANGED
@@ -2,14 +2,15 @@
|
|
2
2
|
"""action_group.py"""
|
3
3
|
import asyncio
|
4
4
|
import random
|
5
|
-
from typing import Any, Callable
|
5
|
+
from typing import Any, Awaitable, Callable, Sequence
|
6
6
|
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
9
|
from falyx.action.action import Action
|
10
|
-
from falyx.action.
|
11
|
-
from falyx.action.
|
10
|
+
from falyx.action.action_mixins import ActionListMixin
|
11
|
+
from falyx.action.base_action import BaseAction
|
12
12
|
from falyx.context import ExecutionContext, SharedContext
|
13
|
+
from falyx.exceptions import EmptyGroupError
|
13
14
|
from falyx.execution_registry import ExecutionRegistry as er
|
14
15
|
from falyx.hook_manager import Hook, HookManager, HookType
|
15
16
|
from falyx.logger import logger
|
@@ -54,7 +55,9 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
54
55
|
def __init__(
|
55
56
|
self,
|
56
57
|
name: str,
|
57
|
-
actions:
|
58
|
+
actions: (
|
59
|
+
Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable]] | None
|
60
|
+
) = None,
|
58
61
|
*,
|
59
62
|
hooks: HookManager | None = None,
|
60
63
|
inject_last_result: bool = False,
|
@@ -70,7 +73,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
70
73
|
if actions:
|
71
74
|
self.set_actions(actions)
|
72
75
|
|
73
|
-
def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
|
76
|
+
def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
|
74
77
|
if isinstance(action, BaseAction):
|
75
78
|
return action
|
76
79
|
elif callable(action):
|
@@ -81,12 +84,18 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
81
84
|
f"{type(action).__name__}"
|
82
85
|
)
|
83
86
|
|
84
|
-
def add_action(self, action: BaseAction | Any) -> None:
|
87
|
+
def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
|
85
88
|
action = self._wrap_if_needed(action)
|
86
89
|
super().add_action(action)
|
87
90
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
88
91
|
action.register_teardown(self.hooks)
|
89
92
|
|
93
|
+
def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
|
94
|
+
"""Replaces the current action list with a new one."""
|
95
|
+
self.actions.clear()
|
96
|
+
for action in actions:
|
97
|
+
self.add_action(action)
|
98
|
+
|
90
99
|
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
91
100
|
arg_defs = same_argument_definitions(self.actions)
|
92
101
|
if arg_defs:
|
@@ -98,6 +107,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
98
107
|
return None, None
|
99
108
|
|
100
109
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
110
|
+
if not self.actions:
|
111
|
+
raise EmptyGroupError(f"[{self.name}] No actions to execute.")
|
101
112
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
102
113
|
if self.shared_context:
|
103
114
|
shared_context.set_shared_result(self.shared_context.last_result())
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""
|
3
|
-
from
|
2
|
+
"""action_mixins.py"""
|
3
|
+
from typing import Sequence
|
4
|
+
|
5
|
+
from falyx.action.base_action import BaseAction
|
4
6
|
|
5
7
|
|
6
8
|
class ActionListMixin:
|
@@ -9,7 +11,7 @@ class ActionListMixin:
|
|
9
11
|
def __init__(self) -> None:
|
10
12
|
self.actions: list[BaseAction] = []
|
11
13
|
|
12
|
-
def set_actions(self, actions:
|
14
|
+
def set_actions(self, actions: Sequence[BaseAction]) -> None:
|
13
15
|
"""Replaces the current action list with a new one."""
|
14
16
|
self.actions.clear()
|
15
17
|
for action in actions:
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from enum import Enum
|
6
6
|
|
7
7
|
|
8
|
-
class
|
8
|
+
class FileType(Enum):
|
9
9
|
"""Enum for file return types."""
|
10
10
|
|
11
11
|
TEXT = "text"
|
@@ -28,7 +28,7 @@ class FileReturnType(Enum):
|
|
28
28
|
return aliases.get(value, value)
|
29
29
|
|
30
30
|
@classmethod
|
31
|
-
def _missing_(cls, value: object) ->
|
31
|
+
def _missing_(cls, value: object) -> FileType:
|
32
32
|
if isinstance(value, str):
|
33
33
|
normalized = value.lower()
|
34
34
|
alias = cls._get_alias(normalized)
|
@@ -36,7 +36,7 @@ class FileReturnType(Enum):
|
|
36
36
|
if member.value == alias:
|
37
37
|
return member
|
38
38
|
valid = ", ".join(member.value for member in cls)
|
39
|
-
raise ValueError(f"Invalid
|
39
|
+
raise ValueError(f"Invalid FileType: '{value}'. Must be one of: {valid}")
|
40
40
|
|
41
41
|
|
42
42
|
class SelectionReturnType(Enum):
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""
|
2
|
+
"""base_action.py
|
3
3
|
|
4
4
|
Core action system for Falyx.
|
5
5
|
|
@@ -38,7 +38,6 @@ from rich.tree import Tree
|
|
38
38
|
|
39
39
|
from falyx.context import SharedContext
|
40
40
|
from falyx.debug import register_debug_hooks
|
41
|
-
from falyx.execution_registry import ExecutionRegistry as er
|
42
41
|
from falyx.hook_manager import Hook, HookManager, HookType
|
43
42
|
from falyx.logger import logger
|
44
43
|
from falyx.options_manager import OptionsManager
|
falyx/action/chained_action.py
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
"""chained_action.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any, Callable
|
5
|
+
from typing import Any, Awaitable, Callable, Sequence
|
6
6
|
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
9
|
from falyx.action.action import Action
|
10
|
-
from falyx.action.
|
10
|
+
from falyx.action.action_mixins import ActionListMixin
|
11
|
+
from falyx.action.base_action import BaseAction
|
11
12
|
from falyx.action.fallback_action import FallbackAction
|
12
13
|
from falyx.action.literal_input_action import LiteralInputAction
|
13
|
-
from falyx.action.mixins import ActionListMixin
|
14
14
|
from falyx.context import ExecutionContext, SharedContext
|
15
15
|
from falyx.exceptions import EmptyChainError
|
16
16
|
from falyx.execution_registry import ExecutionRegistry as er
|
@@ -47,7 +47,10 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
47
47
|
def __init__(
|
48
48
|
self,
|
49
49
|
name: str,
|
50
|
-
actions:
|
50
|
+
actions: (
|
51
|
+
Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable[Any]]]
|
52
|
+
| None
|
53
|
+
) = None,
|
51
54
|
*,
|
52
55
|
hooks: HookManager | None = None,
|
53
56
|
inject_last_result: bool = False,
|
@@ -67,7 +70,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
67
70
|
if actions:
|
68
71
|
self.set_actions(actions)
|
69
72
|
|
70
|
-
def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
|
73
|
+
def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
|
71
74
|
if isinstance(action, BaseAction):
|
72
75
|
return action
|
73
76
|
elif callable(action):
|
@@ -75,7 +78,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
75
78
|
else:
|
76
79
|
return LiteralInputAction(action)
|
77
80
|
|
78
|
-
def add_action(self, action: BaseAction | Any) -> None:
|
81
|
+
def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
|
79
82
|
action = self._wrap_if_needed(action)
|
80
83
|
if self.actions and self.auto_inject and not action.inject_last_result:
|
81
84
|
action.inject_last_result = True
|
@@ -83,6 +86,12 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
83
86
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
84
87
|
action.register_teardown(self.hooks)
|
85
88
|
|
89
|
+
def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
|
90
|
+
"""Replaces the current action list with a new one."""
|
91
|
+
self.actions.clear()
|
92
|
+
for action in actions:
|
93
|
+
self.add_action(action)
|
94
|
+
|
86
95
|
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
87
96
|
if self.actions:
|
88
97
|
return self.actions[0].get_infer_target()
|
falyx/action/io_action.py
CHANGED
@@ -21,7 +21,7 @@ from typing import Any, Callable
|
|
21
21
|
|
22
22
|
from rich.tree import Tree
|
23
23
|
|
24
|
-
from falyx.action.
|
24
|
+
from falyx.action.base_action import BaseAction
|
25
25
|
from falyx.context import ExecutionContext
|
26
26
|
from falyx.execution_registry import ExecutionRegistry as er
|
27
27
|
from falyx.hook_manager import HookManager, HookType
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""load_file_action.py"""
|
3
|
+
import csv
|
4
|
+
import json
|
5
|
+
import xml.etree.ElementTree as ET
|
6
|
+
from datetime import datetime
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
import toml
|
11
|
+
import yaml
|
12
|
+
from rich.tree import Tree
|
13
|
+
|
14
|
+
from falyx.action.action_types import FileType
|
15
|
+
from falyx.action.base_action import BaseAction
|
16
|
+
from falyx.context import ExecutionContext
|
17
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
18
|
+
from falyx.hook_manager import HookType
|
19
|
+
from falyx.logger import logger
|
20
|
+
from falyx.themes import OneColors
|
21
|
+
|
22
|
+
|
23
|
+
class LoadFileAction(BaseAction):
|
24
|
+
"""LoadFileAction allows loading and parsing files of various types."""
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
name: str,
|
29
|
+
file_path: str | Path | None = None,
|
30
|
+
file_type: FileType | str = FileType.TEXT,
|
31
|
+
inject_last_result: bool = False,
|
32
|
+
inject_into: str = "file_path",
|
33
|
+
):
|
34
|
+
super().__init__(
|
35
|
+
name=name, inject_last_result=inject_last_result, inject_into=inject_into
|
36
|
+
)
|
37
|
+
self._file_path = self._coerce_file_path(file_path)
|
38
|
+
self._file_type = self._coerce_file_type(file_type)
|
39
|
+
|
40
|
+
@property
|
41
|
+
def file_path(self) -> Path | None:
|
42
|
+
"""Get the file path as a Path object."""
|
43
|
+
return self._file_path
|
44
|
+
|
45
|
+
@file_path.setter
|
46
|
+
def file_path(self, value: str | Path):
|
47
|
+
"""Set the file path, converting to Path if necessary."""
|
48
|
+
self._file_path = self._coerce_file_path(value)
|
49
|
+
|
50
|
+
def _coerce_file_path(self, file_path: str | Path | None) -> Path | None:
|
51
|
+
"""Coerce the file path to a Path object."""
|
52
|
+
if isinstance(file_path, Path):
|
53
|
+
return file_path
|
54
|
+
elif isinstance(file_path, str):
|
55
|
+
return Path(file_path)
|
56
|
+
elif file_path is None:
|
57
|
+
return None
|
58
|
+
else:
|
59
|
+
raise TypeError("file_path must be a string or Path object")
|
60
|
+
|
61
|
+
@property
|
62
|
+
def file_type(self) -> FileType:
|
63
|
+
"""Get the file type."""
|
64
|
+
return self._file_type
|
65
|
+
|
66
|
+
@file_type.setter
|
67
|
+
def file_type(self, value: FileType | str):
|
68
|
+
"""Set the file type, converting to FileType if necessary."""
|
69
|
+
self._file_type = self._coerce_file_type(value)
|
70
|
+
|
71
|
+
def _coerce_file_type(self, file_type: FileType | str) -> FileType:
|
72
|
+
"""Coerce the file type to a FileType enum."""
|
73
|
+
if isinstance(file_type, FileType):
|
74
|
+
return file_type
|
75
|
+
elif isinstance(file_type, str):
|
76
|
+
return FileType(file_type)
|
77
|
+
else:
|
78
|
+
raise TypeError("file_type must be a FileType enum or string")
|
79
|
+
|
80
|
+
def get_infer_target(self) -> tuple[None, None]:
|
81
|
+
return None, None
|
82
|
+
|
83
|
+
def load_file(self) -> Any:
|
84
|
+
if self.file_path is None:
|
85
|
+
raise ValueError("file_path must be set before loading a file")
|
86
|
+
value: Any = None
|
87
|
+
try:
|
88
|
+
if self.file_type == FileType.TEXT:
|
89
|
+
value = self.file_path.read_text(encoding="UTF-8")
|
90
|
+
elif self.file_type == FileType.PATH:
|
91
|
+
value = self.file_path
|
92
|
+
elif self.file_type == FileType.JSON:
|
93
|
+
value = json.loads(self.file_path.read_text(encoding="UTF-8"))
|
94
|
+
elif self.file_type == FileType.TOML:
|
95
|
+
value = toml.loads(self.file_path.read_text(encoding="UTF-8"))
|
96
|
+
elif self.file_type == FileType.YAML:
|
97
|
+
value = yaml.safe_load(self.file_path.read_text(encoding="UTF-8"))
|
98
|
+
elif self.file_type == FileType.CSV:
|
99
|
+
with open(self.file_path, newline="", encoding="UTF-8") as csvfile:
|
100
|
+
reader = csv.reader(csvfile)
|
101
|
+
value = list(reader)
|
102
|
+
elif self.file_type == FileType.TSV:
|
103
|
+
with open(self.file_path, newline="", encoding="UTF-8") as tsvfile:
|
104
|
+
reader = csv.reader(tsvfile, delimiter="\t")
|
105
|
+
value = list(reader)
|
106
|
+
elif self.file_type == FileType.XML:
|
107
|
+
tree = ET.parse(self.file_path, parser=ET.XMLParser(encoding="UTF-8"))
|
108
|
+
root = tree.getroot()
|
109
|
+
value = ET.tostring(root, encoding="unicode")
|
110
|
+
else:
|
111
|
+
raise ValueError(f"Unsupported return type: {self.file_type}")
|
112
|
+
|
113
|
+
except Exception as error:
|
114
|
+
logger.error("Failed to parse %s: %s", self.file_path.name, error)
|
115
|
+
return value
|
116
|
+
|
117
|
+
async def _run(self, *args, **kwargs) -> Any:
|
118
|
+
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
119
|
+
context.start_timer()
|
120
|
+
try:
|
121
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
122
|
+
|
123
|
+
if "file_path" in kwargs:
|
124
|
+
self.file_path = kwargs["file_path"]
|
125
|
+
elif self.inject_last_result and self.last_result:
|
126
|
+
self.file_path = self.last_result
|
127
|
+
|
128
|
+
if self.file_path is None:
|
129
|
+
raise ValueError("file_path must be set before loading a file")
|
130
|
+
elif not self.file_path.exists():
|
131
|
+
raise FileNotFoundError(f"File not found: {self.file_path}")
|
132
|
+
elif not self.file_path.is_file():
|
133
|
+
raise ValueError(f"Path is not a regular file: {self.file_path}")
|
134
|
+
|
135
|
+
result = self.load_file()
|
136
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
137
|
+
return result
|
138
|
+
except Exception as error:
|
139
|
+
context.exception = error
|
140
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
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
|
+
async def preview(self, parent: Tree | None = None):
|
149
|
+
label = f"[{OneColors.GREEN}]📄 LoadFileAction[/] '{self.name}'"
|
150
|
+
tree = parent.add(label) if parent else Tree(label)
|
151
|
+
|
152
|
+
tree.add(f"[dim]Path:[/] {self.file_path}")
|
153
|
+
tree.add(f"[dim]Type:[/] {self.file_type.name if self.file_type else 'None'}")
|
154
|
+
if self.file_path is None:
|
155
|
+
tree.add(f"[{OneColors.DARK_RED_b}]❌ File path is not set[/]")
|
156
|
+
elif not self.file_path.exists():
|
157
|
+
tree.add(f"[{OneColors.DARK_RED_b}]❌ File does not exist[/]")
|
158
|
+
elif not self.file_path.is_file():
|
159
|
+
tree.add(f"[{OneColors.LIGHT_YELLOW_b}]⚠️ Not a regular file[/]")
|
160
|
+
else:
|
161
|
+
try:
|
162
|
+
stat = self.file_path.stat()
|
163
|
+
tree.add(f"[dim]Size:[/] {stat.st_size:,} bytes")
|
164
|
+
tree.add(
|
165
|
+
f"[dim]Modified:[/] {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}"
|
166
|
+
)
|
167
|
+
tree.add(
|
168
|
+
f"[dim]Created:[/] {datetime.fromtimestamp(stat.st_ctime):%Y-%m-%d %H:%M:%S}"
|
169
|
+
)
|
170
|
+
if self.file_type == FileType.TEXT:
|
171
|
+
preview_lines = self.file_path.read_text(
|
172
|
+
encoding="UTF-8"
|
173
|
+
).splitlines()[:10]
|
174
|
+
content_tree = tree.add("[dim]Preview (first 10 lines):[/]")
|
175
|
+
for line in preview_lines:
|
176
|
+
content_tree.add(f"[dim]{line}[/]")
|
177
|
+
elif self.file_type in {FileType.JSON, FileType.YAML, FileType.TOML}:
|
178
|
+
raw = self.load_file()
|
179
|
+
if raw is not None:
|
180
|
+
preview_str = (
|
181
|
+
json.dumps(raw, indent=2)
|
182
|
+
if isinstance(raw, dict)
|
183
|
+
else str(raw)
|
184
|
+
)
|
185
|
+
preview_lines = preview_str.splitlines()[:10]
|
186
|
+
content_tree = tree.add("[dim]Parsed preview:[/]")
|
187
|
+
for line in preview_lines:
|
188
|
+
content_tree.add(f"[dim]{line}[/]")
|
189
|
+
except Exception as e:
|
190
|
+
tree.add(f"[{OneColors.DARK_RED_b}]❌ Error reading file:[/] {e}")
|
191
|
+
|
192
|
+
if not parent:
|
193
|
+
self.console.print(tree)
|
194
|
+
|
195
|
+
def __str__(self) -> str:
|
196
|
+
return f"LoadFileAction(file_path={self.file_path}, file_type={self.file_type})"
|
falyx/action/menu_action.py
CHANGED
@@ -7,7 +7,7 @@ from rich.console import Console
|
|
7
7
|
from rich.table import Table
|
8
8
|
from rich.tree import Tree
|
9
9
|
|
10
|
-
from falyx.action.
|
10
|
+
from falyx.action.base_action import BaseAction
|
11
11
|
from falyx.context import ExecutionContext
|
12
12
|
from falyx.execution_registry import ExecutionRegistry as er
|
13
13
|
from falyx.hook_manager import HookType
|
falyx/action/process_action.py
CHANGED
@@ -9,7 +9,7 @@ from typing import Any, Callable
|
|
9
9
|
|
10
10
|
from rich.tree import Tree
|
11
11
|
|
12
|
-
from falyx.action.
|
12
|
+
from falyx.action.base_action import BaseAction
|
13
13
|
from falyx.context import ExecutionContext
|
14
14
|
from falyx.execution_registry import ExecutionRegistry as er
|
15
15
|
from falyx.hook_manager import HookManager, HookType
|
@@ -7,12 +7,13 @@ import random
|
|
7
7
|
from concurrent.futures import ProcessPoolExecutor
|
8
8
|
from dataclasses import dataclass, field
|
9
9
|
from functools import partial
|
10
|
-
from typing import Any, Callable
|
10
|
+
from typing import Any, Callable, Sequence
|
11
11
|
|
12
12
|
from rich.tree import Tree
|
13
13
|
|
14
|
-
from falyx.action.
|
14
|
+
from falyx.action.base_action import BaseAction
|
15
15
|
from falyx.context import ExecutionContext, SharedContext
|
16
|
+
from falyx.exceptions import EmptyPoolError
|
16
17
|
from falyx.execution_registry import ExecutionRegistry as er
|
17
18
|
from falyx.hook_manager import HookManager, HookType
|
18
19
|
from falyx.logger import logger
|
@@ -37,7 +38,7 @@ class ProcessPoolAction(BaseAction):
|
|
37
38
|
def __init__(
|
38
39
|
self,
|
39
40
|
name: str,
|
40
|
-
actions:
|
41
|
+
actions: Sequence[ProcessTask] | None = None,
|
41
42
|
*,
|
42
43
|
hooks: HookManager | None = None,
|
43
44
|
executor: ProcessPoolExecutor | None = None,
|
@@ -56,7 +57,7 @@ class ProcessPoolAction(BaseAction):
|
|
56
57
|
if actions:
|
57
58
|
self.set_actions(actions)
|
58
59
|
|
59
|
-
def set_actions(self, actions:
|
60
|
+
def set_actions(self, actions: Sequence[ProcessTask]) -> None:
|
60
61
|
"""Replaces the current action list with a new one."""
|
61
62
|
self.actions.clear()
|
62
63
|
for action in actions:
|
@@ -78,6 +79,8 @@ class ProcessPoolAction(BaseAction):
|
|
78
79
|
return None, None
|
79
80
|
|
80
81
|
async def _run(self, *args, **kwargs) -> Any:
|
82
|
+
if not self.actions:
|
83
|
+
raise EmptyPoolError(f"[{self.name}] No actions to execute.")
|
81
84
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
82
85
|
if self.shared_context:
|
83
86
|
shared_context.set_shared_result(self.shared_context.last_result())
|
@@ -7,7 +7,7 @@ from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
|
|
7
7
|
from rich.console import Console
|
8
8
|
from rich.tree import Tree
|
9
9
|
|
10
|
-
from falyx.action.
|
10
|
+
from falyx.action.base_action import BaseAction
|
11
11
|
from falyx.context import ExecutionContext
|
12
12
|
from falyx.execution_registry import ExecutionRegistry as er
|
13
13
|
from falyx.hook_manager import HookType
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""save_file_action.py"""
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from rich.tree import Tree
|
6
|
+
|
7
|
+
from falyx.action.action_types import FileType
|
8
|
+
from falyx.action.base_action import BaseAction
|
9
|
+
|
10
|
+
|
11
|
+
class SaveFileAction(BaseAction):
|
12
|
+
""" """
|
13
|
+
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
name: str,
|
17
|
+
file_path: str,
|
18
|
+
input_type: str | FileType = "text",
|
19
|
+
output_type: str | FileType = "text",
|
20
|
+
):
|
21
|
+
"""
|
22
|
+
SaveFileAction allows saving data to a file.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
name (str): Name of the action.
|
26
|
+
file_path (str | Path): Path to the file where data will be saved.
|
27
|
+
input_type (str | FileType): Type of data being saved (default is "text").
|
28
|
+
output_type (str | FileType): Type of data to save to the file (default is "text").
|
29
|
+
"""
|
30
|
+
super().__init__(name=name)
|
31
|
+
self.file_path = file_path
|
32
|
+
|
33
|
+
def get_infer_target(self) -> tuple[None, None]:
|
34
|
+
return None, None
|
35
|
+
|
36
|
+
async def _run(self, *args, **kwargs):
|
37
|
+
raise NotImplementedError(
|
38
|
+
"SaveFileAction is not finished yet... Use primitives instead..."
|
39
|
+
)
|
40
|
+
|
41
|
+
async def preview(self, parent: Tree | None = None): ...
|
42
|
+
|
43
|
+
def __str__(self) -> str:
|
44
|
+
return f"SaveFileAction(file_path={self.file_path})"
|