Add loading submenus from config or Falyx object, more examples
This commit is contained in:
parent
2bdca72e04
commit
bba473047c
|
@ -20,3 +20,12 @@ commands:
|
||||||
action: menu_demo.menu
|
action: menu_demo.menu
|
||||||
tags: [menu, demo]
|
tags: [menu, demo]
|
||||||
help_text: Run a menu demo with multiple options.
|
help_text: Run a menu demo with multiple options.
|
||||||
|
|
||||||
|
submenus:
|
||||||
|
- key: C
|
||||||
|
description: Process Menu (From Config)
|
||||||
|
config: process.yaml
|
||||||
|
|
||||||
|
- key: U
|
||||||
|
description: Submenu From Python
|
||||||
|
submenu: submenu.submenu
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
commands:
|
||||||
|
- key: T
|
||||||
|
description: HTTP Test
|
||||||
|
action: single_http.http_action
|
||||||
|
tags: [http, demo]
|
||||||
|
help_text: Run HTTP test.
|
|
@ -0,0 +1,11 @@
|
||||||
|
commands:
|
||||||
|
- key: P
|
||||||
|
description: Pipeline Demo
|
||||||
|
action: pipeline_demo.pipeline
|
||||||
|
tags: [pipeline, demo]
|
||||||
|
help_text: Run Demployment Pipeline with retries.
|
||||||
|
|
||||||
|
submenus:
|
||||||
|
- key: C
|
||||||
|
description: HTTP Test (Nested From Config)
|
||||||
|
config: http.yaml
|
|
@ -0,0 +1,31 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx import Action, Falyx
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
state = {"count": 0}
|
||||||
|
|
||||||
|
async def flaky():
|
||||||
|
if not state["count"]:
|
||||||
|
state["count"] += 1
|
||||||
|
print("Flaky step failed, retrying...")
|
||||||
|
raise RuntimeError("Random failure!")
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
# Add a command that raises an exception
|
||||||
|
falyx.add_command(
|
||||||
|
key="E",
|
||||||
|
description="Error Command",
|
||||||
|
action=Action("flaky", flaky),
|
||||||
|
retry=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await falyx.run_key("E")
|
||||||
|
print(result)
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
falyx = Falyx("Headless Recovery Test")
|
||||||
|
asyncio.run(main())
|
|
@ -0,0 +1,14 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx.action import HTTPAction
|
||||||
|
|
||||||
|
http_action = HTTPAction(
|
||||||
|
name="Get Example",
|
||||||
|
method="GET",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
retry=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(http_action())
|
|
@ -72,10 +72,10 @@ class RawCommand(BaseModel):
|
||||||
description: str
|
description: str
|
||||||
action: str
|
action: str
|
||||||
|
|
||||||
args: tuple[Any, ...] = ()
|
args: tuple[Any, ...] = Field(default_factory=tuple)
|
||||||
kwargs: dict[str, Any] = {}
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||||
aliases: list[str] = []
|
aliases: list[str] = Field(default_factory=list)
|
||||||
tags: list[str] = []
|
tags: list[str] = Field(default_factory=list)
|
||||||
style: str = OneColors.WHITE
|
style: str = OneColors.WHITE
|
||||||
|
|
||||||
confirm: bool = False
|
confirm: bool = False
|
||||||
|
@ -86,13 +86,13 @@ class RawCommand(BaseModel):
|
||||||
spinner_message: str = "Processing..."
|
spinner_message: str = "Processing..."
|
||||||
spinner_type: str = "dots"
|
spinner_type: str = "dots"
|
||||||
spinner_style: str = OneColors.CYAN
|
spinner_style: str = OneColors.CYAN
|
||||||
spinner_kwargs: dict[str, Any] = {}
|
spinner_kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
before_hooks: list[Callable] = []
|
before_hooks: list[Callable] = Field(default_factory=list)
|
||||||
success_hooks: list[Callable] = []
|
success_hooks: list[Callable] = Field(default_factory=list)
|
||||||
error_hooks: list[Callable] = []
|
error_hooks: list[Callable] = Field(default_factory=list)
|
||||||
after_hooks: list[Callable] = []
|
after_hooks: list[Callable] = Field(default_factory=list)
|
||||||
teardown_hooks: list[Callable] = []
|
teardown_hooks: list[Callable] = Field(default_factory=list)
|
||||||
|
|
||||||
logging_hooks: bool = False
|
logging_hooks: bool = False
|
||||||
retry: bool = False
|
retry: bool = False
|
||||||
|
@ -129,6 +129,60 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def convert_submenus(
|
||||||
|
raw_submenus: list[dict[str, Any]], *, parent_path: Path | None = None, depth: int = 0
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
submenus: list[dict[str, Any]] = []
|
||||||
|
for raw_submenu in raw_submenus:
|
||||||
|
if raw_submenu.get("config"):
|
||||||
|
config_path = Path(raw_submenu["config"])
|
||||||
|
if parent_path:
|
||||||
|
config_path = (parent_path.parent / config_path).resolve()
|
||||||
|
submenu = loader(config_path, _depth=depth + 1)
|
||||||
|
else:
|
||||||
|
submenu_module_path = raw_submenu.get("submenu")
|
||||||
|
if not isinstance(submenu_module_path, str):
|
||||||
|
console.print(
|
||||||
|
f"[{OneColors.DARK_RED}]❌ Invalid submenu path:[/] {submenu_module_path}"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
submenu = import_action(submenu_module_path)
|
||||||
|
if not isinstance(submenu, Falyx):
|
||||||
|
console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu:[/] {submenu}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
key = raw_submenu.get("key")
|
||||||
|
if not isinstance(key, str):
|
||||||
|
console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu key:[/] {key}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
description = raw_submenu.get("description")
|
||||||
|
if not isinstance(description, str):
|
||||||
|
console.print(
|
||||||
|
f"[{OneColors.DARK_RED}]❌ Invalid submenu description:[/] {description}"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
submenus.append(
|
||||||
|
Submenu(
|
||||||
|
key=key,
|
||||||
|
description=description,
|
||||||
|
submenu=submenu,
|
||||||
|
style=raw_submenu.get("style", OneColors.CYAN),
|
||||||
|
).model_dump()
|
||||||
|
)
|
||||||
|
return submenus
|
||||||
|
|
||||||
|
|
||||||
|
class Submenu(BaseModel):
|
||||||
|
"""Submenu model for Falyx CLI configuration."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
description: str
|
||||||
|
submenu: Any
|
||||||
|
style: str = OneColors.CYAN
|
||||||
|
|
||||||
|
|
||||||
class FalyxConfig(BaseModel):
|
class FalyxConfig(BaseModel):
|
||||||
"""Falyx CLI configuration model."""
|
"""Falyx CLI configuration model."""
|
||||||
|
|
||||||
|
@ -140,6 +194,7 @@ class FalyxConfig(BaseModel):
|
||||||
welcome_message: str = ""
|
welcome_message: str = ""
|
||||||
exit_message: str = ""
|
exit_message: str = ""
|
||||||
commands: list[Command] | list[dict] = []
|
commands: list[Command] | list[dict] = []
|
||||||
|
submenus: list[dict[str, Any]] = []
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_prompt_format(self) -> FalyxConfig:
|
def validate_prompt_format(self) -> FalyxConfig:
|
||||||
|
@ -160,10 +215,12 @@ class FalyxConfig(BaseModel):
|
||||||
exit_message=self.exit_message,
|
exit_message=self.exit_message,
|
||||||
)
|
)
|
||||||
flx.add_commands(self.commands)
|
flx.add_commands(self.commands)
|
||||||
|
for submenu in self.submenus:
|
||||||
|
flx.add_submenu(**submenu)
|
||||||
return flx
|
return flx
|
||||||
|
|
||||||
|
|
||||||
def loader(file_path: Path | str) -> Falyx:
|
def loader(file_path: Path | str, _depth: int = 0) -> Falyx:
|
||||||
"""
|
"""
|
||||||
Load Falyx CLI configuration from a YAML or TOML file.
|
Load Falyx CLI configuration from a YAML or TOML file.
|
||||||
|
|
||||||
|
@ -183,6 +240,9 @@ def loader(file_path: Path | str) -> Falyx:
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the file format is unsupported or file cannot be parsed.
|
ValueError: If the file format is unsupported or file cannot be parsed.
|
||||||
"""
|
"""
|
||||||
|
if _depth > 5:
|
||||||
|
raise ValueError("Maximum submenu depth exceeded (5 levels deep)")
|
||||||
|
|
||||||
if isinstance(file_path, (str, Path)):
|
if isinstance(file_path, (str, Path)):
|
||||||
path = Path(file_path)
|
path = Path(file_path)
|
||||||
else:
|
else:
|
||||||
|
@ -212,6 +272,7 @@ def loader(file_path: Path | str) -> Falyx:
|
||||||
)
|
)
|
||||||
|
|
||||||
commands = convert_commands(raw_config["commands"])
|
commands = convert_commands(raw_config["commands"])
|
||||||
|
submenus = convert_submenus(raw_config.get("submenus", []))
|
||||||
return FalyxConfig(
|
return FalyxConfig(
|
||||||
title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
|
title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
|
||||||
prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
|
prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
|
||||||
|
@ -219,4 +280,5 @@ def loader(file_path: Path | str) -> Falyx:
|
||||||
welcome_message=raw_config.get("welcome_message", ""),
|
welcome_message=raw_config.get("welcome_message", ""),
|
||||||
exit_message=raw_config.get("exit_message", ""),
|
exit_message=raw_config.get("exit_message", ""),
|
||||||
commands=commands,
|
commands=commands,
|
||||||
|
submenus=submenus,
|
||||||
).to_falyx()
|
).to_falyx()
|
||||||
|
|
|
@ -19,6 +19,8 @@ for running commands, actions, and workflows. It supports:
|
||||||
Falyx enables building flexible, robust, and user-friendly
|
Falyx enables building flexible, robust, and user-friendly
|
||||||
terminal applications with minimal boilerplate.
|
terminal applications with minimal boilerplate.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -528,13 +530,14 @@ class Falyx:
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_submenu(
|
def add_submenu(
|
||||||
self, key: str, description: str, submenu: "Falyx", *, style: str = OneColors.CYAN
|
self, key: str, description: str, submenu: Falyx, *, style: str = OneColors.CYAN
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Adds a submenu to the menu."""
|
"""Adds a submenu to the menu."""
|
||||||
if not isinstance(submenu, Falyx):
|
if not isinstance(submenu, Falyx):
|
||||||
raise NotAFalyxError("submenu must be an instance of Falyx.")
|
raise NotAFalyxError("submenu must be an instance of Falyx.")
|
||||||
self._validate_command_key(key)
|
self._validate_command_key(key)
|
||||||
self.add_command(key, description, submenu.menu, style=style)
|
self.add_command(key, description, submenu.menu, style=style)
|
||||||
|
if submenu.exit_command.key == "Q":
|
||||||
submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
|
submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
|
||||||
|
|
||||||
def add_commands(self, commands: list[Command] | list[dict]) -> None:
|
def add_commands(self, commands: list[Command] | list[dict]) -> None:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.25"
|
__version__ = "0.1.26"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.25"
|
version = "0.1.26"
|
||||||
description = "Reliable and introspectable async CLI action framework."
|
description = "Reliable and introspectable async CLI action framework."
|
||||||
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
Loading…
Reference in New Issue