Skip to content

Commit 5aa09fa

Browse files
committed
Add Sphinx documentation and Read the Docs config
1 parent 0b38b03 commit 5aa09fa

23 files changed

Lines changed: 1087 additions & 2 deletions

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,14 @@ jobs:
3636
- run: ruff check .
3737
- run: mypy src
3838
- run: pytest
39+
40+
docs:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
- uses: actions/setup-python@v5
45+
with:
46+
python-version: "3.11"
47+
cache: pip
48+
- run: pip install -e ".[all,docs]"
49+
- run: sphinx-build -b html docs docs/_build/html

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ ENV/
3232
htmlcov/
3333
.mypy_cache/
3434
.ruff_cache/
35+
docs/_build/
3536

3637
# IDE
3738
.idea/

‎.readthedocs.yaml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Read the Docs configuration file
2+
# https://docs.readthedocs.io/en/stable/config-file/v2.html
3+
4+
version: 2
5+
6+
build:
7+
os: ubuntu-22.04
8+
tools:
9+
python: "3.11"
10+
11+
sphinx:
12+
configuration: docs/conf.py
13+
14+
python:
15+
install:
16+
- method: pip
17+
path: .
18+
extra_requirements:
19+
- docs
20+
- all

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pip install queuebridge[all] # all backends
2626

2727
Requires **Python 3.10+** and **Pydantic v2**.
2828

29+
**Documentation:** https://queuebridge.readthedocs.io
30+
2931
## Usage
3032

3133
### Celery

‎docs/Makefile‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Minimal makefile for Sphinx documentation
2+
3+
SPHINXOPTS ?=
4+
SPHINXBUILD ?= sphinx-build
5+
SOURCEDIR = .
6+
BUILDDIR = _build
7+
8+
help:
9+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
10+
11+
.PHONY: help Makefile
12+
13+
%: Makefile
14+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)

‎docs/_static/.gitkeep‎

Whitespace-only changes.

‎docs/api/index.rst‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
API reference
2+
=============
3+
4+
This section is generated from the package source. For narrative guides, see the tutorials.
5+
6+
Core
7+
----
8+
9+
.. automodule:: queuebridge
10+
:members:
11+
:undoc-members:
12+
13+
Codec
14+
-----
15+
16+
.. automodule:: queuebridge.codec
17+
:members:
18+
:undoc-members:
19+
20+
Types and exceptions
21+
--------------------
22+
23+
.. automodule:: queuebridge.types
24+
:members:
25+
:undoc-members:
26+
:show-inheritance:
27+
28+
Hints
29+
-----
30+
31+
.. automodule:: queuebridge.hints
32+
:members:
33+
:undoc-members:
34+
35+
Celery
36+
------
37+
38+
.. automodule:: queuebridge.celery
39+
:members:
40+
:undoc-members:
41+
42+
.. automodule:: queuebridge.celery.install
43+
:members:
44+
45+
.. automodule:: queuebridge.celery.result
46+
:members:
47+
:show-inheritance:
48+
49+
Dramatiq
50+
--------
51+
52+
.. automodule:: queuebridge.dramatiq
53+
:members:
54+
:undoc-members:
55+
56+
.. automodule:: queuebridge.dramatiq.encoder
57+
:members:
58+
:show-inheritance:
59+
60+
Arq
61+
---
62+
63+
.. automodule:: queuebridge.arq
64+
:members:
65+
:undoc-members:
66+
67+
.. automodule:: queuebridge.arq.install
68+
:members:
69+
70+
.. automodule:: queuebridge.arq.serializer
71+
:members:

