diff --git a/gallagher/cli/__init__.py b/gallagher/cli/__init__.py index ddcd0b2e..8d2245a3 100644 --- a/gallagher/cli/__init__.py +++ b/gallagher/cli/__init__.py @@ -1,16 +1,16 @@ -""" +""" CLI entry point """ -import typer - from gallagher import __version__ +from .utils import AsyncTyper + from .alarms import app as alarms_app from .cardholders import app as cardholders_app # Main Typer app use to create the CLI -app = typer.Typer() +app = AsyncTyper() app.add_typer(alarms_app, name="alarms") app.add_typer(cardholders_app, name="ch") diff --git a/gallagher/cli/alarms.py b/gallagher/cli/alarms.py index 74d2edce..729f1c24 100644 --- a/gallagher/cli/alarms.py +++ b/gallagher/cli/alarms.py @@ -3,12 +3,13 @@ """ -from gallagher import cc import os -import typer + +from gallagher import cc +from .utils import AsyncTyper api_key = os.environ.get("GACC_API_KEY") cc.api_key = api_key -app = typer.Typer() +app = AsyncTyper() diff --git a/gallagher/cli/cardholders.py b/gallagher/cli/cardholders.py index 71a7a7bd..218e0579 100644 --- a/gallagher/cli/cardholders.py +++ b/gallagher/cli/cardholders.py @@ -1,14 +1,17 @@ """ Cardholder cli commands mounted at ch """ -import typer from rich import print as rprint from rich.console import Console from rich.table import Table -from gallagher.cc.cardholders.cardholders import Cardholder +from .utils import AsyncTyper -app = typer.Typer(help="query or manage cardholders") +from gallagher.cc.cardholders.cardholders import ( + Cardholder +) + +app = AsyncTyper(help="query or manage cardholders") @app.command("list") @@ -20,7 +23,7 @@ async def list(): "[bold green]Fetching cardholders...", spinner="clock" ): - cardholders = Cardholder.list() + cardholders = await Cardholder.list() table = Table(title="Cardholders") for header in cardholders.cli_header: @@ -36,5 +39,5 @@ async def list(): async def get(id: int): """ get a cardholder by id """ - cardholder = Cardholder.retrieve(id) + cardholder = await Cardholder.retrieve(id) [rprint(r) for r in cardholder.__rich_repr__()] diff --git a/gallagher/cli/utils.py b/gallagher/cli/utils.py new file mode 100644 index 00000000..d2733375 --- /dev/null +++ b/gallagher/cli/utils.py @@ -0,0 +1,49 @@ +""" Asyncio patches for Typer + +There's a thread open on the typer repo about this: + https://github.com/tiangolo/typer/issues/88 + +Essentially, Typer doesn't support async functions. This is an issue +post migration to async, @csheppard points out that there's a work +around using a decorator on the click repository: +https://github.com/tiangolo/typer/issues/88#issuecomment-612687289 + +@gilcu2 posted a similar solution on the typer repo: +https://github.com/tiangolo/typer/issues/88#issuecomment-1732469681 + +this particular one uses asyncer to run the async function in a thread +we're going in with this with the hope that the official solution is +closer to this than a decorator per command. +""" +import inspect + +from functools import ( + partial, + wraps, +) + +import asyncer +from typer import Typer + + +class AsyncTyper(Typer): + @staticmethod + def maybe_run_async(decorator, f): + if inspect.iscoroutinefunction(f): + + @wraps(f) + def runner(*args, **kwargs): + return asyncer.runnify(f)(*args, **kwargs) + + decorator(runner) + else: + decorator(f) + return f + + def callback(self, *args, **kwargs): + decorator = super().callback(*args, **kwargs) + return partial(self.maybe_run_async, decorator) + + def command(self, *args, **kwargs): + decorator = super().command(*args, **kwargs) + return partial(self.maybe_run_async, decorator) diff --git a/poetry.lock b/poetry.lock index 5429ab5e..251e3065 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] @@ -27,9 +27,23 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "asyncer" +version = "0.0.2" +description = "Asyncer, async and await, focused on developer experience." +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "asyncer-0.0.2-py3-none-any.whl", hash = "sha256:46e0e1423ce21588350ad425875e81795280b9e1f517e8a389de940b86c348bd"}, + {file = "asyncer-0.0.2.tar.gz", hash = "sha256:d546c85f3626ebbaf06bb4395db49761c902a61a6ac802b1a74133cab4f7f433"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<4.0.0" [[package]] name = "certifi" @@ -569,4 +583,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "cb533402e7870ecaffdee556206d07cc1a4c92c5b8f842997e65562d8b729cb5" +content-hash = "e89eca878d5a623d4c943065a133978fafba45e1928b87024480f11a1b16357b" diff --git a/pyproject.toml b/pyproject.toml index 04a91ff3..c17c0b51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ typing-extensions = "^4.9.0" coverage = "^7.3.3" pytest = "^7.4.3" pytest-order = "^1.2.0" -anyio = "^4.2.0" annotated-types = "^0.6.0" certifi = "^2023.11.17" idna = "^3.6" @@ -33,6 +32,7 @@ pluggy = "^1.3.0" typer = {extras = ["all"], version = "^0.9.0"} rich = "^13.7.0" pytest-asyncio = "^0.23.2" +asyncer = "^0.0.2" [tool.poetry.group.dev.dependencies] pytest = "^7.2.2"