Metadata-Version: 2.5
Name: smallage
Version: 0.2.0
Summary: Background tasks, cron and a dead letter queue on Redis Streams, with a Litestar integration
Project-URL: Homepage, https://github.com/smirnoffmg/smallage
Project-URL: Documentation, https://smirnoffmg.dev/smallage/
Project-URL: Source, https://github.com/smirnoffmg/smallage
Project-URL: Issues, https://github.com/smirnoffmg/smallage/issues
Author: Maksim Smirnov
License-Expression: MIT
License-File: LICENSE
Keywords: background-tasks,celery,cron,dead-letter-queue,dlq,faststream,litestar,redis,redis-streams,streams,task-queue
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AnyIO
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: System :: Distributed Computing
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: anyio>=4.2
Requires-Dist: cronsim>=2.7
Requires-Dist: litestar>=2.0
Requires-Dist: msgspec>=0.18
Requires-Dist: redis[hiredis]>=5.0
Requires-Dist: uvloop>=0.21
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/smirnoffmg/smallage/main/docs/assets/logo.png"
       alt="smallage — wild celery (Apium graveolens)" width="180">
</p>

# smallage

[![PyPI version](https://img.shields.io/pypi/v/smallage)](https://pypi.org/project/smallage/)
[![Python versions](https://img.shields.io/pypi/pyversions/smallage)](https://pypi.org/project/smallage/)
[![License](https://img.shields.io/pypi/l/smallage)](https://pypi.org/project/smallage/)
[![CI](https://img.shields.io/github/actions/workflow/status/smirnoffmg/smallage/ci.yml?branch=main)](https://github.com/smirnoffmg/smallage/actions/workflows/ci.yml)
[![Checked with mypy](https://img.shields.io/badge/mypy-strict-2a6db2)](https://mypy-lang.org/)

Background tasks, cron and a dead letter queue on Redis Streams — typed tasks,
retries with backoff, delayed jobs, and consumption of streams somebody else
writes. The core reaches for no web framework; the
[Litestar](https://litestar.dev/) layer adds real dependency injection and a CLI
through the native plugin protocol.

*Smallage is the old name for wild celery.*

📖 **[Documentation](https://smirnoffmg.dev/smallage/)**

## Features

- Tasks are ordinary functions: arguments are serialised, dependencies injected
- Retries with backoff, a delivery ceiling, and a dead letter queue that keeps
  the payload, the traceback and the attempt history
- Delayed jobs and cron with no scheduler process, correct across DST
- Priority queues with a bounded starvation window, and shards for fairness
- Broker mode: consume streams somebody else writes, in the same worker
- Optional results, a deduplication gate, and trace context carried into the task
- Health endpoint served identically by the web process and the worker
- Eager mode, `assert_enqueued` and a real-worker fixture for your own tests

## Installation

```bash
uv add smallage
```

Redis 7 or newer. Standalone, Sentinel and Cluster are all covered by the test
suite.

## Quick start

```python
from dataclasses import dataclass
from uuid import UUID, uuid4

from litestar import Litestar, post
from litestar.di import Provide

from smallage.litestar import QueueConfig, QueuePlugin, TaskRegistry

tasks = TaskRegistry()


@dataclass
class Settings:
    index_name: str = "documents"


def settings() -> Settings:
    return Settings()


@tasks.task
async def reindex(doc_id: UUID, settings: Settings) -> None:
    """`doc_id` is serialised; `settings` comes from the application."""


@post("/documents")
async def create() -> str:
    await reindex.enqueue(doc_id=uuid4())
    return "queued"


app = Litestar(
    route_handlers=[create],
    dependencies={"settings": Provide(settings, sync_to_thread=False)},
    plugins=[QueuePlugin(QueueConfig(registry=tasks, redis_url="redis://localhost"))],
)
```

`doc_id` travels in the payload; `settings` is injected in the worker from the
application's own dependency graph, and a real one would be a database session
or a client. There is no context dictionary.

## Workers

A worker is the same application, started differently:

```bash
litestar workers run --queue high --concurrency 20
```

The lifecycle comes with it: a worker enters the application's lifespan, so
`on_startup` hooks and lifespan managers run there too and a dependency closing
over what they opened is usable in a task. `run_app_lifespan=False` declines
that, for a lifespan whose work belongs to a web process alone.

Anything that can be settled at startup is — a task registered twice, an
argument with no annotation, a dependency the application does not provide, a
cycle, a provider only a request could satisfy — rather than on the first job in
production.

## Delivery guarantee

**At-least-once.** A worker that completed its side effect and died before
`XACK` will be reclaimed and the work repeated. No amount of protocol work
removes that; only an idempotent handler does. A `dedup` key is provided for the
cases where that is not naturally true.

## Documentation

Tasks, scheduling, retries, priorities, broker mode, results, testing and
operations are covered in the
**[full documentation](https://smirnoffmg.dev/smallage/)**. The rules the
library is built to are in
**[Design and invariants](https://smirnoffmg.dev/smallage/design/)**.
Runnable [examples](examples/) are included.

## Development

```bash
make install   # dependencies and git hooks
make check     # lint, types, import contracts, unit tests
make test-int  # integration suite, needs Docker
```

## License

MIT
