django-heavy-water
A reusable Django app that seeds your database with development and test data. Each app in your project defines data builders, and a single management command, heavy_water, runs the ones that apply to the current environment.
Requires Python 3.10+ and Django 4.2+.
Installation
Install django-heavy-water from PyPI:
pip install django-heavy-water
Or, with uv:
uv add django-heavy-water
Release notes and upgrade instructions are in the changelog.
Add it to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"heavy_water",
]
Usage
Write a data builder
Create a fixtures.py module in any installed app and subclass BaseDataBuilder:
# myapp/fixtures.py
from typing import Any
from heavy_water import BaseDataBuilder
from myapp.models import Category, Widget
class WidgetData(BaseDataBuilder):
# Runs only when DEBUG is True (the default).
def handle(self) -> None:
self.get_or_create_superuser()
Widget.objects.get_or_create(name="Sprocket")
class CategoryData(BaseDataBuilder):
# Data every environment needs, so run it everywhere.
def should_run(self, *args: Any, **options: Any) -> bool:
return True
def handle(self) -> None:
Category.objects.get_or_create(name="General")
Run it
python manage.py heavy_water # run all builders
python manage.py heavy_water --wipe # flush the database first
python manage.py heavy_water --database other # seed a different database
python manage.py heavy_water --list # show what would run, without running it
python manage.py heavy_water --dry-run # run everything, then roll it back
python manage.py heavy_water --only orders.OrderData # run one builder (plus its dependencies)
python manage.py heavy_water --exclude orders # skip every builder in an app
--list prints the builders as a tree in the order they would run, with each builder's dependencies under it. Builders that would be skipped are dimmed and marked "(skipped)". It calls each builder's should_run() (with the other options you pass, such as --wipe) but never runs handle() or flushes the database:
Data builders, in run order
├── 1. customers - CustomerData
├── 2. orders - OrderData
│ └── depends on 1. customers - CustomerData
└── 3. orders - DemoData (skipped)
--dry-run runs every builder, handle() included, then rolls back all their database changes, so you can check that they work without keeping the data. Failures are still reported. Anything a builder does outside the database, such as writing files or calling an API, isn't undone. It can't be combined with --wipe.
--only and --exclude take an app label (orders) or an app label and builder class name (orders.OrderData), and can be repeated. --only also runs the selected builders' dependencies, and --exclude also drops builders that depend on an excluded one. --exclude applies after --only. Both work with --list and --dry-run.
--wipe uses Django's flush command, so it accepts flush's options too, such as --no-input.
With --database (or the HEAVY_WATER_DATABASE setting), the command's transaction and get_or_create_superuser() use that database, and the alias is available to builders as self.database. Queries you write in handle() need to use it explicitly:
Widget.objects.using(self.database).get_or_create(name="Sprocket")
How builders run
- The command runs every builder it finds whose
should_run()returnsTrue. By default that's only whenDEBUGisTrue, so seed data doesn't reach production by accident. Override it to run a builder elsewhere, asCategoryDatadoes above.should_run()receives the command's arguments and parsed options (such aswipeandverbosity), so you can use them as conditions too, for examplereturn options["wipe"]. - Builders run in the order they're defined in each module. Apps are processed in
INSTALLED_APPSorder, and modules inHEAVY_WATER_FIXTURE_MODULEorder. Usedepends_on(below) when a builder needs another one to run first. - All builders run in one transaction, and each builder gets its own savepoint. If a builder raises, only its changes are rolled back and the other builders still run.
- If any builder failed, the command exits with an error listing them after committing the rest.
Output
The command prints with Rich via django-rich, including Rich tracebacks for failing builders. Colour follows Django's --no-color and --force-color flags. Inside a builder, print with self.console (stdout) and self.err_console (stderr), which accept Rich markup:
self.console.print(f"Created [bold]{count}[/] widgets")
self.stdout, self.stderr and self.style still work but are deprecated, and raise heavy_water.HeavyWaterDeprecationWarning (a FutureWarning, which Python shows by default) when used. heavy_water also adds a system check, so you find out before running anything: manage.py check (and most other commands) reports heavy_water.W001 for each builder whose source uses them. Use self.console, self.err_console and Rich markup instead.
To silence the check, add "heavy_water.W001" to SILENCED_SYSTEM_CHECKS.
Dependencies between builders
List the builders a builder relies on in depends_on. They run before it, even if they're defined later or in another app:
from otherapp.fixtures import CustomerData
class OrderData(BaseDataBuilder):
depends_on = (CustomerData,)
def handle(self) -> None: ...
- If a dependency fails, the builder doesn't run and is reported as failed too.
- If a dependency is skipped by its
should_run(), the builder is skipped too. - Each dependency must be a builder the command discovers. A missing dependency or a dependency cycle stops the command before anything runs, including
--wipe.
Importing a builder into your fixtures module doesn't make it run twice: builders only run from the module that defines them.
Creating a superuser
get_or_create_superuser() returns the superuser identified by the user model's USERNAME_FIELD, creating it if it doesn't exist. Any argument you leave out falls back to the HEAVY_WATER_SUPERUSER_* settings below. Custom user models are supported, including ones that log in by email.
Pass configure_user to adjust the user after it's fetched or created; the user is saved afterwards:
self.get_or_create_superuser(
username="admin",
configure_user=lambda user: setattr(user, "is_active", True),
)
Note: the default password is
rootroot, and it's only allowed whenDEBUG = True. WithDEBUG = False, creating a superuser raisesImproperlyConfiguredunless you passpassword=or setHEAVY_WATER_SUPERUSER_PASSWORD. Existing users are still returned as normal.
Settings
All settings are optional.
| Setting | Default | Description |
|---|---|---|
HEAVY_WATER_FIXTURE_MODULE |
["fixtures"] |
Submodule names searched for builders in each installed app. |
HEAVY_WATER_DATABASE |
"default" |
Alias from DATABASES to seed. --database overrides it. |
HEAVY_WATER_SUPERUSER_USERNAME |
"root" |
Default login for get_or_create_superuser() (the email is used instead when the login field is the email). |
HEAVY_WATER_SUPERUSER_EMAIL |
"root@example.com" |
Default email. |
HEAVY_WATER_SUPERUSER_PASSWORD |
"rootroot" |
Default password. |
HEAVY_WATER_SUPERUSER_FIRST_NAME |
"Root" |
Default first name, if the user model has the field. |
HEAVY_WATER_SUPERUSER_LAST_NAME |
"User" |
Default last name, if the user model has the field. |
Development
Development tooling is managed by mise. It installs uv and hk, and activates the project's .venv. uv installs the Python dev tools, including ruff, into .venv. Every development script is a mise task, so run everything through mise run.
First-time setup:
mise install # install uv and hk, and the pre-commit hook
That's all: the first mise run or mise x creates .venv with uv sync, and mise re-syncs it automatically whenever pyproject.toml or uv.lock changes (via mise deps, an experimental mise feature this project enables). mise run setup does both steps explicitly.
Tasks:
| Command | What it does |
|---|---|
mise run setup |
Runs sync and hooks. |
mise run sync |
Installs the package and dev dependencies into .venv. |
mise run hooks |
Installs the hk pre-commit hook. |
mise run lint |
Syncs .venv, then runs all checks: ruff lint, ruff format, mypy and uv lock --check. The pre-commit hook runs the same checks. |
mise run fix |
Applies ruff fixes and formatting, and updates uv.lock. |
mise run test |
Runs the test suite with pytest. |
DJANGO=5.2 mise run test-django |
Runs the tests against another Django version. Set UV_PYTHON to pick the Python version too. |
mise run demo [options] |
Runs heavy_water against the test app, so you can see its output. Options are passed to the command, e.g. mise run demo --list. DEMO_MODULES picks the test builder modules. |
mise run typecheck |
Runs mypy in strict mode on the package. |
mise run build |
Builds the sdist and wheel into dist/. |
mise run bump [major|minor|patch] |
Bumps the version (patch by default), dates the changelog's Unreleased section, commits pyproject.toml, uv.lock and CHANGELOG.md, and tags the commit, e.g. v0.2.1. |
mise run publish |
Uploads dist/ to PyPI. Normally run by the publish workflow, not by hand. |
Run mise tasks to list them. To add a script, define it as a task in mise.toml.
Commit messages
Commit messages follow Conventional Commits: <type>[(scope)][!]: <summary>, for example feat(command): add --only and --exclude flags or fix!: refuse the default password when DEBUG is False. The types are feat, fix, docs, refactor, perf, test, build, ci, chore and revert; add ! for breaking changes.
The git hooks installed by mise install (or mise run hooks) enforce this: a commit-msg hook rejects other messages, and when git opens your editor, a prepare-commit-msg hook adds a short guide as comments. Git's own merge and revert messages are allowed. If you set up hooks before this was added, run mise run hooks again.
CI and releases
GitHub Actions (.github/workflows/) runs everything through the same mise tasks:
- CI (
ci.yml), on pushes tomainand on pull requests:lintandbuild, thentest-djangoacross supported Python and Django versions. - Publish (
publish.yml), when a GitHub release is published: checks the release tag matches the version inpyproject.toml(v0.3.0or0.3.0for version0.3.0), runs lint, tests and build, then uploads to PyPI with trusted publishing.
To release:
- Make sure CHANGELOG.md lists the changes under
## Unreleased, including upgrade notes for anything that breaks existing projects. - Commit everything that's part of the release;
bumprefuses to run with uncommitted changes. - Run
mise run bump(orbump minor/bump major). It bumps the version, moves the Unreleased changes under a dated heading for the new version, commits, and tags. - Push with
git push --follow-tags, and publish a GitHub release from the new tag.
The package ships a py.typed marker and is checked with mypy in strict mode, so new code must be fully type-annotated.
License
BSD 3-Clause. See LICENSE.
Metadata
Release files for django-heavy-water 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_heavy_water-0.3.0.tar.gz | 14.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_heavy_water-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.1 kB
Release files / django_heavy_water-0.3.0.tar.gz
| Download URL | django_heavy_water-0.3.0.tar.gz |
|---|---|
| Size | 14.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d79316490288c2db9aab75c67001a2b9673c525854c88cb18cd083adbddda03e
|
|
BLAKE2b-256 checksum How to use checksums |
45dcf49188e4c1fdf8bc3d1701e62273dc707a6331a04a495e80edd5358a1e2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / django_heavy_water-0.3.0-py3-none-any.whl
| Download URL | django_heavy_water-0.3.0-py3-none-any.whl |
|---|---|
| Size | 17.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
99312647352044b62a73ec66268eb1c9d0358ba3e034307706c6501d697a2081
|
|
BLAKE2b-256 checksum How to use checksums |
934a731980ef819de113f2bca194d608a1cd4ff9b2be2b9c92bb5062cd625c54
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|