Rename falyx.action.types.FileReturnType -> falyx.action.action_types.FileType, falyx.action.base -> falyx.action.base_action, argparse tweaks for custom cli programs

This commit is contained in:
2025-06-10 23:03:09 -04:00
parent 2d1177e820
commit 38f5f1e934
29 changed files with 91 additions and 65 deletions

View File

@ -2,19 +2,24 @@ import asyncio
from falyx import Falyx
from falyx.action import SelectFileAction
from falyx.action.types import FileReturnType
from falyx.action.action_types import FileType
sf = SelectFileAction(
name="select_file",
suffix_filter=".yaml",
title="Select a YAML file",
prompt_message="Choose 2 > ",
return_type=FileReturnType.TEXT,
return_type=FileType.TEXT,
columns=3,
number_selections=2,
)
flx = Falyx()
flx = Falyx(
title="File Selection Example",
description="This example demonstrates how to select files using Falyx.",
version="1.0.0",
program="file_select.py",
)
flx.add_command(
key="S",

View File

@ -8,7 +8,7 @@ Licensed under the MIT License. See LICENSE file for details.
from .action import Action
from .action_factory import ActionFactoryAction
from .action_group import ActionGroup
from .base import BaseAction
from .base_action import BaseAction
from .chained_action import ChainedAction
from .fallback_action import FallbackAction
from .http_action import HTTPAction

View File

@ -6,7 +6,7 @@ from typing import Any, Callable
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookManager, HookType

View File

@ -4,7 +4,7 @@ from typing import Any, Callable
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType

View File

@ -2,12 +2,12 @@
"""action_group.py"""
import asyncio
import random
from typing import Any, Callable
from typing import Any, Callable, Sequence
from rich.tree import Tree
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.action.mixins import ActionListMixin
from falyx.context import ExecutionContext, SharedContext
from falyx.execution_registry import ExecutionRegistry as er
@ -54,7 +54,7 @@ class ActionGroup(BaseAction, ActionListMixin):
def __init__(
self,
name: str,
actions: list[BaseAction] | None = None,
actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
*,
hooks: HookManager | None = None,
inject_last_result: bool = False,
@ -70,7 +70,7 @@ class ActionGroup(BaseAction, ActionListMixin):
if actions:
self.set_actions(actions)
def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
if isinstance(action, BaseAction):
return action
elif callable(action):
@ -81,12 +81,18 @@ class ActionGroup(BaseAction, ActionListMixin):
f"{type(action).__name__}"
)
def add_action(self, action: BaseAction | Any) -> None:
def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
action = self._wrap_if_needed(action)
super().add_action(action)
if hasattr(action, "register_teardown") and callable(action.register_teardown):
action.register_teardown(self.hooks)
def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
"""Replaces the current action list with a new one."""
self.actions.clear()
for action in actions:
self.add_action(action)
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
arg_defs = same_argument_definitions(self.actions)
if arg_defs:

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from enum import Enum
class FileReturnType(Enum):
class FileType(Enum):
"""Enum for file return types."""
TEXT = "text"
@ -28,7 +28,7 @@ class FileReturnType(Enum):
return aliases.get(value, value)
@classmethod
def _missing_(cls, value: object) -> FileReturnType:
def _missing_(cls, value: object) -> FileType:
if isinstance(value, str):
normalized = value.lower()
alias = cls._get_alias(normalized)
@ -36,7 +36,7 @@ class FileReturnType(Enum):
if member.value == alias:
return member
valid = ", ".join(member.value for member in cls)
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
raise ValueError(f"Invalid FileType: '{value}'. Must be one of: {valid}")
class SelectionReturnType(Enum):

View File

@ -1,5 +1,5 @@
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""base.py
"""base_action.py
Core action system for Falyx.

View File

@ -2,12 +2,12 @@
"""chained_action.py"""
from __future__ import annotations
from typing import Any, Callable
from typing import Any, Callable, Sequence
from rich.tree import Tree
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.action.fallback_action import FallbackAction
from falyx.action.literal_input_action import LiteralInputAction
from falyx.action.mixins import ActionListMixin
@ -47,7 +47,7 @@ class ChainedAction(BaseAction, ActionListMixin):
def __init__(
self,
name: str,
actions: list[BaseAction | Any] | None = None,
actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
*,
hooks: HookManager | None = None,
inject_last_result: bool = False,
@ -67,7 +67,7 @@ class ChainedAction(BaseAction, ActionListMixin):
if actions:
self.set_actions(actions)
def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
if isinstance(action, BaseAction):
return action
elif callable(action):
@ -75,7 +75,7 @@ class ChainedAction(BaseAction, ActionListMixin):
else:
return LiteralInputAction(action)
def add_action(self, action: BaseAction | Any) -> None:
def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
action = self._wrap_if_needed(action)
if self.actions and self.auto_inject and not action.inject_last_result:
action.inject_last_result = True
@ -83,6 +83,12 @@ class ChainedAction(BaseAction, ActionListMixin):
if hasattr(action, "register_teardown") and callable(action.register_teardown):
action.register_teardown(self.hooks)
def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
"""Replaces the current action list with a new one."""
self.actions.clear()
for action in actions:
self.add_action(action)
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
if self.actions:
return self.actions[0].get_infer_target()

View File

@ -21,7 +21,7 @@ from typing import Any, Callable
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookManager, HookType

View File

@ -7,7 +7,7 @@ from rich.console import Console
from rich.table import Table
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType

View File

@ -1,6 +1,8 @@
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""mixins.py"""
from falyx.action.base import BaseAction
from typing import Sequence
from falyx.action.base_action import BaseAction
class ActionListMixin:
@ -9,7 +11,7 @@ class ActionListMixin:
def __init__(self) -> None:
self.actions: list[BaseAction] = []
def set_actions(self, actions: list[BaseAction]) -> None:
def set_actions(self, actions: Sequence[BaseAction]) -> None:
"""Replaces the current action list with a new one."""
self.actions.clear()
for action in actions:

View File

@ -9,7 +9,7 @@ from typing import Any, Callable
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookManager, HookType

View File

@ -11,7 +11,7 @@ from typing import Any, Callable
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext, SharedContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookManager, HookType

View File

@ -7,7 +7,7 @@ from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
from rich.console import Console
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType

View File

@ -14,8 +14,8 @@ from prompt_toolkit import PromptSession
from rich.console import Console
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.types import FileReturnType
from falyx.action.action_types import FileType
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType
@ -50,7 +50,7 @@ class SelectFileAction(BaseAction):
prompt_message (str): Message to display when prompting for selection.
style (str): Style for the selection options.
suffix_filter (str | None): Restrict to certain file types.
return_type (FileReturnType): What to return (path, content, parsed).
return_type (FileType): What to return (path, content, parsed).
console (Console | None): Console instance for output.
prompt_session (PromptSession | None): Prompt session for user input.
"""
@ -65,7 +65,7 @@ class SelectFileAction(BaseAction):
prompt_message: str = "Choose > ",
style: str = OneColors.WHITE,
suffix_filter: str | None = None,
return_type: FileReturnType | str = FileReturnType.PATH,
return_type: FileType | str = FileType.PATH,
number_selections: int | str = 1,
separator: str = ",",
allow_duplicates: bool = False,
@ -104,35 +104,35 @@ class SelectFileAction(BaseAction):
else:
raise ValueError("number_selections must be a positive integer or one of '*'")
def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType:
if isinstance(return_type, FileReturnType):
def _coerce_return_type(self, return_type: FileType | str) -> FileType:
if isinstance(return_type, FileType):
return return_type
return FileReturnType(return_type)
return FileType(return_type)
def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
value: Any
options = {}
for index, file in enumerate(files):
try:
if self.return_type == FileReturnType.TEXT:
if self.return_type == FileType.TEXT:
value = file.read_text(encoding="UTF-8")
elif self.return_type == FileReturnType.PATH:
elif self.return_type == FileType.PATH:
value = file
elif self.return_type == FileReturnType.JSON:
elif self.return_type == FileType.JSON:
value = json.loads(file.read_text(encoding="UTF-8"))
elif self.return_type == FileReturnType.TOML:
elif self.return_type == FileType.TOML:
value = toml.loads(file.read_text(encoding="UTF-8"))
elif self.return_type == FileReturnType.YAML:
elif self.return_type == FileType.YAML:
value = yaml.safe_load(file.read_text(encoding="UTF-8"))
elif self.return_type == FileReturnType.CSV:
elif self.return_type == FileType.CSV:
with open(file, newline="", encoding="UTF-8") as csvfile:
reader = csv.reader(csvfile)
value = list(reader)
elif self.return_type == FileReturnType.TSV:
elif self.return_type == FileType.TSV:
with open(file, newline="", encoding="UTF-8") as tsvfile:
reader = csv.reader(tsvfile, delimiter="\t")
value = list(reader)
elif self.return_type == FileReturnType.XML:
elif self.return_type == FileType.XML:
tree = ET.parse(file, parser=ET.XMLParser(encoding="UTF-8"))
root = tree.getroot()
value = ET.tostring(root, encoding="unicode")

View File

@ -6,8 +6,8 @@ from prompt_toolkit import PromptSession
from rich.console import Console
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.types import SelectionReturnType
from falyx.action.action_types import SelectionReturnType
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType

View File

@ -5,7 +5,7 @@ from prompt_toolkit.validation import Validator
from rich.console import Console
from rich.tree import Tree
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType

View File

@ -27,7 +27,7 @@ from rich.console import Console
from rich.tree import Tree
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.context import ExecutionContext
from falyx.debug import register_debug_hooks
from falyx.execution_registry import ExecutionRegistry as er

View File

@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
from rich.console import Console
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.command import Command
from falyx.falyx import Falyx
from falyx.logger import logger

View File

@ -43,7 +43,7 @@ from rich.markdown import Markdown
from rich.table import Table
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.bottom_bar import BottomBar
from falyx.command import Command
from falyx.context import ExecutionContext
@ -346,7 +346,6 @@ class Falyx:
aliases=["HISTORY"],
action=Action(name="View Execution History", action=er.summary),
style=OneColors.DARK_YELLOW,
simple_help_signature=True,
arg_parser=parser,
help_text="View the execution history of commands.",
)
@ -1152,7 +1151,7 @@ class Falyx:
sys.exit(0)
if self.cli_args.command == "version" or self.cli_args.version:
self.console.print(f"[{self.version_style}]{self.program} v{__version__}[/]")
self.console.print(f"[{self.version_style}]{self.program} v{self.version}[/]")
sys.exit(0)
if self.cli_args.command == "preview":

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
from prompt_toolkit.formatted_text import FormattedText
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.signals import BackSignal, QuitSignal
from falyx.themes import OneColors
from falyx.utils import CaseInsensitiveDict

View File

@ -3,7 +3,7 @@
from dataclasses import dataclass
from typing import Any
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.parser.argument_action import ArgumentAction

View File

@ -9,7 +9,7 @@ from rich.console import Console
from rich.markup import escape
from rich.text import Text
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.exceptions import CommandArgumentError
from falyx.parser.argument import Argument
from falyx.parser.argument_action import ArgumentAction

View File

@ -76,14 +76,14 @@ def get_root_parser(
help="Run in non-interactive mode with all prompts bypassed.",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable debug logging for Falyx."
"-v", "--verbose", action="store_true", help=f"Enable debug logging for {prog}."
)
parser.add_argument(
"--debug-hooks",
action="store_true",
help="Enable default lifecycle debug logging",
)
parser.add_argument("--version", action="store_true", help="Show Falyx version")
parser.add_argument("--version", action="store_true", help=f"Show {prog} version")
return parser
@ -98,7 +98,6 @@ def get_subparsers(
subparsers = parser.add_subparsers(
title=title,
description=description,
metavar="COMMAND",
dest="command",
)
return subparsers
@ -124,6 +123,8 @@ def get_arg_parsers(
subparsers: _SubParsersAction | None = None,
) -> FalyxParsers:
"""Returns the argument parser for the CLI."""
if epilog is None:
epilog = f"Tip: Use '{prog} run ?[COMMAND]' to preview any command from the CLI."
if root_parser is None:
parser = get_root_parser(
prog=prog,
@ -145,7 +146,14 @@ def get_arg_parsers(
parser = root_parser
if subparsers is None:
subparsers = get_subparsers(parser)
if prog == "falyx":
subparsers = get_subparsers(
parser,
title="Falyx Commands",
description="Available commands for the Falyx CLI.",
)
else:
subparsers = get_subparsers(parser, title="subcommands", description=None)
if not isinstance(subparsers, _SubParsersAction):
raise TypeError("subparsers must be an instance of _SubParsersAction")
@ -154,10 +162,10 @@ def get_arg_parsers(
if isinstance(commands, dict):
for command in commands.values():
run_description.append(command.usage)
command_description = command.description or command.help_text
command_description = command.help_text or command.description
run_description.append(f"{' '*24}{command_description}")
run_epilog = (
"Tip: Use 'falyx run ?[COMMAND]' to preview commands by their key or alias."
f"Tip: Use '{prog} run ?[COMMAND]' to preview commands by their key or alias."
)
run_parser = subparsers.add_parser(
"run",
@ -259,7 +267,7 @@ def get_arg_parsers(
"-t", "--tag", help="Filter commands by tag (case-insensitive)", default=None
)
version_parser = subparsers.add_parser("version", help="Show the Falyx version")
version_parser = subparsers.add_parser("version", help=f"Show {prog} version")
return FalyxParsers(
root=parser,

View File

@ -6,7 +6,7 @@ from typing import Any, Literal, Union, get_args, get_origin
from dateutil import parser as date_parser
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.logger import logger
from falyx.parser.signature import infer_args_from_func

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any, Awaitable, Protocol, runtime_checkable
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
@runtime_checkable

View File

@ -1,7 +1,7 @@
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""retry_utils.py"""
from falyx.action.action import Action
from falyx.action.base import BaseAction
from falyx.action.base_action import BaseAction
from falyx.hook_manager import HookType
from falyx.retry import RetryHandler, RetryPolicy

View File

@ -1 +1 @@
__version__ = "0.1.52"
__version__ = "0.1.53"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "falyx"
version = "0.1.52"
version = "0.1.53"
description = "Reliable and introspectable async CLI action framework."
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
license = "MIT"