falyx 0.1.36__py3-none-any.whl → 0.1.38__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/mixins.py ADDED
@@ -0,0 +1,33 @@
1
+ from falyx.action.base import BaseAction
2
+
3
+
4
+ class ActionListMixin:
5
+ """Mixin for managing a list of actions."""
6
+
7
+ def __init__(self) -> None:
8
+ self.actions: list[BaseAction] = []
9
+
10
+ def set_actions(self, actions: list[BaseAction]) -> None:
11
+ """Replaces the current action list with a new one."""
12
+ self.actions.clear()
13
+ for action in actions:
14
+ self.add_action(action)
15
+
16
+ def add_action(self, action: BaseAction) -> None:
17
+ """Adds an action to the list."""
18
+ self.actions.append(action)
19
+
20
+ def remove_action(self, name: str) -> None:
21
+ """Removes an action by name."""
22
+ self.actions = [action for action in self.actions if action.name != name]
23
+
24
+ def has_action(self, name: str) -> bool:
25
+ """Checks if an action with the given name exists."""
26
+ return any(action.name == name for action in self.actions)
27
+
28
+ def get_action(self, name: str) -> BaseAction | None:
29
+ """Retrieves an action by name."""
30
+ for action in self.actions:
31
+ if action.name == name:
32
+ return action
33
+ return None
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from concurrent.futures import ProcessPoolExecutor
5
+ from functools import partial
6
+ from typing import Any, Callable
7
+
8
+ from rich.tree import Tree
9
+
10
+ from falyx.action.base import BaseAction
11
+ from falyx.context import ExecutionContext
12
+ from falyx.execution_registry import ExecutionRegistry as er
13
+ from falyx.hook_manager import HookManager, HookType
14
+ from falyx.themes import OneColors
15
+
16
+
17
+ class ProcessAction(BaseAction):
18
+ """
19
+ ProcessAction runs a function in a separate process using ProcessPoolExecutor.
20
+
21
+ Features:
22
+ - Executes CPU-bound or blocking tasks without blocking the main event loop.
23
+ - Supports last_result injection into the subprocess.
24
+ - Validates that last_result is pickleable when injection is enabled.
25
+
26
+ Args:
27
+ name (str): Name of the action.
28
+ func (Callable): Function to execute in a new process.
29
+ args (tuple, optional): Positional arguments.
30
+ kwargs (dict, optional): Keyword arguments.
31
+ hooks (HookManager, optional): Hook manager for lifecycle events.
32
+ executor (ProcessPoolExecutor, optional): Custom executor if desired.
33
+ inject_last_result (bool, optional): Inject last result into the function.
34
+ inject_into (str, optional): Name of the injected key.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ name: str,
40
+ action: Callable[..., Any],
41
+ *,
42
+ args: tuple = (),
43
+ kwargs: dict[str, Any] | None = None,
44
+ hooks: HookManager | None = None,
45
+ executor: ProcessPoolExecutor | None = None,
46
+ inject_last_result: bool = False,
47
+ inject_into: str = "last_result",
48
+ ):
49
+ super().__init__(
50
+ name,
51
+ hooks=hooks,
52
+ inject_last_result=inject_last_result,
53
+ inject_into=inject_into,
54
+ )
55
+ self.action = action
56
+ self.args = args
57
+ self.kwargs = kwargs or {}
58
+ self.executor = executor or ProcessPoolExecutor()
59
+ self.is_retryable = True
60
+
61
+ def get_infer_target(self) -> tuple[Callable[..., Any] | None, None]:
62
+ return self.action, None
63
+
64
+ async def _run(self, *args, **kwargs) -> Any:
65
+ if self.inject_last_result and self.shared_context:
66
+ last_result = self.shared_context.last_result()
67
+ if not self._validate_pickleable(last_result):
68
+ raise ValueError(
69
+ f"Cannot inject last result into {self.name}: "
70
+ f"last result is not pickleable."
71
+ )
72
+ combined_args = args + self.args
73
+ combined_kwargs = self._maybe_inject_last_result({**self.kwargs, **kwargs})
74
+ context = ExecutionContext(
75
+ name=self.name,
76
+ args=combined_args,
77
+ kwargs=combined_kwargs,
78
+ action=self,
79
+ )
80
+ loop = asyncio.get_running_loop()
81
+
82
+ context.start_timer()
83
+ try:
84
+ await self.hooks.trigger(HookType.BEFORE, context)
85
+ result = await loop.run_in_executor(
86
+ self.executor, partial(self.action, *combined_args, **combined_kwargs)
87
+ )
88
+ context.result = result
89
+ await self.hooks.trigger(HookType.ON_SUCCESS, context)
90
+ return result
91
+ except Exception as error:
92
+ context.exception = error
93
+ await self.hooks.trigger(HookType.ON_ERROR, context)
94
+ if context.result is not None:
95
+ return context.result
96
+ raise
97
+ finally:
98
+ context.stop_timer()
99
+ await self.hooks.trigger(HookType.AFTER, context)
100
+ await self.hooks.trigger(HookType.ON_TEARDOWN, context)
101
+ er.record(context)
102
+
103
+ def _validate_pickleable(self, obj: Any) -> bool:
104
+ try:
105
+ import pickle
106
+
107
+ pickle.dumps(obj)
108
+ return True
109
+ except (pickle.PicklingError, TypeError):
110
+ return False
111
+
112
+ async def preview(self, parent: Tree | None = None):
113
+ label = [
114
+ f"[{OneColors.DARK_YELLOW_b}]🧠 ProcessAction (new process)[/] '{self.name}'"
115
+ ]
116
+ if self.inject_last_result:
117
+ label.append(f" [dim](injects '{self.inject_into}')[/dim]")
118
+ if parent:
119
+ parent.add("".join(label))
120
+ else:
121
+ self.console.print(Tree("".join(label)))
122
+
123
+ def __str__(self) -> str:
124
+ return (
125
+ f"ProcessAction(name={self.name!r}, "
126
+ f"action={getattr(self.action, '__name__', repr(self.action))}, "
127
+ f"args={self.args!r}, kwargs={self.kwargs!r})"
128
+ )
@@ -7,7 +7,7 @@ from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
7
7
  from rich.console import Console
8
8
  from rich.tree import Tree
9
9
 
10
- from falyx.action.action import BaseAction
10
+ from falyx.action.base import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -14,7 +14,7 @@ from prompt_toolkit import PromptSession
14
14
  from rich.console import Console
15
15
  from rich.tree import Tree
16
16
 
17
- from falyx.action.action import BaseAction
17
+ from falyx.action.base import BaseAction
18
18
  from falyx.action.types import FileReturnType
19
19
  from falyx.context import ExecutionContext
20
20
  from falyx.execution_registry import ExecutionRegistry as er
@@ -1,13 +1,12 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """selection_action.py"""
3
- from copy import copy
4
3
  from typing import Any
5
4
 
6
5
  from prompt_toolkit import PromptSession
7
6
  from rich.console import Console
8
7
  from rich.tree import Tree
9
8
 
10
- from falyx.action.action import BaseAction
9
+ from falyx.action.base import BaseAction
11
10
  from falyx.action.types import SelectionReturnType
12
11
  from falyx.context import ExecutionContext
13
12
  from falyx.execution_registry import ExecutionRegistry as er
@@ -3,7 +3,7 @@ from prompt_toolkit.validation import Validator
3
3
  from rich.console import Console
4
4
  from rich.tree import Tree
5
5
 
6
- from falyx.action import BaseAction
6
+ from falyx.action.base import BaseAction
7
7
  from falyx.context import ExecutionContext
8
8
  from falyx.execution_registry import ExecutionRegistry as er
9
9
  from falyx.hook_manager import HookType
falyx/command.py CHANGED
@@ -26,7 +26,8 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
26
26
  from rich.console import Console
27
27
  from rich.tree import Tree
28
28
 
29
- from falyx.action.action import Action, BaseAction
29
+ from falyx.action.action import Action
30
+ from falyx.action.base import BaseAction
30
31
  from falyx.context import ExecutionContext
31
32
  from falyx.debug import register_debug_hooks
32
33
  from falyx.execution_registry import ExecutionRegistry as er
falyx/config.py CHANGED
@@ -13,7 +13,8 @@ import yaml
13
13
  from pydantic import BaseModel, Field, field_validator, model_validator
14
14
  from rich.console import Console
15
15
 
16
- from falyx.action.action import Action, BaseAction
16
+ from falyx.action.action import Action
17
+ from falyx.action.base import BaseAction
17
18
  from falyx.command import Command
18
19
  from falyx.falyx import Falyx
19
20
  from falyx.logger import logger
falyx/debug.py CHANGED
@@ -10,7 +10,7 @@ def log_before(context: ExecutionContext):
10
10
  args = ", ".join(map(repr, context.args))
11
11
  kwargs = ", ".join(f"{k}={v!r}" for k, v in context.kwargs.items())
12
12
  signature = ", ".join(filter(None, [args, kwargs]))
13
- logger.info("[%s] Starting %s(%s)", context.name, context.action, signature)
13
+ logger.info("[%s] Starting -> %s(%s)", context.name, context.action, signature)
14
14
 
15
15
 
16
16
  def log_success(context: ExecutionContext):
@@ -18,7 +18,7 @@ def log_success(context: ExecutionContext):
18
18
  result_str = repr(context.result)
19
19
  if len(result_str) > 100:
20
20
  result_str = f"{result_str[:100]} ..."
21
- logger.debug("[%s] Success Result: %s", context.name, result_str)
21
+ logger.debug("[%s] Success -> Result: %s", context.name, result_str)
22
22
 
23
23
 
24
24
  def log_after(context: ExecutionContext):
falyx/falyx.py CHANGED
@@ -42,7 +42,8 @@ from rich.console import Console
42
42
  from rich.markdown import Markdown
43
43
  from rich.table import Table
44
44
 
45
- from falyx.action.action import Action, BaseAction
45
+ from falyx.action.action import Action
46
+ from falyx.action.base import BaseAction
46
47
  from falyx.bottom_bar import BottomBar
47
48
  from falyx.command import Command
48
49
  from falyx.context import ExecutionContext
@@ -82,7 +83,7 @@ class CommandValidator(Validator):
82
83
  self.falyx = falyx
83
84
  self.error_message = error_message
84
85
 
85
- def validate(self, document) -> None:
86
+ def validate(self, _) -> None:
86
87
  pass
87
88
 
88
89
  async def validate_async(self, document) -> None:
@@ -449,7 +450,7 @@ class Falyx:
449
450
  validator=CommandValidator(self, self._get_validator_error_message()),
450
451
  bottom_toolbar=self._get_bottom_bar_render(),
451
452
  key_bindings=self.key_bindings,
452
- validate_while_typing=False,
453
+ validate_while_typing=True,
453
454
  )
454
455
  return self._prompt_session
455
456
 
@@ -761,7 +762,7 @@ class Falyx:
761
762
  is_preview = False
762
763
  choice = "?"
763
764
  elif is_preview and not choice:
764
- # No help command enabled
765
+ # No help (list) command enabled
765
766
  if not from_validate:
766
767
  self.console.print(
767
768
  f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode."
@@ -781,12 +782,9 @@ class Falyx:
781
782
  )
782
783
  except CommandArgumentError as error:
783
784
  if not from_validate:
784
- if not name_map[choice].show_help():
785
- self.console.print(
786
- f"[{OneColors.DARK_RED}]❌ Invalid arguments for '{choice}': {error}"
787
- )
788
- else:
789
785
  name_map[choice].show_help()
786
+ self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}")
787
+ else:
790
788
  raise ValidationError(
791
789
  message=str(error), cursor_position=len(raw_choices)
792
790
  )
@@ -806,14 +804,24 @@ class Falyx:
806
804
  f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'. "
807
805
  "Did you mean:"
808
806
  )
809
- for match in fuzzy_matches:
810
- cmd = name_map[match]
811
- self.console.print(f" • [bold]{match}[/] → {cmd.description}")
807
+ for match in fuzzy_matches:
808
+ cmd = name_map[match]
809
+ self.console.print(f" • [bold]{match}[/] → {cmd.description}")
810
+ else:
811
+ raise ValidationError(
812
+ message=f"Unknown command '{choice}'. Did you mean: "
813
+ f"{', '.join(fuzzy_matches)}?",
814
+ cursor_position=len(raw_choices),
815
+ )
812
816
  else:
813
817
  if not from_validate:
814
818
  self.console.print(
815
819
  f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
816
820
  )
821
+ raise ValidationError(
822
+ message=f"Unknown command '{choice}'.",
823
+ cursor_position=len(raw_choices),
824
+ )
817
825
  return is_preview, None, args, kwargs
818
826
 
819
827
  def _create_context(self, selected_command: Command) -> ExecutionContext:
@@ -974,7 +982,7 @@ class Falyx:
974
982
 
975
983
  async def menu(self) -> None:
976
984
  """Runs the menu and handles user input."""
977
- logger.info("Running menu: %s", self.get_title())
985
+ logger.info("Starting menu: %s", self.get_title())
978
986
  self.debug_hooks()
979
987
  if self.welcome_message:
980
988
  self.print_message(self.welcome_message)
@@ -994,12 +1002,12 @@ class Falyx:
994
1002
  logger.info("EOF or KeyboardInterrupt. Exiting menu.")
995
1003
  break
996
1004
  except QuitSignal:
997
- logger.info("QuitSignal received. Exiting menu.")
1005
+ logger.info("[QuitSignal]. <- Exiting menu.")
998
1006
  break
999
1007
  except BackSignal:
1000
- logger.info("BackSignal received.")
1008
+ logger.info("[BackSignal]. <- Returning to the menu.")
1001
1009
  except CancelSignal:
1002
- logger.info("CancelSignal received.")
1010
+ logger.info("[CancelSignal]. <- Returning to the menu.")
1003
1011
  finally:
1004
1012
  logger.info("Exiting menu: %s", self.get_title())
1005
1013
  if self.exit_message:
falyx/menu.py CHANGED
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
 
5
5
  from prompt_toolkit.formatted_text import FormattedText
6
6
 
7
- from falyx.action import BaseAction
7
+ from falyx.action.base import BaseAction
8
8
  from falyx.signals import BackSignal, QuitSignal
9
9
  from falyx.themes import OneColors
10
10
  from falyx.utils import CaseInsensitiveDict