Skip to content

Commit 1dcb9ba

Browse files
committed
Add Django integration
Override the runserver and runserver_plus management commands to wrap inner_run in activate(), providing automatic broker lifecycle management during development. Support configurable base class resolution via tool.queueio.django.runserver in pyproject.toml to allow chaining with providers like daphne or django.contrib.staticfiles. Integrate with django-cmd for settings discovery from tool.django.settings in pyproject.toml, and with djp for automatic INSTALLED_APPS registration. Fall back to DJANGO_SETTINGS_MODULE environment variable when django-cmd is not installed. Include a sample Django app in queueio/samples/django/ with an end-to-end integration test that spins up a real Django server and queueio worker, submits a job via HTTP, and verifies ORM updates.
1 parent 0700a98 commit 1dcb9ba

22 files changed

+436
-3
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
88
[Unreleased]
99
------------
1010

11+
[0.6.0] - 2026-02-16
12+
--------------------
13+
1114
### Added
1215

16+
- Django integration (`queueio.django`) with `runserver` lifecycle management.
17+
- [djp](https://djp.readthedocs.io/) support for automatic `INSTALLED_APPS` registration.
18+
- [django-cmd](https://pypi.org/project/django-cmd/) integration for settings discovery.
1319
- Documentation site.
1420

1521
[0.5.0] - 2026-02-08
@@ -100,7 +106,8 @@ Thank you to Nick Anderegg for allowing me to use the queueio name for this proj
100106
- The queuespec syntax to `queue run` to consume multiple queues with shared capacity.
101107
- `queueio monitor` command to monitor activity in the queueio system.
102108

103-
[Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...HEAD
109+
[Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.6.0...HEAD
110+
[0.6.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...tag/0.6.0
104111
[0.5.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.4.0...tag/0.5.0
105112
[0.4.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.3.0...tag/0.4.0
106113
[0.3.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.2...tag/0.3.0

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ theme:
2020
primary: black
2121

2222
markdown_extensions:
23+
- admonition
2324
- pymdownx.highlight
2425
- pymdownx.superfences
2526

@@ -36,4 +37,5 @@ nav:
3637
- Invocation: queueio/invocation.md
3738
- QueueSpec: queueio/queuespec.md
3839
- QueueVar: queueio/queuevar.md
40+
- Django: queueio/django.md
3941
- Changelog: CHANGELOG.md

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "queueio"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "Python background queues with an async twist"
55
readme = "README.md"
66
license = "MIT"
@@ -27,12 +27,21 @@ dependencies = [
2727
"typer>=0.15.4",
2828
]
2929

30+
[project.optional-dependencies]
31+
django = ["django>=6.0", "django-cmd>=3.0"]
32+
33+
[project.entry-points.djp]
34+
queueio = "queueio.django.djp"
35+
3036
[project.scripts]
3137
queueio = "queueio.__main__:app"
3238

3339
[dependency-groups]
3440
dev = [
3541
"basedpyright>=1.32.1",
42+
"django-extensions",
43+
"djp",
44+
"queueio[django]",
3645
"mkdocs-material>=9.7.1",
3746
"mkdocs-same-dir>=0.1.3",
3847
"pytest>=8.4.1",
@@ -60,6 +69,7 @@ pika = "amqp://localhost:5672"
6069

6170
[tool.pytest.ini_options]
6271
timeout = 30
72+
norecursedirs = ["site"]
6373

6474
[tool.coverage.run]
6575
branch = true

queueio/django.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Django Integration
2+
3+
## Settings
4+
5+
When running `queueio run`,
6+
Django isn't started automatically.
7+
If your routines use the Django ORM or other Django features,
8+
queueio needs to call `django.setup()` before importing routine modules.
9+
10+
The `queueio[django]` extra includes
11+
[django-cmd](https://pypi.org/project/django-cmd/),
12+
which reads `tool.django.settings` from `pyproject.toml`
13+
and sets `DJANGO_SETTINGS_MODULE` automatically:
14+
15+
```toml
16+
[tool.django]
17+
settings = "myproject.settings"
18+
```
19+
20+
If `DJANGO_SETTINGS_MODULE` is set in the environment
21+
(whether by `django-cmd` or directly),
22+
queueio calls `django.setup()` before registering routines.
23+
24+
## Runserver
25+
26+
!!! warning "Development server only"
27+
28+
The runserver integration only affects the `runserver` management command.
29+
Production servers like gunicorn need their own lifecycle hooks.
30+
31+
queueio ships a Django app
32+
that overrides the `runserver` management command
33+
to wrap the server in `with activate():`,
34+
giving you automatic lifecycle management
35+
of the broker connection and background threads
36+
during development.
37+
38+
### Setup
39+
40+
If you use [djp](https://djp.readthedocs.io/),
41+
`queueio.django` is added to `INSTALLED_APPS` automatically.
42+
43+
Otherwise, add it manually:
44+
45+
```python
46+
INSTALLED_APPS = [
47+
"queueio.django",
48+
# ...
49+
]
50+
```
51+
52+
This must come **after** whichever app provides the `runserver` command
53+
you want to chain with
54+
(e.g. `daphne`, `django.contrib.staticfiles`),
55+
because Django resolves management commands by last-app-wins order.
56+
57+
### Custom provider
58+
59+
If you use a custom `runserver` provider
60+
like `django.contrib.staticfiles` or `daphne`,
61+
tell queueio which app to chain with:
62+
63+
```toml
64+
[tool.queueio.django]
65+
runserver = "daphne"
66+
```
67+
68+
The value is the app label (or dotted module path).
69+
queueio resolves it to `{value}.management.commands.runserver`
70+
and subclasses that command.
71+
If omitted, it defaults to Django's built-in `runserver`.
72+
73+
### `runserver_plus`
74+
75+
There is also a built-in `runserver_plus` command
76+
that wraps [django-extensions](https://django-extensions.readthedocs.io/)' `runserver_plus`
77+
with `activate()`.
78+
This is available automatically
79+
when `django-extensions` is installed
80+
and `queueio.django` is in `INSTALLED_APPS`.
81+
82+
A complete working example is in `queueio/samples/django/`.

queueio/django/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
3+
4+
def setup():
5+
try:
6+
from django_cmd import configure
7+
8+
configure()
9+
except ImportError:
10+
pass
11+
12+
if "DJANGO_SETTINGS_MODULE" not in os.environ:
13+
return
14+
15+
import django
16+
17+
django.setup()

queueio/django/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class QueueIOConfig(AppConfig):
5+
name = "queueio.django"
6+
label = "queueio"

queueio/django/djp.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import djp
2+
3+
4+
@djp.hookimpl
5+
def installed_apps():
6+
return ["queueio.django"]

queueio/django/management/__init__.py

Whitespace-only changes.

queueio/django/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import tomllib
2+
from importlib import import_module
3+
from pathlib import Path
4+
5+
6+
def _find_pyproject() -> Path | None:
7+
for path in [cwd := Path.cwd(), *cwd.parents]:
8+
candidate = path / "pyproject.toml"
9+
if candidate.is_file():
10+
return candidate
11+
return None
12+
13+
14+
def _queueio_django_config() -> dict:
15+
if pyproject := _find_pyproject():
16+
with pyproject.open("rb") as f:
17+
config = tomllib.load(f)
18+
return config.get("tool", {}).get("queueio", {}).get("django", {})
19+
return {}
20+
21+
22+
def _resolve_base_command(config: dict):
23+
runserver_app = config.get("runserver", "django.core")
24+
module = import_module(f"{runserver_app}.management.commands.runserver")
25+
return module.Command
26+
27+
28+
def _build_command():
29+
base = _resolve_base_command(_queueio_django_config())
30+
31+
class Command(base):
32+
def inner_run(self, *args, **options):
33+
from queueio import activate
34+
35+
with activate():
36+
super().inner_run(*args, **options)
37+
38+
return Command
39+
40+
41+
Command = _build_command()

0 commit comments

Comments
 (0)