Skip to content

Async plain Python example

The async equivalent of the plain_python example: a task module, a worker script, and a producer script that schedules work and awaits the result -- built on AsyncAlchemicalQueues / AsyncWorker instead of their sync equivalents.

Run it

cd examples/async_plain_python

In one terminal, start a worker (--with aiosqlite for the async SQLite driver used here):

uv run --with aiosqlite python worker.py "sqlite+aiosqlite:///example.db"

In another terminal, schedule a task and await its result:

uv run --with aiosqlite python producer.py

You should see the worker print that it is running the task, and the producer print the result once it is done.

There is no alchemical_worker-style CLI for async queues yet, which is why worker.py is a plain script rather than a command -- see its docstring.

tasks.py

"""The task(s) our async worker can run. Needs to be importable by the
worker process under this exact module path, i.e. run `worker.py` from this
directory so it can `import tasks`.
"""

import asyncio

from alchemical_queues.tasks.aio import async_task
from alchemical_queues.tasks.main import TaskInfo


@async_task
async def add_numbers(taskinfo: TaskInfo, a: int, b: int) -> int:
    print(f"Running task {taskinfo.entry_id}: {a} + {b}")
    # Stand in for an awaitable call (an HTTP request, another query, ...) --
    # this is the kind of work AsyncWorker lets you run concurrently with
    # other tasks on the same event loop, unlike the sync Worker.
    await asyncio.sleep(0)
    return a + b

worker.py

"""Runs an AsyncWorker on the `tasks` queue forever.

There is no `alchemical_worker`-style CLI for async queues yet -- run this
script directly instead, from this directory (so it can `import tasks`):

    python worker.py "sqlite+aiosqlite:///example.db"
"""

import asyncio
import os
import sys
from datetime import timedelta

from sqlalchemy.ext.asyncio import create_async_engine
from tasks import add_numbers  # noqa: F401  (registers the handler by import)

from alchemical_queues.aio import AsyncAlchemicalQueues
from alchemical_queues.tasks.aio import AsyncWorker


async def main() -> None:
    database_url = (
        sys.argv[1]
        if len(sys.argv) > 1
        else os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///example.db")
    )
    engine = create_async_engine(database_url)
    queues = AsyncAlchemicalQueues(engine)
    await queues.create_all()

    task_queue = queues.get_task_queue("tasks")
    worker = AsyncWorker(task_queue, poll_every=timedelta(milliseconds=200))
    await worker.work()


if __name__ == "__main__":
    asyncio.run(main())

producer.py

"""Schedules an `add_numbers` task and awaits its result.

Run a worker in another terminal first (from this directory, so it can
import `tasks`):

    python worker.py "sqlite+aiosqlite:///example.db"

Then run this script:

    python producer.py
"""

import asyncio
import os

from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from tasks import add_numbers

from alchemical_queues.aio import AsyncAlchemicalQueues


def build_queues(database_url: str) -> AsyncAlchemicalQueues:
    engine: AsyncEngine = create_async_engine(database_url)
    return AsyncAlchemicalQueues(engine)


async def main() -> None:
    database_url = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///example.db")
    queues = build_queues(database_url)
    await queues.create_all()
    task_queue = queues.get_task_queue("tasks")

    task = await add_numbers(2, 3).schedule(task_queue)
    print(f"Scheduled task {task.entry_id}, waiting for a worker to pick it up...")

    while not await task.done():
        await asyncio.sleep(0.5)

    print(f"Result: {await task.result()}")


if __name__ == "__main__":
    asyncio.run(main())