‎docs/concepts/how-it-works.rst‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
How it works
2+
============
3+
4+
queuebridge has two layers:
5+
6+
1. **Codec** (``queuebridge.codec``): encode/decode Python values to a JSON-safe wire format.
7+
2. **Backend adapters**: plug the codec into Celery, Dramatiq, or Arq.
8+
9+
.. code-block:: text
10+
11+
Your app queuebridge Task queue
12+
-------- ----------- ----------
13+
OrderCreate model --> encode() --> JSON / msgpack on wire
14+
__qb__ tags
15+
16+
Worker task <-- decode + hints <-- wire dict
17+
receives model
18+
19+
The flow (Celery example)
20+
-------------------------
21+
22+
1. **Producer**: ``process_order.delay(OrderCreate(...))``
23+
2. **Serializer**: Kombu calls ``queuebridge.encode()`` on the message body.
24+
3. **Broker**: JSON bytes travel over Redis/RabbitMQ.
25+
4. **Worker**: Celery ``pydantic=True`` validates kwargs dicts into models.
26+
5. **Return**: Worker returns ``OrderResult``; Celery dumps to dict; codec encodes for result backend.
27+
6. **Client**: ``typed_result(ar, OrderResult).get()`` decodes to a model.
28+
29+
What each backend does
30+
----------------------
31+
32+
.. list-table::
33+
:header-rows: 1
34+
:widths: 20 40 40
35+
36+
* - Backend
37+
- Encode hook
38+
- Decode hook
39+
* - Celery
40+
- Kombu ``queuebridge-json`` serializer
41+
- Worker ``pydantic=True``; client uses ``typed_result``
42+
* - Dramatiq
43+
- ``QueuebridgeEncoder.encode()``
44+
- ``QueuebridgeEncoder.decode()`` + ``validate_call``
45+
* - Arq
46+
- msgpack + ``encode()``
47+
- ``@qb_task`` + ``validate_call``
48+
49+
Type hints matter
50+
-----------------
51+
52+
Decoding uses your function annotations via ``typing.get_type_hints`` and Pydantic's ``TypeAdapter``.
53+
54+
If the wire value is a plain dict and your parameter is ``order: OrderCreate``, queuebridge calls ``OrderCreate.model_validate(dict)``.
55+
56+
If the wire value has a ``__qb__`` tag, the tag's type name is used to reconstruct the object.
57+
58+
See :doc:`wire-format` for tag details.
59+
60+
When you do not need tags
61+
-------------------------
62+
63+
If you always pass type hints and use plain ``model_dump()`` dicts, decoding still works. Tags help when:
64+
65+
* The type is ambiguous (``Union``, ``Any``)
66+
* You have nested UUID/datetime outside a model
67+
* You decode without a concrete hint
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Why not Celery pydantic=True alone?
2+
====================================
3+
4+
Celery 5.5+ supports ``@app.task(pydantic=True)``. That is useful, but it only covers **half** the story.
5+
6+
What pydantic=True does
7+
-----------------------
8+
9+
On the **worker**, Celery:
10+
11+
* Converts kwargs dicts into Pydantic models using your annotations
12+
* Dumps return models to dicts via ``model_dump()`` for the result backend
13+
14+
What it does not do
15+
-------------------
16+
17+
On the **producer** (your API, CLI, or script calling ``.delay()``):
18+
19+
* You must still serialize models yourself
20+
* Passing ``OrderCreate(...)`` directly raises ``TypeError: not JSON serializable`
21+
22+
On the **client** (calling ``.get()``):
23+
24+
* You receive a **dict**, not your return model
25+
26+
Official docs quote
27+
-------------------
28+
29+
From the `Celery task documentation <https://docs.celeryq.dev/en/stable/userguide/tasks.html#argument-validation-with-pydantic>`_:
30+
31+
Argument validation only covers arguments/return values on the task side.
32+
You still have serialize arguments yourself when invoking a task with
33+
delay() or apply_async().
34+
35+
Community reports
36+
-----------------
37+
38+
* `celery#9442 <https://github.com/celery/celery/issues/9442>`_ - models not serializable on enqueue
39+
* `dramatiq#660 <https://github.com/Bogdanp/dramatiq/issues/660>`_ - JSON encoder fails on models
40+
* `arq#497 <https://github.com/python-arq/arq/issues/497>`_ - request for native Pydantic support
41+
42+
What queuebridge adds
43+
---------------------
44+
45+
.. list-table::
46+
:header-rows: 1
47+
48+
* - Stage
49+
- Celery alone
50+
- With queuebridge
51+
* - ``.delay(model)``
52+
- Fails or needs ``model_dump()``
53+
- Works
54+
* - Worker receives
55+
- Model (with pydantic=True)
56+
- Model (with pydantic=True)
57+
* - ``.get()`` returns
58+
- ``dict``
59+
- ``dict`` (use ``typed_result`` for model)
60+
61+
queuebridge does not replace Celery's worker validation. It complements it by fixing producer serialization and offering client-side typed results.

