From 294bbc906255c9f45aaa017561c9b6fa12713eec Mon Sep 17 00:00:00 2001 From: Roland Thomas Date: Sat, 12 Jul 2025 21:12:34 -0400 Subject: [PATCH] Add data, create_dirs to SaveFileAction --- examples/confirm_example.py | 28 +++++++++++++++------ falyx/action/save_file_action.py | 23 ++++++++++++++--- falyx/parser/argument.py | 43 +++++++++++++++++++++----------- falyx/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/examples/confirm_example.py b/examples/confirm_example.py index b53da30..1c5bee6 100644 --- a/examples/confirm_example.py +++ b/examples/confirm_example.py @@ -4,7 +4,13 @@ from typing import Any from pydantic import BaseModel from falyx import Falyx -from falyx.action import Action, ActionFactory, ChainedAction, ConfirmAction +from falyx.action import ( + Action, + ActionFactory, + ChainedAction, + ConfirmAction, + SaveFileAction, +) from falyx.parser import CommandArgumentParser @@ -39,12 +45,18 @@ async def build_json_updates(dogs: list[Dog]) -> list[dict[str, Any]]: return [dog.model_dump(mode="json") for dog in dogs] -def after_action(dogs) -> None: +async def save_dogs(dogs) -> None: if not dogs: print("No dogs processed.") return for result in dogs: - print(Dog(**result)) + print(f"Saving {Dog(**result)} to file.") + await SaveFileAction( + name="Save Dog Data", + file_path=f"dogs/{result['name']}.json", + data=result, + file_type="json", + )() async def build_chain(dogs: list[Dog]) -> ChainedAction: @@ -64,8 +76,8 @@ async def build_chain(dogs: list[Dog]) -> ChainedAction: inject_into="dogs", ), Action( - name="after_action", - action=after_action, + name="save_dogs", + action=save_dogs, inject_into="dogs", ), ], @@ -91,13 +103,13 @@ def dog_config(parser: CommandArgumentParser) -> None: async def main(): - flx = Falyx("Dog Post Example") + flx = Falyx("Save Dogs Example") flx.add_command( key="D", - description="Post Dog Data", + description="Save Dog Data", action=factory, - aliases=["post_dogs"], + aliases=["save_dogs"], argument_config=dog_config, ) diff --git a/falyx/action/save_file_action.py b/falyx/action/save_file_action.py index 2b7580a..96bdea4 100644 --- a/falyx/action/save_file_action.py +++ b/falyx/action/save_file_action.py @@ -36,9 +36,11 @@ class SaveFileAction(BaseAction): file_path: str, file_type: FileType | str = FileType.TEXT, mode: Literal["w", "a"] = "w", - inject_last_result: bool = True, - inject_into: str = "data", + data: Any = None, overwrite: bool = True, + create_dirs: bool = True, + inject_last_result: bool = False, + inject_into: str = "data", ): """ SaveFileAction allows saving data to a file. @@ -47,17 +49,22 @@ class SaveFileAction(BaseAction): name (str): Name of the action. file_path (str | Path): Path to the file where data will be saved. file_type (FileType | str): Format to write to (e.g. TEXT, JSON, YAML). + mode (Literal["w", "a"]): File mode (default: "w"). + data (Any): Data to be saved (if not using inject_last_result). + overwrite (bool): Whether to overwrite the file if it exists. + create_dirs (bool): Whether to create parent directories if they do not exist. inject_last_result (bool): Whether to inject result from previous action. inject_into (str): Kwarg name to inject the last result as. - overwrite (bool): Whether to overwrite the file if it exists. """ super().__init__( name=name, inject_last_result=inject_last_result, inject_into=inject_into ) self._file_path = self._coerce_file_path(file_path) self._file_type = self._coerce_file_type(file_type) + self.data = data self.overwrite = overwrite self.mode = mode + self.create_dirs = create_dirs @property def file_path(self) -> Path | None: @@ -126,6 +133,14 @@ class SaveFileAction(BaseAction): elif self.file_path.exists() and not self.overwrite: raise FileExistsError(f"File already exists: {self.file_path}") + if self.file_path.parent and not self.file_path.parent.exists(): + if self.create_dirs: + self.file_path.parent.mkdir(parents=True, exist_ok=True) + else: + raise FileNotFoundError( + f"Directory does not exist: {self.file_path.parent}" + ) + try: if self.file_type == FileType.TEXT: self.file_path.write_text(data, encoding="UTF-8") @@ -175,7 +190,7 @@ class SaveFileAction(BaseAction): async def _run(self, *args, **kwargs): combined_kwargs = self._maybe_inject_last_result(kwargs) - data = combined_kwargs.get(self.inject_into) + data = self.data or combined_kwargs.get(self.inject_into) context = ExecutionContext( name=self.name, args=args, kwargs=combined_kwargs, action=self diff --git a/falyx/parser/argument.py b/falyx/parser/argument.py index 653937f..18850ad 100644 --- a/falyx/parser/argument.py +++ b/falyx/parser/argument.py @@ -9,22 +9,37 @@ from falyx.parser.argument_action import ArgumentAction @dataclass class Argument: - """Represents a command-line argument.""" + """ + Represents a command-line argument. + + Attributes: + flags (tuple[str, ...]): Short and long flags for the argument. + dest (str): The destination name for the argument. + action (ArgumentAction): The action to be taken when the argument is encountered. + type (Any): The type of the argument (e.g., str, int, float) or a callable that converts the argument value. + default (Any): The default value if the argument is not provided. + choices (list[str] | None): A list of valid choices for the argument. + required (bool): True if the argument is required, False otherwise. + help (str): Help text for the argument. + nargs (int | str | None): Number of arguments expected. Can be an int, '?', '*', '+', or None. + positional (bool): True if the argument is positional (no leading - or -- in flags), False otherwise. + resolver (BaseAction | None): + An action object that resolves the argument, if applicable. + lazy_resolver (bool): True if the resolver should be called lazily, False otherwise + """ flags: tuple[str, ...] - dest: str # Destination name for the argument - action: ArgumentAction = ( - ArgumentAction.STORE - ) # Action to be taken when the argument is encountered - type: Any = str # Type of the argument (e.g., str, int, float) or callable - default: Any = None # Default value if the argument is not provided - choices: list[str] | None = None # List of valid choices for the argument - required: bool = False # True if the argument is required - help: str = "" # Help text for the argument - nargs: int | str | None = None # int, '?', '*', '+', None - positional: bool = False # True if no leading - or -- in flags - resolver: BaseAction | None = None # Action object for the argument - lazy_resolver: bool = False # True if resolver should be called lazily + dest: str + action: ArgumentAction = ArgumentAction.STORE + type: Any = str + default: Any = None + choices: list[str] | None = None + required: bool = False + help: str = "" + nargs: int | str | None = None + positional: bool = False + resolver: BaseAction | None = None + lazy_resolver: bool = False def get_positional_text(self) -> str: """Get the positional text for the argument.""" diff --git a/falyx/version.py b/falyx/version.py index f1fe4d9..79d1955 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.57" +__version__ = "0.1.58" diff --git a/pyproject.toml b/pyproject.toml index 82a7374..6c0c32b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.57" +version = "0.1.58" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT"