falyx 0.1.24__py3-none-any.whl → 0.1.26__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/__init__.py CHANGED
@@ -7,7 +7,7 @@ Licensed under the MIT License. See LICENSE file for details.
7
7
 
8
8
  import logging
9
9
 
10
- from .action import Action, ActionGroup, ChainedAction, ProcessAction
10
+ from .action.action import Action, ActionGroup, ChainedAction, ProcessAction
11
11
  from .command import Command
12
12
  from .context import ExecutionContext, SharedContext
13
13
  from .execution_registry import ExecutionRegistry
@@ -0,0 +1,41 @@
1
+ """
2
+ Falyx CLI Framework
3
+
4
+ Copyright (c) 2025 rtj.dev LLC.
5
+ Licensed under the MIT License. See LICENSE file for details.
6
+ """
7
+
8
+ from .action import (
9
+ Action,
10
+ ActionGroup,
11
+ BaseAction,
12
+ ChainedAction,
13
+ FallbackAction,
14
+ LiteralInputAction,
15
+ ProcessAction,
16
+ )
17
+ from .action_factory import ActionFactoryAction
18
+ from .http_action import HTTPAction
19
+ from .io_action import BaseIOAction, ShellAction
20
+ from .menu_action import MenuAction
21
+ from .select_file_action import SelectFileAction
22
+ from .selection_action import SelectionAction
23
+ from .signal_action import SignalAction
24
+
25
+ __all__ = [
26
+ "Action",
27
+ "ActionGroup",
28
+ "BaseAction",
29
+ "ChainedAction",
30
+ "ProcessAction",
31
+ "ActionFactoryAction",
32
+ "HTTPAction",
33
+ "BaseIOAction",
34
+ "ShellAction",
35
+ "SelectionAction",
36
+ "SelectFileAction",
37
+ "MenuAction",
38
+ "SignalAction",
39
+ "FallbackAction",
40
+ "LiteralInputAction",
41
+ ]
@@ -48,7 +48,7 @@ from falyx.hook_manager import Hook, HookManager, HookType
48
48
  from falyx.logger import logger
49
49
  from falyx.options_manager import OptionsManager
50
50
  from falyx.retry import RetryHandler, RetryPolicy
51
- from falyx.themes.colors import OneColors
51
+ from falyx.themes import OneColors
52
52
  from falyx.utils import ensure_async
53
53
 
54
54
 
@@ -4,13 +4,13 @@ from typing import Any
4
4
 
5
5
  from rich.tree import Tree
6
6
 
7
- from falyx.action import BaseAction
7
+ from falyx.action.action import BaseAction
8
8
  from falyx.context import ExecutionContext
9
9
  from falyx.execution_registry import ExecutionRegistry as er
10
10
  from falyx.hook_manager import HookType
11
11
  from falyx.logger import logger
12
12
  from falyx.protocols import ActionFactoryProtocol
13
- from falyx.themes.colors import OneColors
13
+ from falyx.themes import OneColors
14
14
 
15
15
 
16
16
  class ActionFactoryAction(BaseAction):
@@ -13,11 +13,11 @@ from typing import Any
13
13
  import aiohttp
14
14
  from rich.tree import Tree
15
15
 
16
- from falyx.action import Action
16
+ from falyx.action.action import Action
17
17
  from falyx.context import ExecutionContext, SharedContext
18
18
  from falyx.hook_manager import HookManager, HookType
19
19
  from falyx.logger import logger
20
- from falyx.themes.colors import OneColors
20
+ from falyx.themes import OneColors
21
21
 
22
22
 
23
23
  async def close_shared_http_session(context: ExecutionContext) -> None:
@@ -23,13 +23,13 @@ from typing import Any
23
23
 
24
24
  from rich.tree import Tree
25
25
 
26
- from falyx.action import BaseAction
26
+ from falyx.action.action import BaseAction
27
27
  from falyx.context import ExecutionContext
28
28
  from falyx.exceptions import FalyxError
29
29
  from falyx.execution_registry import ExecutionRegistry as er
30
30
  from falyx.hook_manager import HookManager, HookType
31
31
  from falyx.logger import logger
32
- from falyx.themes.colors import OneColors
32
+ from falyx.themes import OneColors
33
33
 
34
34
 
35
35
  class BaseIOAction(BaseAction):
@@ -1,6 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """menu_action.py"""
3
- from dataclasses import dataclass
4
3
  from typing import Any
5
4
 
6
5
  from prompt_toolkit import PromptSession
@@ -8,91 +7,16 @@ from rich.console import Console
8
7
  from rich.table import Table
9
8
  from rich.tree import Tree
10
9
 
11
- from falyx.action import BaseAction
10
+ from falyx.action.action import BaseAction
12
11
  from falyx.context import ExecutionContext
