falyx 0.1.19__py3-none-any.whl → 0.1.21__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.py +51 -28
- falyx/action_factory.py +82 -10
- falyx/bottom_bar.py +1 -1
- falyx/command.py +1 -1
- falyx/execution_registry.py +1 -1
- falyx/falyx.py +44 -12
- falyx/http_action.py +4 -4
- falyx/io_action.py +5 -55
- falyx/menu_action.py +5 -3
- falyx/protocols.py +9 -0
- falyx/select_file_action.py +193 -0
- falyx/selection_action.py +2 -2
- falyx/version.py +1 -1
- {falyx-0.1.19.dist-info → falyx-0.1.21.dist-info}/METADATA +1 -1
- {falyx-0.1.19.dist-info → falyx-0.1.21.dist-info}/RECORD +18 -17
- falyx/select_files_action.py +0 -68
- {falyx-0.1.19.dist-info → falyx-0.1.21.dist-info}/LICENSE +0 -0
- {falyx-0.1.19.dist-info → falyx-0.1.21.dist-info}/WHEEL +0 -0
- {falyx-0.1.19.dist-info → falyx-0.1.21.dist-info}/entry_points.txt +0 -0
falyx/action.py
CHANGED
@@ -56,7 +56,7 @@ class BaseAction(ABC):
|
|
56
56
|
be run independently or as part of Falyx.
|
57
57
|
|
58
58
|
inject_last_result (bool): Whether to inject the previous action's result into kwargs.
|
59
|
-
|
59
|
+
inject_into (str): The name of the kwarg key to inject the result as
|
60
60
|
(default: 'last_result').
|
61
61
|
_requires_injection (bool): Whether the action requires input injection.
|
62
62
|
"""
|
@@ -64,9 +64,10 @@ class BaseAction(ABC):
|
|
64
64
|
def __init__(
|
65
65
|
self,
|
66
66
|
name: str,
|
67
|
+
*,
|
67
68
|
hooks: HookManager | None = None,
|
68
69
|
inject_last_result: bool = False,
|
69
|
-
|
70
|
+
inject_into: str = "last_result",
|
70
71
|
never_prompt: bool = False,
|
71
72
|
logging_hooks: bool = False,
|
72
73
|
) -> None:
|
@@ -75,7 +76,7 @@ class BaseAction(ABC):
|
|
75
76
|
self.is_retryable: bool = False
|
76
77
|
self.shared_context: SharedContext | None = None
|
77
78
|
self.inject_last_result: bool = inject_last_result
|
78
|
-
self.
|
79
|
+
self.inject_into: str = inject_into
|
79
80
|
self._never_prompt: bool = never_prompt
|
80
81
|
self._requires_injection: bool = False
|
81
82
|
self._skip_in_chain: bool = False
|
@@ -133,7 +134,7 @@ class BaseAction(ABC):
|
|
133
134
|
|
134
135
|
def _maybe_inject_last_result(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
135
136
|
if self.inject_last_result and self.shared_context:
|
136
|
-
key = self.
|
137
|
+
key = self.inject_into
|
137
138
|
if key in kwargs:
|
138
139
|
logger.warning("[%s] ⚠️ Overriding '%s' with last_result", self.name, key)
|
139
140
|
kwargs = dict(kwargs)
|
@@ -173,7 +174,7 @@ class Action(BaseAction):
|
|
173
174
|
kwargs (dict, optional): Static keyword arguments.
|
174
175
|
hooks (HookManager, optional): Hook manager for lifecycle events.
|
175
176
|
inject_last_result (bool, optional): Enable last_result injection.
|
176
|
-
|
177
|
+
inject_into (str, optional): Name of injected key.
|
177
178
|
retry (bool, optional): Enable retry logic.
|
178
179
|
retry_policy (RetryPolicy, optional): Retry settings.
|
179
180
|
"""
|
@@ -182,16 +183,22 @@ class Action(BaseAction):
|
|
182
183
|
self,
|
183
184
|
name: str,
|
184
185
|
action: Callable[..., Any],
|
186
|
+
*,
|
185
187
|
rollback: Callable[..., Any] | None = None,
|
186
188
|
args: tuple[Any, ...] = (),
|
187
189
|
kwargs: dict[str, Any] | None = None,
|
188
190
|
hooks: HookManager | None = None,
|
189
191
|
inject_last_result: bool = False,
|
190
|
-
|
192
|
+
inject_into: str = "last_result",
|
191
193
|
retry: bool = False,
|
192
194
|
retry_policy: RetryPolicy | None = None,
|
193
195
|
) -> None:
|
194
|
-
super().__init__(
|
196
|
+
super().__init__(
|
197
|
+
name,
|
198
|
+
hooks=hooks,
|
199
|
+
inject_last_result=inject_last_result,
|
200
|
+
inject_into=inject_into,
|
201
|
+
)
|
195
202
|
self.action = action
|
196
203
|
self.rollback = rollback
|
197
204
|
self.args = args
|
@@ -257,7 +264,7 @@ class Action(BaseAction):
|
|
257
264
|
if context.result is not None:
|
258
265
|
logger.info("[%s] ✅ Recovered: %s", self.name, self.name)
|
259
266
|
return context.result
|
260
|
-
raise
|
267
|
+
raise
|
261
268
|
finally:
|
262
269
|
context.stop_timer()
|
263
270
|
await self.hooks.trigger(HookType.AFTER, context)
|
@@ -267,7 +274,7 @@ class Action(BaseAction):
|
|
267
274
|
async def preview(self, parent: Tree | None = None):
|
268
275
|
label = [f"[{OneColors.GREEN_b}]⚙ Action[/] '{self.name}'"]
|
269
276
|
if self.inject_last_result:
|
270
|
-
label.append(f" [dim](injects '{self.
|
277
|
+
label.append(f" [dim](injects '{self.inject_into}')[/dim]")
|
271
278
|
if self.retry_policy.enabled:
|
272
279
|
label.append(
|
273
280
|
f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, "
|
@@ -413,7 +420,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
413
420
|
actions (list): List of actions or literals to execute.
|
414
421
|
hooks (HookManager, optional): Hooks for lifecycle events.
|
415
422
|
inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
|
416
|
-
|
423
|
+
inject_into (str, optional): Key name for injection.
|
417
424
|
auto_inject (bool, optional): Auto-enable injection for subsequent actions.
|
418
425
|
return_list (bool, optional): Whether to return a list of all results. False returns the last result.
|
419
426
|
"""
|
@@ -422,13 +429,19 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
422
429
|
self,
|
423
430
|
name: str,
|
424
431
|
actions: list[BaseAction | Any] | None = None,
|
432
|
+
*,
|
425
433
|
hooks: HookManager | None = None,
|
426
434
|
inject_last_result: bool = False,
|
427
|
-
|
435
|
+
inject_into: str = "last_result",
|
428
436
|
auto_inject: bool = False,
|
429
437
|
return_list: bool = False,
|
430
438
|
) -> None:
|
431
|
-
super().__init__(
|
439
|
+
super().__init__(
|
440
|
+
name,
|
441
|
+
hooks=hooks,
|
442
|
+
inject_last_result=inject_last_result,
|
443
|
+
inject_into=inject_into,
|
444
|
+
)
|
432
445
|
ActionListMixin.__init__(self)
|
433
446
|
self.auto_inject = auto_inject
|
434
447
|
self.return_list = return_list
|
@@ -482,9 +495,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
482
495
|
last_result = shared_context.last_result()
|
483
496
|
try:
|
484
497
|
if self.requires_io_injection() and last_result is not None:
|
485
|
-
result = await prepared(
|
486
|
-
**{prepared.inject_last_result_as: last_result}
|
487
|
-
)
|
498
|
+
result = await prepared(**{prepared.inject_into: last_result})
|
488
499
|
else:
|
489
500
|
result = await prepared(*args, **updated_kwargs)
|
490
501
|
except Exception as error:
|
@@ -559,7 +570,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
559
570
|
async def preview(self, parent: Tree | None = None):
|
560
571
|
label = [f"[{OneColors.CYAN_b}]⛓ ChainedAction[/] '{self.name}'"]
|
561
572
|
if self.inject_last_result:
|
562
|
-
label.append(f" [dim](injects '{self.
|
573
|
+
label.append(f" [dim](injects '{self.inject_into}')[/dim]")
|
563
574
|
tree = parent.add("".join(label)) if parent else Tree("".join(label))
|
564
575
|
for action in self.actions:
|
565
576
|
await action.preview(parent=tree)
|
@@ -603,18 +614,24 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
603
614
|
actions (list): List of actions or literals to execute.
|
604
615
|
hooks (HookManager, optional): Hooks for lifecycle events.
|
605
616
|
inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
|
606
|
-
|
617
|
+
inject_into (str, optional): Key name for injection.
|
607
618
|
"""
|
608
619
|
|
609
620
|
def __init__(
|
610
621
|
self,
|
611
622
|
name: str,
|
612
623
|
actions: list[BaseAction] | None = None,
|
624
|
+
*,
|
613
625
|
hooks: HookManager | None = None,
|
614
626
|
inject_last_result: bool = False,
|
615
|
-
|
627
|
+
inject_into: str = "last_result",
|
616
628
|
):
|
617
|
-
super().__init__(
|
629
|
+
super().__init__(
|
630
|
+
name,
|
631
|
+
hooks=hooks,
|
632
|
+
inject_last_result=inject_last_result,
|
633
|
+
inject_into=inject_into,
|
634
|
+
)
|
618
635
|
ActionListMixin.__init__(self)
|
619
636
|
if actions:
|
620
637
|
self.set_actions(actions)
|
@@ -694,7 +711,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
694
711
|
async def preview(self, parent: Tree | None = None):
|
695
712
|
label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (parallel)[/] '{self.name}'"]
|
696
713
|
if self.inject_last_result:
|
697
|
-
label.append(f" [dim](receives '{self.
|
714
|
+
label.append(f" [dim](receives '{self.inject_into}')[/dim]")
|
698
715
|
tree = parent.add("".join(label)) if parent else Tree("".join(label))
|
699
716
|
actions = self.actions.copy()
|
700
717
|
random.shuffle(actions)
|
@@ -726,22 +743,28 @@ class ProcessAction(BaseAction):
|
|
726
743
|
hooks (HookManager, optional): Hook manager for lifecycle events.
|
727
744
|
executor (ProcessPoolExecutor, optional): Custom executor if desired.
|
728
745
|
inject_last_result (bool, optional): Inject last result into the function.
|
729
|
-
|
746
|
+
inject_into (str, optional): Name of the injected key.
|
730
747
|
"""
|
731
748
|
|
732
749
|
def __init__(
|
733
750
|
self,
|
734
751
|
name: str,
|
735
|
-
|
752
|
+
action: Callable[..., Any],
|
753
|
+
*,
|
736
754
|
args: tuple = (),
|
737
755
|
kwargs: dict[str, Any] | None = None,
|
738
756
|
hooks: HookManager | None = None,
|
739
757
|
executor: ProcessPoolExecutor | None = None,
|
740
758
|
inject_last_result: bool = False,
|
741
|
-
|
759
|
+
inject_into: str = "last_result",
|
742
760
|
):
|
743
|
-
super().__init__(
|
744
|
-
|
761
|
+
super().__init__(
|
762
|
+
name,
|
763
|
+
hooks=hooks,
|
764
|
+
inject_last_result=inject_last_result,
|
765
|
+
inject_into=inject_into,
|
766
|
+
)
|
767
|
+
self.action = action
|
745
768
|
self.args = args
|
746
769
|
self.kwargs = kwargs or {}
|
747
770
|
self.executor = executor or ProcessPoolExecutor()
|
@@ -769,7 +792,7 @@ class ProcessAction(BaseAction):
|
|
769
792
|
try:
|
770
793
|
await self.hooks.trigger(HookType.BEFORE, context)
|
771
794
|
result = await loop.run_in_executor(
|
772
|
-
self.executor, partial(self.
|
795
|
+
self.executor, partial(self.action, *combined_args, **combined_kwargs)
|
773
796
|
)
|
774
797
|
context.result = result
|
775
798
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
@@ -800,7 +823,7 @@ class ProcessAction(BaseAction):
|
|
800
823
|
f"[{OneColors.DARK_YELLOW_b}]🧠 ProcessAction (new process)[/] '{self.name}'"
|
801
824
|
]
|
802
825
|
if self.inject_last_result:
|
803
|
-
label.append(f" [dim](injects '{self.
|
826
|
+
label.append(f" [dim](injects '{self.inject_into}')[/dim]")
|
804
827
|
if parent:
|
805
828
|
parent.add("".join(label))
|
806
829
|
else:
|
@@ -808,6 +831,6 @@ class ProcessAction(BaseAction):
|
|
808
831
|
|
809
832
|
def __str__(self) -> str:
|
810
833
|
return (
|
811
|
-
f"ProcessAction(name={self.name!r},
|
834
|
+
f"ProcessAction(name={self.name!r}, action={getattr(self.action, '__name__', repr(self.action))}, "
|
812
835
|
f"args={self.args!r}, kwargs={self.kwargs!r})"
|
813
836
|
)
|
falyx/action_factory.py
CHANGED
@@ -1,23 +1,95 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from rich.tree import Tree
|
2
4
|
|
3
5
|
from falyx.action import BaseAction
|
6
|
+
from falyx.context import ExecutionContext
|
7
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
8
|
+
from falyx.hook_manager import HookType
|
9
|
+
from falyx.protocols import ActionFactoryProtocol
|
10
|
+
from falyx.themes.colors import OneColors
|
4
11
|
|
5
12
|
|
6
13
|
class ActionFactoryAction(BaseAction):
|
14
|
+
"""
|
15
|
+
Dynamically creates and runs another Action at runtime using a factory function.
|
16
|
+
|
17
|
+
This is useful for generating context-specific behavior (e.g., dynamic HTTPActions)
|
18
|
+
where the structure of the next action depends on runtime values.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
name (str): Name of the action.
|
22
|
+
factory (Callable): A function that returns a BaseAction given args/kwargs.
|
23
|
+
inject_last_result (bool): Whether to inject last_result into the factory.
|
24
|
+
inject_into (str): The name of the kwarg to inject last_result as.
|
25
|
+
"""
|
26
|
+
|
7
27
|
def __init__(
|
8
28
|
self,
|
9
29
|
name: str,
|
10
|
-
factory:
|
30
|
+
factory: ActionFactoryProtocol,
|
11
31
|
*,
|
12
32
|
inject_last_result: bool = False,
|
13
|
-
|
33
|
+
inject_into: str = "last_result",
|
34
|
+
preview_args: tuple[Any, ...] = (),
|
35
|
+
preview_kwargs: dict[str, Any] = {},
|
14
36
|
):
|
15
|
-
super().__init__(
|
37
|
+
super().__init__(
|
38
|
+
name=name,
|
39
|
+
inject_last_result=inject_last_result,
|
40
|
+
inject_into=inject_into,
|
41
|
+
)
|
16
42
|
self.factory = factory
|
43
|
+
self.preview_args = preview_args
|
44
|
+
self.preview_kwargs = preview_kwargs
|
45
|
+
|
46
|
+
async def _run(self, *args, **kwargs) -> Any:
|
47
|
+
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
48
|
+
context = ExecutionContext(
|
49
|
+
name=f"{self.name} (factory)",
|
50
|
+
args=args,
|
51
|
+
kwargs=updated_kwargs,
|
52
|
+
action=self,
|
53
|
+
)
|
54
|
+
context.start_timer()
|
55
|
+
try:
|
56
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
57
|
+
generated_action = self.factory(*args, **updated_kwargs)
|
58
|
+
if not isinstance(generated_action, BaseAction):
|
59
|
+
raise TypeError(
|
60
|
+
f"[{self.name}] Factory must return a BaseAction, got {type(generated_action).__name__}"
|
61
|
+
)
|
62
|
+
if self.shared_context:
|
63
|
+
generated_action.set_shared_context(self.shared_context)
|
64
|
+
if self.options_manager:
|
65
|
+
generated_action.set_options_manager(self.options_manager)
|
66
|
+
context.result = await generated_action(*args, **kwargs)
|
67
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
68
|
+
return context.result
|
69
|
+
except Exception as error:
|
70
|
+
context.exception = error
|
71
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
72
|
+
raise
|
73
|
+
finally:
|
74
|
+
context.stop_timer()
|
75
|
+
await self.hooks.trigger(HookType.AFTER, context)
|
76
|
+
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
77
|
+
er.record(context)
|
78
|
+
|
79
|
+
async def preview(self, parent: Tree | None = None):
|
80
|
+
label = f"[{OneColors.CYAN_b}]🏗️ ActionFactory[/] '{self.name}'"
|
81
|
+
tree = parent.add(label) if parent else Tree(label)
|
82
|
+
|
83
|
+
try:
|
84
|
+
generated = self.factory(*self.preview_args, **self.preview_kwargs)
|
85
|
+
if isinstance(generated, BaseAction):
|
86
|
+
await generated.preview(parent=tree)
|
87
|
+
else:
|
88
|
+
tree.add(
|
89
|
+
f"[{OneColors.DARK_RED}]⚠️ Factory did not return a BaseAction[/]"
|
90
|
+
)
|
91
|
+
except Exception as error:
|
92
|
+
tree.add(f"[{OneColors.DARK_RED}]⚠️ Preview failed: {error}[/]")
|
17
93
|
|
18
|
-
|
19
|
-
|
20
|
-
action = self.factory(kwargs)
|
21
|
-
if not isinstance(action, BaseAction):
|
22
|
-
raise TypeError(f"[{self.name}] Factory did not return a valid BaseAction.")
|
23
|
-
return action
|
94
|
+
if not parent:
|
95
|
+
self.console.print(tree)
|
falyx/bottom_bar.py
CHANGED
@@ -30,7 +30,7 @@ class BottomBar:
|
|
30
30
|
key_validator: Callable[[str], bool] | None = None,
|
31
31
|
) -> None:
|
32
32
|
self.columns = columns
|
33
|
-
self.console = Console()
|
33
|
+
self.console = Console(color_system="auto")
|
34
34
|
self._named_items: dict[str, Callable[[], HTML]] = {}
|
35
35
|
self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict()
|
36
36
|
self.toggle_keys: list[str] = []
|
falyx/command.py
CHANGED
@@ -40,7 +40,7 @@ from falyx.retry_utils import enable_retries_recursively
|
|
40
40
|
from falyx.themes.colors import OneColors
|
41
41
|
from falyx.utils import _noop, confirm_async, ensure_async, logger
|
42
42
|
|
43
|
-
console = Console()
|
43
|
+
console = Console(color_system="auto")
|
44
44
|
|
45
45
|
|
46
46
|
class Command(BaseModel):
|
falyx/execution_registry.py
CHANGED
@@ -15,7 +15,7 @@ from falyx.utils import logger
|
|
15
15
|
class ExecutionRegistry:
|
16
16
|
_store_by_name: Dict[str, List[ExecutionContext]] = defaultdict(list)
|
17
17
|
_store_all: List[ExecutionContext] = []
|
18
|
-
_console = Console(color_system="
|
18
|
+
_console = Console(color_system="auto")
|
19
19
|
|
20
20
|
@classmethod
|
21
21
|
def record(cls, context: ExecutionContext):
|
falyx/falyx.py
CHANGED
@@ -24,6 +24,7 @@ import logging
|
|
24
24
|
import sys
|
25
25
|
from argparse import Namespace
|
26
26
|
from difflib import get_close_matches
|
27
|
+
from enum import Enum
|
27
28
|
from functools import cached_property
|
28
29
|
from typing import Any, Callable
|
29
30
|
|
@@ -59,6 +60,13 @@ from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation, log
|
|
59
60
|
from falyx.version import __version__
|
60
61
|
|
61
62
|
|
63
|
+
class FalyxMode(str, Enum):
|
64
|
+
MENU = "menu"
|
65
|
+
RUN = "run"
|
66
|
+
PREVIEW = "preview"
|
67
|
+
RUN_ALL = "run-all"
|
68
|
+
|
69
|
+
|
62
70
|
class Falyx:
|
63
71
|
"""
|
64
72
|
Main menu controller for Falyx CLI applications.
|
@@ -102,12 +110,12 @@ class Falyx:
|
|
102
110
|
register_all_hooks(): Register hooks across all commands and submenus.
|
103
111
|
debug_hooks(): Log hook registration for debugging.
|
104
112
|
build_default_table(): Construct the standard Rich table layout.
|
105
|
-
|
106
113
|
"""
|
107
114
|
|
108
115
|
def __init__(
|
109
116
|
self,
|
110
117
|
title: str | Markdown = "Menu",
|
118
|
+
*,
|
111
119
|
prompt: str | AnyFormattedText = "> ",
|
112
120
|
columns: int = 3,
|
113
121
|
bottom_bar: BottomBar | str | Callable[[], Any] | None = None,
|
@@ -135,7 +143,7 @@ class Falyx:
|
|
135
143
|
self.help_command: Command | None = (
|
136
144
|
self._get_help_command() if include_help_command else None
|
137
145
|
)
|
138
|
-
self.console: Console = Console(color_system="
|
146
|
+
self.console: Console = Console(color_system="auto", theme=get_nord_theme())
|
139
147
|
self.welcome_message: str | Markdown | dict[str, Any] = welcome_message
|
140
148
|
self.exit_message: str | Markdown | dict[str, Any] = exit_message
|
141
149
|
self.hooks: HookManager = HookManager()
|
@@ -149,6 +157,7 @@ class Falyx:
|
|
149
157
|
self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table
|
150
158
|
self.validate_options(cli_args, options)
|
151
159
|
self._prompt_session: PromptSession | None = None
|
160
|
+
self.mode = FalyxMode.MENU
|
152
161
|
|
153
162
|
def validate_options(
|
154
163
|
self,
|
@@ -272,6 +281,11 @@ class Falyx:
|
|
272
281
|
)
|
273
282
|
|
274
283
|
self.console.print(table, justify="center")
|
284
|
+
if self.mode == FalyxMode.MENU:
|
285
|
+
self.console.print(
|
286
|
+
f"📦 Tip: Type '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command before running it.\n",
|
287
|
+
justify="center",
|
288
|
+
)
|
275
289
|
|
276
290
|
def _get_help_command(self) -> Command:
|
277
291
|
"""Returns the help command for the menu."""
|
@@ -329,7 +343,8 @@ class Falyx:
|
|
329
343
|
error_message = " ".join(message_lines)
|
330
344
|
|
331
345
|
def validator(text):
|
332
|
-
|
346
|
+
_, choice = self.get_command(text, from_validate=True)
|
347
|
+
return True if choice else False
|
333
348
|
|
334
349
|
return Validator.from_callable(
|
335
350
|
validator,
|
@@ -534,7 +549,7 @@ class Falyx:
|
|
534
549
|
)
|
535
550
|
|
536
551
|
def add_submenu(
|
537
|
-
self, key: str, description: str, submenu: "Falyx", style: str = OneColors.CYAN
|
552
|
+
self, key: str, description: str, submenu: "Falyx", *, style: str = OneColors.CYAN
|
538
553
|
) -> None:
|
539
554
|
"""Adds a submenu to the menu."""
|
540
555
|
if not isinstance(submenu, Falyx):
|
@@ -553,6 +568,7 @@ class Falyx:
|
|
553
568
|
key: str,
|
554
569
|
description: str,
|
555
570
|
action: BaseAction | Callable[[], Any],
|
571
|
+
*,
|
556
572
|
args: tuple = (),
|
557
573
|
kwargs: dict[str, Any] = {},
|
558
574
|
hidden: bool = False,
|
@@ -668,17 +684,25 @@ class Falyx:
|
|
668
684
|
else:
|
669
685
|
return self.build_default_table()
|
670
686
|
|
671
|
-
def
|
687
|
+
def parse_preview_command(self, input_str: str) -> tuple[bool, str]:
|
688
|
+
if input_str.startswith("?"):
|
689
|
+
return True, input_str[1:].strip()
|
690
|
+
return False, input_str.strip()
|
691
|
+
|
692
|
+
def get_command(
|
693
|
+
self, choice: str, from_validate=False
|
694
|
+
) -> tuple[bool, Command | None]:
|
672
695
|
"""Returns the selected command based on user input. Supports keys, aliases, and abbreviations."""
|
696
|
+
is_preview, choice = self.parse_preview_command(choice)
|
673
697
|
choice = choice.upper()
|
674
698
|
name_map = self._name_map
|
675
699
|
|
676
700
|
if choice in name_map:
|
677
|
-
return name_map[choice]
|
701
|
+
return is_preview, name_map[choice]
|
678
702
|
|
679
703
|
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
|
680
704
|
if len(prefix_matches) == 1:
|
681
|
-
return prefix_matches[0]
|
705
|
+
return is_preview, prefix_matches[0]
|
682
706
|
|
683
707
|
fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7)
|
684
708
|
if fuzzy_matches:
|
@@ -694,7 +718,7 @@ class Falyx:
|
|
694
718
|
self.console.print(
|
695
719
|
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
|
696
720
|
)
|
697
|
-
return None
|
721
|
+
return is_preview, None
|
698
722
|
|
699
723
|
def _create_context(self, selected_command: Command) -> ExecutionContext:
|
700
724
|
"""Creates a context dictionary for the selected command."""
|
@@ -718,11 +742,16 @@ class Falyx:
|
|
718
742
|
async def process_command(self) -> bool:
|
719
743
|
"""Processes the action of the selected command."""
|
720
744
|
choice = await self.prompt_session.prompt_async()
|
721
|
-
selected_command = self.get_command(choice)
|
745
|
+
is_preview, selected_command = self.get_command(choice)
|
722
746
|
if not selected_command:
|
723
747
|
logger.info(f"Invalid command '{choice}'.")
|
724
748
|
return True
|
725
749
|
|
750
|
+
if is_preview:
|
751
|
+
logger.info(f"Preview command '{selected_command.key}' selected.")
|
752
|
+
await selected_command.preview()
|
753
|
+
return True
|
754
|
+
|
726
755
|
if selected_command.requires_input:
|
727
756
|
program = get_program_invocation()
|
728
757
|
self.console.print(
|
@@ -759,7 +788,7 @@ class Falyx:
|
|
759
788
|
async def run_key(self, command_key: str, return_context: bool = False) -> Any:
|
760
789
|
"""Run a command by key without displaying the menu (non-interactive mode)."""
|
761
790
|
self.debug_hooks()
|
762
|
-
selected_command = self.get_command(command_key)
|
791
|
+
_, selected_command = self.get_command(command_key)
|
763
792
|
self.last_run_command = selected_command
|
764
793
|
|
765
794
|
if not selected_command:
|
@@ -899,7 +928,8 @@ class Falyx:
|
|
899
928
|
sys.exit(0)
|
900
929
|
|
901
930
|
if self.cli_args.command == "preview":
|
902
|
-
|
931
|
+
self.mode = FalyxMode.PREVIEW
|
932
|
+
_, command = self.get_command(self.cli_args.name)
|
903
933
|
if not command:
|
904
934
|
self.console.print(
|
905
935
|
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
|
@@ -912,7 +942,8 @@ class Falyx:
|
|
912
942
|
sys.exit(0)
|
913
943
|
|
914
944
|
if self.cli_args.command == "run":
|
915
|
-
|
945
|
+
self.mode = FalyxMode.RUN
|
946
|
+
_, command = self.get_command(self.cli_args.name)
|
916
947
|
if not command:
|
917
948
|
self.console.print(
|
918
949
|
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
|
@@ -927,6 +958,7 @@ class Falyx:
|
|
927
958
|
sys.exit(0)
|
928
959
|
|
929
960
|
if self.cli_args.command == "run-all":
|
961
|
+
self.mode = FalyxMode.RUN_ALL
|
930
962
|
matching = [
|
931
963
|
cmd
|
932
964
|
for cmd in self.commands.values()
|
falyx/http_action.py
CHANGED
@@ -56,7 +56,7 @@ class HTTPAction(Action):
|
|
56
56
|
data (Any, optional): Raw data or form-encoded body.
|
57
57
|
hooks (HookManager, optional): Hook manager for lifecycle events.
|
58
58
|
inject_last_result (bool): Enable last_result injection.
|
59
|
-
|
59
|
+
inject_into (str): Name of injected key.
|
60
60
|
retry (bool): Enable retry logic.
|
61
61
|
retry_policy (RetryPolicy): Retry settings.
|
62
62
|
"""
|
@@ -74,7 +74,7 @@ class HTTPAction(Action):
|
|
74
74
|
data: Any = None,
|
75
75
|
hooks=None,
|
76
76
|
inject_last_result: bool = False,
|
77
|
-
|
77
|
+
inject_into: str = "last_result",
|
78
78
|
retry: bool = False,
|
79
79
|
retry_policy=None,
|
80
80
|
):
|
@@ -92,7 +92,7 @@ class HTTPAction(Action):
|
|
92
92
|
kwargs={},
|
93
93
|
hooks=hooks,
|
94
94
|
inject_last_result=inject_last_result,
|
95
|
-
|
95
|
+
inject_into=inject_into,
|
96
96
|
retry=retry,
|
97
97
|
retry_policy=retry_policy,
|
98
98
|
)
|
@@ -138,7 +138,7 @@ class HTTPAction(Action):
|
|
138
138
|
f"\n[dim]URL:[/] {self.url}",
|
139
139
|
]
|
140
140
|
if self.inject_last_result:
|
141
|
-
label.append(f"\n[dim]Injects:[/] '{self.
|
141
|
+
label.append(f"\n[dim]Injects:[/] '{self.inject_into}'")
|
142
142
|
if self.retry_policy and self.retry_policy.enabled:
|
143
143
|
label.append(
|
144
144
|
f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, "
|
falyx/io_action.py
CHANGED
@@ -20,7 +20,6 @@ import subprocess
|
|
20
20
|
import sys
|
21
21
|
from typing import Any
|
22
22
|
|
23
|
-
from rich.console import Console
|
24
23
|
from rich.tree import Tree
|
25
24
|
|
26
25
|
from falyx.action import BaseAction
|
@@ -31,8 +30,6 @@ from falyx.hook_manager import HookManager, HookType
|
|
31
30
|
from falyx.themes.colors import OneColors
|
32
31
|
from falyx.utils import logger
|
33
32
|
|
34
|
-
console = Console()
|
35
|
-
|
36
33
|
|
37
34
|
class BaseIOAction(BaseAction):
|
38
35
|
"""
|
@@ -83,7 +80,7 @@ class BaseIOAction(BaseAction):
|
|
83
80
|
raise NotImplementedError
|
84
81
|
|
85
82
|
async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes:
|
86
|
-
last_result = kwargs.pop(self.
|
83
|
+
last_result = kwargs.pop(self.inject_into, None)
|
87
84
|
|
88
85
|
data = await self._read_stdin()
|
89
86
|
if data:
|
@@ -168,26 +165,11 @@ class BaseIOAction(BaseAction):
|
|
168
165
|
async def preview(self, parent: Tree | None = None):
|
169
166
|
label = [f"[{OneColors.GREEN_b}]⚙ IOAction[/] '{self.name}'"]
|
170
167
|
if self.inject_last_result:
|
171
|
-
label.append(f" [dim](injects '{self.
|
168
|
+
label.append(f" [dim](injects '{self.inject_into}')[/dim]")
|
172
169
|
if parent:
|
173
170
|
parent.add("".join(label))
|
174
171
|
else:
|
175
|
-
console.print(Tree("".join(label)))
|
176
|
-
|
177
|
-
|
178
|
-
class UppercaseIO(BaseIOAction):
|
179
|
-
def from_input(self, raw: str | bytes) -> str:
|
180
|
-
if not isinstance(raw, (str, bytes)):
|
181
|
-
raise TypeError(
|
182
|
-
f"{self.name} expected str or bytes input, got {type(raw).__name__}"
|
183
|
-
)
|
184
|
-
return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
|
185
|
-
|
186
|
-
async def _run(self, parsed_input: str, *args, **kwargs) -> str:
|
187
|
-
return parsed_input.upper()
|
188
|
-
|
189
|
-
def to_output(self, data: str) -> str:
|
190
|
-
return data + "\n"
|
172
|
+
self.console.print(Tree("".join(label)))
|
191
173
|
|
192
174
|
|
193
175
|
class ShellAction(BaseIOAction):
|
@@ -243,45 +225,13 @@ class ShellAction(BaseIOAction):
|
|
243
225
|
async def preview(self, parent: Tree | None = None):
|
244
226
|
label = [f"[{OneColors.GREEN_b}]⚙ ShellAction[/] '{self.name}'"]
|
245
227
|
if self.inject_last_result:
|
246
|
-
label.append(f" [dim](injects '{self.
|
228
|
+
label.append(f" [dim](injects '{self.inject_into}')[/dim]")
|
247
229
|
if parent:
|
248
230
|
parent.add("".join(label))
|
249
231
|
else:
|
250
|
-
console.print(Tree("".join(label)))
|
232
|
+
self.console.print(Tree("".join(label)))
|
251
233
|
|
252
234
|
def __str__(self):
|
253
235
|
return (
|
254
236
|
f"ShellAction(name={self.name!r}, command_template={self.command_template!r})"
|
255
237
|
)
|
256
|
-
|
257
|
-
|
258
|
-
class GrepAction(BaseIOAction):
|
259
|
-
def __init__(self, name: str, pattern: str, **kwargs):
|
260
|
-
super().__init__(name=name, **kwargs)
|
261
|
-
self.pattern = pattern
|
262
|
-
|
263
|
-
def from_input(self, raw: str | bytes) -> str:
|
264
|
-
if not isinstance(raw, (str, bytes)):
|
265
|
-
raise TypeError(
|
266
|
-
f"{self.name} expected str or bytes input, got {type(raw).__name__}"
|
267
|
-
)
|
268
|
-
return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
|
269
|
-
|
270
|
-
async def _run(self, parsed_input: str) -> str:
|
271
|
-
command = ["grep", "-n", self.pattern]
|
272
|
-
process = subprocess.Popen(
|
273
|
-
command,
|
274
|
-
stdin=subprocess.PIPE,
|
275
|
-
stdout=subprocess.PIPE,
|
276
|
-
stderr=subprocess.PIPE,
|
277
|
-
text=True,
|
278
|
-
)
|
279
|
-
stdout, stderr = process.communicate(input=parsed_input)
|
280
|
-
if process.returncode == 1:
|
281
|
-
return ""
|
282
|
-
if process.returncode != 0:
|
283
|
-
raise RuntimeError(stderr.strip())
|
284
|
-
return stdout.strip()
|
285
|
-
|
286
|
-
def to_output(self, result: str) -> str:
|
287
|
-
return result
|
falyx/menu_action.py
CHANGED
@@ -45,7 +45,9 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
45
45
|
RESERVED_KEYS = {"Q", "B"}
|
46
46
|
|
47
47
|
def __init__(
|
48
|
-
self,
|
48
|
+
self,
|
49
|
+
options: dict[str, MenuOption] | None = None,
|
50
|
+
allow_reserved: bool = False,
|
49
51
|
):
|
50
52
|
super().__init__()
|
51
53
|
self.allow_reserved = allow_reserved
|
@@ -101,7 +103,7 @@ class MenuAction(BaseAction):
|
|
101
103
|
prompt_message: str = "Select > ",
|
102
104
|
default_selection: str = "",
|
103
105
|
inject_last_result: bool = False,
|
104
|
-
|
106
|
+
inject_into: str = "last_result",
|
105
107
|
console: Console | None = None,
|
106
108
|
prompt_session: PromptSession | None = None,
|
107
109
|
never_prompt: bool = False,
|
@@ -111,7 +113,7 @@ class MenuAction(BaseAction):
|
|
111
113
|
super().__init__(
|
112
114
|
name,
|
113
115
|
inject_last_result=inject_last_result,
|
114
|
-
|
116
|
+
inject_into=inject_into,
|
115
117
|
never_prompt=never_prompt,
|
116
118
|
)
|
117
119
|
self.menu_options = menu_options
|
falyx/protocols.py
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import csv
|
4
|
+
import json
|
5
|
+
import xml.etree.ElementTree as ET
|
6
|
+
from enum import Enum
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
import toml
|
11
|
+
import yaml
|
12
|
+
from prompt_toolkit import PromptSession
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.tree import Tree
|
15
|
+
|
16
|
+
from falyx.action import BaseAction
|
17
|
+
from falyx.context import ExecutionContext
|
18
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
19
|
+
from falyx.hook_manager import HookType
|
20
|
+
from falyx.selection import (
|
21
|
+
SelectionOption,
|
22
|
+
prompt_for_selection,
|
23
|
+
render_selection_dict_table,
|
24
|
+
)
|
25
|
+
from falyx.themes.colors import OneColors
|
26
|
+
from falyx.utils import logger
|
27
|
+
|
28
|
+
|
29
|
+
class FileReturnType(Enum):
|
30
|
+
TEXT = "text"
|
31
|
+
PATH = "path"
|
32
|
+
JSON = "json"
|
33
|
+
TOML = "toml"
|
34
|
+
YAML = "yaml"
|
35
|
+
CSV = "csv"
|
36
|
+
XML = "xml"
|
37
|
+
|
38
|
+
|
39
|
+
class SelectFileAction(BaseAction):
|
40
|
+
"""
|
41
|
+
SelectFileAction allows users to select a file from a directory and return:
|
42
|
+
- file content (as text, JSON, CSV, etc.)
|
43
|
+
- or the file path itself.
|
44
|
+
|
45
|
+
Supported formats: text, json, yaml, toml, csv, xml.
|
46
|
+
|
47
|
+
Useful for:
|
48
|
+
- dynamically loading config files
|
49
|
+
- interacting with user-selected data
|
50
|
+
- chaining file contents into workflows
|
51
|
+
|
52
|
+
Args:
|
53
|
+
name (str): Name of the action.
|
54
|
+
directory (Path | str): Where to search for files.
|
55
|
+
title (str): Title of the selection menu.
|
56
|
+
columns (int): Number of columns in the selection menu.
|
57
|
+
prompt_message (str): Message to display when prompting for selection.
|
58
|
+
style (str): Style for the selection options.
|
59
|
+
suffix_filter (str | None): Restrict to certain file types.
|
60
|
+
return_type (FileReturnType): What to return (path, content, parsed).
|
61
|
+
console (Console | None): Console instance for output.
|
62
|
+
prompt_session (PromptSession | None): Prompt session for user input.
|
63
|
+
"""
|
64
|
+
|
65
|
+
def __init__(
|
66
|
+
self,
|
67
|
+
name: str,
|
68
|
+
directory: Path | str = ".",
|
69
|
+
*,
|
70
|
+
title: str = "Select a file",
|
71
|
+
columns: int = 3,
|
72
|
+
prompt_message: str = "Choose > ",
|
73
|
+
style: str = OneColors.WHITE,
|
74
|
+
suffix_filter: str | None = None,
|
75
|
+
return_type: FileReturnType = FileReturnType.PATH,
|
76
|
+
console: Console | None = None,
|
77
|
+
prompt_session: PromptSession | None = None,
|
78
|
+
):
|
79
|
+
super().__init__(name)
|
80
|
+
self.directory = Path(directory).resolve()
|
81
|
+
self.title = title
|
82
|
+
self.columns = columns
|
83
|
+
self.prompt_message = prompt_message
|
84
|
+
self.suffix_filter = suffix_filter
|
85
|
+
self.style = style
|
86
|
+
self.return_type = return_type
|
87
|
+
self.console = console or Console(color_system="auto")
|
88
|
+
self.prompt_session = prompt_session or PromptSession()
|
89
|
+
|
90
|
+
def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
|
91
|
+
value: Any
|
92
|
+
options = {}
|
93
|
+
for index, file in enumerate(files):
|
94
|
+
try:
|
95
|
+
if self.return_type == FileReturnType.TEXT:
|
96
|
+
value = file.read_text(encoding="UTF-8")
|
97
|
+
elif self.return_type == FileReturnType.PATH:
|
98
|
+
value = file
|
99
|
+
elif self.return_type == FileReturnType.JSON:
|
100
|
+
value = json.loads(file.read_text(encoding="UTF-8"))
|
101
|
+
elif self.return_type == FileReturnType.TOML:
|
102
|
+
value = toml.loads(file.read_text(encoding="UTF-8"))
|
103
|
+
elif self.return_type == FileReturnType.YAML:
|
104
|
+
value = yaml.safe_load(file.read_text(encoding="UTF-8"))
|
105
|
+
elif self.return_type == FileReturnType.CSV:
|
106
|
+
with open(file, newline="", encoding="UTF-8") as csvfile:
|
107
|
+
reader = csv.reader(csvfile)
|
108
|
+
value = list(reader)
|
109
|
+
elif self.return_type == FileReturnType.XML:
|
110
|
+
tree = ET.parse(file, parser=ET.XMLParser(encoding="UTF-8"))
|
111
|
+
root = tree.getroot()
|
112
|
+
value = ET.tostring(root, encoding="unicode")
|
113
|
+
else:
|
114
|
+
raise ValueError(f"Unsupported return type: {self.return_type}")
|
115
|
+
|
116
|
+
options[str(index)] = SelectionOption(
|
117
|
+
description=file.name, value=value, style=self.style
|
118
|
+
)
|
119
|
+
except Exception as error:
|
120
|
+
logger.warning("[ERROR] Failed to parse %s: %s", file.name, error)
|
121
|
+
return options
|
122
|
+
|
123
|
+
async def _run(self, *args, **kwargs) -> Any:
|
124
|
+
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
125
|
+
context.start_timer()
|
126
|
+
try:
|
127
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
128
|
+
|
129
|
+
files = [
|
130
|
+
f
|
131
|
+
for f in self.directory.iterdir()
|
132
|
+
if f.is_file()
|
133
|
+
and (self.suffix_filter is None or f.suffix == self.suffix_filter)
|
134
|
+
]
|
135
|
+
if not files:
|
136
|
+
raise FileNotFoundError("No files found in directory.")
|
137
|
+
|
138
|
+
options = self.get_options(files)
|
139
|
+
|
140
|
+
table = render_selection_dict_table(self.title, options, self.columns)
|
141
|
+
|
142
|
+
key = await prompt_for_selection(
|
143
|
+
options.keys(),
|
144
|
+
table,
|
145
|
+
console=self.console,
|
146
|
+
prompt_session=self.prompt_session,
|
147
|
+
prompt_message=self.prompt_message,
|
148
|
+
)
|
149
|
+
|
150
|
+
result = options[key].value
|
151
|
+
context.result = result
|
152
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
153
|
+
return result
|
154
|
+
except Exception as error:
|
155
|
+
context.exception = error
|
156
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
157
|
+
raise
|
158
|
+
finally:
|
159
|
+
context.stop_timer()
|
160
|
+
await self.hooks.trigger(HookType.AFTER, context)
|
161
|
+
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
162
|
+
er.record(context)
|
163
|
+
|
164
|
+
async def preview(self, parent: Tree | None = None):
|
165
|
+
label = f"[{OneColors.GREEN}]📁 SelectFilesAction[/] '{self.name}'"
|
166
|
+
tree = parent.add(label) if parent else Tree(label)
|
167
|
+
|
168
|
+
tree.add(f"[dim]Directory:[/] {str(self.directory)}")
|
169
|
+
tree.add(f"[dim]Suffix filter:[/] {self.suffix_filter or 'None'}")
|
170
|
+
tree.add(f"[dim]Return type:[/] {self.return_type}")
|
171
|
+
tree.add(f"[dim]Prompt:[/] {self.prompt_message}")
|
172
|
+
tree.add(f"[dim]Columns:[/] {self.columns}")
|
173
|
+
try:
|
174
|
+
files = list(self.directory.iterdir())
|
175
|
+
if self.suffix_filter:
|
176
|
+
files = [f for f in files if f.suffix == self.suffix_filter]
|
177
|
+
sample = files[:10]
|
178
|
+
file_list = tree.add("[dim]Files:[/]")
|
179
|
+
for f in sample:
|
180
|
+
file_list.add(f"[dim]{f.name}[/]")
|
181
|
+
if len(files) > 10:
|
182
|
+
file_list.add(f"[dim]... ({len(files) - 10} more)[/]")
|
183
|
+
except Exception as error:
|
184
|
+
tree.add(f"[bold red]⚠️ Error scanning directory: {error}[/]")
|
185
|
+
|
186
|
+
if not parent:
|
187
|
+
self.console.print(tree)
|
188
|
+
|
189
|
+
def __str__(self) -> str:
|
190
|
+
return (
|
191
|
+
f"SelectFilesAction(name={self.name!r}, dir={str(self.directory)!r}, "
|
192
|
+
f"suffix_filter={self.suffix_filter!r}, return_type={self.return_type})"
|
193
|
+
)
|
falyx/selection_action.py
CHANGED
@@ -33,7 +33,7 @@ class SelectionAction(BaseAction):
|
|
33
33
|
prompt_message: str = "Select > ",
|
34
34
|
default_selection: str = "",
|
35
35
|
inject_last_result: bool = False,
|
36
|
-
|
36
|
+
inject_into: str = "last_result",
|
37
37
|
return_key: bool = False,
|
38
38
|
console: Console | None = None,
|
39
39
|
prompt_session: PromptSession | None = None,
|
@@ -43,7 +43,7 @@ class SelectionAction(BaseAction):
|
|
43
43
|
super().__init__(
|
44
44
|
name,
|
45
45
|
inject_last_result=inject_last_result,
|
46
|
-
|
46
|
+
inject_into=inject_into,
|
47
47
|
never_prompt=never_prompt,
|
48
48
|
)
|
49
49
|
self.selections: list[str] | CaseInsensitiveDict = selections
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.21"
|
@@ -1,39 +1,40 @@
|
|
1
1
|
falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
falyx/__init__.py,sha256=dYRamQJlT1Zoy5Uu1uG4NCV05Xk98nN1LAQrSR1CT2A,643
|
3
3
|
falyx/__main__.py,sha256=pXxXLlDot33dc4mR11Njpr4M_xbSTdKEqKWMS2aUqfk,2195
|
4
|
-
falyx/action.py,sha256=
|
5
|
-
falyx/action_factory.py,sha256=
|
6
|
-
falyx/bottom_bar.py,sha256=
|
7
|
-
falyx/command.py,sha256=
|
4
|
+
falyx/action.py,sha256=J-SG5zltYbqtdvTwBBUeEj4jp44DOKBR6G5rvmdkkTs,32147
|
5
|
+
falyx/action_factory.py,sha256=SMucCBuigKk3rlKXCEN69Sew4dVaBUxQqxyUUAHMZeo,3629
|
6
|
+
falyx/bottom_bar.py,sha256=NTen52Nfz32eWSBmJtEUuJO33u5sGQj-33IeudPVsqQ,7403
|
7
|
+
falyx/command.py,sha256=sKUcH0QQ-OXQf6UwxL_cM1lLvBPuNi8yi9cFE8HOGzY,11910
|
8
8
|
falyx/config.py,sha256=czt_EjOCT0lzWcoRE2F-oS2k1AVBFdiNuTcQXHlUVD0,4534
|
9
9
|
falyx/context.py,sha256=Dm7HV-eigU-aTv5ERah6Ow9fIRdrOsB1G6ETPIu42Gw,10070
|
10
10
|
falyx/debug.py,sha256=-jbTti29UC5zP9qQlWs3TbkOQR2f3zKSuNluh-r56wY,1551
|
11
11
|
falyx/exceptions.py,sha256=YVbhPp2BNvZoO_xqeGSRKHVQ2rdLOLf1HCjH4JTj9w8,776
|
12
|
-
falyx/execution_registry.py,sha256=
|
13
|
-
falyx/falyx.py,sha256=
|
12
|
+
falyx/execution_registry.py,sha256=IZvZr2hElzsYcMX3bAXg7sc7V2zi4RURn0OR7de2iCo,2864
|
13
|
+
falyx/falyx.py,sha256=BlzQaCnrw2uG6YG-6SOKf_LkaZAnOiBXKOfAkbLCrqw,39701
|
14
14
|
falyx/hook_manager.py,sha256=E9Vk4bdoUTeXPQ_BQEvY2Jt-jUAusc40LI8JDy3NLUw,2381
|
15
15
|
falyx/hooks.py,sha256=9zXk62DsJLJrmwTdyeNy5s-rVRvl8feuYRrfMmz6cVQ,2802
|
16
|
-
falyx/http_action.py,sha256=
|
16
|
+
falyx/http_action.py,sha256=JfopEleXJ0goVHi0VCn983c22GrmJhobnPIP7sTRqzU,5796
|
17
17
|
falyx/init.py,sha256=jP4ZNw7ycDMKw4n1HDifxWSa0NYHaGLq7_LiFt85NpA,1832
|
18
|
-
falyx/io_action.py,sha256=
|
19
|
-
falyx/menu_action.py,sha256=
|
18
|
+
falyx/io_action.py,sha256=DTMoIFgbfKvKZvjT_XW_EOSDnnCeEze9XxNyWBFKdcI,8986
|
19
|
+
falyx/menu_action.py,sha256=kagtnn3djDxUm_Cyynp0lj-sZ9D_FZn4IEBYnFYqB74,7986
|
20
20
|
falyx/options_manager.py,sha256=yYpn-moYN-bRYgMLccmi_de4mUzhTT7cv_bR2FFWZ8c,2798
|
21
21
|
falyx/parsers.py,sha256=Ki0rn2wryPDmMI9WUNBLQ5J5Y64BNten0POMZM8wPKU,5189
|
22
22
|
falyx/prompt_utils.py,sha256=JOg3p8Juv6ZdY1srfy_HlMNYfE-ajggDWLqNsjZq87I,560
|
23
|
+
falyx/protocols.py,sha256=yNtQEugq9poN-SbOJf5LL_j6HBWdglbTNghpyopLpTs,216
|
23
24
|
falyx/retry.py,sha256=GncBUiDDfDHUvLsWsWQw2Nq2XYL0TR0Fne3iXPzvQ48,3551
|
24
25
|
falyx/retry_utils.py,sha256=SN5apcsg71IG2-KylysqdJd-PkPBLoCVwsgrSTF9wrQ,666
|
25
|
-
falyx/
|
26
|
+
falyx/select_file_action.py,sha256=hKTiGEA4JcJEj3oxOiTK0eKDURCv51x5tC26NfKu08w,7260
|
26
27
|
falyx/selection.py,sha256=Hi8vfu6IuKdZ7URLiavb3uD1_Gb50D8XoKpZwAHLjmE,9859
|
27
|
-
falyx/selection_action.py,sha256=
|
28
|
+
falyx/selection_action.py,sha256=hWsfyRO0cn6Z-sBeDYNTJKZq8CCBxY2GY917U7a8Uo0,8258
|
28
29
|
falyx/signal_action.py,sha256=wfhW9miSUj9MUoc1WOyk4tU9CtYKAXusHxQdBPYLoyQ,829
|
29
30
|
falyx/signals.py,sha256=tlUbz3x6z3rYlUggan_Ntoy4bU5RbOd8UfR4cNcV6kQ,694
|
30
31
|
falyx/tagged_table.py,sha256=sn2kosRRpcpeMB8vKk47c9yjpffSz_9FXH_e6kw15mA,1019
|
31
32
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
32
33
|
falyx/utils.py,sha256=b1GQ3ooz4Io3zPE7MsoDm7j42AioTG-ZcWH-N2TRpbI,7710
|
33
34
|
falyx/validators.py,sha256=NMxqCk8Fr8HQGVDYpg8B_JRk5SKR41E_G9gj1YfQnxg,1316
|
34
|
-
falyx/version.py,sha256=
|
35
|
-
falyx-0.1.
|
36
|
-
falyx-0.1.
|
37
|
-
falyx-0.1.
|
38
|
-
falyx-0.1.
|
39
|
-
falyx-0.1.
|
35
|
+
falyx/version.py,sha256=qEmNtjnOwhDYQ0cHPPtUkUaghzD2xl0thJEznl4giYw,23
|
36
|
+
falyx-0.1.21.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
37
|
+
falyx-0.1.21.dist-info/METADATA,sha256=4T1HDUvMbwiq35W1YvIwjctQU87jTi8F62XzTGgDpKo,5484
|
38
|
+
falyx-0.1.21.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
39
|
+
falyx-0.1.21.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
40
|
+
falyx-0.1.21.dist-info/RECORD,,
|
falyx/select_files_action.py
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
|
2
|
-
class SelectFilesAction(BaseAction):
|
3
|
-
def __init__(
|
4
|
-
self,
|
5
|
-
name: str,
|
6
|
-
directory: Path | str = ".",
|
7
|
-
title: str = "Select a file",
|
8
|
-
prompt_message: str = "Choose > ",
|
9
|
-
style: str = OneColors.WHITE,
|
10
|
-
suffix_filter: str | None = None,
|
11
|
-
return_path: bool = True,
|
12
|
-
console: Console | None = None,
|
13
|
-
prompt_session: PromptSession | None = None,
|
14
|
-
):
|
15
|
-
super().__init__(name)
|
16
|
-
self.directory = Path(directory).resolve()
|
17
|
-
self.title = title
|
18
|
-
self.prompt_message = prompt_message
|
19
|
-
self.suffix_filter = suffix_filter
|
20
|
-
self.style = style
|
21
|
-
self.return_path = return_path
|
22
|
-
self.console = console or Console()
|
23
|
-
self.prompt_session = prompt_session or PromptSession()
|
24
|
-
|
25
|
-
async def _run(self, *args, **kwargs) -> Any:
|
26
|
-
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
27
|
-
context.start_timer()
|
28
|
-
try:
|
29
|
-
await self.hooks.trigger(HookType.BEFORE, context)
|
30
|
-
|
31
|
-
files = [
|
32
|
-
f
|
33
|
-
for f in self.directory.iterdir()
|
34
|
-
if f.is_file()
|
35
|
-
and (self.suffix_filter is None or f.suffix == self.suffix_filter)
|
36
|
-
]
|
37
|
-
if not files:
|
38
|
-
raise FileNotFoundError("No files found in directory.")
|
39
|
-
|
40
|
-
options = {
|
41
|
-
str(i): SelectionOption(
|
42
|
-
f.name, f if self.return_path else f.read_text(), self.style
|
43
|
-
)
|
44
|
-
for i, f in enumerate(files)
|
45
|
-
}
|
46
|
-
table = render_selection_dict_table(self.title, options)
|
47
|
-
|
48
|
-
key = await prompt_for_selection(
|
49
|
-
options.keys(),
|
50
|
-
table,
|
51
|
-
console=self.console,
|
52
|
-
prompt_session=self.prompt_session,
|
53
|
-
prompt_message=self.prompt_message,
|
54
|
-
)
|
55
|
-
|
56
|
-
result = options[key].value
|
57
|
-
context.result = result
|
58
|
-
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
59
|
-
return result
|
60
|
-
except Exception as error:
|
61
|
-
context.exception = error
|
62
|
-
await self.hooks.trigger(HookType.ON_ERROR, context)
|
63
|
-
raise
|
64
|
-
finally:
|
65
|
-
context.stop_timer()
|
66
|
-
await self.hooks.trigger(HookType.AFTER, context)
|
67
|
-
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
68
|
-
er.record(context)
|
File without changes
|
File without changes
|
File without changes
|