falyx 0.1.26__py3-none-any.whl → 0.1.27__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/__init__.py CHANGED
@@ -21,6 +21,7 @@ from .menu_action import MenuAction
21
21
  from .select_file_action import SelectFileAction
22
22
  from .selection_action import SelectionAction
23
23
  from .signal_action import SignalAction
24
+ from .user_input_action import UserInputAction
24
25
 
25
26
  __all__ = [
26
27
  "Action",
@@ -38,4 +39,5 @@ __all__ = [
38
39
  "SignalAction",
39
40
  "FallbackAction",
40
41
  "LiteralInputAction",
42
+ "UserInputAction",
41
43
  ]
@@ -11,6 +11,7 @@ from falyx.hook_manager import HookType
11
11
  from falyx.logger import logger
12
12
  from falyx.protocols import ActionFactoryProtocol
13
13
  from falyx.themes import OneColors
14
+ from falyx.utils import ensure_async
14
15
 
15
16
 
16
17
  class ActionFactoryAction(BaseAction):
@@ -46,6 +47,14 @@ class ActionFactoryAction(BaseAction):
46
47
  self.preview_args = preview_args
47
48
  self.preview_kwargs = preview_kwargs or {}
48
49
 
50
+ @property
51
+ def factory(self) -> ActionFactoryProtocol:
52
+ return self._factory # type: ignore[return-value]
53
+
54
+ @factory.setter
55
+ def factory(self, value: ActionFactoryProtocol):
56
+ self._factory = ensure_async(value)
57
+
49
58
  async def _run(self, *args, **kwargs) -> Any:
50
59
  updated_kwargs = self._maybe_inject_last_result(kwargs)
51
60
  context = ExecutionContext(
@@ -57,7 +66,7 @@ class ActionFactoryAction(BaseAction):
57
66
  context.start_timer()
58
67
  try:
59
68
  await self.hooks.trigger(HookType.BEFORE, context)
60
- generated_action = self.factory(*args, **updated_kwargs)
69
+ generated_action = await self.factory(*args, **updated_kwargs)
61
70
  if not isinstance(generated_action, BaseAction):
62
71
  raise TypeError(
63
72
  f"[{self.name}] Factory must return a BaseAction, got "
@@ -94,7 +103,7 @@ class ActionFactoryAction(BaseAction):
94
103
  tree = parent.add(label) if parent else Tree(label)
95
104
 
96
105
  try:
97
- generated = self.factory(*self.preview_args, **self.preview_kwargs)
106
+ generated = await self.factory(*self.preview_args, **self.preview_kwargs)
98
107
  if isinstance(generated, BaseAction):
99
108
  await generated.preview(parent=tree)
100
109
  else:
@@ -38,6 +38,7 @@ class MenuAction(BaseAction):
38
38
  never_prompt: bool = False,
39
39
  include_reserved: bool = True,
40
40
  show_table: bool = True,
41
+ custom_table: Table | None = None,
41
42
  ):
42
43
  super().__init__(
43
44
  name,
@@ -54,8 +55,11 @@ class MenuAction(BaseAction):
54
55
  self.prompt_session = prompt_session or PromptSession()
55
56
  self.include_reserved = include_reserved
56
57
  self.show_table = show_table
58
+ self.custom_table = custom_table
57
59
 
58
60
  def _build_table(self) -> Table:
61
+ if self.custom_table:
62
+ return self.custom_table
59
63
  table = render_table_base(
60
64
  title=self.title,
61
65
  columns=self.columns,
@@ -0,0 +1,94 @@
1
+ from prompt_toolkit import PromptSession
2
+ from prompt_toolkit.validation import Validator
3
+ from rich.console import Console
4
+ from rich.tree import Tree
5
+
6
+ from falyx.action import BaseAction
7
+ from falyx.context import ExecutionContext
8
+ from falyx.execution_registry import ExecutionRegistry as er
9
+ from falyx.hook_manager import HookType
10
+ from falyx.themes.colors import OneColors
11
+
12
+
13
+ class UserInputAction(BaseAction):
14
+ """
15
+ Prompts the user for input via PromptSession and returns the result.
16
+
17
+ Args:
18
+ name (str): Action name.
19
+ prompt_text (str): Prompt text (can include '{last_result}' for interpolation).
20
+ validator (Validator, optional): Prompt Toolkit validator.
21
+ console (Console, optional): Rich console for rendering.
22
+ prompt_session (PromptSession, optional): Reusable prompt session.
23
+ inject_last_result (bool): Whether to inject last_result into prompt.
24
+ inject_into (str): Key to use for injection (default: 'last_result').
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ *,
31
+ prompt_text: str = "Input > ",
32
+ validator: Validator | None = None,
33
+ console: Console | None = None,
34
+ prompt_session: PromptSession | None = None,
35
+ inject_last_result: bool = False,
36
+ ):
37
+ super().__init__(
38
+ name=name,
39
+ inject_last_result=inject_last_result,
40
+ )
41
+ self.prompt_text = prompt_text
42
+ self.validator = validator
43
+ self.console = console or Console(color_system="auto")
44
+ self.prompt_session = prompt_session or PromptSession()
45
+
46
+ async def _run(self, *args, **kwargs) -> str:
47
+ context = ExecutionContext(
48
+ name=self.name,
49
+ args=args,
50
+ kwargs=kwargs,
51
+ action=self,
52
+ )
53
+ context.start_timer()
54
+ try:
55
+ await self.hooks.trigger(HookType.BEFORE, context)
56
+
57
+ prompt_text = self.prompt_text
58
+ if self.inject_last_result and self.last_result:
59
+ prompt_text = prompt_text.format(last_result=self.last_result)
60
+
61
+ answer = await self.prompt_session.prompt_async(
62
+ prompt_text,
63
+ validator=self.validator,
64
+ )
65
+ context.result = answer
66
+ await self.hooks.trigger(HookType.ON_SUCCESS, context)
67
+ return answer
68
+ except Exception as error:
69
+ context.exception = error
70
+ await self.hooks.trigger(HookType.ON_ERROR, context)
71
+ raise
72
+ finally:
73
+ context.stop_timer()
74
+ await self.hooks.trigger(HookType.AFTER, context)
75
+ await self.hooks.trigger(HookType.ON_TEARDOWN, context)
76
+ er.record(context)
77
+
78
+ async def preview(self, parent: Tree | None = None):
79
+ label = f"[{OneColors.MAGENTA}]⌨ UserInputAction[/] '{self.name}'"
80
+ tree = parent.add(label) if parent else Tree(label)
81
+
82
+ prompt_text = (
83
+ self.prompt_text.replace("{last_result}", "<last_result>")
84
+ if "{last_result}" in self.prompt_text
85
+ else self.prompt_text
86
+ )
87
+ tree.add(f"[dim]Prompt:[/] {prompt_text}")
88
+ if self.validator:
89
+ tree.add("[dim]Validator:[/] Yes")
90
+ if not parent:
91
+ self.console.print(tree)
92
+
93
+ def __str__(self):
94
+ return f"UserInputAction(name={self.name!r}, prompt={self.prompt!r})"
falyx/command.py CHANGED
@@ -30,7 +30,6 @@ from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
30
30
  from falyx.action.io_action import BaseIOAction
31
31
  from falyx.context import ExecutionContext
32
32
  from falyx.debug import register_debug_hooks
33
- from falyx.exceptions import FalyxError
34
33
  from falyx.execution_registry import ExecutionRegistry as er
35
34
  from falyx.hook_manager import HookManager, HookType
36
35
  from falyx.logger import logger
@@ -38,8 +37,9 @@ from falyx.options_manager import OptionsManager
38
37
  from falyx.prompt_utils import confirm_async, should_prompt_user
39
38
  from falyx.retry import RetryPolicy
40
39
  from falyx.retry_utils import enable_retries_recursively
40
+ from falyx.signals import CancelSignal
41
41
  from falyx.themes import OneColors
42
- from falyx.utils import _noop, ensure_async
42
+ from falyx.utils import ensure_async
43
43
 
44
44
  console = Console(color_system="auto")
45
45
 
@@ -98,7 +98,7 @@ class Command(BaseModel):
98
98
 
99
99
  key: str
100
100
  description: str
101
- action: BaseAction | Callable[[], Any] = _noop
101
+ action: BaseAction | Callable[[], Any]
102
102
  args: tuple = ()
103
103
  kwargs: dict[str, Any] = Field(default_factory=dict)
104
104
  hidden: bool = False
@@ -205,7 +205,7 @@ class Command(BaseModel):
205
205
  await self.preview()
206
206
  if not await confirm_async(self.confirmation_prompt):
207
207
  logger.info("[Command:%s] ❌ Cancelled by user.", self.key)
208
- raise FalyxError(f"[Command:{self.key}] Cancelled by confirmation.")
208
+ raise CancelSignal(f"[Command:{self.key}] Cancelled by confirmation.")
209
209
 
210
210
  context.start_timer()
211
211
 
@@ -100,7 +100,7 @@ class ExecutionRegistry:
100
100
 
101
101
  @classmethod
102
102
  def summary(cls):
103
- table = Table(title="[📊] Execution History", expand=True, box=box.SIMPLE)
103
+ table = Table(title="📊 Execution History", expand=True, box=box.SIMPLE)
104
104
 
105
105
  table.add_column("Name", style="bold cyan")
106
106
  table.add_column("Start", justify="right", style="dim")
falyx/falyx.py CHANGED
@@ -57,9 +57,9 @@ from falyx.logger import logger
57
57
  from falyx.options_manager import OptionsManager
58
58
  from falyx.parsers import get_arg_parsers
59
59
  from falyx.retry import RetryPolicy
60
- from falyx.signals import BackSignal, QuitSignal
60
+ from falyx.signals import BackSignal, CancelSignal, QuitSignal
61
61
  from falyx.themes import OneColors, get_nord_theme
62
- from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
62
+ from falyx.utils import CaseInsensitiveDict, _noop, chunks, get_program_invocation
63
63
  from falyx.version import __version__
64
64
 
65
65
 
@@ -237,8 +237,9 @@ class Falyx:
237
237
  def _get_exit_command(self) -> Command:
238
238
  """Returns the back command for the menu."""
239
239
  return Command(
240
- key="Q",
240
+ key="X",
241
241
  description="Exit",
242
+ action=Action("Exit", action=_noop),
242
243
  aliases=["EXIT", "QUIT"],
243
244
  style=OneColors.DARK_RED,
244
245
  )
@@ -266,9 +267,9 @@ class Falyx:
266
267
  help_text += " [dim](requires input)[/dim]"
267
268
  table.add_row(
268
269
  f"[{command.style}]{command.key}[/]",
269
- ", ".join(command.aliases) if command.aliases else "None",
270
+ ", ".join(command.aliases) if command.aliases else "",
270
271
  help_text,
271
- ", ".join(command.tags) if command.tags else "None",
272
+ ", ".join(command.tags) if command.tags else "",
272
273
  )
273
274
 
274
275
  table.add_row(
@@ -305,7 +306,7 @@ class Falyx:
305
306
  key="H",
306
307
  aliases=["HELP", "?"],
307
308
  description="Help",
308
- action=self._show_help,
309
+ action=Action("Help", self._show_help),
309
310
  style=OneColors.LIGHT_YELLOW,
310
311
  )
311
312
 
@@ -507,18 +508,19 @@ class Falyx:
507
508
 
508
509
  def update_exit_command(
509
510
  self,
510
- key: str = "Q",
511
+ key: str = "X",
511
512
  description: str = "Exit",
512
513
  aliases: list[str] | None = None,
513
- action: Callable[[], Any] = lambda: None,
514
+ action: Callable[[], Any] | None = None,
514
515
  style: str = OneColors.DARK_RED,
515
516
  confirm: bool = False,
516
517
  confirm_message: str = "Are you sure?",
517
518
  ) -> None:
518
519
  """Updates the back command of the menu."""
520
+ self._validate_command_key(key)
521
+ action = action or Action(description, action=_noop)
519
522
  if not callable(action):
520
523
  raise InvalidActionError("Action must be a callable.")
521
- self._validate_command_key(key)
522
524
  self.exit_command = Command(
523
525
  key=key,
524
526
  description=description,
@@ -537,7 +539,7 @@ class Falyx:
537
539
  raise NotAFalyxError("submenu must be an instance of Falyx.")
538
540
  self._validate_command_key(key)
539
541
  self.add_command(key, description, submenu.menu, style=style)
540
- if submenu.exit_command.key == "Q":
542
+ if submenu.exit_command.key == "X":
541
543
  submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
542
544
 
543
545
  def add_commands(self, commands: list[Command] | list[dict]) -> None:
@@ -918,6 +920,8 @@ class Falyx:
918
920
  break
919
921
  except BackSignal:
920
922
  logger.info("BackSignal received.")
923
+ except CancelSignal:
924
+ logger.info("CancelSignal received.")
921
925
  finally:
922
926
  logger.info("Exiting menu: %s", self.get_title())
923
927
  if self.exit_message:
falyx/protocols.py CHANGED
@@ -2,10 +2,10 @@
2
2
  """protocols.py"""
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Protocol
5
+ from typing import Any, Awaitable, Protocol
6
6
 
7
7
  from falyx.action.action import BaseAction
8
8
 
9
9
 
10
10
  class ActionFactoryProtocol(Protocol):
11
- def __call__(self, *args: Any, **kwargs: Any) -> BaseAction: ...
11
+ async def __call__(self, *args: Any, **kwargs: Any) -> Awaitable[BaseAction]: ...
falyx/signals.py CHANGED
@@ -22,3 +22,10 @@ class BackSignal(FlowSignal):
22
22
 
23
23
  def __init__(self, message: str = "Back signal received."):
24
24
  super().__init__(message)
25
+
26
+
27
+ class CancelSignal(FlowSignal):
28
+ """Raised to cancel the current command or action."""
29
+
30
+ def __init__(self, message: str = "Cancel signal received."):
31
+ super().__init__(message)
falyx/utils.py CHANGED
@@ -48,6 +48,7 @@ def ensure_async(function: Callable[..., T]) -> Callable[..., Awaitable[T]]:
48
48
 
49
49
  if not callable(function):
50
50
  raise TypeError(f"{function} is not callable")
51
+
51
52
  return async_wrapper
52
53
 
53
54
 
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.26"
1
+ __version__ = "0.1.27"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.26
3
+ Version: 0.1.27
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -1,25 +1,26 @@
1
1
  falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  falyx/__init__.py,sha256=L40665QyjAqHQxHdxxY2_yPeDa4p0LE7Nu_2dkm08Ls,650
3
3
  falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
4
- falyx/action/__init__.py,sha256=P3Eh0lE2VunzxqwfLbZKxE6FEcdR_u5OROkDTBYgeP0,904
4
+ falyx/action/__init__.py,sha256=zpOK5g4DybydV8d3QI0Zq52aWaKFPYi-J6szAQTsQ2c,974
5
5
  falyx/action/action.py,sha256=CJB9eeeEqBGkZHjMpG24eXHRjouKSfESCI1zWzoE7JQ,32488
6
- falyx/action/action_factory.py,sha256=qhlx8-BAEiNDJrbzF0HZzipY09Ple6J7FxxNvoBda9Y,4228
6
+ falyx/action/action_factory.py,sha256=qNtEnsbKsNl-WijChbTQYfdI3k14fN-1bzDsGFx8yZI,4517
7
7
  falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
8
8
  falyx/action/io_action.py,sha256=zdDq07zSLlaShBQ3ztXTRC6aZL0JoERNZSmvHy1V22w,9718
9
- falyx/action/menu_action.py,sha256=8qttPuq0AUh_oFaa3A82e19dSl4ZhQ9Y_j5CU7zHAlc,5532
9
+ falyx/action/menu_action.py,sha256=cboCpXyl0fZUxpFsvEPu0dGhFfr_vdfllceQnICA0gU,5683
10
10
  falyx/action/select_file_action.py,sha256=hHLhmTSacWaUXhRTeIIiXt8gR7zbjkXJ2MAkKQYCpp4,7799
11
11
  falyx/action/selection_action.py,sha256=22rF7UqRrQAMjGIheDqAbUizVMBg9aCl9e4VOLLZZJo,8811
12
12
  falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
13
13
  falyx/action/types.py,sha256=iVD-bHm1GRXOTIlHOeT_KcDBRZm4Hz5Xzl_BOalvEf4,961
14
+ falyx/action/user_input_action.py,sha256=LSTzC_3TfsfXdz-qV3GlOIGpZWAOgO9J5DnNsHO7ee8,3398
14
15
  falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
15
- falyx/command.py,sha256=s7r9aeUYEk9iUNE69JQtlFoPx9AehTxkHMPxpLKVIOA,12238
16
+ falyx/command.py,sha256=CeleZJ039996d6qn895JXagLeh7gMZltx7jABecjSXY,12224
16
17
  falyx/config.py,sha256=8dkQfL-Ro-vWw1AcO2fD1PGZ92Cyfnwl885ZlpLkp4Y,9636
17
18
  falyx/config_schema.py,sha256=j5GQuHVlaU-VLxLF9t8idZRjqOP9MIKp1hyd9NhpAGU,3124
18
19
  falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
19
20
  falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
20
21
  falyx/exceptions.py,sha256=Qxp6UScZWEyno-6Lgksrv3s9iwjbr2U-d6hun-_xpc0,798
21
- falyx/execution_registry.py,sha256=re56TImfL67p30ZlVBjqxz9Nn34SD4gvTlwFVPSzVCM,4712
22
- falyx/falyx.py,sha256=JvFbq_7tiyW5axGRIy4UDf3s0gBDbw1MZr_ivkUqH3k,40627
22
+ falyx/execution_registry.py,sha256=rctsz0mrIHPToLZqylblVjDdKWdq1x_JBc8GwMP5sJ8,4710
23
+ falyx/falyx.py,sha256=ECL6nDgqxS0s8lzOlMnBOSqwZEsLN0kYzeBCs0lUsYI,40860
23
24
  falyx/hook_manager.py,sha256=GuGxVVz9FXrU39Tk220QcsLsMXeut7ZDkGW3hU9GcwQ,2952
24
25
  falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
25
26
  falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
@@ -28,19 +29,19 @@ falyx/menu.py,sha256=faxGgocqQYY6HtzVbenHaFj8YqsmycBEyziC8Ahzqjo,2870
28
29
  falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
29
30
  falyx/parsers.py,sha256=hxrBouQEqdgk6aWzNa7UwTg7u55vJffSEUUTiiQoI0U,5602
30
31
  falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
31
- falyx/protocols.py,sha256=dXNS-kh-5XB92PE5POy4uJ4KLT0O3ZAoiqw55jgR2IM,306
32
+ falyx/protocols.py,sha256=ejSz18D8Qg63ONdgwbvn2YanKK9bGF0e3Bjxh9y3Buc,334
32
33
  falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
33
34
  falyx/retry_utils.py,sha256=EAzc-ECTu8AxKkmlw28ioOW9y-Y9tLQ0KasvSkBRYgs,694
34
35
  falyx/selection.py,sha256=l2LLISqgP8xfHdcTAEbTTqs_Bae4-LVUKMN7VQH7tM0,10731
35
- falyx/signals.py,sha256=4PTuVRB_P_aWfnU8pANqhMxGTLq7TJDEyk9jCp0Bx2c,713
36
+ falyx/signals.py,sha256=PGlc0Cm8DfUB3lCf58u_kwTwm2XUEFQ2joFq0Qc_GXI,906
36
37
  falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
37
38
  falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
38
39
  falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
39
- falyx/utils.py,sha256=uss-FV8p164pmhoqYtQt8gNp5z8fGbuMAk4dRJ6RopI,6717
40
+ falyx/utils.py,sha256=u3puR4Bh-unNBw9a0V9sw7PDTIzRaNLolap0oz5bVIk,6718
40
41
  falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
41
- falyx/version.py,sha256=3_QdGLpuk_SDY7k9PpNcHpSTjlPdhadPiEgF82wzkqk,23
42
- falyx-0.1.26.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
43
- falyx-0.1.26.dist-info/METADATA,sha256=Z1v5YL-vgfSBwAaYzcaZI4oUq0ee_3ul2D6jHqbpv_M,5521
44
- falyx-0.1.26.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
45
- falyx-0.1.26.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
46
- falyx-0.1.26.dist-info/RECORD,,
42
+ falyx/version.py,sha256=vEF032D64gj-9WJp4kp0yS1eFIq4XHIqJr91sJJNwWg,23
43
+ falyx-0.1.27.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
44
+ falyx-0.1.27.dist-info/METADATA,sha256=rs1IR_MPVQKge3QAZjwHUbh3sOfIutI5qckcMIaVR5w,5521
45
+ falyx-0.1.27.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
46
+ falyx-0.1.27.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
47
+ falyx-0.1.27.dist-info/RECORD,,
File without changes