13
12
  from falyx.execution_registry import ExecutionRegistry as er
14
13
  from falyx.hook_manager import HookType
15
14
  from falyx.logger import logger
15
+ from falyx.menu import MenuOptionMap
16
16
  from falyx.selection import prompt_for_selection, render_table_base
17
- from falyx.signal_action import SignalAction
18
17
  from falyx.signals import BackSignal, QuitSignal
19
- from falyx.themes.colors import OneColors
20
- from falyx.utils import CaseInsensitiveDict, chunks
21
-
22
-
23
- @dataclass
24
- class MenuOption:
25
- """Represents a single menu option with a description and an action to execute."""
26
-
27
- description: str
28
- action: BaseAction
29
- style: str = OneColors.WHITE
30
-
31
- def __post_init__(self):
32
- if not isinstance(self.description, str):
33
- raise TypeError("MenuOption description must be a string.")
34
- if not isinstance(self.action, BaseAction):
35
- raise TypeError("MenuOption action must be a BaseAction instance.")
36
-
37
- def render(self, key: str) -> str:
38
- """Render the menu option for display."""
39
- return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
40
-
41
-
42
- class MenuOptionMap(CaseInsensitiveDict):
43
- """
44
- Manages menu options including validation, reserved key protection,
45
- and special signal entries like Quit and Back.
46
- """
47
-
48
- RESERVED_KEYS = {"Q", "B"}
49
-
50
- def __init__(
51
- self,
52
- options: dict[str, MenuOption] | None = None,
53
- allow_reserved: bool = False,
54
- ):
55
- super().__init__()
56
- self.allow_reserved = allow_reserved
57
- if options:
58
- self.update(options)
59
- self._inject_reserved_defaults()
60
-
61
- def _inject_reserved_defaults(self):
62
- self._add_reserved(
63
- "Q",
64
- MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
65
- )
66
- self._add_reserved(
67
- "B",
68
- MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
69
- )
70
-
71
- def _add_reserved(self, key: str, option: MenuOption) -> None:
72
- """Add a reserved key, bypassing validation."""
73
- norm_key = key.upper()
74
- super().__setitem__(norm_key, option)
75
-
76
- def __setitem__(self, key: str, option: MenuOption) -> None:
77
- if not isinstance(option, MenuOption):
78
- raise TypeError(f"Value for key '{key}' must be a MenuOption.")
79
- norm_key = key.upper()
80
- if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
81
- raise ValueError(
82
- f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
83
- )
84
- super().__setitem__(norm_key, option)
85
-
86
- def __delitem__(self, key: str) -> None:
87
- if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
88
- raise ValueError(f"Cannot delete reserved option '{key}'.")
89
- super().__delitem__(key)
90
-
91
- def items(self, include_reserved: bool = True):
92
- for k, v in super().items():
93
- if not include_reserved and k in self.RESERVED_KEYS:
94
- continue
95
- yield k, v
18
+ from falyx.themes import OneColors
19
+ from falyx.utils import chunks
96
20
 
97
21
 
98
22
  class MenuAction(BaseAction):
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import csv
6
6
  import json
7
7
  import xml.etree.ElementTree as ET
8
- from enum import Enum
9
8
  from pathlib import Path
10
9
  from typing import Any
11
10
 
@@ -15,7 +14,8 @@ from prompt_toolkit import PromptSession
15
14
  from rich.console import Console
16
15
  from rich.tree import Tree
17
16
 
18
- from falyx.action import BaseAction
17
+ from falyx.action.action import BaseAction
18
+ from falyx.action.types import FileReturnType
19
19
  from falyx.context import ExecutionContext
20
20
  from falyx.execution_registry import ExecutionRegistry as er
21
21
  from falyx.hook_manager import HookType
@@ -25,41 +25,7 @@ from falyx.selection import (
25
25
  prompt_for_selection,
26
26
  render_selection_dict_table,
27
27
  )
28
- from falyx.themes.colors import OneColors
29
-
30
-
31
- class FileReturnType(Enum):
32
- """Enum for file return types."""
33
-
34
- TEXT = "text"
35
- PATH = "path"
36
- JSON = "json"
37
- TOML = "toml"
38
- YAML = "yaml"
39
- CSV = "csv"
40
- TSV = "tsv"
41
- XML = "xml"
42
-
43
- @classmethod
44
- def _get_alias(cls, value: str) -> str:
45
- aliases = {
46
- "yml": "yaml",
47
- "txt": "text",
48
- "file": "path",
49
- "filepath": "path",
50
- }
51
- return aliases.get(value, value)
52
-
53
- @classmethod
54
- def _missing_(cls, value: object) -> FileReturnType:
55
- if isinstance(value, str):
56
- normalized = value.lower()
57
- alias = cls._get_alias(normalized)
58
- for member in cls:
59
- if member.value == alias:
60
- return member
61
- valid = ", ".join(member.value for member in cls)
62
- raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
28
+ from falyx.themes import OneColors
63
29
 
