diff --git a/examples/file_select.py b/examples/file_select.py index b55c564..d2289e5 100644 --- a/examples/file_select.py +++ b/examples/file_select.py @@ -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", diff --git a/falyx/action/__init__.py b/falyx/action/__init__.py index 85d7fb9..09113d6 100644 --- a/falyx/action/__init__.py +++ b/falyx/action/__init__.py @@ -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 diff --git a/falyx/action/action.py b/falyx/action/action.py index a6d3009..48a2731 100644 --- a/falyx/action/action.py +++ b/falyx/action/action.py @@ -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 diff --git a/falyx/action/action_factory.py b/falyx/action/action_factory.py index 5b9ef1d..0842fe1 100644 --- a/falyx/action/action_factory.py +++ b/falyx/action/action_factory.py @@ -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 diff --git a/falyx/action/action_group.py b/falyx/action/action_group.py index f3140bd..db44008 100644 --- a/falyx/action/action_group.py +++ b/falyx/action/action_group.py @@ -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: diff --git a/falyx/action/types.py b/falyx/action/action_types.py similarity index 88% rename from falyx/action/types.py rename to falyx/action/action_types.py index bc43b63..42af84a 100644 --- a/falyx/action/types.py +++ b/falyx/action/action_types.py @@ -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): diff --git a/falyx/action/base.py b/falyx/action/base_action.py similarity index 99% rename from falyx/action/base.py rename to falyx/action/base_action.py index 9d26ee6..f49e12f 100644 --- a/falyx/action/base.py +++ b/falyx/action/base_action.py @@ -1,5 +1,5 @@ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -"""base.py +"""base_action.py Core action system for Falyx. diff --git a/falyx/action/chained_action.py b/falyx/action/chained_action.py index 57ec8cf..c5dfcc6 100644 --- a/falyx/action/chained_action.py +++ b/falyx/action/chained_action.py @@ -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() diff --git a/falyx/action/io_action.py b/falyx/action/io_action.py index 95f820e..820989a 100644 --- a/falyx/action/io_action.py +++ b/falyx/action/io_action.py @@ -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 diff --git a/falyx/action/menu_action.py b/falyx/action/menu_action.py index 455e579..d52089a 100644 --- a/falyx/action/menu_action.py +++ b/falyx/action/menu_action.py @@ -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 diff --git a/falyx/action/mixins.py b/falyx/action/mixins.py index da9d615..7e0f099 100644 --- a/falyx/action/mixins.py +++ b/falyx/action/mixins.py @@ -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: diff --git a/falyx/action/process_action.py b/falyx/action/process_action.py index d711ec6..3343506 100644 --- a/falyx/action/process_action.py +++ b/falyx/action/process_action.py @@ -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 diff --git a/falyx/action/process_pool_action.py b/falyx/action/process_pool_action.py index 027eb6c..cc3baee 100644 --- a/falyx/action/process_pool_action.py +++ b/falyx/action/process_pool_action.py @@ -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 diff --git a/falyx/action/prompt_menu_action.py b/falyx/action/prompt_menu_action.py index ceece0b..ba71d13 100644 --- a/falyx/action/prompt_menu_action.py +++ b/falyx/action/prompt_menu_action.py @@ -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 diff --git a/falyx/action/select_file_action.py b/falyx/action/select_file_action.py index 02d9ef2..867728a 100644 --- a/falyx/action/select_file_action.py +++ b/falyx/action/select_file_action.py @@ -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") diff --git a/falyx/action/selection_action.py b/falyx/action/selection_action.py index d4f460f..39de339 100644 --- a/falyx/action/selection_action.py +++ b/falyx/action/selection_action.py @@ -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 diff --git a/falyx/action/user_input_action.py b/falyx/action/user_input_action.py index 73c2cbc..926e9cb 100644 --- a/falyx/action/user_input_action.py +++ b/falyx/action/user_input_action.py @@ -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 diff --git a/falyx/command.py b/falyx/command.py index a9e4cc9..9336aba 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -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 diff --git a/falyx/config.py b/falyx/config.py index b035940..ba8bde6 100644 --- a/falyx/config.py +++ b/falyx/config.py @@ -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 diff --git a/falyx/falyx.py b/falyx/falyx.py index 8e2fd59..62390d7 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -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": diff --git a/falyx/menu.py b/falyx/menu.py index b7e75f0..a641223 100644 --- a/falyx/menu.py +++ b/falyx/menu.py @@ -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 diff --git a/falyx/parser/argument.py b/falyx/parser/argument.py index e3d3e04..0c4bd27 100644 --- a/falyx/parser/argument.py +++ b/falyx/parser/argument.py @@ -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 diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index 58bff85..245b920 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -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 diff --git a/falyx/parser/parsers.py b/falyx/parser/parsers.py index ee71fb3..532d43f 100644 --- a/falyx/parser/parsers.py +++ b/falyx/parser/parsers.py @@ -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, diff --git a/falyx/parser/utils.py b/falyx/parser/utils.py index 78c507c..06b41fa 100644 --- a/falyx/parser/utils.py +++ b/falyx/parser/utils.py @@ -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 diff --git a/falyx/protocols.py b/falyx/protocols.py index f1c326f..2415869 100644 --- a/falyx/protocols.py +++ b/falyx/protocols.py @@ -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 diff --git a/falyx/retry_utils.py b/falyx/retry_utils.py index 7393f9f..ee2c1c3 100644 --- a/falyx/retry_utils.py +++ b/falyx/retry_utils.py @@ -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 diff --git a/falyx/version.py b/falyx/version.py index 5c02d64..276b9f6 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.52" +__version__ = "0.1.53" diff --git a/pyproject.toml b/pyproject.toml index 21469f6..7cef5b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "MIT"