falyx 0.1.53__tar.gz → 0.1.54__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.53 → falyx-0.1.54}/PKG-INFO +1 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/__init__.py +4 -2
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/action.py +7 -7
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/action_factory.py +2 -2
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/action_group.py +8 -3
- falyx-0.1.53/falyx/action/mixins.py → falyx-0.1.54/falyx/action/action_mixins.py +1 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/base_action.py +0 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/chained_action.py +6 -3
- falyx-0.1.54/falyx/action/load_file_action.py +196 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/process_pool_action.py +6 -3
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/save_file_action.py +17 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/select_file_action.py +4 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/command.py +2 -2
- {falyx-0.1.53 → falyx-0.1.54}/falyx/exceptions.py +8 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/logger.py +1 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/argument.py +2 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/command_argument_parser.py +170 -152
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/utils.py +0 -1
- {falyx-0.1.53 → falyx-0.1.54}/falyx/protocols.py +4 -2
- falyx-0.1.54/falyx/version.py +1 -0
- {falyx-0.1.53 → falyx-0.1.54}/pyproject.toml +5 -1
- falyx-0.1.53/falyx/action/load_file_action.py +0 -28
- falyx-0.1.53/falyx/version.py +0 -1
- {falyx-0.1.53 → falyx-0.1.54}/LICENSE +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/README.md +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/.pytyped +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/__init__.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/__main__.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/.pytyped +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/action_types.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/fallback_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/http_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/io_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/literal_input_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/menu_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/process_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/prompt_menu_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/selection_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/shell_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/signal_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/action/user_input_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/bottom_bar.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/config.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/context.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/debug.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/execution_registry.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/falyx.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/hook_manager.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/hooks.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/init.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/menu.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/options_manager.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/.pytyped +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/__init__.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/argument_action.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/parsers.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/parser/signature.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/prompt_utils.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/retry.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/retry_utils.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/selection.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/signals.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/tagged_table.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/themes/__init__.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/themes/colors.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/utils.py +0 -0
- {falyx-0.1.53 → falyx-0.1.54}/falyx/validators.py +0 -0
@@ -6,7 +6,7 @@ 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
11
|
from .base_action import BaseAction
|
12
12
|
from .chained_action import ChainedAction
|
@@ -14,6 +14,7 @@ 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
|
]
|
@@ -2,7 +2,7 @@
|
|
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
|
|
@@ -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:
|
@@ -1,5 +1,5 @@
|
|
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
|
@@ -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
|
|
@@ -2,14 +2,15 @@
|
|
2
2
|
"""action_group.py"""
|
3
3
|
import asyncio
|
4
4
|
import random
|
5
|
-
from typing import Any, Callable, Sequence
|
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.action_mixins import ActionListMixin
|
10
11
|
from falyx.action.base_action import BaseAction
|
11
|
-
from falyx.action.mixins import ActionListMixin
|
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,
|
@@ -104,6 +107,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
104
107
|
return None, None
|
105
108
|
|
106
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.")
|
107
112
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
108
113
|
if self.shared_context:
|
109
114
|
shared_context.set_shared_result(self.shared_context.last_result())
|
@@ -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
|
@@ -2,15 +2,15 @@
|
|
2
2
|
"""chained_action.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any, Callable, Sequence
|
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.action_mixins import ActionListMixin
|
10
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,
|
@@ -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})"
|
@@ -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
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())
|
@@ -4,13 +4,29 @@ from pathlib import Path
|
|
4
4
|
|
5
5
|
from rich.tree import Tree
|
6
6
|
|
7
|
+
from falyx.action.action_types import FileType
|
7
8
|
from falyx.action.base_action import BaseAction
|
8
9
|
|
9
10
|
|
10
11
|
class SaveFileAction(BaseAction):
|
11
12
|
""" """
|
12
13
|
|
13
|
-
def __init__(
|
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
|
+
"""
|
14
30
|
super().__init__(name=name)
|
15
31
|
self.file_path = file_path
|
16
32
|
|
@@ -107,7 +107,10 @@ class SelectFileAction(BaseAction):
|
|
107
107
|
def _coerce_return_type(self, return_type: FileType | str) -> FileType:
|
108
108
|
if isinstance(return_type, FileType):
|
109
109
|
return return_type
|
110
|
-
|
110
|
+
elif isinstance(return_type, str):
|
111
|
+
return FileType(return_type)
|
112
|
+
else:
|
113
|
+
raise TypeError("return_type must be a FileType enum or string")
|
111
114
|
|
112
115
|
def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
|
113
116
|
value: Any
|
@@ -19,7 +19,7 @@ in building robust interactive menus.
|
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
21
|
import shlex
|
22
|
-
from typing import Any, Callable
|
22
|
+
from typing import Any, Awaitable, Callable
|
23
23
|
|
24
24
|
from prompt_toolkit.formatted_text import FormattedText
|
25
25
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
@@ -105,7 +105,7 @@ class Command(BaseModel):
|
|
105
105
|
|
106
106
|
key: str
|
107
107
|
description: str
|
108
|
-
action: BaseAction | Callable[..., Any]
|
108
|
+
action: BaseAction | Callable[..., Any] | Callable[..., Awaitable[Any]]
|
109
109
|
args: tuple = ()
|
110
110
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
111
111
|
hidden: bool = False
|
@@ -30,5 +30,13 @@ class EmptyChainError(FalyxError):
|
|
30
30
|
"""Exception raised when the chain is empty."""
|
31
31
|
|
32
32
|
|
33
|
+
class EmptyGroupError(FalyxError):
|
34
|
+
"""Exception raised when the chain is empty."""
|
35
|
+
|
36
|
+
|
37
|
+
class EmptyPoolError(FalyxError):
|
38
|
+
"""Exception raised when the chain is empty."""
|
39
|
+
|
40
|
+
|
33
41
|
class CommandArgumentError(FalyxError):
|
34
42
|
"""Exception raised when there is an error in the command argument parser."""
|
@@ -46,6 +46,7 @@ class Argument:
|
|
46
46
|
ArgumentAction.STORE,
|
47
47
|
ArgumentAction.APPEND,
|
48
48
|
ArgumentAction.EXTEND,
|
49
|
+
ArgumentAction.ACTION,
|
49
50
|
)
|
50
51
|
and not self.positional
|
51
52
|
):
|
@@ -54,6 +55,7 @@ class Argument:
|
|
54
55
|
ArgumentAction.STORE,
|
55
56
|
ArgumentAction.APPEND,
|
56
57
|
ArgumentAction.EXTEND,
|
58
|
+
ArgumentAction.ACTION,
|
57
59
|
) or isinstance(self.nargs, str):
|
58
60
|
choice_text = self.dest
|
59
61
|
|
@@ -177,20 +177,19 @@ class CommandArgumentParser:
|
|
177
177
|
else:
|
178
178
|
choices = []
|
179
179
|
for choice in choices:
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
) from error
|
180
|
+
try:
|
181
|
+
coerce_value(choice, expected_type)
|
182
|
+
except Exception as error:
|
183
|
+
raise CommandArgumentError(
|
184
|
+
f"Invalid choice {choice!r}: not coercible to {expected_type.__name__} error: {error}"
|
185
|
+
) from error
|
187
186
|
return choices
|
188
187
|
|
189
188
|
def _validate_default_type(
|
190
189
|
self, default: Any, expected_type: type, dest: str
|
191
190
|
) -> None:
|
192
191
|
"""Validate the default value type."""
|
193
|
-
if default is not None
|
192
|
+
if default is not None:
|
194
193
|
try:
|
195
194
|
coerce_value(default, expected_type)
|
196
195
|
except Exception as error:
|
@@ -203,13 +202,12 @@ class CommandArgumentParser:
|
|
203
202
|
) -> None:
|
204
203
|
if isinstance(default, list):
|
205
204
|
for item in default:
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
) from error
|
205
|
+
try:
|
206
|
+
coerce_value(item, expected_type)
|
207
|
+
except Exception as error:
|
208
|
+
raise CommandArgumentError(
|
209
|
+
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__} error: {error}"
|
210
|
+
) from error
|
213
211
|
|
214
212
|
def _validate_resolver(
|
215
213
|
self, action: ArgumentAction, resolver: BaseAction | None
|
@@ -422,22 +420,22 @@ class CommandArgumentParser:
|
|
422
420
|
raise CommandArgumentError(
|
423
421
|
f"Expected at least one value for '{spec.dest}'"
|
424
422
|
)
|
425
|
-
while i < len(args) and
|
423
|
+
while i < len(args) and args[i] not in self._keyword:
|
426
424
|
values.append(args[i])
|
427
425
|
i += 1
|
428
426
|
assert values, "Expected at least one value for '+' nargs: shouldn't happen"
|
429
427
|
return values, i
|
430
428
|
elif spec.nargs == "*":
|
431
|
-
while i < len(args) and
|
429
|
+
while i < len(args) and args[i] not in self._keyword:
|
432
430
|
values.append(args[i])
|
433
431
|
i += 1
|
434
432
|
return values, i
|
435
433
|
elif spec.nargs == "?":
|
436
|
-
if i < len(args) and
|
434
|
+
if i < len(args) and args[i] not in self._keyword:
|
437
435
|
return [args[i]], i + 1
|
438
436
|
return [], i
|
439
437
|
elif spec.nargs is None:
|
440
|
-
if i < len(args) and
|
438
|
+
if i < len(args) and args[i] not in self._keyword:
|
441
439
|
return [args[i]], i + 1
|
442
440
|
return [], i
|
443
441
|
assert False, "Invalid nargs value: shouldn't happen"
|
@@ -524,23 +522,142 @@ class CommandArgumentParser:
|
|
524
522
|
|
525
523
|
return i
|
526
524
|
|
527
|
-
def _expand_posix_bundling(self,
|
525
|
+
def _expand_posix_bundling(self, token: str) -> list[str] | str:
|
528
526
|
"""Expand POSIX-style bundled arguments into separate arguments."""
|
529
527
|
expanded = []
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
expanded.append(token)
|
528
|
+
if token.startswith("-") and not token.startswith("--") and len(token) > 2:
|
529
|
+
# POSIX bundle
|
530
|
+
# e.g. -abc -> -a -b -c
|
531
|
+
for char in token[1:]:
|
532
|
+
flag = f"-{char}"
|
533
|
+
arg = self._flag_map.get(flag)
|
534
|
+
if not arg:
|
535
|
+
raise CommandArgumentError(f"Unrecognized option: {flag}")
|
536
|
+
expanded.append(flag)
|
537
|
+
else:
|
538
|
+
return token
|
542
539
|
return expanded
|
543
540
|
|
541
|
+
async def _handle_token(
|
542
|
+
self,
|
543
|
+
token: str,
|
544
|
+
args: list[str],
|
545
|
+
i: int,
|
546
|
+
result: dict[str, Any],
|
547
|
+
positional_args: list[Argument],
|
548
|
+
consumed_positional_indices: set[int],
|
549
|
+
consumed_indices: set[int],
|
550
|
+
from_validate: bool = False,
|
551
|
+
) -> int:
|
552
|
+
if token in self._keyword:
|
553
|
+
spec = self._keyword[token]
|
554
|
+
action = spec.action
|
555
|
+
|
556
|
+
if action == ArgumentAction.HELP:
|
557
|
+
if not from_validate:
|
558
|
+
self.render_help()
|
559
|
+
raise HelpSignal()
|
560
|
+
elif action == ArgumentAction.ACTION:
|
561
|
+
assert isinstance(
|
562
|
+
spec.resolver, BaseAction
|
563
|
+
), "resolver should be an instance of BaseAction"
|
564
|
+
values, new_i = self._consume_nargs(args, i + 1, spec)
|
565
|
+
try:
|
566
|
+
typed_values = [coerce_value(value, spec.type) for value in values]
|
567
|
+
except ValueError as error:
|
568
|
+
raise CommandArgumentError(
|
569
|
+
f"Invalid value for '{spec.dest}': {error}"
|
570
|
+
) from error
|
571
|
+
try:
|
572
|
+
result[spec.dest] = await spec.resolver(*typed_values)
|
573
|
+
except Exception as error:
|
574
|
+
raise CommandArgumentError(
|
575
|
+
f"[{spec.dest}] Action failed: {error}"
|
576
|
+
) from error
|
577
|
+
consumed_indices.update(range(i, new_i))
|
578
|
+
i = new_i
|
579
|
+
elif action == ArgumentAction.STORE_TRUE:
|
580
|
+
result[spec.dest] = True
|
581
|
+
consumed_indices.add(i)
|
582
|
+
i += 1
|
583
|
+
elif action == ArgumentAction.STORE_FALSE:
|
584
|
+
result[spec.dest] = False
|
585
|
+
consumed_indices.add(i)
|
586
|
+
i += 1
|
587
|
+
elif action == ArgumentAction.COUNT:
|
588
|
+
result[spec.dest] = result.get(spec.dest, 0) + 1
|
589
|
+
consumed_indices.add(i)
|
590
|
+
i += 1
|
591
|
+
elif action == ArgumentAction.APPEND:
|
592
|
+
assert result.get(spec.dest) is not None, "dest should not be None"
|
593
|
+
values, new_i = self._consume_nargs(args, i + 1, spec)
|
594
|
+
try:
|
595
|
+
typed_values = [coerce_value(value, spec.type) for value in values]
|
596
|
+
except ValueError as error:
|
597
|
+
raise CommandArgumentError(
|
598
|
+
f"Invalid value for '{spec.dest}': {error}"
|
599
|
+
) from error
|
600
|
+
if spec.nargs is None:
|
601
|
+
result[spec.dest].append(spec.type(values[0]))
|
602
|
+
else:
|
603
|
+
result[spec.dest].append(typed_values)
|
604
|
+
consumed_indices.update(range(i, new_i))
|
605
|
+
i = new_i
|
606
|
+
elif action == ArgumentAction.EXTEND:
|
607
|
+
assert result.get(spec.dest) is not None, "dest should not be None"
|
608
|
+
values, new_i = self._consume_nargs(args, i + 1, spec)
|
609
|
+
try:
|
610
|
+
typed_values = [coerce_value(value, spec.type) for value in values]
|
611
|
+
except ValueError as error:
|
612
|
+
raise CommandArgumentError(
|
613
|
+
f"Invalid value for '{spec.dest}': {error}"
|
614
|
+
) from error
|
615
|
+
result[spec.dest].extend(typed_values)
|
616
|
+
consumed_indices.update(range(i, new_i))
|
617
|
+
i = new_i
|
618
|
+
else:
|
619
|
+
values, new_i = self._consume_nargs(args, i + 1, spec)
|
620
|
+
try:
|
621
|
+
typed_values = [coerce_value(value, spec.type) for value in values]
|
622
|
+
except ValueError as error:
|
623
|
+
raise CommandArgumentError(
|
624
|
+
f"Invalid value for '{spec.dest}': {error}"
|
625
|
+
) from error
|
626
|
+
if not typed_values and spec.nargs not in ("*", "?"):
|
627
|
+
raise CommandArgumentError(
|
628
|
+
f"Expected at least one value for '{spec.dest}'"
|
629
|
+
)
|
630
|
+
if spec.nargs in (None, 1, "?") and spec.action != ArgumentAction.APPEND:
|
631
|
+
result[spec.dest] = (
|
632
|
+
typed_values[0] if len(typed_values) == 1 else typed_values
|
633
|
+
)
|
634
|
+
else:
|
635
|
+
result[spec.dest] = typed_values
|
636
|
+
consumed_indices.update(range(i, new_i))
|
637
|
+
i = new_i
|
638
|
+
elif token.startswith("-"):
|
639
|
+
# Handle unrecognized option
|
640
|
+
raise CommandArgumentError(f"Unrecognized flag: {token}")
|
641
|
+
else:
|
642
|
+
# Get the next flagged argument index if it exists
|
643
|
+
next_flagged_index = -1
|
644
|
+
for index, arg in enumerate(args[i:], start=i):
|
645
|
+
if arg in self._keyword:
|
646
|
+
next_flagged_index = index
|
647
|
+
break
|
648
|
+
print(f"next_flagged_index: {next_flagged_index}")
|
649
|
+
print(f"{self._keyword_list=}")
|
650
|
+
if next_flagged_index == -1:
|
651
|
+
next_flagged_index = len(args)
|
652
|
+
args_consumed = await self._consume_all_positional_args(
|
653
|
+
args[i:next_flagged_index],
|
654
|
+
result,
|
655
|
+
positional_args,
|
656
|
+
consumed_positional_indices,
|
657
|
+
)
|
658
|
+
i += args_consumed
|
659
|
+
return i
|
660
|
+
|
544
661
|
async def parse_args(
|
545
662
|
self, args: list[str] | None = None, from_validate: bool = False
|
546
663
|
) -> dict[str, Any]:
|
@@ -548,132 +665,29 @@ class CommandArgumentParser:
|
|
548
665
|
if args is None:
|
549
666
|
args = []
|
550
667
|
|
551
|
-
args = self._expand_posix_bundling(args)
|
552
|
-
|
553
668
|
result = {arg.dest: deepcopy(arg.default) for arg in self._arguments}
|
554
|
-
positional_args = [
|
669
|
+
positional_args: list[Argument] = [
|
670
|
+
arg for arg in self._arguments if arg.positional
|
671
|
+
]
|
555
672
|
consumed_positional_indices: set[int] = set()
|
556
673
|
consumed_indices: set[int] = set()
|
557
674
|
|
558
675
|
i = 0
|
559
676
|
while i < len(args):
|
560
|
-
token = args[i]
|
561
|
-
if token
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
try:
|
575
|
-
typed_values = [
|
576
|
-
coerce_value(value, spec.type) for value in values
|
577
|
-
]
|
578
|
-
except ValueError as error:
|
579
|
-
raise CommandArgumentError(
|
580
|
-
f"Invalid value for '{spec.dest}': {error}"
|
581
|
-
) from error
|
582
|
-
try:
|
583
|
-
result[spec.dest] = await spec.resolver(*typed_values)
|
584
|
-
except Exception as error:
|
585
|
-
raise CommandArgumentError(
|
586
|
-
f"[{spec.dest}] Action failed: {error}"
|
587
|
-
) from error
|
588
|
-
consumed_indices.update(range(i, new_i))
|
589
|
-
i = new_i
|
590
|
-
elif action == ArgumentAction.STORE_TRUE:
|
591
|
-
result[spec.dest] = True
|
592
|
-
consumed_indices.add(i)
|
593
|
-
i += 1
|
594
|
-
elif action == ArgumentAction.STORE_FALSE:
|
595
|
-
result[spec.dest] = False
|
596
|
-
consumed_indices.add(i)
|
597
|
-
i += 1
|
598
|
-
elif action == ArgumentAction.COUNT:
|
599
|
-
result[spec.dest] = result.get(spec.dest, 0) + 1
|
600
|
-
consumed_indices.add(i)
|
601
|
-
i += 1
|
602
|
-
elif action == ArgumentAction.APPEND:
|
603
|
-
assert result.get(spec.dest) is not None, "dest should not be None"
|
604
|
-
values, new_i = self._consume_nargs(args, i + 1, spec)
|
605
|
-
try:
|
606
|
-
typed_values = [
|
607
|
-
coerce_value(value, spec.type) for value in values
|
608
|
-
]
|
609
|
-
except ValueError as error:
|
610
|
-
raise CommandArgumentError(
|
611
|
-
f"Invalid value for '{spec.dest}': {error}"
|
612
|
-
) from error
|
613
|
-
if spec.nargs is None:
|
614
|
-
result[spec.dest].append(spec.type(values[0]))
|
615
|
-
else:
|
616
|
-
result[spec.dest].append(typed_values)
|
617
|
-
consumed_indices.update(range(i, new_i))
|
618
|
-
i = new_i
|
619
|
-
elif action == ArgumentAction.EXTEND:
|
620
|
-
assert result.get(spec.dest) is not None, "dest should not be None"
|
621
|
-
values, new_i = self._consume_nargs(args, i + 1, spec)
|
622
|
-
try:
|
623
|
-
typed_values = [
|
624
|
-
coerce_value(value, spec.type) for value in values
|
625
|
-
]
|
626
|
-
except ValueError as error:
|
627
|
-
raise CommandArgumentError(
|
628
|
-
f"Invalid value for '{spec.dest}': {error}"
|
629
|
-
) from error
|
630
|
-
result[spec.dest].extend(typed_values)
|
631
|
-
consumed_indices.update(range(i, new_i))
|
632
|
-
i = new_i
|
633
|
-
else:
|
634
|
-
values, new_i = self._consume_nargs(args, i + 1, spec)
|
635
|
-
try:
|
636
|
-
typed_values = [
|
637
|
-
coerce_value(value, spec.type) for value in values
|
638
|
-
]
|
639
|
-
except ValueError as error:
|
640
|
-
raise CommandArgumentError(
|
641
|
-
f"Invalid value for '{spec.dest}': {error}"
|
642
|
-
) from error
|
643
|
-
if not typed_values and spec.nargs not in ("*", "?"):
|
644
|
-
raise CommandArgumentError(
|
645
|
-
f"Expected at least one value for '{spec.dest}'"
|
646
|
-
)
|
647
|
-
if (
|
648
|
-
spec.nargs in (None, 1, "?")
|
649
|
-
and spec.action != ArgumentAction.APPEND
|
650
|
-
):
|
651
|
-
result[spec.dest] = (
|
652
|
-
typed_values[0] if len(typed_values) == 1 else typed_values
|
653
|
-
)
|
654
|
-
else:
|
655
|
-
result[spec.dest] = typed_values
|
656
|
-
consumed_indices.update(range(i, new_i))
|
657
|
-
i = new_i
|
658
|
-
elif token.startswith("-"):
|
659
|
-
# Handle unrecognized option
|
660
|
-
raise CommandArgumentError(f"Unrecognized flag: {token}")
|
661
|
-
else:
|
662
|
-
# Get the next flagged argument index if it exists
|
663
|
-
next_flagged_index = -1
|
664
|
-
for index, arg in enumerate(args[i:], start=i):
|
665
|
-
if arg.startswith("-"):
|
666
|
-
next_flagged_index = index
|
667
|
-
break
|
668
|
-
if next_flagged_index == -1:
|
669
|
-
next_flagged_index = len(args)
|
670
|
-
args_consumed = await self._consume_all_positional_args(
|
671
|
-
args[i:next_flagged_index],
|
672
|
-
result,
|
673
|
-
positional_args,
|
674
|
-
consumed_positional_indices,
|
675
|
-
)
|
676
|
-
i += args_consumed
|
677
|
+
token = self._expand_posix_bundling(args[i])
|
678
|
+
if isinstance(token, list):
|
679
|
+
args[i : i + 1] = token
|
680
|
+
token = args[i]
|
681
|
+
i = await self._handle_token(
|
682
|
+
token,
|
683
|
+
args,
|
684
|
+
i,
|
685
|
+
result,
|
686
|
+
positional_args,
|
687
|
+
consumed_positional_indices,
|
688
|
+
consumed_indices,
|
689
|
+
from_validate=from_validate,
|
690
|
+
)
|
677
691
|
|
678
692
|
# Required validation
|
679
693
|
for spec in self._arguments:
|
@@ -797,6 +811,8 @@ class CommandArgumentParser:
|
|
797
811
|
flags = arg.get_positional_text()
|
798
812
|
arg_line = Text(f" {flags:<30} ")
|
799
813
|
help_text = arg.help or ""
|
814
|
+
if help_text and len(flags) > 30:
|
815
|
+
help_text = f"\n{'':<33}{help_text}"
|
800
816
|
arg_line.append(help_text)
|
801
817
|
self.console.print(arg_line)
|
802
818
|
self.console.print("[bold]options:[/bold]")
|
@@ -805,6 +821,8 @@ class CommandArgumentParser:
|
|
805
821
|
flags_choice = f"{flags} {arg.get_choice_text()}"
|
806
822
|
arg_line = Text(f" {flags_choice:<30} ")
|
807
823
|
help_text = arg.help or ""
|
824
|
+
if help_text and len(flags_choice) > 30:
|
825
|
+
help_text = f"\n{'':<33}{help_text}"
|
808
826
|
arg_line.append(help_text)
|
809
827
|
self.console.print(arg_line)
|
810
828
|
|
@@ -2,14 +2,16 @@
|
|
2
2
|
"""protocols.py"""
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any, Awaitable, Protocol, runtime_checkable
|
5
|
+
from typing import Any, Awaitable, Callable, Protocol, runtime_checkable
|
6
6
|
|
7
7
|
from falyx.action.base_action import BaseAction
|
8
8
|
|
9
9
|
|
10
10
|
@runtime_checkable
|
11
11
|
class ActionFactoryProtocol(Protocol):
|
12
|
-
async def __call__(
|
12
|
+
async def __call__(
|
13
|
+
self, *args: Any, **kwargs: Any
|
14
|
+
) -> Callable[..., Awaitable[BaseAction]]: ...
|
13
15
|
|
14
16
|
|
15
17
|
@runtime_checkable
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.54"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "falyx"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.54"
|
4
4
|
description = "Reliable and introspectable async CLI action framework."
|
5
5
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
6
6
|
license = "MIT"
|
@@ -27,6 +27,10 @@ black = { version = "^25.0", allow-prereleases = true }
|
|
27
27
|
mypy = { version = "^1.0", allow-prereleases = true }
|
28
28
|
isort = { version = "^5.0", allow-prereleases = true }
|
29
29
|
pytest-cov = "^4.0"
|
30
|
+
mkdocs = "^1.6.1"
|
31
|
+
mkdocs-material = "^9.6.14"
|
32
|
+
mkdocstrings = {extras = ["python"], version = "^0.29.1"}
|
33
|
+
mike = "^2.1.3"
|
30
34
|
|
31
35
|
[tool.poetry.scripts]
|
32
36
|
falyx = "falyx.__main__:main"
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""load_file_action.py"""
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
|
-
from rich.tree import Tree
|
6
|
-
|
7
|
-
from falyx.action.base_action import BaseAction
|
8
|
-
|
9
|
-
|
10
|
-
class LoadFileAction(BaseAction):
|
11
|
-
""" """
|
12
|
-
|
13
|
-
def __init__(self, name: str, file_path: str):
|
14
|
-
super().__init__(name=name)
|
15
|
-
self.file_path = file_path
|
16
|
-
|
17
|
-
def get_infer_target(self) -> tuple[None, None]:
|
18
|
-
return None, None
|
19
|
-
|
20
|
-
async def _run(self, *args, **kwargs):
|
21
|
-
raise NotImplementedError(
|
22
|
-
"LoadFileAction is not finished yet... Use primatives instead..."
|
23
|
-
)
|
24
|
-
|
25
|
-
async def preview(self, parent: Tree | None = None): ...
|
26
|
-
|
27
|
-
def __str__(self) -> str:
|
28
|
-
return f"LoadFileAction(file_path={self.file_path})"
|
falyx-0.1.53/falyx/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.53"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|