64
30
 
65
31
  class SelectFileAction(BaseAction):
@@ -6,7 +6,7 @@ from prompt_toolkit import PromptSession
6
6
  from rich.console import Console
7
7
  from rich.tree import Tree
8
8
 
9
- from falyx.action import BaseAction
9
+ from falyx.action.action import BaseAction
10
10
  from falyx.context import ExecutionContext
11
11
  from falyx.execution_registry import ExecutionRegistry as er
12
12
  from falyx.hook_manager import HookType
@@ -18,7 +18,7 @@ from falyx.selection import (
18
18
  render_selection_dict_table,
19
19
  render_selection_indexed_table,
20
20
  )
21
- from falyx.themes.colors import OneColors
21
+ from falyx.themes import OneColors
22
22
  from falyx.utils import CaseInsensitiveDict
23
23
 
24
24
 
@@ -0,0 +1,43 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """signal_action.py"""
3
+ from rich.tree import Tree
4
+
5
+ from falyx.action.action import Action
6
+ from falyx.signals import FlowSignal
7
+ from falyx.themes import OneColors
8
+
9
+
10
+ class SignalAction(Action):
11
+ """
12
+ An action that raises a control flow signal when executed.
13
+
14
+ Useful for exiting a menu, going back, or halting execution gracefully.
15
+ """
16
+
17
+ def __init__(self, name: str, signal: Exception):
18
+ self.signal = signal
19
+ super().__init__(name, action=self.raise_signal)
20
+
21
+ async def raise_signal(self, *args, **kwargs):
22
+ raise self.signal
23
+
24
+ @property
25
+ def signal(self):
26
+ return self._signal
27
+
28
+ @signal.setter
29
+ def signal(self, value: FlowSignal):
30
+ if not isinstance(value, FlowSignal):
31
+ raise TypeError(
32
+ f"Signal must be an FlowSignal instance, got {type(value).__name__}"
33
+ )
34
+ self._signal = value
35
+
36
+ def __str__(self):
37
+ return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
38
+
39
+ async def preview(self, parent: Tree | None = None):
40
+ label = f"[{OneColors.LIGHT_RED}]⚡ SignalAction[/] '{self.signal.__class__.__name__}'"
41
+ tree = parent.add(label) if parent else Tree(label)
42
+ if not parent:
43
+ self.console.print(tree)
falyx/action/types.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class FileReturnType(Enum):
7
+ """Enum for file return types."""
8
+
9
+ TEXT = "text"
10
+ PATH = "path"
11
+ JSON = "json"
12
+ TOML = "toml"
13
+ YAML = "yaml"
14
+ CSV = "csv"
15
+ TSV = "tsv"
16
+ XML = "xml"
17
+
18
+ @classmethod
19
+ def _get_alias(cls, value: str) -> str:
20
+ aliases = {
21
+ "yml": "yaml",
22
+ "txt": "text",
23
+ "file": "path",
24
+ "filepath": "path",
25
+ }
26
+ return aliases.get(value, value)
27
+
28
+ @classmethod
29
+ def _missing_(cls, value: object) -> FileReturnType:
30
+ if isinstance(value, str):
31
+ normalized = value.lower()
32
+ alias = cls._get_alias(normalized)
33
+ for member in cls:
34
+ if member.value == alias:
35
+ return member
36
+ valid = ", ".join(member.value for member in cls)
37
+ raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
falyx/bottom_bar.py CHANGED
@@ -8,7 +8,7 @@ from prompt_toolkit.key_binding import KeyBindings
8
8
  from rich.console import Console
9
9
 
10
10
  from falyx.options_manager import OptionsManager
11
- from falyx.themes.colors import OneColors
11
+ from falyx.themes import OneColors
12
12
  from falyx.utils import CaseInsensitiveDict, chunks
13
13
 
14
14
 
falyx/command.py CHANGED
@@ -26,19 +26,19 @@ 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 import Action, ActionGroup, BaseAction, ChainedAction
29
+ from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
30
+ from falyx.action.io_action import BaseIOAction
30
31
  from falyx.context import ExecutionContext
31
32
  from falyx.debug import register_debug_hooks
32
33
  from falyx.exceptions import FalyxError
33
34
  from falyx.execution_registry import ExecutionRegistry as er
34
35
  from falyx.hook_manager import HookManager, HookType
35
- from falyx.io_action import BaseIOAction
36
36
  from falyx.logger import logger
37
37
  from falyx.options_manager import OptionsManager
38
38
  from falyx.prompt_utils import confirm_async, should_prompt_user
39
39
  from falyx.retry import RetryPolicy
