Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async callbacks #141

Open
bckohan opened this issue Nov 15, 2024 · 3 comments
Open

Support async callbacks #141

bckohan opened this issue Nov 15, 2024 · 3 comments
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@bckohan
Copy link
Member

bckohan commented Nov 15, 2024

  • start event loops automatically
  • allow asynchronous execution of chained subcommands

Is this possible at the django-typer interface or does it require upstream changes?

@bckohan bckohan added the enhancement New feature or request label Nov 15, 2024
@bckohan bckohan self-assigned this Nov 15, 2024
@bckohan
Copy link
Member Author

bckohan commented Nov 17, 2024

To be clear, it'd be nice to be able to do something like:

class Command(TyperCommand, chain=True):

    @command()
    async def sub1(self):
        ...

    @command()
    async def sub2(self):
        ...

Then run both asynchronously like this:

./manage.py command sub1 sub2

@bckohan bckohan added this to the Version 3.0 milestone Nov 20, 2024
@bckohan
Copy link
Member Author

bckohan commented Nov 21, 2024

From upstream: fastapi/typer#950

@pySilver
Copy link

pySilver commented Nov 22, 2024

That would be awesome. At the moment I'm doing something similar manually.

Here are the utils for commands:

import asyncio
import signal
from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any, Concatenate, ParamSpec, TypeVar

from django_typer.management import TyperCommand

from config.context import StateManager

T = TypeVar("T", bound="StateManagerCommand")
P = ParamSpec("P")
R = TypeVar("R")


class StateManagerCommand(TyperCommand):
    state_manager: StateManager

    async def startup(self) -> None:
        self.state_manager = StateManager()
        await self.state_manager.startup()

    async def shutdown(self) -> None:
        await self.state_manager.shutdown()

    @staticmethod
    def with_state_manager() -> (
        Callable[
            [Callable[Concatenate[T, P], Awaitable[R]]],
            Callable[Concatenate[T, P], Awaitable[R]],
        ]
    ):
        def decorator(
            func: Callable[Concatenate[T, P], Awaitable[R]],
        ) -> Callable[Concatenate[T, P], Awaitable[R]]:
            @wraps(func)
            async def wrapper(
                self: T,
                *args: P.args,
                **kwargs: P.kwargs,
            ) -> R:
                await self.startup()
                try:
                    return await func(self, *args, **kwargs)
                finally:
                    await self.shutdown()

            return wrapper

        return decorator


def run_in_loop(
    signals: tuple[signal.Signals, ...] = (
        signal.SIGHUP,
        signal.SIGTERM,
        signal.SIGINT,
    ),
    shutdown_func: Callable[[signal.Signals, asyncio.AbstractEventLoop], Any]
    | None = None,
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, R]]:
    """
    Decorator function that allows defining coroutines with click.

    Args:
        signals: Tuple of signal types to handle
        shutdown_func: Optional callback function for signal handling

    Returns:
        A wrapped coroutine function that handles signal management
    """

    def decorator(
        func: Callable[P, Awaitable[R]],
    ) -> Callable[P, R]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
            if shutdown_func:
                for ss in signals:
                    loop.add_signal_handler(ss, shutdown_func, ss, loop)
            return loop.run_until_complete(func(*args, **kwargs))

        return wrapper

    return decorator

and there is some basic usage:

import typer
from django_typer.management import command

from project.core.management.utils import StateManagerCommand, run_in_loop
from project.core.tasks import awesome_task


class Command(StateManagerCommand):
    @command()
    @run_in_loop()
    @StateManagerCommand.with_state_manager()
    async def default(self) -> None:
        await awesome_task.kiq()
        self.secho("Awesome task scheduled for execution", fg="green")
        self.secho(
            f"State: {self.state_manager.state.keys()}",
            fg=typer.colors.MAGENTA,
        )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants