From fe9758adbf44e8e6c4826271411d5f77aa11ff36 Mon Sep 17 00:00:00 2001 From: Roland Thomas Date: Wed, 30 Apr 2025 22:23:59 -0400 Subject: [PATCH] Add HTTPAction, update main demo --- falyx/__main__.py | 151 ++++++++++++++++++++++++++++++++++--------- falyx/falyx.py | 7 +- falyx/http_action.py | 109 +++++++++++++++++++++++++++++++ pyproject.toml | 1 - 4 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 falyx/http_action.py diff --git a/falyx/__main__.py b/falyx/__main__.py index 33698dd..59f509c 100644 --- a/falyx/__main__.py +++ b/falyx/__main__.py @@ -5,73 +5,166 @@ Copyright (c) 2025 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ import asyncio +import random +from argparse import Namespace from falyx.action import Action, ActionGroup, ChainedAction from falyx.falyx import Falyx +from falyx.parsers import FalyxParsers, get_arg_parsers +from falyx.version import __version__ -def build_falyx() -> Falyx: +class Foo: + def __init__(self, flx: Falyx) -> None: + self.flx = flx + + async def build(self): + await asyncio.sleep(1) + print("โœ… Build complete!") + return "Build complete!" + + async def test(self): + await asyncio.sleep(1) + print("โœ… Tests passed!") + return "Tests passed!" + + async def deploy(self): + await asyncio.sleep(1) + print("โœ… Deployment complete!") + return "Deployment complete!" + + async def clean(self): + print("๐Ÿงน Cleaning...") + await asyncio.sleep(1) + print("โœ… Clean complete!") + return "Clean complete!" + + async def build_package(self): + print("๐Ÿ”จ Building...") + await asyncio.sleep(1) + print("โœ… Build finished!") + return "Build finished!" + + async def package(self): + print("๐Ÿ“ฆ Packaging...") + await asyncio.sleep(1) + print("โœ… Package complete!") + return "Package complete!" + + async def run_tests(self): + print("๐Ÿงช Running tests...") + await asyncio.sleep(random.randint(1, 3)) + print("โœ… Tests passed!") + return "Tests passed!" + + async def run_integration_tests(self): + print("๐Ÿ”— Running integration tests...") + await asyncio.sleep(random.randint(1, 3)) + print("โœ… Integration tests passed!") + return "Integration tests passed!" + + async def run_linter(self): + print("๐Ÿงน Running linter...") + await asyncio.sleep(random.randint(1, 3)) + print("โœ… Linter passed!") + return "Linter passed!" + + async def run(self): + await self.flx.run() + + +def parse_args() -> Namespace: + parsers: FalyxParsers = get_arg_parsers() + return parsers.parse_args() + + +async def main() -> None: """Build and return a Falyx instance with all your commands.""" - flx = Falyx(title="๐Ÿš€ Falyx CLI") + args = parse_args() + flx = Falyx( + title="๐Ÿš€ Falyx CLI", + cli_args=args, + columns=5, + welcome_message="Welcome to Falyx CLI!", + exit_message="Goodbye!", + ) + foo = Foo(flx) - # Example commands + # --- Bottom bar info --- + flx.bottom_bar.columns = 3 + flx.bottom_bar.add_toggle_from_option("V", "Verbose", flx.options, "verbose") + flx.bottom_bar.add_toggle_from_option("U", "Debug Hooks", flx.options, "debug_hooks") + flx.bottom_bar.add_static("Version", f"Falyx v{__version__}") + + # --- Command actions --- + + # --- Single Actions --- flx.add_command( key="B", description="Build project", - action=Action("Build", lambda: print("๐Ÿ“ฆ Building...")), - tags=["build"] + action=Action("Build", foo.build), + tags=["build"], + spinner=True, + spinner_message="๐Ÿ“ฆ Building...", ) - flx.add_command( key="T", description="Run tests", - action=Action("Test", lambda: print("๐Ÿงช Running tests...")), - tags=["test"] + action=Action("Test", foo.test), + tags=["test"], + spinner=True, + spinner_message="๐Ÿงช Running tests...", ) - flx.add_command( key="D", description="Deploy project", - action=Action("Deploy", lambda: print("๐Ÿš€ Deploying...")), - tags=["deploy"] + action=Action("Deploy", foo.deploy), + tags=["deploy"], + spinner=True, + spinner_message="๐Ÿš€ Deploying...", ) - # Example of ChainedAction (pipeline) - build_pipeline = ChainedAction( + # --- Build pipeline (ChainedAction) --- + pipeline = ChainedAction( name="Full Build Pipeline", actions=[ - Action("Clean", lambda: print("๐Ÿงน Cleaning...")), - Action("Build", lambda: print("๐Ÿ”จ Building...")), - Action("Package", lambda: print("๐Ÿ“ฆ Packaging...")), - ], - auto_inject=False, + Action("Clean", foo.clean), + Action("Build", foo.build_package), + Action("Package", foo.package), + ] ) flx.add_command( key="P", description="Run Build Pipeline", - action=build_pipeline, - tags=["build", "pipeline"] + action=pipeline, + tags=["build", "pipeline"], + spinner=True, + spinner_message="๐Ÿ”จ Running build pipeline...", + spinner_type="line", ) - # Example of ActionGroup (parallel tasks) + # --- Test suite (ActionGroup) --- test_suite = ActionGroup( name="Test Suite", actions=[ - Action("Unit Tests", lambda: print("๐Ÿงช Running unit tests...")), - Action("Integration Tests", lambda: print("๐Ÿ”— Running integration tests...")), - Action("Lint", lambda: print("๐Ÿงน Running linter...")), + Action("Unit Tests", foo.run_tests), + Action("Integration Tests", foo.run_integration_tests), + Action("Lint", foo.run_linter), ] ) flx.add_command( key="G", description="Run All Tests", action=test_suite, - tags=["test", "parallel"] + tags=["test", "parallel"], + spinner=True, + spinner_type="line", ) + await foo.run() - return flx if __name__ == "__main__": - flx = build_falyx() - asyncio.run(flx.run()) - + try: + asyncio.run(main()) + except (KeyboardInterrupt, EOFError): + pass diff --git a/falyx/falyx.py b/falyx/falyx.py index 209f5de..8651711 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -830,9 +830,10 @@ class Falyx: except (EOFError, KeyboardInterrupt): logger.info("EOF or KeyboardInterrupt. Exiting menu.") break - logger.info(f"Exiting menu: {self.get_title()}") - if self.exit_message: - self.print_message(self.exit_message) + finally: + logger.info(f"Exiting menu: {self.get_title()}") + if self.exit_message: + self.print_message(self.exit_message) async def run(self) -> None: """Run Falyx CLI with structured subcommands.""" diff --git a/falyx/http_action.py b/falyx/http_action.py new file mode 100644 index 0000000..be9ed46 --- /dev/null +++ b/falyx/http_action.py @@ -0,0 +1,109 @@ +from typing import Any + +import aiohttp +from rich.tree import Tree + +from falyx.action import Action +from falyx.context import ExecutionContext, SharedContext +from falyx.themes.colors import OneColors +from falyx.utils import logger + + +async def close_shared_http_session(context: ExecutionContext) -> None: + try: + shared_context: SharedContext = context.get_shared_context() + session = shared_context.get("http_session") + should_close = shared_context.get("_session_should_close", False) + if session and should_close: + await session.close() + except Exception as error: + logger.warning("โš ๏ธ Error closing shared HTTP session: %s", error) + + +class HTTPAction(Action): + """ + Specialized Action that performs an HTTP request using aiohttp and the shared context. + + Automatically reuses a shared aiohttp.ClientSession stored in SharedContext. + Closes the session at the end of the ActionGroup (via an after-hook). + """ + def __init__( + self, + name: str, + method: str, + url: str, + *, + args: tuple[Any, ...] = (), + headers: dict[str, str] | None = None, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + data: Any = None, + hooks=None, + inject_last_result: bool = False, + inject_last_result_as: str = "last_result", + retry: bool = False, + retry_policy=None, + ): + self.method = method.upper() + self.url = url + self.headers = headers + self.params = params + self.json = json + self.data = data + + super().__init__( + name=name, + action=self._request, + args=args, + kwargs={}, + hooks=hooks, + inject_last_result=inject_last_result, + inject_last_result_as=inject_last_result_as, + retry=retry, + retry_policy=retry_policy, + ) + + async def _request(self, *args, **kwargs) -> dict[str, Any]: + assert self.shared_context is not None, "SharedContext is not set" + context: SharedContext = self.shared_context + + session = context.get("http_session") + if session is None: + session = aiohttp.ClientSession() + context.set("http_session", session) + context.set("_session_should_close", True) + + async with session.request( + self.method, + self.url, + headers=self.headers, + params=self.params, + json=self.json, + data=self.data, + ) as response: + body = await response.text() + return { + "status": response.status, + "url": str(response.url), + "headers": dict(response.headers), + "body": body, + } + + async def preview(self, parent: Tree | None = None): + label = [ + f"[{OneColors.CYAN_b}]๐ŸŒ HTTPAction[/] '{self.name}'", + f"\n[dim]Method:[/] {self.method}", + f"\n[dim]URL:[/] {self.url}", + ] + if self.inject_last_result: + label.append(f"\n[dim]Injects:[/] '{self.inject_last_result_as}'") + if self.retry_policy and self.retry_policy.enabled: + label.append( + f"\n[dim]โ†ป Retries:[/] {self.retry_policy.max_retries}x, " + f"delay {self.retry_policy.delay}s, backoff {self.retry_policy.backoff}x" + ) + + if parent: + parent.add("".join(label)) + else: + self.console.print(Tree("".join(label))) diff --git a/pyproject.toml b/pyproject.toml index 6de3bd8..b3abb44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ pytest-asyncio = "^0.20" ruff = "^0.3" [tool.poetry.scripts] -falyx = "falyx.cli.main:main" sync-version = "scripts.sync_version:main" [build-system]