40
40
  from falyx.retry_utils import enable_retries_recursively
41
- from falyx.themes.colors import OneColors
41
+ from falyx.themes import OneColors
42
42
  from falyx.utils import _noop, ensure_async
43
43
 
44
44
  console = Console(color_system="auto")
falyx/config.py CHANGED
@@ -13,12 +13,12 @@ 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 import Action, BaseAction
16
+ from falyx.action.action import Action, BaseAction
17
17
  from falyx.command import Command
18
18
  from falyx.falyx import Falyx
19
19
  from falyx.logger import logger
20
20
  from falyx.retry import RetryPolicy
21
- from falyx.themes.colors import OneColors
21
+ from falyx.themes import OneColors
22
22
 
23
23
  console = Console(color_system="auto")
24
24
 
@@ -72,10 +72,10 @@ class RawCommand(BaseModel):
72
72
  description: str
73
73
  action: str
74
74
 
75
- args: tuple[Any, ...] = ()
76
- kwargs: dict[str, Any] = {}
77
- aliases: list[str] = []
78
- tags: list[str] = []
75
+ args: tuple[Any, ...] = Field(default_factory=tuple)
76
+ kwargs: dict[str, Any] = Field(default_factory=dict)
77
+ aliases: list[str] = Field(default_factory=list)
78
+ tags: list[str] = Field(default_factory=list)
79
79
  style: str = OneColors.WHITE
80
80
 
81
81
  confirm: bool = False
@@ -86,13 +86,13 @@ class RawCommand(BaseModel):
86
86
  spinner_message: str = "Processing..."
87
87
  spinner_type: str = "dots"
88
88
  spinner_style: str = OneColors.CYAN
89
- spinner_kwargs: dict[str, Any] = {}
89
+ spinner_kwargs: dict[str, Any] = Field(default_factory=dict)
90
90
 
91
- before_hooks: list[Callable] = []
92
- success_hooks: list[Callable] = []
93
- error_hooks: list[Callable] = []
94
- after_hooks: list[Callable] = []
95
- teardown_hooks: list[Callable] = []
91
+ before_hooks: list[Callable] = Field(default_factory=list)
92
+ success_hooks: list[Callable] = Field(default_factory=list)
93
+ error_hooks: list[Callable] = Field(default_factory=list)
94
+ after_hooks: list[Callable] = Field(default_factory=list)
95
+ teardown_hooks: list[Callable] = Field(default_factory=list)
96
96
 
97
97
  logging_hooks: bool = False
98
98
  retry: bool = False
@@ -129,6 +129,60 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
129
129
  return commands
130
130
 
131
131
 
132
+ def convert_submenus(
133
+ raw_submenus: list[dict[str, Any]], *, parent_path: Path | None = None, depth: int = 0
134
+ ) -> list[dict[str, Any]]:
135
+ submenus: list[dict[str, Any]] = []
136
+ for raw_submenu in raw_submenus:
137
+ if raw_submenu.get("config"):
138
+ config_path = Path(raw_submenu["config"])
139
+ if parent_path:
140
+ config_path = (parent_path.parent / config_path).resolve()
141
+ submenu = loader(config_path, _depth=depth + 1)
142
+ else:
143
+ submenu_module_path = raw_submenu.get("submenu")
144
+ if not isinstance(submenu_module_path, str):
145
+ console.print(
146
+ f"[{OneColors.DARK_RED}]❌ Invalid submenu path:[/] {submenu_module_path}"
147
+ )
148
+ sys.exit(1)
149
+ submenu = import_action(submenu_module_path)
150
+ if not isinstance(submenu, Falyx):
151
+ console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu:[/] {submenu}")
152
+ sys.exit(1)
153
+
154
+ key = raw_submenu.get("key")
155
+ if not isinstance(key, str):
156
+ console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu key:[/] {key}")
157
+ sys.exit(1)
158
+
159
+ description = raw_submenu.get("description")
160
+ if not isinstance(description, str):
161
+ console.print(
162
+ f"[{OneColors.DARK_RED}]❌ Invalid submenu description:[/] {description}"
163
+ )
164
+ sys.exit(1)
165
+
166
+ submenus.append(
167
+ Submenu(
168
+ key=key,
169
+ description=description,
170
+ submenu=submenu,
171
+ style=raw_submenu.get("style", OneColors.CYAN),
172
+ ).model_dump()
173
+ )
174
+ return submenus
175
+
176
+
177
+ class Submenu(BaseModel):
178
+ """Submenu model for Falyx CLI configuration."""
179
+
180
+ key: str
181
+ description: str
182
+ submenu: Any
183
+ style: str = OneColors.CYAN
184
+
185
+
132
186
  class FalyxConfig(BaseModel):
133
187
  """Falyx CLI configuration model."""
134
188
 
@@ -140,6 +194,7 @@ class FalyxConfig(BaseModel):
140
194
  welcome_message: str = ""
141
195
  exit_message: str = ""
142
196
  commands: list[Command] | list[dict] = []
197
+ submenus: list[dict[str, Any]] = []
143
198
 
144
199
  @model_validator(mode="after")
145
200
  def validate_prompt_format(self) -> FalyxConfig:
@@ -160,10 +215,12 @@ class FalyxConfig(BaseModel):
160
215
  exit_message=self.exit_message,
161
216
  )
162
217
  flx.add_commands(self.commands)
218
+ for submenu in self.submenus:
219
+ flx.add_submenu(**submenu)
163
220
  return flx
164
221
 
165
222
 
166
- def loader(file_path: Path | str) -> Falyx:
223
+ def loader(file_path: Path | str, _depth: int = 0) -> Falyx:
167
224
  """
168
225
  Load Falyx CLI configuration from a YAML or TOML file.
169
226
 
@@ -183,6 +240,9 @@ def loader(file_path: Path | str) -> Falyx:
183
240
  Raises:
184
241
  ValueError: If the file format is unsupported or file cannot be parsed.
185
242
  """
243
+ if _depth > 5:
244
+ raise ValueError("Maximum submenu depth exceeded (5 levels deep)")
245
+
186
246
  if isinstance(file_path, (str, Path)):
187
247
  path = Path(file_path)
188
248
  else:
@@ -212,6 +272,7 @@ def loader(file_path: Path | str) -> Falyx:
212
272
  )
213
273
 
214
274
  commands = convert_commands(raw_config["commands"])
275
+ submenus = convert_submenus(raw_config.get("submenus", []))
215
276
  return FalyxConfig(
216
277
  title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
217
278
  prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
@@ -219,4 +280,5 @@ def loader(file_path: Path | str) -> Falyx:
219
280
  welcome_message=raw_config.get("welcome_message", ""),
220
281
  exit_message=raw_config.get("exit_message", ""),
221
282
  commands=commands,
283
+ submenus=submenus,
222
284
  ).to_falyx()
@@ -37,7 +37,7 @@ from rich.table import Table
37
37
 
38
38
  from falyx.context import ExecutionContext
39
39
  from falyx.logger import logger
40
- from falyx.themes.colors import OneColors
40
+ from falyx.themes import OneColors
41
41
 
42
42
 
43
43
  class ExecutionRegistry:
falyx/falyx.py CHANGED
@@ -19,6 +19,8 @@ for running commands, actions, and workflows. It supports:
19
19
  Falyx enables building flexible, robust, and user-friendly
20
20
  terminal applications with minimal boilerplate.