‎docs/concepts/wire-format.rst‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
Wire format
2+
===========
3+
4+
queuebridge uses a **JSON-first** wire format with optional **type tags** for values that lose type information in JSON.
5+
6+
The ``__qb__`` envelope
7+
-----------------------
8+
9+
Non-primitive values that need type information are wrapped like this:
10+
11+
.. code-block:: json
12+
13+
{
14+
"__qb__": {
15+
"t": "myapp.models.OrderCreate",
16+
"v": 1,
17+
"d": {"id": 1, "sku": "ABC"}
18+
}
19+
}
20+
21+
.. list-table::
22+
:header-rows: 1
23+
24+
* - Field
25+
- Meaning
26+
* - ``t``
27+
- Fully-qualified type name (module + class)
28+
* - ``v``
29+
- Wire format version (always ``1`` for now)
30+
* - ``d``
31+
- JSON-safe payload
32+
33+
Constants (Python)
34+
------------------
35+
36+
.. code-block:: python
37+
38+
from queuebridge.types import QB_TAG, QB_VERSION
39+
40+
QB_TAG == "__qb__"
41+
QB_VERSION == 1
42+
43+
Types and their wire representation
44+
-----------------------------------
45+
46+
.. list-table::
47+
:header-rows: 1
48+
:widths: 25 35 40
49+
50+
* - Python type
51+
- On the wire
52+
- Decoded via
53+
* - ``BaseModel``
54+
- ``__qb__`` envelope, ``d`` = ``model_dump(mode="json")``
55+
- ``model_validate`` or FQN import
56+
* - ``UUID``
57+
- ``__qb__`` with ``t: uuid.UUID``, ``d: "<uuid string>"``
58+
- ``UUID(d)``
59+
* - ``datetime``, ``date``, ``time``
60+
- ``__qb__`` with ISO string in ``d``
61+
- ``fromisoformat``
62+
* - ``Decimal``
63+
- ``__qb__`` with string in ``d``
64+
- ``Decimal(d)``
65+
* - ``Enum``
66+
- ``__qb__`` with enum FQN and value
67+
- Enum class lookup
68+
* - ``list``, ``tuple``, ``set``
69+
- JSON list (elements encoded recursively)
70+
- Hint-driven (``list[T]``, etc.)
71+
* - ``dict``
72+
- JSON object (keys coerced to str)
73+
- Hint-driven
74+
* - ``str``, ``int``, ``float``, ``bool``, ``null``
75+
- Pass through unchanged
76+
- Pass through
77+
78+
Encode and decode API
79+
---------------------
80+
81+
.. code-block:: python
82+
83+
from queuebridge import encode, decode
84+
85+
wire = encode(my_model)
86+
restored = decode(wire, OrderCreate)
87+
88+
``decode_wire()`` unwraps all ``__qb__`` envelopes recursively without hints. Dramatiq uses this on incoming messages.
89+
90+
Celery note
91+
-----------
92+
93+
The Celery serializer uses ``encode(..., tag_models=False)`` for compatibility with ``pydantic=True``. Models become plain dicts on the wire; UUID/datetime inside ``model_dump(mode="json")`` are already strings.
94+
95+
Arq note
96+
--------
97+
98+
Job dicts are msgpack-encoded **after** ``encode()``, so the binary blob contains queuebridge-tagged JSON-compatible structures.

0 commit comments

Comments
 (0)