falyx 0.1.30__py3-none-any.whl → 0.1.31__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/__init__.py +0 -1
- falyx/action/action.py +10 -16
- falyx/action/action_factory.py +6 -2
- falyx/action/io_action.py +5 -7
- falyx/action/menu_action.py +2 -2
- falyx/action/select_file_action.py +29 -11
- falyx/action/selection_action.py +82 -21
- falyx/action/types.py +15 -0
- falyx/action/user_input_action.py +2 -2
- falyx/command.py +9 -32
- falyx/config.py +0 -1
- falyx/falyx.py +30 -29
- falyx/hook_manager.py +8 -7
- falyx/menu.py +20 -8
- falyx/parsers/argparse.py +11 -0
- falyx/parsers/utils.py +2 -1
- falyx/selection.py +57 -1
- falyx/version.py +1 -1
- {falyx-0.1.30.dist-info → falyx-0.1.31.dist-info}/METADATA +1 -1
- {falyx-0.1.30.dist-info → falyx-0.1.31.dist-info}/RECORD +23 -23
- {falyx-0.1.30.dist-info → falyx-0.1.31.dist-info}/LICENSE +0 -0
- {falyx-0.1.30.dist-info → falyx-0.1.31.dist-info}/WHEEL +0 -0
- {falyx-0.1.30.dist-info → falyx-0.1.31.dist-info}/entry_points.txt +0 -0
falyx/__init__.py
CHANGED
falyx/action/action.py
CHANGED
@@ -62,8 +62,7 @@ class BaseAction(ABC):
|
|
62
62
|
inject_last_result (bool): Whether to inject the previous action's result
|
63
63
|
into kwargs.
|
64
64
|
inject_into (str): The name of the kwarg key to inject the result as
|
65
|
-
|
66
|
-
_requires_injection (bool): Whether the action requires input injection.
|
65
|
+
(default: 'last_result').
|
67
66
|
"""
|
68
67
|
|
69
68
|
def __init__(
|
@@ -83,7 +82,6 @@ class BaseAction(ABC):
|
|
83
82
|
self.inject_last_result: bool = inject_last_result
|
84
83
|
self.inject_into: str = inject_into
|
85
84
|
self._never_prompt: bool = never_prompt
|
86
|
-
self._requires_injection: bool = False
|
87
85
|
self._skip_in_chain: bool = False
|
88
86
|
self.console = Console(color_system="auto")
|
89
87
|
self.options_manager: OptionsManager | None = None
|
@@ -103,7 +101,7 @@ class BaseAction(ABC):
|
|
103
101
|
raise NotImplementedError("preview must be implemented by subclasses")
|
104
102
|
|
105
103
|
@abstractmethod
|
106
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
104
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
107
105
|
"""
|
108
106
|
Returns the callable to be used for argument inference.
|
109
107
|
By default, it returns None.
|
@@ -163,10 +161,6 @@ class BaseAction(ABC):
|
|
163
161
|
async def _write_stdout(self, data: str) -> None:
|
164
162
|
"""Override in subclasses that produce terminal output."""
|
165
163
|
|
166
|
-
def requires_io_injection(self) -> bool:
|
167
|
-
"""Checks to see if the action requires input injection."""
|
168
|
-
return self._requires_injection
|
169
|
-
|
170
164
|
def __repr__(self) -> str:
|
171
165
|
return str(self)
|
172
166
|
|
@@ -255,12 +249,12 @@ class Action(BaseAction):
|
|
255
249
|
if policy.enabled:
|
256
250
|
self.enable_retry()
|
257
251
|
|
258
|
-
def get_infer_target(self) -> Callable[..., Any]:
|
252
|
+
def get_infer_target(self) -> tuple[Callable[..., Any], None]:
|
259
253
|
"""
|
260
254
|
Returns the callable to be used for argument inference.
|
261
255
|
By default, it returns the action itself.
|
262
256
|
"""
|
263
|
-
return self.action
|
257
|
+
return self.action, None
|
264
258
|
|
265
259
|
async def _run(self, *args, **kwargs) -> Any:
|
266
260
|
combined_args = args + self.args
|
@@ -493,10 +487,10 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
493
487
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
494
488
|
action.register_teardown(self.hooks)
|
495
489
|
|
496
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
490
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
497
491
|
if self.actions:
|
498
492
|
return self.actions[0].get_infer_target()
|
499
|
-
return None
|
493
|
+
return None, None
|
500
494
|
|
501
495
|
def _clear_args(self):
|
502
496
|
return (), {}
|
@@ -690,7 +684,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
690
684
|
if hasattr(action, "register_teardown") and callable(action.register_teardown):
|
691
685
|
action.register_teardown(self.hooks)
|
692
686
|
|
693
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
687
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
694
688
|
arg_defs = same_argument_definitions(self.actions)
|
695
689
|
if arg_defs:
|
696
690
|
return self.actions[0].get_infer_target()
|
@@ -698,7 +692,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
698
692
|
"[%s] auto_args disabled: mismatched ActionGroup arguments",
|
699
693
|
self.name,
|
700
694
|
)
|
701
|
-
return None
|
695
|
+
return None, None
|
702
696
|
|
703
697
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
704
698
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
@@ -818,8 +812,8 @@ class ProcessAction(BaseAction):
|
|
818
812
|
self.executor = executor or ProcessPoolExecutor()
|
819
813
|
self.is_retryable = True
|
820
814
|
|
821
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
822
|
-
return self.action
|
815
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, None]:
|
816
|
+
return self.action, None
|
823
817
|
|
824
818
|
async def _run(self, *args, **kwargs) -> Any:
|
825
819
|
if self.inject_last_result and self.shared_context:
|
falyx/action/action_factory.py
CHANGED
@@ -35,6 +35,8 @@ class ActionFactoryAction(BaseAction):
|
|
35
35
|
*,
|
36
36
|
inject_last_result: bool = False,
|
37
37
|
inject_into: str = "last_result",
|
38
|
+
args: tuple[Any, ...] = (),
|
39
|
+
kwargs: dict[str, Any] | None = None,
|
38
40
|
preview_args: tuple[Any, ...] = (),
|
39
41
|
preview_kwargs: dict[str, Any] | None = None,
|
40
42
|
):
|
@@ -44,6 +46,8 @@ class ActionFactoryAction(BaseAction):
|
|
44
46
|
inject_into=inject_into,
|
45
47
|
)
|
46
48
|
self.factory = factory
|
49
|
+
self.args = args
|
50
|
+
self.kwargs = kwargs or {}
|
47
51
|
self.preview_args = preview_args
|
48
52
|
self.preview_kwargs = preview_kwargs or {}
|
49
53
|
|
@@ -55,8 +59,8 @@ class ActionFactoryAction(BaseAction):
|
|
55
59
|
def factory(self, value: ActionFactoryProtocol):
|
56
60
|
self._factory = ensure_async(value)
|
57
61
|
|
58
|
-
def get_infer_target(self) -> Callable[..., Any]:
|
59
|
-
return self.factory
|
62
|
+
def get_infer_target(self) -> tuple[Callable[..., Any], None]:
|
63
|
+
return self.factory, None
|
60
64
|
|
61
65
|
async def _run(self, *args, **kwargs) -> Any:
|
62
66
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
falyx/action/io_action.py
CHANGED
@@ -73,7 +73,6 @@ class BaseIOAction(BaseAction):
|
|
73
73
|
inject_last_result=inject_last_result,
|
74
74
|
)
|
75
75
|
self.mode = mode
|
76
|
-
self._requires_injection = True
|
77
76
|
|
78
77
|
def from_input(self, raw: str | bytes) -> Any:
|
79
78
|
raise NotImplementedError
|
@@ -99,8 +98,8 @@ class BaseIOAction(BaseAction):
|
|
99
98
|
)
|
100
99
|
raise FalyxError("No input provided and no last result to inject.")
|
101
100
|
|
102
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
103
|
-
return None
|
101
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
102
|
+
return None, None
|
104
103
|
|
105
104
|
async def __call__(self, *args, **kwargs):
|
106
105
|
context = ExecutionContext(
|
@@ -198,7 +197,6 @@ class ShellAction(BaseIOAction):
|
|
198
197
|
- Captures stdout and stderr from shell execution
|
199
198
|
- Raises on non-zero exit codes with stderr as the error
|
200
199
|
- Result is returned as trimmed stdout string
|
201
|
-
- Compatible with ChainedAction and Command.requires_input detection
|
202
200
|
|
203
201
|
Args:
|
204
202
|
name (str): Name of the action.
|
@@ -223,10 +221,10 @@ class ShellAction(BaseIOAction):
|
|
223
221
|
)
|
224
222
|
return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
|
225
223
|
|
226
|
-
def get_infer_target(self) -> Callable[..., Any] | None:
|
224
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
227
225
|
if sys.stdin.isatty():
|
228
|
-
return self._run
|
229
|
-
return None
|
226
|
+
return self._run, {"parsed_input": {"help": self.command_template}}
|
227
|
+
return None, None
|
230
228
|
|
231
229
|
async def _run(self, parsed_input: str) -> str:
|
232
230
|
# Replace placeholder in template, or use raw input as full command
|
falyx/action/menu_action.py
CHANGED
@@ -73,8 +73,8 @@ class MenuAction(BaseAction):
|
|
73
73
|
table.add_row(*row)
|
74
74
|
return table
|
75
75
|
|
76
|
-
def get_infer_target(self) -> None:
|
77
|
-
return None
|
76
|
+
def get_infer_target(self) -> tuple[None, None]:
|
77
|
+
return None, None
|
78
78
|
|
79
79
|
async def _run(self, *args, **kwargs) -> Any:
|
80
80
|
kwargs = self._maybe_inject_last_result(kwargs)
|
@@ -25,6 +25,7 @@ from falyx.selection import (
|
|
25
25
|
prompt_for_selection,
|
26
26
|
render_selection_dict_table,
|
27
27
|
)
|
28
|
+
from falyx.signals import CancelSignal
|
28
29
|
from falyx.themes import OneColors
|
29
30
|
|
30
31
|
|
@@ -121,8 +122,15 @@ class SelectFileAction(BaseAction):
|
|
121
122
|
logger.warning("[ERROR] Failed to parse %s: %s", file.name, error)
|
122
123
|
return options
|
123
124
|
|
124
|
-
def
|
125
|
-
|
125
|
+
def _find_cancel_key(self, options) -> str:
|
126
|
+
"""Return first numeric value not already used in the selection dict."""
|
127
|
+
for index in range(len(options)):
|
128
|
+
if str(index) not in options:
|
129
|
+
return str(index)
|
130
|
+
return str(len(options))
|
131
|
+
|
132
|
+
def get_infer_target(self) -> tuple[None, None]:
|
133
|
+
return None, None
|
126
134
|
|
127
135
|
async def _run(self, *args, **kwargs) -> Any:
|
128
136
|
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
@@ -131,28 +139,38 @@ class SelectFileAction(BaseAction):
|
|
131
139
|
await self.hooks.trigger(HookType.BEFORE, context)
|
132
140
|
|
133
141
|
files = [
|
134
|
-
|
135
|
-
for
|
136
|
-
if
|
137
|
-
and (self.suffix_filter is None or
|
142
|
+
file
|
143
|
+
for file in self.directory.iterdir()
|
144
|
+
if file.is_file()
|
145
|
+
and (self.suffix_filter is None or file.suffix == self.suffix_filter)
|
138
146
|
]
|
139
147
|
if not files:
|
140
148
|
raise FileNotFoundError("No files found in directory.")
|
141
149
|
|
142
150
|
options = self.get_options(files)
|
143
151
|
|
152
|
+
cancel_key = self._find_cancel_key(options)
|
153
|
+
cancel_option = {
|
154
|
+
cancel_key: SelectionOption(
|
155
|
+
description="Cancel", value=CancelSignal(), style=OneColors.DARK_RED
|
156
|
+
)
|
157
|
+
}
|
158
|
+
|
144
159
|
table = render_selection_dict_table(
|
145
|
-
title=self.title, selections=options, columns=self.columns
|
160
|
+
title=self.title, selections=options | cancel_option, columns=self.columns
|
146
161
|
)
|
147
162
|
|
148
163
|
key = await prompt_for_selection(
|
149
|
-
options.keys(),
|
164
|
+
(options | cancel_option).keys(),
|
150
165
|
table,
|
151
166
|
console=self.console,
|
152
167
|
prompt_session=self.prompt_session,
|
153
168
|
prompt_message=self.prompt_message,
|
154
169
|
)
|
155
170
|
|
171
|
+
if key == cancel_key:
|
172
|
+
raise CancelSignal("User canceled the selection.")
|
173
|
+
|
156
174
|
result = options[key].value
|
157
175
|
context.result = result
|
158
176
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
@@ -179,11 +197,11 @@ class SelectFileAction(BaseAction):
|
|
179
197
|
try:
|
180
198
|
files = list(self.directory.iterdir())
|
181
199
|
if self.suffix_filter:
|
182
|
-
files = [
|
200
|
+
files = [file for file in files if file.suffix == self.suffix_filter]
|
183
201
|
sample = files[:10]
|
184
202
|
file_list = tree.add("[dim]Files:[/]")
|
185
|
-
for
|
186
|
-
file_list.add(f"[dim]{
|
203
|
+
for file in sample:
|
204
|
+
file_list.add(f"[dim]{file.name}[/]")
|
187
205
|
if len(files) > 10:
|
188
206
|
file_list.add(f"[dim]... ({len(files) - 10} more)[/]")
|
189
207
|
except Exception as error:
|
falyx/action/selection_action.py
CHANGED
@@ -7,19 +7,21 @@ from rich.console import Console
|
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
9
|
from falyx.action.action import BaseAction
|
10
|
+
from falyx.action.types import SelectionReturnType
|
10
11
|
from falyx.context import ExecutionContext
|
11
12
|
from falyx.execution_registry import ExecutionRegistry as er
|
12
13
|
from falyx.hook_manager import HookType
|
13
14
|
from falyx.logger import logger
|
14
15
|
from falyx.selection import (
|
15
16
|
SelectionOption,
|
17
|
+
SelectionOptionMap,
|
16
18
|
prompt_for_index,
|
17
19
|
prompt_for_selection,
|
18
20
|
render_selection_dict_table,
|
19
21
|
render_selection_indexed_table,
|
20
22
|
)
|
23
|
+
from falyx.signals import CancelSignal
|
21
24
|
from falyx.themes import OneColors
|
22
|
-
from falyx.utils import CaseInsensitiveDict
|
23
25
|
|
24
26
|
|
25
27
|
class SelectionAction(BaseAction):
|
@@ -34,7 +36,13 @@ class SelectionAction(BaseAction):
|
|
34
36
|
def __init__(
|
35
37
|
self,
|
36
38
|
name: str,
|
37
|
-
selections:
|
39
|
+
selections: (
|
40
|
+
list[str]
|
41
|
+
| set[str]
|
42
|
+
| tuple[str, ...]
|
43
|
+
| dict[str, SelectionOption]
|
44
|
+
| dict[str, Any]
|
45
|
+
),
|
38
46
|
*,
|
39
47
|
title: str = "Select an option",
|
40
48
|
columns: int = 5,
|
@@ -42,7 +50,7 @@ class SelectionAction(BaseAction):
|
|
42
50
|
default_selection: str = "",
|
43
51
|
inject_last_result: bool = False,
|
44
52
|
inject_into: str = "last_result",
|
45
|
-
|
53
|
+
return_type: SelectionReturnType | str = "value",
|
46
54
|
console: Console | None = None,
|
47
55
|
prompt_session: PromptSession | None = None,
|
48
56
|
never_prompt: bool = False,
|
@@ -55,8 +63,8 @@ class SelectionAction(BaseAction):
|
|
55
63
|
never_prompt=never_prompt,
|
56
64
|
)
|
57
65
|
# Setter normalizes to correct type, mypy can't infer that
|
58
|
-
self.selections: list[str] |
|
59
|
-
self.
|
66
|
+
self.selections: list[str] | SelectionOptionMap = selections # type: ignore[assignment]
|
67
|
+
self.return_type: SelectionReturnType = self._coerce_return_type(return_type)
|
60
68
|
self.title = title
|
61
69
|
self.columns = columns
|
62
70
|
self.console = console or Console(color_system="auto")
|
@@ -65,8 +73,15 @@ class SelectionAction(BaseAction):
|
|
65
73
|
self.prompt_message = prompt_message
|
66
74
|
self.show_table = show_table
|
67
75
|
|
76
|
+
def _coerce_return_type(
|
77
|
+
self, return_type: SelectionReturnType | str
|
78
|
+
) -> SelectionReturnType:
|
79
|
+
if isinstance(return_type, SelectionReturnType):
|
80
|
+
return return_type
|
81
|
+
return SelectionReturnType(return_type)
|
82
|
+
|
68
83
|
@property
|
69
|
-
def selections(self) -> list[str] |
|
84
|
+
def selections(self) -> list[str] | SelectionOptionMap:
|
70
85
|
return self._selections
|
71
86
|
|
72
87
|
@selections.setter
|
@@ -74,19 +89,40 @@ class SelectionAction(BaseAction):
|
|
74
89
|
self, value: list[str] | set[str] | tuple[str, ...] | dict[str, SelectionOption]
|
75
90
|
):
|
76
91
|
if isinstance(value, (list, tuple, set)):
|
77
|
-
self._selections: list[str] |
|
92
|
+
self._selections: list[str] | SelectionOptionMap = list(value)
|
78
93
|
elif isinstance(value, dict):
|
79
|
-
|
80
|
-
|
81
|
-
|
94
|
+
som = SelectionOptionMap()
|
95
|
+
if all(isinstance(key, str) for key in value) and all(
|
96
|
+
not isinstance(value[key], SelectionOption) for key in value
|
97
|
+
):
|
98
|
+
som.update(
|
99
|
+
{
|
100
|
+
str(index): SelectionOption(key, option)
|
101
|
+
for index, (key, option) in enumerate(value.items())
|
102
|
+
}
|
103
|
+
)
|
104
|
+
elif all(isinstance(key, str) for key in value) and all(
|
105
|
+
isinstance(value[key], SelectionOption) for key in value
|
106
|
+
):
|
107
|
+
som.update(value)
|
108
|
+
else:
|
109
|
+
raise ValueError("Invalid dictionary format. Keys must be strings")
|
110
|
+
self._selections = som
|
82
111
|
else:
|
83
112
|
raise TypeError(
|
84
113
|
"'selections' must be a list[str] or dict[str, SelectionOption], "
|
85
114
|
f"got {type(value).__name__}"
|
86
115
|
)
|
87
116
|
|
88
|
-
def
|
89
|
-
|
117
|
+
def _find_cancel_key(self) -> str:
|
118
|
+
"""Return first numeric value not already used in the selection dict."""
|
119
|
+
for index in range(len(self.selections)):
|
120
|
+
if str(index) not in self.selections:
|
121
|
+
return str(index)
|
122
|
+
return str(len(self.selections))
|
123
|
+
|
124
|
+
def get_infer_target(self) -> tuple[None, None]:
|
125
|
+
return None, None
|
90
126
|
|
91
127
|
async def _run(self, *args, **kwargs) -> Any:
|
92
128
|
kwargs = self._maybe_inject_last_result(kwargs)
|
@@ -128,16 +164,17 @@ class SelectionAction(BaseAction):
|
|
128
164
|
|
129
165
|
context.start_timer()
|
130
166
|
try:
|
167
|
+
cancel_key = self._find_cancel_key()
|
131
168
|
await self.hooks.trigger(HookType.BEFORE, context)
|
132
169
|
if isinstance(self.selections, list):
|
133
170
|
table = render_selection_indexed_table(
|
134
171
|
title=self.title,
|
135
|
-
selections=self.selections,
|
172
|
+
selections=self.selections + ["Cancel"],
|
136
173
|
columns=self.columns,
|
137
174
|
)
|
138
175
|
if not self.never_prompt:
|
139
176
|
index = await prompt_for_index(
|
140
|
-
len(self.selections)
|
177
|
+
len(self.selections),
|
141
178
|
table,
|
142
179
|
default_selection=effective_default,
|
143
180
|
console=self.console,
|
@@ -147,14 +184,23 @@ class SelectionAction(BaseAction):
|
|
147
184
|
)
|
148
185
|
else:
|
149
186
|
index = effective_default
|
150
|
-
|
187
|
+
if index == cancel_key:
|
188
|
+
raise CancelSignal("User cancelled the selection.")
|
189
|
+
result: Any = self.selections[int(index)]
|
151
190
|
elif isinstance(self.selections, dict):
|
191
|
+
cancel_option = {
|
192
|
+
cancel_key: SelectionOption(
|
193
|
+
description="Cancel", value=CancelSignal, style=OneColors.DARK_RED
|
194
|
+
)
|
195
|
+
}
|
152
196
|
table = render_selection_dict_table(
|
153
|
-
title=self.title,
|
197
|
+
title=self.title,
|
198
|
+
selections=self.selections | cancel_option,
|
199
|
+
columns=self.columns,
|
154
200
|
)
|
155
201
|
if not self.never_prompt:
|
156
202
|
key = await prompt_for_selection(
|
157
|
-
self.selections.keys(),
|
203
|
+
(self.selections | cancel_option).keys(),
|
158
204
|
table,
|
159
205
|
default_selection=effective_default,
|
160
206
|
console=self.console,
|
@@ -164,10 +210,25 @@ class SelectionAction(BaseAction):
|
|
164
210
|
)
|
165
211
|
else:
|
166
212
|
key = effective_default
|
167
|
-
|
213
|
+
if key == cancel_key:
|
214
|
+
raise CancelSignal("User cancelled the selection.")
|
215
|
+
if self.return_type == SelectionReturnType.KEY:
|
216
|
+
result = key
|
217
|
+
elif self.return_type == SelectionReturnType.VALUE:
|
218
|
+
result = self.selections[key].value
|
219
|
+
elif self.return_type == SelectionReturnType.ITEMS:
|
220
|
+
result = {key: self.selections[key]}
|
221
|
+
elif self.return_type == SelectionReturnType.DESCRIPTION:
|
222
|
+
result = self.selections[key].description
|
223
|
+
elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
|
224
|
+
result = {
|
225
|
+
self.selections[key].description: self.selections[key].value
|
226
|
+
}
|
227
|
+
else:
|
228
|
+
raise ValueError(f"Unsupported return type: {self.return_type}")
|
168
229
|
else:
|
169
230
|
raise TypeError(
|
170
|
-
"'selections' must be a list[str] or dict[str,
|
231
|
+
"'selections' must be a list[str] or dict[str, Any], "
|
171
232
|
f"got {type(self.selections).__name__}"
|
172
233
|
)
|
173
234
|
context.result = result
|
@@ -206,7 +267,7 @@ class SelectionAction(BaseAction):
|
|
206
267
|
return
|
207
268
|
|
208
269
|
tree.add(f"[dim]Default:[/] '{self.default_selection or self.last_result}'")
|
209
|
-
tree.add(f"[dim]Return:[/] {
|
270
|
+
tree.add(f"[dim]Return:[/] {self.return_type.name.capitalize()}")
|
210
271
|
tree.add(f"[dim]Prompt:[/] {'Disabled' if self.never_prompt else 'Enabled'}")
|
211
272
|
|
212
273
|
if not parent:
|
@@ -221,6 +282,6 @@ class SelectionAction(BaseAction):
|
|
221
282
|
return (
|
222
283
|
f"SelectionAction(name={self.name!r}, type={selection_type}, "
|
223
284
|
f"default_selection={self.default_selection!r}, "
|
224
|
-
f"
|
285
|
+
f"return_type={self.return_type!r}, "
|
225
286
|
f"prompt={'off' if self.never_prompt else 'on'})"
|
226
287
|
)
|
falyx/action/types.py
CHANGED
@@ -35,3 +35,18 @@ class FileReturnType(Enum):
|
|
35
35
|
return member
|
36
36
|
valid = ", ".join(member.value for member in cls)
|
37
37
|
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
38
|
+
|
39
|
+
|
40
|
+
class SelectionReturnType(Enum):
|
41
|
+
"""Enum for dictionary return types."""
|
42
|
+
|
43
|
+
KEY = "key"
|
44
|
+
VALUE = "value"
|
45
|
+
DESCRIPTION = "description"
|
46
|
+
DESCRIPTION_VALUE = "description_value"
|
47
|
+
ITEMS = "items"
|
48
|
+
|
49
|
+
@classmethod
|
50
|
+
def _missing_(cls, value: object) -> SelectionReturnType:
|
51
|
+
valid = ", ".join(member.value for member in cls)
|
52
|
+
raise ValueError(f"Invalid DictReturnType: '{value}'. Must be one of: {valid}")
|
@@ -43,8 +43,8 @@ class UserInputAction(BaseAction):
|
|
43
43
|
self.console = console or Console(color_system="auto")
|
44
44
|
self.prompt_session = prompt_session or PromptSession()
|
45
45
|
|
46
|
-
def get_infer_target(self) -> None:
|
47
|
-
return None
|
46
|
+
def get_infer_target(self) -> tuple[None, None]:
|
47
|
+
return None, None
|
48
48
|
|
49
49
|
async def _run(self, *args, **kwargs) -> str:
|
50
50
|
context = ExecutionContext(
|
falyx/command.py
CHANGED
@@ -19,7 +19,6 @@ in building robust interactive menus.
|
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
21
|
import shlex
|
22
|
-
from functools import cached_property
|
23
22
|
from typing import Any, Callable
|
24
23
|
|
25
24
|
from prompt_toolkit.formatted_text import FormattedText
|
@@ -27,8 +26,7 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
27
26
|
from rich.console import Console
|
28
27
|
from rich.tree import Tree
|
29
28
|
|
30
|
-
from falyx.action.action import Action,
|
31
|
-
from falyx.action.io_action import BaseIOAction
|
29
|
+
from falyx.action.action import Action, BaseAction
|
32
30
|
from falyx.context import ExecutionContext
|
33
31
|
from falyx.debug import register_debug_hooks
|
34
32
|
from falyx.execution_registry import ExecutionRegistry as er
|
@@ -90,7 +88,6 @@ class Command(BaseModel):
|
|
90
88
|
retry_policy (RetryPolicy): Retry behavior configuration.
|
91
89
|
tags (list[str]): Organizational tags for the command.
|
92
90
|
logging_hooks (bool): Whether to attach logging hooks automatically.
|
93
|
-
requires_input (bool | None): Indicates if the action needs input.
|
94
91
|
options_manager (OptionsManager): Manages global command-line options.
|
95
92
|
arg_parser (CommandArgumentParser): Parses command arguments.
|
96
93
|
custom_parser (ArgParserProtocol | None): Custom argument parser.
|
@@ -129,7 +126,6 @@ class Command(BaseModel):
|
|
129
126
|
retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
|
130
127
|
tags: list[str] = Field(default_factory=list)
|
131
128
|
logging_hooks: bool = False
|
132
|
-
requires_input: bool | None = None
|
133
129
|
options_manager: OptionsManager = Field(default_factory=OptionsManager)
|
134
130
|
arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
|
135
131
|
arguments: list[dict[str, Any]] = Field(default_factory=list)
|
@@ -146,7 +142,7 @@ class Command(BaseModel):
|
|
146
142
|
def parse_args(
|
147
143
|
self, raw_args: list[str] | str, from_validate: bool = False
|
148
144
|
) -> tuple[tuple, dict]:
|
149
|
-
if self.custom_parser:
|
145
|
+
if callable(self.custom_parser):
|
150
146
|
if isinstance(raw_args, str):
|
151
147
|
try:
|
152
148
|
raw_args = shlex.split(raw_args)
|
@@ -183,13 +179,15 @@ class Command(BaseModel):
|
|
183
179
|
def get_argument_definitions(self) -> list[dict[str, Any]]:
|
184
180
|
if self.arguments:
|
185
181
|
return self.arguments
|
186
|
-
elif self.argument_config:
|
182
|
+
elif callable(self.argument_config):
|
187
183
|
self.argument_config(self.arg_parser)
|
188
184
|
elif self.auto_args:
|
189
185
|
if isinstance(self.action, BaseAction):
|
190
|
-
|
191
|
-
|
192
|
-
|
186
|
+
infer_target, maybe_metadata = self.action.get_infer_target()
|
187
|
+
# merge metadata with the action's metadata if not already in self.arg_metadata
|
188
|
+
if maybe_metadata:
|
189
|
+
self.arg_metadata = {**maybe_metadata, **self.arg_metadata}
|
190
|
+
return infer_args_from_func(infer_target, self.arg_metadata)
|
193
191
|
elif callable(self.action):
|
194
192
|
return infer_args_from_func(self.action, self.arg_metadata)
|
195
193
|
return []
|
@@ -217,30 +215,9 @@ class Command(BaseModel):
|
|
217
215
|
if self.logging_hooks and isinstance(self.action, BaseAction):
|
218
216
|
register_debug_hooks(self.action.hooks)
|
219
217
|
|
220
|
-
if self.requires_input is None and self.detect_requires_input:
|
221
|
-
self.requires_input = True
|
222
|
-
self.hidden = True
|
223
|
-
elif self.requires_input is None:
|
224
|
-
self.requires_input = False
|
225
|
-
|
226
218
|
for arg_def in self.get_argument_definitions():
|
227
219
|
self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
|
228
220
|
|
229
|
-
@cached_property
|
230
|
-
def detect_requires_input(self) -> bool:
|
231
|
-
"""Detect if the action requires input based on its type."""
|
232
|
-
if isinstance(self.action, BaseIOAction):
|
233
|
-
return True
|
234
|
-
elif isinstance(self.action, ChainedAction):
|
235
|
-
return (
|
236
|
-
isinstance(self.action.actions[0], BaseIOAction)
|
237
|
-
if self.action.actions
|
238
|
-
else False
|
239
|
-
)
|
240
|
-
elif isinstance(self.action, ActionGroup):
|
241
|
-
return any(isinstance(action, BaseIOAction) for action in self.action.actions)
|
242
|
-
return False
|
243
|
-
|
244
221
|
def _inject_options_manager(self) -> None:
|
245
222
|
"""Inject the options manager into the action if applicable."""
|
246
223
|
if isinstance(self.action, BaseAction):
|
@@ -333,7 +310,7 @@ class Command(BaseModel):
|
|
333
310
|
|
334
311
|
def show_help(self) -> bool:
|
335
312
|
"""Display the help message for the command."""
|
336
|
-
if self.custom_help:
|
313
|
+
if callable(self.custom_help):
|
337
314
|
output = self.custom_help()
|
338
315
|
if output:
|
339
316
|
console.print(output)
|
falyx/config.py
CHANGED
falyx/falyx.py
CHANGED
@@ -61,7 +61,7 @@ from falyx.options_manager import OptionsManager
|
|
61
61
|
from falyx.parsers import CommandArgumentParser, get_arg_parsers
|
62
62
|
from falyx.protocols import ArgParserProtocol
|
63
63
|
from falyx.retry import RetryPolicy
|
64
|
-
from falyx.signals import BackSignal, CancelSignal,
|
64
|
+
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
65
65
|
from falyx.themes import OneColors, get_nord_theme
|
66
66
|
from falyx.utils import CaseInsensitiveDict, _noop, chunks
|
67
67
|
from falyx.version import __version__
|
@@ -90,7 +90,7 @@ class CommandValidator(Validator):
|
|
90
90
|
if not choice:
|
91
91
|
raise ValidationError(
|
92
92
|
message=self.error_message,
|
93
|
-
cursor_position=
|
93
|
+
cursor_position=len(text),
|
94
94
|
)
|
95
95
|
|
96
96
|
|
@@ -111,6 +111,8 @@ class Falyx:
|
|
111
111
|
- Submenu nesting and action chaining
|
112
112
|
- History tracking, help generation, and run key execution modes
|
113
113
|
- Seamless CLI argument parsing and integration via argparse
|
114
|
+
- Declarative option management with OptionsManager
|
115
|
+
- Command level argument parsing and validation
|
114
116
|
- Extensible with user-defined hooks, bottom bars, and custom layouts
|
115
117
|
|
116
118
|
Args:
|
@@ -126,7 +128,7 @@ class Falyx:
|
|
126
128
|
never_prompt (bool): Seed default for `OptionsManager["never_prompt"]`
|
127
129
|
force_confirm (bool): Seed default for `OptionsManager["force_confirm"]`
|
128
130
|
cli_args (Namespace | None): Parsed CLI arguments, usually from argparse.
|
129
|
-
options (OptionsManager | None): Declarative option mappings.
|
131
|
+
options (OptionsManager | None): Declarative option mappings for global state.
|
130
132
|
custom_table (Callable[[Falyx], Table] | Table | None): Custom menu table
|
131
133
|
generator.
|
132
134
|
|
@@ -160,6 +162,7 @@ class Falyx:
|
|
160
162
|
options: OptionsManager | None = None,
|
161
163
|
render_menu: Callable[[Falyx], None] | None = None,
|
162
164
|
custom_table: Callable[[Falyx], Table] | Table | None = None,
|
165
|
+
hide_menu_table: bool = False,
|
163
166
|
) -> None:
|
164
167
|
"""Initializes the Falyx object."""
|
165
168
|
self.title: str | Markdown = title
|
@@ -185,6 +188,7 @@ class Falyx:
|
|
185
188
|
self.cli_args: Namespace | None = cli_args
|
186
189
|
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
187
190
|
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
191
|
+
self.hide_menu_table: bool = hide_menu_table
|
188
192
|
self.validate_options(cli_args, options)
|
189
193
|
self._prompt_session: PromptSession | None = None
|
190
194
|
self.mode = FalyxMode.MENU
|
@@ -287,8 +291,6 @@ class Falyx:
|
|
287
291
|
|
288
292
|
for command in self.commands.values():
|
289
293
|
help_text = command.help_text or command.description
|
290
|
-
if command.requires_input:
|
291
|
-
help_text += " [dim](requires input)[/dim]"
|
292
294
|
table.add_row(
|
293
295
|
f"[{command.style}]{command.key}[/]",
|
294
296
|
", ".join(command.aliases) if command.aliases else "",
|
@@ -445,7 +447,6 @@ class Falyx:
|
|
445
447
|
bottom_toolbar=self._get_bottom_bar_render(),
|
446
448
|
key_bindings=self.key_bindings,
|
447
449
|
validate_while_typing=False,
|
448
|
-
interrupt_exception=FlowSignal,
|
449
450
|
)
|
450
451
|
return self._prompt_session
|
451
452
|
|
@@ -608,7 +609,6 @@ class Falyx:
|
|
608
609
|
retry: bool = False,
|
609
610
|
retry_all: bool = False,
|
610
611
|
retry_policy: RetryPolicy | None = None,
|
611
|
-
requires_input: bool | None = None,
|
612
612
|
arg_parser: CommandArgumentParser | None = None,
|
613
613
|
arguments: list[dict[str, Any]] | None = None,
|
614
614
|
argument_config: Callable[[CommandArgumentParser], None] | None = None,
|
@@ -660,7 +660,6 @@ class Falyx:
|
|
660
660
|
retry=retry,
|
661
661
|
retry_all=retry_all,
|
662
662
|
retry_policy=retry_policy or RetryPolicy(),
|
663
|
-
requires_input=requires_input,
|
664
663
|
options_manager=self.options,
|
665
664
|
arg_parser=arg_parser,
|
666
665
|
arguments=arguments or [],
|
@@ -768,26 +767,27 @@ class Falyx:
|
|
768
767
|
|
769
768
|
choice = choice.upper()
|
770
769
|
name_map = self._name_map
|
771
|
-
if choice
|
770
|
+
if name_map.get(choice):
|
772
771
|
if not from_validate:
|
773
772
|
logger.info("Command '%s' selected.", choice)
|
774
|
-
if
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
else:
|
784
|
-
name_map[choice].show_help()
|
785
|
-
raise ValidationError(
|
786
|
-
message=str(error), cursor_position=len(raw_choices)
|
773
|
+
if is_preview:
|
774
|
+
return True, name_map[choice], args, kwargs
|
775
|
+
try:
|
776
|
+
args, kwargs = name_map[choice].parse_args(input_args, from_validate)
|
777
|
+
except CommandArgumentError as error:
|
778
|
+
if not from_validate:
|
779
|
+
if not name_map[choice].show_help():
|
780
|
+
self.console.print(
|
781
|
+
f"[{OneColors.DARK_RED}]❌ Invalid arguments for '{choice}': {error}"
|
787
782
|
)
|
788
|
-
|
789
|
-
|
790
|
-
|
783
|
+
else:
|
784
|
+
name_map[choice].show_help()
|
785
|
+
raise ValidationError(
|
786
|
+
message=str(error), cursor_position=len(raw_choices)
|
787
|
+
)
|
788
|
+
return is_preview, None, args, kwargs
|
789
|
+
except HelpSignal:
|
790
|
+
return True, None, args, kwargs
|
791
791
|
return is_preview, name_map[choice], args, kwargs
|
792
792
|
|
793
793
|
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
|
@@ -975,10 +975,11 @@ class Falyx:
|
|
975
975
|
self.print_message(self.welcome_message)
|
976
976
|
try:
|
977
977
|
while True:
|
978
|
-
if
|
979
|
-
self.render_menu
|
980
|
-
|
981
|
-
|
978
|
+
if not self.hide_menu_table:
|
979
|
+
if callable(self.render_menu):
|
980
|
+
self.render_menu(self)
|
981
|
+
else:
|
982
|
+
self.console.print(self.table, justify="center")
|
982
983
|
try:
|
983
984
|
task = asyncio.create_task(self.process_command())
|
984
985
|
should_continue = await task
|
falyx/hook_manager.py
CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import inspect
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Awaitable, Callable,
|
7
|
+
from typing import Awaitable, Callable, Union
|
8
8
|
|
9
9
|
from falyx.context import ExecutionContext
|
10
10
|
from falyx.logger import logger
|
@@ -24,7 +24,7 @@ class HookType(Enum):
|
|
24
24
|
ON_TEARDOWN = "on_teardown"
|
25
25
|
|
26
26
|
@classmethod
|
27
|
-
def choices(cls) ->
|
27
|
+
def choices(cls) -> list[HookType]:
|
28
28
|
"""Return a list of all hook type choices."""
|
29
29
|
return list(cls)
|
30
30
|
|
@@ -37,16 +37,17 @@ class HookManager:
|
|
37
37
|
"""HookManager"""
|
38
38
|
|
39
39
|
def __init__(self) -> None:
|
40
|
-
self._hooks:
|
40
|
+
self._hooks: dict[HookType, list[Hook]] = {
|
41
41
|
hook_type: [] for hook_type in HookType
|
42
42
|
}
|
43
43
|
|
44
|
-
def register(self, hook_type: HookType, hook: Hook):
|
45
|
-
if
|
46
|
-
|
44
|
+
def register(self, hook_type: HookType | str, hook: Hook):
|
45
|
+
"""Raises ValueError if the hook type is not supported."""
|
46
|
+
if not isinstance(hook_type, HookType):
|
47
|
+
hook_type = HookType(hook_type)
|
47
48
|
self._hooks[hook_type].append(hook)
|
48
49
|
|
49
|
-
def clear(self, hook_type:
|
50
|
+
def clear(self, hook_type: HookType | None = None):
|
50
51
|
if hook_type:
|
51
52
|
self._hooks[hook_type] = []
|
52
53
|
else:
|
falyx/menu.py
CHANGED
@@ -33,7 +33,7 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
33
33
|
and special signal entries like Quit and Back.
|
34
34
|
"""
|
35
35
|
|
36
|
-
RESERVED_KEYS = {"
|
36
|
+
RESERVED_KEYS = {"B", "X"}
|
37
37
|
|
38
38
|
def __init__(
|
39
39
|
self,
|
@@ -49,14 +49,14 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
49
49
|
def _inject_reserved_defaults(self):
|
50
50
|
from falyx.action import SignalAction
|
51
51
|
|
52
|
-
self._add_reserved(
|
53
|
-
"Q",
|
54
|
-
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
55
|
-
)
|
56
52
|
self._add_reserved(
|
57
53
|
"B",
|
58
54
|
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
59
55
|
)
|
56
|
+
self._add_reserved(
|
57
|
+
"X",
|
58
|
+
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
59
|
+
)
|
60
60
|
|
61
61
|
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
62
62
|
"""Add a reserved key, bypassing validation."""
|
@@ -78,8 +78,20 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
78
78
|
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
79
79
|
super().__delitem__(key)
|
80
80
|
|
81
|
+
def update(self, other=None, **kwargs):
|
82
|
+
"""Update the selection options with another dictionary."""
|
83
|
+
if other:
|
84
|
+
for key, option in other.items():
|
85
|
+
if not isinstance(option, MenuOption):
|
86
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
87
|
+
self[key] = option
|
88
|
+
for key, option in kwargs.items():
|
89
|
+
if not isinstance(option, MenuOption):
|
90
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
91
|
+
self[key] = option
|
92
|
+
|
81
93
|
def items(self, include_reserved: bool = True):
|
82
|
-
for
|
83
|
-
if not include_reserved and
|
94
|
+
for key, option in super().items():
|
95
|
+
if not include_reserved and key in self.RESERVED_KEYS:
|
84
96
|
continue
|
85
|
-
yield
|
97
|
+
yield key, option
|
falyx/parsers/argparse.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
from __future__ import annotations
|
3
|
+
|
2
4
|
from copy import deepcopy
|
3
5
|
from dataclasses import dataclass
|
4
6
|
from enum import Enum
|
@@ -23,6 +25,15 @@ class ArgumentAction(Enum):
|
|
23
25
|
COUNT = "count"
|
24
26
|
HELP = "help"
|
25
27
|
|
28
|
+
@classmethod
|
29
|
+
def choices(cls) -> list[ArgumentAction]:
|
30
|
+
"""Return a list of all argument actions."""
|
31
|
+
return list(cls)
|
32
|
+
|
33
|
+
def __str__(self) -> str:
|
34
|
+
"""Return the string representation of the argument action."""
|
35
|
+
return self.value
|
36
|
+
|
26
37
|
|
27
38
|
@dataclass
|
28
39
|
class Argument:
|
falyx/parsers/utils.py
CHANGED
@@ -13,7 +13,8 @@ def same_argument_definitions(
|
|
13
13
|
arg_sets = []
|
14
14
|
for action in actions:
|
15
15
|
if isinstance(action, BaseAction):
|
16
|
-
|
16
|
+
infer_target, _ = action.get_infer_target()
|
17
|
+
arg_defs = infer_args_from_func(infer_target, arg_metadata)
|
17
18
|
elif callable(action):
|
18
19
|
arg_defs = infer_args_from_func(action, arg_metadata)
|
19
20
|
else:
|
falyx/selection.py
CHANGED
@@ -10,7 +10,7 @@ from rich.markup import escape
|
|
10
10
|
from rich.table import Table
|
11
11
|
|
12
12
|
from falyx.themes import OneColors
|
13
|
-
from falyx.utils import chunks
|
13
|
+
from falyx.utils import CaseInsensitiveDict, chunks
|
14
14
|
from falyx.validators import int_range_validator, key_validator
|
15
15
|
|
16
16
|
|
@@ -32,6 +32,62 @@ class SelectionOption:
|
|
32
32
|
return f"[{OneColors.WHITE}]{key}[/] [{self.style}]{self.description}[/]"
|
33
33
|
|
34
34
|
|
35
|
+
class SelectionOptionMap(CaseInsensitiveDict):
|
36
|
+
"""
|
37
|
+
Manages selection options including validation and reserved key protection.
|
38
|
+
"""
|
39
|
+
|
40
|
+
RESERVED_KEYS: set[str] = set()
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
options: dict[str, SelectionOption] | None = None,
|
45
|
+
allow_reserved: bool = False,
|
46
|
+
):
|
47
|
+
super().__init__()
|
48
|
+
self.allow_reserved = allow_reserved
|
49
|
+
if options:
|
50
|
+
self.update(options)
|
51
|
+
|
52
|
+
def _add_reserved(self, key: str, option: SelectionOption) -> None:
|
53
|
+
"""Add a reserved key, bypassing validation."""
|
54
|
+
norm_key = key.upper()
|
55
|
+
super().__setitem__(norm_key, option)
|
56
|
+
|
57
|
+
def __setitem__(self, key: str, option: SelectionOption) -> None:
|
58
|
+
if not isinstance(option, SelectionOption):
|
59
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
60
|
+
norm_key = key.upper()
|
61
|
+
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
62
|
+
raise ValueError(
|
63
|
+
f"Key '{key}' is reserved and cannot be used in SelectionOptionMap."
|
64
|
+
)
|
65
|
+
super().__setitem__(norm_key, option)
|
66
|
+
|
67
|
+
def __delitem__(self, key: str) -> None:
|
68
|
+
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
69
|
+
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
70
|
+
super().__delitem__(key)
|
71
|
+
|
72
|
+
def update(self, other=None, **kwargs):
|
73
|
+
"""Update the selection options with another dictionary."""
|
74
|
+
if other:
|
75
|
+
for key, option in other.items():
|
76
|
+
if not isinstance(option, SelectionOption):
|
77
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
78
|
+
self[key] = option
|
79
|
+
for key, option in kwargs.items():
|
80
|
+
if not isinstance(option, SelectionOption):
|
81
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
82
|
+
self[key] = option
|
83
|
+
|
84
|
+
def items(self, include_reserved: bool = True):
|
85
|
+
for k, v in super().items():
|
86
|
+
if not include_reserved and k in self.RESERVED_KEYS:
|
87
|
+
continue
|
88
|
+
yield k, v
|
89
|
+
|
90
|
+
|
35
91
|
def render_table_base(
|
36
92
|
title: str,
|
37
93
|
*,
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.31"
|
@@ -1,52 +1,52 @@
|
|
1
1
|
falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
falyx/__init__.py,sha256=
|
2
|
+
falyx/__init__.py,sha256=MZzno-7HvIYM6pDjDP4t22aN7OaBTTlQYOb7W3Gw_7g,615
|
3
3
|
falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
|
4
4
|
falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
falyx/action/__init__.py,sha256=zpOK5g4DybydV8d3QI0Zq52aWaKFPYi-J6szAQTsQ2c,974
|
6
|
-
falyx/action/action.py,sha256=
|
7
|
-
falyx/action/action_factory.py,sha256=
|
6
|
+
falyx/action/action.py,sha256=GWVkkbIJBPeS75YXJS9lEYdZD6LXdyMr8yU5KWkt_m4,33389
|
7
|
+
falyx/action/action_factory.py,sha256=UuEkx7x0khUe8V4vZhhXQbqkc-V0hYef1Yb3eHIIvjw,4771
|
8
8
|
falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
|
9
|
-
falyx/action/io_action.py,sha256=
|
10
|
-
falyx/action/menu_action.py,sha256=
|
11
|
-
falyx/action/select_file_action.py,sha256=
|
12
|
-
falyx/action/selection_action.py,sha256=
|
9
|
+
falyx/action/io_action.py,sha256=tQVonWst44ZXR_87H6-aQ62DZf7qfpbBiKE1oTp9qVA,10061
|
10
|
+
falyx/action/menu_action.py,sha256=Qv8iUqlQeXMT6lCXg99YuRGswGWS_tkU8VeuqkC_uQA,5763
|
11
|
+
falyx/action/select_file_action.py,sha256=VZkz4H17-XbGooniBMN9ZG406GLerHOrI_BS7hZkkTI,8615
|
12
|
+
falyx/action/selection_action.py,sha256=i0CWS70wlhnfRCCBbmHpsytHL0FynIfxc-VQqOao5g8,11608
|
13
13
|
falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
|
14
|
-
falyx/action/types.py,sha256=
|
15
|
-
falyx/action/user_input_action.py,sha256=
|
14
|
+
falyx/action/types.py,sha256=NfZz1ufZuvCgp-he2JIItbnjX7LjOUadjtKbjpRlSIY,1399
|
15
|
+
falyx/action/user_input_action.py,sha256=TQ7BlH9lQ5h_t28q6UXCH0hbr2b0vJP7syJmM-okwS0,3478
|
16
16
|
falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
|
17
|
-
falyx/command.py,sha256=
|
18
|
-
falyx/config.py,sha256=
|
17
|
+
falyx/command.py,sha256=TstVl5bkStISfVfGhOyKP9FpL6glbxXVqPw1lssMCUk,14564
|
18
|
+
falyx/config.py,sha256=sApU53PHM8h8eV9YIuTDM1qSGp24AbNy_jJ6GAZhjEo,9597
|
19
19
|
falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
|
20
20
|
falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
|
21
21
|
falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
|
22
22
|
falyx/execution_registry.py,sha256=rctsz0mrIHPToLZqylblVjDdKWdq1x_JBc8GwMP5sJ8,4710
|
23
|
-
falyx/falyx.py,sha256=
|
24
|
-
falyx/hook_manager.py,sha256=
|
23
|
+
falyx/falyx.py,sha256=s5EMkM2EOpS8Uljf-BpEfcJtVshwb3slVuvnZmbIZ_0,44105
|
24
|
+
falyx/hook_manager.py,sha256=LRRoVG3gomxI6C9HnLa24WOSZtA-UP9HmpnkT2nvBRo,2986
|
25
25
|
falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
|
26
26
|
falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
|
27
27
|
falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
|
28
|
-
falyx/menu.py,sha256=
|
28
|
+
falyx/menu.py,sha256=vZCboxLOfQfUAzTY7Y6wIwoOyRHEPq9rgEelbJ_yNaM,3452
|
29
29
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
30
30
|
falyx/parsers/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
31
|
falyx/parsers/__init__.py,sha256=l0QMf89uJHhTpOqQfiV3tx7aAHvELqDFWAyjCbwEgBQ,370
|
32
|
-
falyx/parsers/argparse.py,sha256=
|
32
|
+
falyx/parsers/argparse.py,sha256=rg4S5rJNM25ZzlTgkvV9glOcXYCsdBWSioJvbQXXpUA,30195
|
33
33
|
falyx/parsers/parsers.py,sha256=KsDFEmJLM86d2X4Kh4SHA9mBbUk351NjLhhFYzQkaPk,5762
|
34
34
|
falyx/parsers/signature.py,sha256=kniazHBDFIY-cb4JC-gxPL4fviAsoYf8wX0AmWKetGM,2252
|
35
|
-
falyx/parsers/utils.py,sha256=
|
35
|
+
falyx/parsers/utils.py,sha256=_DUrqOdHDBZdeAqSLCFGnfNljbXTBS0d2T5MDqxGbWQ,888
|
36
36
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
37
37
|
falyx/protocols.py,sha256=mesdq5CjPF_5Kyu7Evwr6qMT71tUHlw0SjjtmnggTZw,495
|
38
38
|
falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
|
39
39
|
falyx/retry_utils.py,sha256=EAzc-ECTu8AxKkmlw28ioOW9y-Y9tLQ0KasvSkBRYgs,694
|
40
|
-
falyx/selection.py,sha256=
|
40
|
+
falyx/selection.py,sha256=oVc2xSDnNi6BYOT7I7p24dS3CYHsLBOMcQt6bkz77B0,12887
|
41
41
|
falyx/signals.py,sha256=Y_neFXpfHs7qY0syw9XcfR9WeAGRcRw1nG_2L1JJqKE,1083
|
42
42
|
falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
|
43
43
|
falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
44
44
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
45
45
|
falyx/utils.py,sha256=u3puR4Bh-unNBw9a0V9sw7PDTIzRaNLolap0oz5bVIk,6718
|
46
46
|
falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
|
47
|
-
falyx/version.py,sha256=
|
48
|
-
falyx-0.1.
|
49
|
-
falyx-0.1.
|
50
|
-
falyx-0.1.
|
51
|
-
falyx-0.1.
|
52
|
-
falyx-0.1.
|
47
|
+
falyx/version.py,sha256=i-fDEsQ0iAiPKXFaj9eERDqcxl3BqNnavaCEqpNxmVI,23
|
48
|
+
falyx-0.1.31.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
49
|
+
falyx-0.1.31.dist-info/METADATA,sha256=IFXJ3n8MaZDsO53drqGeltDS4dqWI_QUqYihQbFCwhI,5521
|
50
|
+
falyx-0.1.31.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
51
|
+
falyx-0.1.31.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
52
|
+
falyx-0.1.31.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|