21
21
  """
22
+ from __future__ import annotations
23
+
22
24
  import asyncio
23
25
  import logging
24
26
  import sys
@@ -38,7 +40,7 @@ from rich.console import Console
38
40
  from rich.markdown import Markdown
39
41
  from rich.table import Table
40
42
 
41
- from falyx.action import Action, BaseAction
43
+ from falyx.action.action import Action, BaseAction
42
44
  from falyx.bottom_bar import BottomBar
43
45
  from falyx.command import Command
44
46
  from falyx.context import ExecutionContext
@@ -56,7 +58,7 @@ from falyx.options_manager import OptionsManager
56
58
  from falyx.parsers import get_arg_parsers
57
59
  from falyx.retry import RetryPolicy
58
60
  from falyx.signals import BackSignal, QuitSignal
59
- from falyx.themes.colors import OneColors, get_nord_theme
61
+ from falyx.themes import OneColors, get_nord_theme
60
62
  from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
61
63
  from falyx.version import __version__
62
64
 
@@ -528,14 +530,15 @@ class Falyx:
528
530
  )
529
531
 
530
532
  def add_submenu(
531
- self, key: str, description: str, submenu: "Falyx", *, style: str = OneColors.CYAN
533
+ self, key: str, description: str, submenu: Falyx, *, style: str = OneColors.CYAN
532
534
  ) -> None:
533
535
  """Adds a submenu to the menu."""
534
536
  if not isinstance(submenu, Falyx):
535
537
  raise NotAFalyxError("submenu must be an instance of Falyx.")
536
538
  self._validate_command_key(key)
537
539
  self.add_command(key, description, submenu.menu, style=style)
538
- submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
540
+ if submenu.exit_command.key == "Q":
541
+ submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
539
542
 
540
543
  def add_commands(self, commands: list[Command] | list[dict]) -> None:
541
544
  """Adds a list of Command instances or config dicts."""
falyx/hooks.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, Callable
6
6
  from falyx.context import ExecutionContext
7
7
  from falyx.exceptions import CircuitBreakerOpen
8
8
  from falyx.logger import logger
9
- from falyx.themes.colors import OneColors
9
+ from falyx.themes import OneColors
10
10
 
11
11
 
12
12
  class ResultReporter:
falyx/menu.py ADDED
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from falyx.action import BaseAction
6
+ from falyx.signals import BackSignal, QuitSignal
7
+ from falyx.themes import OneColors
8
+ from falyx.utils import CaseInsensitiveDict
9
+
10
+
11
+ @dataclass
12
+ class MenuOption:
13
+ """Represents a single menu option with a description and an action to execute."""
14
+
15
+ description: str
16
+ action: BaseAction
17
+ style: str = OneColors.WHITE
18
+
19
+ def __post_init__(self):
20
+ if not isinstance(self.description, str):
21
+ raise TypeError("MenuOption description must be a string.")
22
+ if not isinstance(self.action, BaseAction):
23
+ raise TypeError("MenuOption action must be a BaseAction instance.")
24
+
25
+ def render(self, key: str) -> str:
26
+ """Render the menu option for display."""
27
+ return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
28
+
29
+
30
+ class MenuOptionMap(CaseInsensitiveDict):
31
+ """
32
+ Manages menu options including validation, reserved key protection,
33
+ and special signal entries like Quit and Back.
34
+ """
35
+
36
+ RESERVED_KEYS = {"Q", "B"}
37
+
38
+ def __init__(
39
+ self,
40
+ options: dict[str, MenuOption] | None = None,
41
+ allow_reserved: bool = False,
42
+ ):
43
+ super().__init__()
44
+ self.allow_reserved = allow_reserved
45
+ if options:
46
+ self.update(options)
47
+ self._inject_reserved_defaults()
48
+
49
+ def _inject_reserved_defaults(self):
50
+ from falyx.action import SignalAction
51
+
52
+ self._add_reserved(
53
+ "Q",
54
+ MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
55
+ )
56
+ self._add_reserved(
57
+ "B",
58
+ MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
59
+ )
60
+
61
+ def _add_reserved(self, key: str, option: MenuOption) -> None:
62
+ """Add a reserved key, bypassing validation."""
63
+ norm_key = key.upper()
64
+ super().__setitem__(norm_key, option)
65
+
66
+ def __setitem__(self, key: str, option: MenuOption) -> None:
67
+ if not isinstance(option, MenuOption):
68
+ raise TypeError(f"Value for key '{key}' must be a MenuOption.")
69
+ norm_key = key.upper()
70
+ if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
71
+ raise ValueError(
72
+ f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
73
+ )
74
+ super().__setitem__(norm_key, option)
75
+
76
+ def __delitem__(self, key: str) -> None:
77
+ if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
78
+ raise ValueError(f"Cannot delete reserved option '{key}'.")
79
+ super().__delitem__(key)
80
+
81
+ def items(self, include_reserved: bool = True):
82
+ for k, v in super().items():
83
+ if not include_reserved and k in self.RESERVED_KEYS:
84
+ continue
85
+ yield k, v
falyx/prompt_utils.py CHANGED
@@ -8,7 +8,7 @@ from prompt_toolkit.formatted_text import (
8
8
  )
9
9
 
10
10
  from falyx.options_manager import OptionsManager
11
- from falyx.themes.colors import OneColors
11
+ from falyx.themes import OneColors
12
12
  from falyx.validators import yes_no_validator
13
13
 
14
14
 
falyx/protocols.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, Protocol
6
6
 
7
- from falyx.action import BaseAction
7
+ from falyx.action.action import BaseAction
8
8
 
9
9
 
10
10
  class ActionFactoryProtocol(Protocol):
falyx/retry_utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """retry_utils.py"""
3
- from falyx.action import Action, BaseAction
3
+ from falyx.action.action import Action, BaseAction
4
4
  from falyx.hook_manager import HookType
5
5
  from falyx.retry import RetryHandler, RetryPolicy
6
6
 
falyx/selection.py CHANGED
@@ -9,7 +9,7 @@ from rich.console import Console
9
9
  from rich.markup import escape
10
10
  from rich.table import Table
11
11
 
12
- from falyx.themes.colors import OneColors
12
+ from falyx.themes import OneColors
13
13
  from falyx.utils import chunks
14
14
  from falyx.validators import int_range_validator, key_validator
15
15
 
@@ -0,0 +1,15 @@
1
+ """
2
+ Falyx CLI Framework
3
+
4
+ Copyright (c) 2025 rtj.dev LLC.
5
+ Licensed under the MIT License. See LICENSE file for details.
6
+ """
7
+
8
+ from .colors import ColorsMeta, NordColors, OneColors, get_nord_theme
9
+
10
+ __all__ = [
11
+ "OneColors",
12
+ "NordColors",
13
+ "get_nord_theme",
14
+ "ColorsMeta",
15
+ ]
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.24"
1
+ __version__ = "0.1.26"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: aiohttp (>=3.11,<4.0)
15
16
  Requires-Dist: prompt_toolkit (>=3.0,<4.0)
16
17
  Requires-Dist: pydantic (>=2.0,<3.0)
17
18
  Requires-Dist: python-json-logger (>=3.3.0,<4.0.0)
@@ -0,0 +1,46 @@
1
+ falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ falyx/__init__.py,sha256=L40665QyjAqHQxHdxxY2_yPeDa4p0LE7Nu_2dkm08Ls,650
3
+ falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
4
+ falyx/action/__init__.py,sha256=P3Eh0lE2VunzxqwfLbZKxE6FEcdR_u5OROkDTBYgeP0,904
5
+ falyx/action/action.py,sha256=CJB9eeeEqBGkZHjMpG24eXHRjouKSfESCI1zWzoE7JQ,32488
6
+ falyx/action/action_factory.py,sha256=qhlx8-BAEiNDJrbzF0HZzipY09Ple6J7FxxNvoBda9Y,4228
7
+ falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
8
+ falyx/action/io_action.py,sha256=zdDq07zSLlaShBQ3ztXTRC6aZL0JoERNZSmvHy1V22w,9718
9
+ falyx/action/menu_action.py,sha256=8qttPuq0AUh_oFaa3A82e19dSl4ZhQ9Y_j5CU7zHAlc,5532
10
+ falyx/action/select_file_action.py,sha256=hHLhmTSacWaUXhRTeIIiXt8gR7zbjkXJ2MAkKQYCpp4,7799
11
+ falyx/action/selection_action.py,sha256=22rF7UqRrQAMjGIheDqAbUizVMBg9aCl9e4VOLLZZJo,8811
12
+ falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
13
+ falyx/action/types.py,sha256=iVD-bHm1GRXOTIlHOeT_KcDBRZm4Hz5Xzl_BOalvEf4,961
14
+ falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
15
+ falyx/command.py,sha256=s7r9aeUYEk9iUNE69JQtlFoPx9AehTxkHMPxpLKVIOA,12238
16
+ falyx/config.py,sha256=8dkQfL-Ro-vWw1AcO2fD1PGZ92Cyfnwl885ZlpLkp4Y,9636
17
+ falyx/config_schema.py,sha256=j5GQuHVlaU-VLxLF9t8idZRjqOP9MIKp1hyd9NhpAGU,3124
18
+ falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
19
+ falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
20
+ 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
23
+ falyx/hook_manager.py,sha256=GuGxVVz9FXrU39Tk220QcsLsMXeut7ZDkGW3hU9GcwQ,2952
24
+ falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
25
+ falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
26
+ falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
27
+ falyx/menu.py,sha256=faxGgocqQYY6HtzVbenHaFj8YqsmycBEyziC8Ahzqjo,2870
28
+ falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
29
+ falyx/parsers.py,sha256=hxrBouQEqdgk6aWzNa7UwTg7u55vJffSEUUTiiQoI0U,5602
30
+ falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
31
+ falyx/protocols.py,sha256=dXNS-kh-5XB92PE5POy4uJ4KLT0O3ZAoiqw55jgR2IM,306
32
+ falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
33
+ falyx/retry_utils.py,sha256=EAzc-ECTu8AxKkmlw28ioOW9y-Y9tLQ0KasvSkBRYgs,694
34
+ falyx/selection.py,sha256=l2LLISqgP8xfHdcTAEbTTqs_Bae4-LVUKMN7VQH7tM0,10731
35
+ falyx/signals.py,sha256=4PTuVRB_P_aWfnU8pANqhMxGTLq7TJDEyk9jCp0Bx2c,713
36
+ falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
37
+ falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
38
+ falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
39
+ falyx/utils.py,sha256=uss-FV8p164pmhoqYtQt8gNp5z8fGbuMAk4dRJ6RopI,6717
40
+ 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,,
falyx/signal_action.py DELETED
@@ -1,31 +0,0 @@
1
- # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """signal_action.py"""
3
- from falyx.action import Action
4
- from falyx.signals import FlowSignal
5
-
6
-
7
- class SignalAction(Action):
8
- """
9
- An action that raises a control flow signal when executed.
10
-
11
- Useful for exiting a menu, going back, or halting execution gracefully.
12
- """
13
-
14
- def __init__(self, name: str, signal: Exception):
15
- if not isinstance(signal, FlowSignal):
16
- raise TypeError(
17
- f"Signal must be an FlowSignal instance, got {type(signal).__name__}"
18
- )
19
-
20
- async def raise_signal(*args, **kwargs):
21
- raise signal
22
-
23
- super().__init__(name=name, action=raise_signal)
24
- self._signal = signal
25
-
26
- @property
27
- def signal(self):
28
- return self._signal
29
-
30
- def __str__(self):
31
- return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
@@ -1,42 +0,0 @@
1
- falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- falyx/__init__.py,sha256=dYRamQJlT1Zoy5Uu1uG4NCV05Xk98nN1LAQrSR1CT2A,643
3
- falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
4
- falyx/action.py,sha256=9iOsqi-7tiNdBcGFrAQHGBCsTKZoBYXkClZE0DM9qhQ,32495
5
- falyx/action_factory.py,sha256=RKKb9C49xYNnMBgNvaDYTspVF3jmpBtNibrxlzLGiQQ,4228
6
- falyx/bottom_bar.py,sha256=GdyM-IY-E08c2GSaeAGVv1h1suOQuohFTRwbqtawbW0,7390
7
- falyx/command.py,sha256=PCnGDbx8E-168FcuUvz87YqR94FvYCQX8dj6tVXnMtw,12231
8
- falyx/config.py,sha256=nbAEea1-3zNhycrQ9aKuMqqa_EaQGBK0HcCAGsj_rZ0,7204
9
- falyx/config_schema.py,sha256=j5GQuHVlaU-VLxLF9t8idZRjqOP9MIKp1hyd9NhpAGU,3124
10
- falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
11
- falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
12
- falyx/exceptions.py,sha256=Qxp6UScZWEyno-6Lgksrv3s9iwjbr2U-d6hun-_xpc0,798
13
- falyx/execution_registry.py,sha256=io2hX9VkWJBRch-G7thla1eH_PgyVjjWf9qU5foOOEA,4719
14
- falyx/falyx.py,sha256=KuIAaZj4RgiFdiAzOMswZ8n6lUKN00L5k4zWoJA1PBk,40545
15
- falyx/hook_manager.py,sha256=GuGxVVz9FXrU39Tk220QcsLsMXeut7ZDkGW3hU9GcwQ,2952
16
- falyx/hooks.py,sha256=KOmUGP6xWU-eTW8QOl-qEflNRxZRf_OHA0N7gph13UM,2947
17
- falyx/http_action.py,sha256=sGADtRhSRuNu4UiEo1oeTHeC2rgAlALZm08-X4Pne34,5806
18
- falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
19
- falyx/io_action.py,sha256=5TIWeRUIffvkbdMbureLMiNk6mcy-tCaMMBRCeFFfgM,9718
20
- falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
21
- falyx/menu_action.py,sha256=jLYUR6-6F5NIDb9kP0Ysovmy3mtfO8xTXRRZMRaTtY0,8183
22
- falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
23
- falyx/parsers.py,sha256=hxrBouQEqdgk6aWzNa7UwTg7u55vJffSEUUTiiQoI0U,5602
24
- falyx/prompt_utils.py,sha256=6qt65HESo79-rZhIWpgndrYG6yJwk8tMSJCKXar0dP0,1549
25
- falyx/protocols.py,sha256=Sk2a1rz5Tk7iDUVTeitItNX-Kg3kwXOSwIjojEOE1mI,299
26
- falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
27
- falyx/retry_utils.py,sha256=1xPSr-1ZJsUXnXFyGNZZJ9h9eE4ExHjJMkyjHIm5cd8,687
28
- falyx/select_file_action.py,sha256=698t0Mc5_6dPXjxuKBC_O7u35o08nLqkgpQH82dZafc,8678
29
- falyx/selection.py,sha256=r__wrXaLz4oJobeqniKvHrEhe9vYVsZo1SoB3Cn6qRM,10738
30
- falyx/selection_action.py,sha256=PdY-3ewMhgC8F3DKS6TCWcAoOD3va9Hp8bPkdZSMWBE,8811
31
- falyx/signal_action.py,sha256=DSwVkL81PLWAHRHZbpiYlPH1ew97KL6TPs3XlU--RJY,916
32
- falyx/signals.py,sha256=4PTuVRB_P_aWfnU8pANqhMxGTLq7TJDEyk9jCp0Bx2c,713
33
- falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
34
- falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
35
- falyx/utils.py,sha256=uss-FV8p164pmhoqYtQt8gNp5z8fGbuMAk4dRJ6RopI,6717
36
- falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
37
- falyx/version.py,sha256=Jq7e1LcKcQSNVg4EOJ-acPyPgs8Os5cYEZWXrQsI7Pg,23
38
- falyx-0.1.24.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
39
- falyx-0.1.24.dist-info/METADATA,sha256=b7Ca1bcIlHrVsb3YLlSUQCgzoh0Jvd7qfUeD6ms5mWo,5484
40
- falyx-0.1.24.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
41
- falyx-0.1.24.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
42
- falyx-0.1.24.dist-info/RECORD,,
File without changes