diff --git a/README.md b/README.md index 0643d5a..1a11e27 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,34 @@ # modern-di-arq +[![PyPI version](https://img.shields.io/pypi/v/modern-di-arq.svg)](https://pypi.org/project/modern-di-arq/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/modern-di-arq.svg)](https://pypi.org/project/modern-di-arq/) +[![Downloads](https://static.pepy.tech/badge/modern-di-arq/month)](https://pepy.tech/projects/modern-di-arq) +[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/modern-python/modern-di-arq/actions/workflows/ci.yml) +[![CI](https://github.com/modern-python/modern-di-arq/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/modern-di-arq/actions/workflows/ci.yml) +[![License](https://img.shields.io/github/license/modern-python/modern-di-arq.svg)](https://github.com/modern-python/modern-di-arq/blob/main/LICENSE) +[![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di-arq)](https://github.com/modern-python/modern-di-arq/stargazers) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty) + [Modern-DI](https://github.com/modern-python/modern-di) integration for [arq](https://arq-docs.helpmanual.io). -## Install +Full guide: [arq integration docs](https://modern-di.modern-python.org/integrations/arq/) + +## Installation ```bash -pip install modern-di-arq +uv add modern-di-arq # or: pip install modern-di-arq ``` -## Quickstart +## Usage + +`setup_di` seeds the root container into arq's `ctx` dict and wires four of arq's lifecycle hooks: `on_startup`/`on_shutdown` open and close the root container, and `on_job_start`/`on_job_end` build and close a `Scope.REQUEST` child container around each job. Decorate a task with `@inject` to resolve its `FromDI`-marked parameters from that per-job child. ```python import typing +from arq.connections import RedisSettings from modern_di import Container, Group, Scope, providers from modern_di_arq import FromDI, inject, setup_di @@ -22,24 +38,54 @@ class Settings: self.greeting = "hello" -class Dependencies(Group): - settings = providers.Factory(scope=Scope.APP, creator=Settings) +class Greeter: + def __init__(self, settings: Settings) -> None: # auto-injected by type + self._settings = settings + + def greet(self, name: str) -> str: + return f"{self._settings.greeting}, {name}" + + +class AppGroup(Group): + settings = providers.Factory(Settings, scope=Scope.APP, cache=True) + greeter = providers.Factory(Greeter, scope=Scope.REQUEST) @inject -async def greet(ctx: dict, name: str, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> str: - return f"{settings.greeting}, {name}" +async def greet( + ctx: dict[str, typing.Any], # arq passes its context dict as the first argument + name: str, + greeter: typing.Annotated[Greeter, FromDI(Greeter)], # resolve by type +) -> str: + return greeter.greet(name) class WorkerSettings: functions = [greet] + redis_settings = RedisSettings(host="localhost") -setup_di(WorkerSettings, Container(groups=[Dependencies], validate=True)) +setup_di(WorkerSettings, Container(groups=[AppGroup], validate=True)) ``` -`setup_di` wires the root container's lifecycle to arq's `on_startup`/`on_shutdown` -and seeds it into arq's `ctx` dict; a per-job `Scope.REQUEST` child is built in -`on_job_start` and closed in `on_job_end`. Decorate individual tasks with -`@inject` to resolve `FromDI` markers from that child. See the -[documentation](https://modern-di.modern-python.org). +Run the worker as usual (`arq mymodule.WorkerSettings`) and enqueue jobs with only their real arguments — `await pool.enqueue_job("greet", "world")` — the `FromDI` parameters are resolved for you. A task **must** declare arq's `ctx` dict as its first parameter; injection is order-insensitive otherwise. arq's `ctx` is a plain `dict` (not a dedicated message type), so no context provider is registered — read job metadata from `ctx`, and `fetch_di_container(ctx)` returns the root container. + +## API + +| Symbol | Description | +|---|---| +| `setup_di(worker_settings, container)` | Seeds the root container into arq's `ctx` and wires root + per-job lifecycle onto `on_startup`/`on_shutdown`/`on_job_start`/`on_job_end`. Accepts a `WorkerSettings` class/object or a settings `dict`; composes with existing hooks; returns the container. Raises `TypeError` if called twice on the same `worker_settings` | +| `FromDI(dependency)` | Inert marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a type | +| `inject(task)` | Decorator that resolves `FromDI`-annotated parameters from the per-job `Scope.REQUEST` child. Order-insensitive; passthrough for tasks with no `FromDI`; raises `TypeError` at decoration if the task also declares `*args`/`**kwargs` | +| `fetch_di_container(ctx)` | Returns the root container from an arq `ctx` dict | + +## 📦 [PyPI](https://pypi.org/project/modern-di-arq) + +## 📝 [License](LICENSE) + +## Part of `modern-python` + +Built on [`modern-di`](https://github.com/modern-python/modern-di), a dependency-injection framework with IoC container and scopes. + +Browse the full list of templates and libraries in +[`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index. diff --git a/pyproject.toml b/pyproject.toml index fe1ec87..64ac331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ version = "0" [project.urls] Homepage = "https://modern-di.modern-python.org" -Documentation = "https://modern-di.modern-python.org" +Documentation = "https://modern-di.modern-python.org/integrations/arq/" Repository = "https://github.com/modern-python/modern-di-arq" Issues = "https://github.com/modern-python/modern-di-arq/issues" Changelog = "https://github.com/modern-python/modern-di-arq/releases"