falyx 0.1.38__tar.gz → 0.1.39__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.38 → falyx-0.1.39}/PKG-INFO +6 -5
- {falyx-0.1.38 → falyx-0.1.39}/README.md +5 -4
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/__init__.py +2 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action_group.py +2 -1
- falyx-0.1.39/falyx/action/process_pool_action.py +166 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/argparse.py +13 -13
- falyx-0.1.39/falyx/version.py +1 -0
- {falyx-0.1.38 → falyx-0.1.39}/pyproject.toml +1 -1
- falyx-0.1.38/falyx/version.py +0 -1
- {falyx-0.1.38 → falyx-0.1.39}/LICENSE +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/.pytyped +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/__init__.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/__main__.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/.pytyped +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action_factory.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/base.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/chained_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/fallback_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/http_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/io_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/literal_input_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/menu_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/mixins.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/process_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/prompt_menu_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/select_file_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/selection_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/signal_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/types.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/action/user_input_action.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/bottom_bar.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/command.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/config.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/context.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/debug.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/exceptions.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/execution_registry.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/falyx.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/hook_manager.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/hooks.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/init.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/logger.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/menu.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/options_manager.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/.pytyped +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/__init__.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/parsers.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/signature.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/utils.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/prompt_utils.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/protocols.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/retry.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/retry_utils.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/selection.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/signals.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/tagged_table.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/themes/__init__.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/themes/colors.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/utils.py +0 -0
- {falyx-0.1.38 → falyx-0.1.39}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: falyx
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.39
|
4
4
|
Summary: Reliable and introspectable async CLI action framework.
|
5
5
|
License: MIT
|
6
6
|
Author: Roland Thomas Jr
|
@@ -75,7 +75,8 @@ poetry install
|
|
75
75
|
import asyncio
|
76
76
|
import random
|
77
77
|
|
78
|
-
from falyx import Falyx
|
78
|
+
from falyx import Falyx
|
79
|
+
from falyx.action import Action, ChainedAction
|
79
80
|
|
80
81
|
# A flaky async step that fails randomly
|
81
82
|
async def flaky_step():
|
@@ -85,8 +86,8 @@ async def flaky_step():
|
|
85
86
|
return "ok"
|
86
87
|
|
87
88
|
# Create the actions
|
88
|
-
step1 = Action(name="step_1", action=flaky_step
|
89
|
-
step2 = Action(name="step_2", action=flaky_step
|
89
|
+
step1 = Action(name="step_1", action=flaky_step)
|
90
|
+
step2 = Action(name="step_2", action=flaky_step)
|
90
91
|
|
91
92
|
# Chain the actions
|
92
93
|
chain = ChainedAction(name="my_pipeline", actions=[step1, step2])
|
@@ -97,9 +98,9 @@ falyx.add_command(
|
|
97
98
|
key="R",
|
98
99
|
description="Run My Pipeline",
|
99
100
|
action=chain,
|
100
|
-
logging_hooks=True,
|
101
101
|
preview_before_confirm=True,
|
102
102
|
confirm=True,
|
103
|
+
retry_all=True,
|
103
104
|
)
|
104
105
|
|
105
106
|
# Entry point
|
@@ -52,7 +52,8 @@ poetry install
|
|
52
52
|
import asyncio
|
53
53
|
import random
|
54
54
|
|
55
|
-
from falyx import Falyx
|
55
|
+
from falyx import Falyx
|
56
|
+
from falyx.action import Action, ChainedAction
|
56
57
|
|
57
58
|
# A flaky async step that fails randomly
|
58
59
|
async def flaky_step():
|
@@ -62,8 +63,8 @@ async def flaky_step():
|
|
62
63
|
return "ok"
|
63
64
|
|
64
65
|
# Create the actions
|
65
|
-
step1 = Action(name="step_1", action=flaky_step
|
66
|
-
step2 = Action(name="step_2", action=flaky_step
|
66
|
+
step1 = Action(name="step_1", action=flaky_step)
|
67
|
+
step2 = Action(name="step_2", action=flaky_step)
|
67
68
|
|
68
69
|
# Chain the actions
|
69
70
|
chain = ChainedAction(name="my_pipeline", actions=[step1, step2])
|
@@ -74,9 +75,9 @@ falyx.add_command(
|
|
74
75
|
key="R",
|
75
76
|
description="Run My Pipeline",
|
76
77
|
action=chain,
|
77
|
-
logging_hooks=True,
|
78
78
|
preview_before_confirm=True,
|
79
79
|
confirm=True,
|
80
|
+
retry_all=True,
|
80
81
|
)
|
81
82
|
|
82
83
|
# Entry point
|
@@ -16,6 +16,7 @@ from .io_action import BaseIOAction, ShellAction
|
|
16
16
|
from .literal_input_action import LiteralInputAction
|
17
17
|
from .menu_action import MenuAction
|
18
18
|
from .process_action import ProcessAction
|
19
|
+
from .process_pool_action import ProcessPoolAction
|
19
20
|
from .prompt_menu_action import PromptMenuAction
|
20
21
|
from .select_file_action import SelectFileAction
|
21
22
|
from .selection_action import SelectionAction
|
@@ -40,4 +41,5 @@ __all__ = [
|
|
40
41
|
"LiteralInputAction",
|
41
42
|
"UserInputAction",
|
42
43
|
"PromptMenuAction",
|
44
|
+
"ProcessPoolAction",
|
43
45
|
]
|
@@ -165,5 +165,6 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
165
165
|
def __str__(self):
|
166
166
|
return (
|
167
167
|
f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},"
|
168
|
-
f" inject_last_result={self.inject_last_result}
|
168
|
+
f" inject_last_result={self.inject_last_result}, "
|
169
|
+
f"inject_into={self.inject_into!r})"
|
169
170
|
)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import random
|
5
|
+
from concurrent.futures import ProcessPoolExecutor
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from functools import partial
|
8
|
+
from typing import Any, Callable
|
9
|
+
|
10
|
+
from rich.tree import Tree
|
11
|
+
|
12
|
+
from falyx.action.base import BaseAction
|
13
|
+
from falyx.context import ExecutionContext, SharedContext
|
14
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
15
|
+
from falyx.hook_manager import HookManager, HookType
|
16
|
+
from falyx.logger import logger
|
17
|
+
from falyx.parsers.utils import same_argument_definitions
|
18
|
+
from falyx.themes import OneColors
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class ProcessTask:
|
23
|
+
task: Callable[..., Any]
|
24
|
+
args: tuple = ()
|
25
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
26
|
+
|
27
|
+
def __post_init__(self):
|
28
|
+
if not callable(self.task):
|
29
|
+
raise TypeError(f"Expected a callable task, got {type(self.task).__name__}")
|
30
|
+
|
31
|
+
|
32
|
+
class ProcessPoolAction(BaseAction):
|
33
|
+
""" """
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
name: str,
|
38
|
+
actions: list[ProcessTask] | None = None,
|
39
|
+
*,
|
40
|
+
hooks: HookManager | None = None,
|
41
|
+
executor: ProcessPoolExecutor | None = None,
|
42
|
+
inject_last_result: bool = False,
|
43
|
+
inject_into: str = "last_result",
|
44
|
+
):
|
45
|
+
super().__init__(
|
46
|
+
name,
|
47
|
+
hooks=hooks,
|
48
|
+
inject_last_result=inject_last_result,
|
49
|
+
inject_into=inject_into,
|
50
|
+
)
|
51
|
+
self.executor = executor or ProcessPoolExecutor()
|
52
|
+
self.is_retryable = True
|
53
|
+
self.actions: list[ProcessTask] = []
|
54
|
+
if actions:
|
55
|
+
self.set_actions(actions)
|
56
|
+
|
57
|
+
def set_actions(self, actions: list[ProcessTask]) -> None:
|
58
|
+
"""Replaces the current action list with a new one."""
|
59
|
+
self.actions.clear()
|
60
|
+
for action in actions:
|
61
|
+
self.add_action(action)
|
62
|
+
|
63
|
+
def add_action(self, action: ProcessTask) -> None:
|
64
|
+
if not isinstance(action, ProcessTask):
|
65
|
+
raise TypeError(f"Expected a ProcessTask, got {type(action).__name__}")
|
66
|
+
self.actions.append(action)
|
67
|
+
|
68
|
+
def get_infer_target(self) -> tuple[Callable[..., Any] | None, None]:
|
69
|
+
arg_defs = same_argument_definitions([action.task for action in self.actions])
|
70
|
+
if arg_defs:
|
71
|
+
return self.actions[0].task, None
|
72
|
+
logger.debug(
|
73
|
+
"[%s] auto_args disabled: mismatched ProcessPoolAction arguments",
|
74
|
+
self.name,
|
75
|
+
)
|
76
|
+
return None, None
|
77
|
+
|
78
|
+
async def _run(self, *args, **kwargs) -> Any:
|
79
|
+
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
80
|
+
if self.shared_context:
|
81
|
+
shared_context.set_shared_result(self.shared_context.last_result())
|
82
|
+
if self.inject_last_result and self.shared_context:
|
83
|
+
last_result = self.shared_context.last_result()
|
84
|
+
if not self._validate_pickleable(last_result):
|
85
|
+
raise ValueError(
|
86
|
+
f"Cannot inject last result into {self.name}: "
|
87
|
+
f"last result is not pickleable."
|
88
|
+
)
|
89
|
+
print(kwargs)
|
90
|
+
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
91
|
+
print(updated_kwargs)
|
92
|
+
context = ExecutionContext(
|
93
|
+
name=self.name,
|
94
|
+
args=args,
|
95
|
+
kwargs=updated_kwargs,
|
96
|
+
action=self,
|
97
|
+
)
|
98
|
+
loop = asyncio.get_running_loop()
|
99
|
+
|
100
|
+
context.start_timer()
|
101
|
+
try:
|
102
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
103
|
+
futures = [
|
104
|
+
loop.run_in_executor(
|
105
|
+
self.executor,
|
106
|
+
partial(
|
107
|
+
task.task,
|
108
|
+
*(*args, *task.args),
|
109
|
+
**{**updated_kwargs, **task.kwargs},
|
110
|
+
),
|
111
|
+
)
|
112
|
+
for task in self.actions
|
113
|
+
]
|
114
|
+
results = await asyncio.gather(*futures, return_exceptions=True)
|
115
|
+
context.result = results
|
116
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
117
|
+
return results
|
118
|
+
except Exception as error:
|
119
|
+
context.exception = error
|
120
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
121
|
+
if context.result is not None:
|
122
|
+
return context.result
|
123
|
+
raise
|
124
|
+
finally:
|
125
|
+
context.stop_timer()
|
126
|
+
await self.hooks.trigger(HookType.AFTER, context)
|
127
|
+
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
128
|
+
er.record(context)
|
129
|
+
|
130
|
+
def _validate_pickleable(self, obj: Any) -> bool:
|
131
|
+
try:
|
132
|
+
import pickle
|
133
|
+
|
134
|
+
pickle.dumps(obj)
|
135
|
+
return True
|
136
|
+
except (pickle.PicklingError, TypeError):
|
137
|
+
return False
|
138
|
+
|
139
|
+
async def preview(self, parent: Tree | None = None):
|
140
|
+
label = [f"[{OneColors.DARK_YELLOW_b}]🧠 ProcessPoolAction[/] '{self.name}'"]
|
141
|
+
if self.inject_last_result:
|
142
|
+
label.append(f" [dim](receives '{self.inject_into}')[/dim]")
|
143
|
+
tree = parent.add("".join(label)) if parent else Tree("".join(label))
|
144
|
+
actions = self.actions.copy()
|
145
|
+
random.shuffle(actions)
|
146
|
+
for action in actions:
|
147
|
+
label = [
|
148
|
+
f"[{OneColors.DARK_YELLOW_b}] - {getattr(action.task, '__name__', repr(action.task))}[/] "
|
149
|
+
f"[dim]({', '.join(map(repr, action.args))})[/]"
|
150
|
+
]
|
151
|
+
if action.kwargs:
|
152
|
+
label.append(
|
153
|
+
f" [dim]({', '.join(f'{k}={v!r}' for k, v in action.kwargs.items())})[/]"
|
154
|
+
)
|
155
|
+
tree.add("".join(label))
|
156
|
+
|
157
|
+
if not parent:
|
158
|
+
self.console.print(tree)
|
159
|
+
|
160
|
+
def __str__(self) -> str:
|
161
|
+
return (
|
162
|
+
f"ProcessPoolAction(name={self.name!r}, "
|
163
|
+
f"actions={[getattr(action.task, '__name__', repr(action.task)) for action in self.actions]}, "
|
164
|
+
f"inject_last_result={self.inject_last_result}, "
|
165
|
+
f"inject_into={self.inject_into!r})"
|
166
|
+
)
|
@@ -166,8 +166,8 @@ class CommandArgumentParser:
|
|
166
166
|
self.help_epilogue: str = help_epilogue
|
167
167
|
self.aliases: list[str] = aliases or []
|
168
168
|
self._arguments: list[Argument] = []
|
169
|
-
self._positional:
|
170
|
-
self._keyword:
|
169
|
+
self._positional: dict[str, Argument] = {}
|
170
|
+
self._keyword: dict[str, Argument] = {}
|
171
171
|
self._flag_map: dict[str, Argument] = {}
|
172
172
|
self._dest_set: set[str] = set()
|
173
173
|
self._add_help()
|
@@ -482,12 +482,12 @@ class CommandArgumentParser:
|
|
482
482
|
)
|
483
483
|
for flag in flags:
|
484
484
|
self._flag_map[flag] = argument
|
485
|
+
if not positional:
|
486
|
+
self._keyword[flag] = argument
|
485
487
|
self._dest_set.add(dest)
|
486
488
|
self._arguments.append(argument)
|
487
489
|
if positional:
|
488
|
-
self._positional
|
489
|
-
else:
|
490
|
-
self._keyword.append(argument)
|
490
|
+
self._positional[dest] = argument
|
491
491
|
|
492
492
|
def get_argument(self, dest: str) -> Argument | None:
|
493
493
|
return next((a for a in self._arguments if a.dest == dest), None)
|
@@ -663,8 +663,8 @@ class CommandArgumentParser:
|
|
663
663
|
i = 0
|
664
664
|
while i < len(args):
|
665
665
|
token = args[i]
|
666
|
-
if token in self.
|
667
|
-
spec = self.
|
666
|
+
if token in self._keyword:
|
667
|
+
spec = self._keyword[token]
|
668
668
|
action = spec.action
|
669
669
|
|
670
670
|
if action == ArgumentAction.HELP:
|
@@ -836,7 +836,7 @@ class CommandArgumentParser:
|
|
836
836
|
# Options
|
837
837
|
# Add all keyword arguments to the options list
|
838
838
|
options_list = []
|
839
|
-
for arg in self._keyword:
|
839
|
+
for arg in self._keyword.values():
|
840
840
|
choice_text = arg.get_choice_text()
|
841
841
|
if choice_text:
|
842
842
|
options_list.extend([f"[{arg.flags[0]} {choice_text}]"])
|
@@ -844,7 +844,7 @@ class CommandArgumentParser:
|
|
844
844
|
options_list.extend([f"[{arg.flags[0]}]"])
|
845
845
|
|
846
846
|
# Add positional arguments to the options list
|
847
|
-
for arg in self._positional:
|
847
|
+
for arg in self._positional.values():
|
848
848
|
choice_text = arg.get_choice_text()
|
849
849
|
if isinstance(arg.nargs, int):
|
850
850
|
choice_text = " ".join([choice_text] * arg.nargs)
|
@@ -870,14 +870,14 @@ class CommandArgumentParser:
|
|
870
870
|
if self._arguments:
|
871
871
|
if self._positional:
|
872
872
|
self.console.print("[bold]positional:[/bold]")
|
873
|
-
for arg in self._positional:
|
873
|
+
for arg in self._positional.values():
|
874
874
|
flags = arg.get_positional_text()
|
875
875
|
arg_line = Text(f" {flags:<30} ")
|
876
876
|
help_text = arg.help or ""
|
877
877
|
arg_line.append(help_text)
|
878
878
|
self.console.print(arg_line)
|
879
879
|
self.console.print("[bold]options:[/bold]")
|
880
|
-
for arg in self._keyword:
|
880
|
+
for arg in self._keyword.values():
|
881
881
|
flags = ", ".join(arg.flags)
|
882
882
|
flags_choice = f"{flags} {arg.get_choice_text()}"
|
883
883
|
arg_line = Text(f" {flags_choice:<30} ")
|
@@ -906,8 +906,8 @@ class CommandArgumentParser:
|
|
906
906
|
required = sum(arg.required for arg in self._arguments)
|
907
907
|
return (
|
908
908
|
f"CommandArgumentParser(args={len(self._arguments)}, "
|
909
|
-
f"flags={len(self._flag_map)},
|
910
|
-
f"
|
909
|
+
f"flags={len(self._flag_map)}, keywords={len(self._keyword)}, "
|
910
|
+
f"positional={positional}, required={required})"
|
911
911
|
)
|
912
912
|
|
913
913
|
def __repr__(self) -> str:
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.39"
|
falyx-0.1.38/falyx/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.38"
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|