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.
Files changed (61) hide show
  1. {falyx-0.1.38 → falyx-0.1.39}/PKG-INFO +6 -5
  2. {falyx-0.1.38 → falyx-0.1.39}/README.md +5 -4
  3. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/__init__.py +2 -0
  4. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action_group.py +2 -1
  5. falyx-0.1.39/falyx/action/process_pool_action.py +166 -0
  6. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/argparse.py +13 -13
  7. falyx-0.1.39/falyx/version.py +1 -0
  8. {falyx-0.1.38 → falyx-0.1.39}/pyproject.toml +1 -1
  9. falyx-0.1.38/falyx/version.py +0 -1
  10. {falyx-0.1.38 → falyx-0.1.39}/LICENSE +0 -0
  11. {falyx-0.1.38 → falyx-0.1.39}/falyx/.pytyped +0 -0
  12. {falyx-0.1.38 → falyx-0.1.39}/falyx/__init__.py +0 -0
  13. {falyx-0.1.38 → falyx-0.1.39}/falyx/__main__.py +0 -0
  14. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/.pytyped +0 -0
  15. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action.py +0 -0
  16. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/action_factory.py +0 -0
  17. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/base.py +0 -0
  18. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/chained_action.py +0 -0
  19. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/fallback_action.py +0 -0
  20. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/http_action.py +0 -0
  21. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/io_action.py +0 -0
  22. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/literal_input_action.py +0 -0
  23. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/menu_action.py +0 -0
  24. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/mixins.py +0 -0
  25. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/process_action.py +0 -0
  26. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/prompt_menu_action.py +0 -0
  27. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/select_file_action.py +0 -0
  28. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/selection_action.py +0 -0
  29. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/signal_action.py +0 -0
  30. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/types.py +0 -0
  31. {falyx-0.1.38 → falyx-0.1.39}/falyx/action/user_input_action.py +0 -0
  32. {falyx-0.1.38 → falyx-0.1.39}/falyx/bottom_bar.py +0 -0
  33. {falyx-0.1.38 → falyx-0.1.39}/falyx/command.py +0 -0
  34. {falyx-0.1.38 → falyx-0.1.39}/falyx/config.py +0 -0
  35. {falyx-0.1.38 → falyx-0.1.39}/falyx/context.py +0 -0
  36. {falyx-0.1.38 → falyx-0.1.39}/falyx/debug.py +0 -0
  37. {falyx-0.1.38 → falyx-0.1.39}/falyx/exceptions.py +0 -0
  38. {falyx-0.1.38 → falyx-0.1.39}/falyx/execution_registry.py +0 -0
  39. {falyx-0.1.38 → falyx-0.1.39}/falyx/falyx.py +0 -0
  40. {falyx-0.1.38 → falyx-0.1.39}/falyx/hook_manager.py +0 -0
  41. {falyx-0.1.38 → falyx-0.1.39}/falyx/hooks.py +0 -0
  42. {falyx-0.1.38 → falyx-0.1.39}/falyx/init.py +0 -0
  43. {falyx-0.1.38 → falyx-0.1.39}/falyx/logger.py +0 -0
  44. {falyx-0.1.38 → falyx-0.1.39}/falyx/menu.py +0 -0
  45. {falyx-0.1.38 → falyx-0.1.39}/falyx/options_manager.py +0 -0
  46. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/.pytyped +0 -0
  47. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/__init__.py +0 -0
  48. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/parsers.py +0 -0
  49. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/signature.py +0 -0
  50. {falyx-0.1.38 → falyx-0.1.39}/falyx/parsers/utils.py +0 -0
  51. {falyx-0.1.38 → falyx-0.1.39}/falyx/prompt_utils.py +0 -0
  52. {falyx-0.1.38 → falyx-0.1.39}/falyx/protocols.py +0 -0
  53. {falyx-0.1.38 → falyx-0.1.39}/falyx/retry.py +0 -0
  54. {falyx-0.1.38 → falyx-0.1.39}/falyx/retry_utils.py +0 -0
  55. {falyx-0.1.38 → falyx-0.1.39}/falyx/selection.py +0 -0
  56. {falyx-0.1.38 → falyx-0.1.39}/falyx/signals.py +0 -0
  57. {falyx-0.1.38 → falyx-0.1.39}/falyx/tagged_table.py +0 -0
  58. {falyx-0.1.38 → falyx-0.1.39}/falyx/themes/__init__.py +0 -0
  59. {falyx-0.1.38 → falyx-0.1.39}/falyx/themes/colors.py +0 -0
  60. {falyx-0.1.38 → falyx-0.1.39}/falyx/utils.py +0 -0
  61. {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.38
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, Action, ChainedAction
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, retry=True)
89
- step2 = Action(name="step_2", action=flaky_step, retry=True)
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, Action, ChainedAction
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, retry=True)
66
- step2 = Action(name="step_2", action=flaky_step, retry=True)
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: list[Argument] = []
170
- self._keyword: list[Argument] = []
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.append(argument)
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._flag_map:
667
- spec = self._flag_map[token]
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)}, dests={len(self._dest_set)}, "
910
- f"required={required}, positional={positional})"
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"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.38"
3
+ version = "0.1.39"
4
4
  description = "Reliable and introspectable async CLI action framework."
5
5
  authors = ["Roland Thomas Jr <roland@rtj.dev>"]
6
6
  license = "MIT"
@@ -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