Drop-in versioning for Django models — releases, data status workflow, and CI integration
Project description
django-versioned-models
Drop-in versioning for Django models. Every model that inherits from VersionedModel gets full release management, a data status workflow, and CI integration — automatically.
🚀 Features
- Release management — every row in every table is tagged to a release. Branch, lock, patch, and deprecate with simple commands.
- Data status workflow —
DRAFT → FUTURE → APPROVED. CI only sees approved rows. Live architect edits never break tests. - Lock enforcement — locked releases are fully immutable at the model level. No edits, no deletes — from Admin, API, or shell.
- Soft-delete — rows can be deactivated without deletion. Deactivating an approved row resets it to
DRAFTso it must be re-approved before CI sees it again. - Auto-discovery — inherit
VersionedModeland your model is versioned. No registration needed. - Topological FK sort — models are duplicated in the correct dependency order automatically when branching a release.
- Soft deprecation — old releases are hidden by default but data is always preserved and reversible.
- CI-ready commands —
create_release,approve_release,lock_release, and more, ready to plug into any pipeline.
📦 Installation
pip install django-versioned-models
⚡ Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
...
"django_versioned_models",
]
2. Run migrations
python manage.py migrate
3. Define your versioned models
from django.db import models
from django_versioned_models.mixins import VersionedModel
class Product(VersionedModel):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta(VersionedModel.Meta):
unique_together = [("release", "name")] # unique per release, not globally
Important: always inherit
MetafromVersionedModel.Metato preserve the composite index on(release, status, active).
4. Run migrations for your models
python manage.py makemigrations
python manage.py migrate
5. Create the first release
python manage.py create_release --release-version v1.0.0
6. Add data, then lock
# Add data via Admin, API, or shell — all rows start as DRAFT
python manage.py lock_release --release-version v1.0.0
🔄 Ongoing Workflow
create_release → architects edit (DRAFT) → lock_release → CI approves → lock_release → deploy_release → ship
# Branch from the previous locked release
python manage.py create_release --release-version v1.1.0 --based-on v1.0.0
# Architects add and edit rows (status=DRAFT by default)
# Need a correction? Unlock is allowed before deployment
python manage.py unlock_release --release-version v1.1.0 # interactive
python manage.py unlock_release --release-version v1.1.0 --force # CI / automation
# Lock before CI runs — no more edits after this point
python manage.py lock_release --release-version v1.1.0
# CI approves all stable DRAFT rows (FUTURE rows are left untouched)
python manage.py approve_release --release-version v1.1.0
# Run tests against approved data only
pytest
# Lock again if unlocked for corrections, then deploy
python manage.py lock_release --release-version v1.1.0 # skip if already locked
python manage.py deploy_release --release-version v1.1.0 # requires locked — permanently blocks unlock
# Bug found after deployment? Create a patch — never unlock a deployed release
python manage.py create_release --release-version v1.1.1 --based-on v1.1.0
🔍 How It Works
Every model that inherits from VersionedModel automatically gets:
| Field | Type | Description |
|---|---|---|
release |
FK → Release |
Which release this row belongs to |
status |
CharField | Data readiness: draft / future / approved |
active |
BooleanField | Soft-delete flag. Inactive rows are invisible to for_release() and approved() |
Release state fields:
| Field | Type | Description |
|---|---|---|
is_locked |
BooleanField | Immutable — no edits or deletes allowed |
deployed |
BooleanField | Deployed to production — unlock is permanently blocked |
is_deprecated |
BooleanField | Hidden by default, data preserved |
Status Flow
DRAFT <-> FUTURE -> APPROVED (APPROVED is one-way — CI only)
| Status | Set by | Meaning |
|---|---|---|
DRAFT |
Architects | Being worked on |
FUTURE |
Architects | Planned but not ready |
APPROVED |
CI only | Stable — what tests run against |
Active / Inactive (Soft-Delete)
row.deactivate() # sets active=False, resets APPROVED → DRAFT
row.reactivate() # sets active=True, status stays DRAFT — must re-approve
Deactivating an approved row resets its status to DRAFT, so it must go through the approval flow again before CI can see it.
Lock Enforcement
Locked releases are immutable. Any save() or delete() on a non-approved row raises ValidationError — from Admin, API, or shell:
row.save() # raises ValidationError if release is locked and status != APPROVED
row.delete() # raises ValidationError if release is locked
The only operation permitted on a locked release is approve() — this is how CI stamps rows after locking.
📋 Management Commands
| Command | Description |
|---|---|
create_release --release-version v1.0.0 |
Create a standalone release (unlocked) |
create_release --release-version v1.1.0 --based-on v1.0.0 |
Branch from a locked release — copies all rows |
lock_release --release-version v1.1.0 |
Lock a release (immutable) |
approve_release --release-version v1.1.0 |
Approve all active DRAFT rows (CI only — FUTURE and inactive untouched) |
deploy_release --release-version v1.1.0 |
Mark as deployed — permanently blocks unlock |
unlock_release --release-version v1.1.0 |
Unlock with interactive confirmation (pre-deployment only) |
unlock_release --release-version v1.1.0 --force |
Unlock without prompt — safe for CI/automation (pre-deployment only) |
deprecate_release --release-version v1.0.0 |
Soft-deprecate (data preserved, hidden by default) |
deprecate_release --release-version v1.0.0 --undo |
Restore a deprecated release |
🗄 Querying
from django_versioned_models.models import Release
release = Release.objects.get(version="v1.1.0")
# Active rows only, all statuses — use in architect-facing GUI views
Product.objects.for_release(release)
# Approved + active rows only — use in CI and production logic
Product.objects.approved(release)
# All rows including inactive — use for copy/audit only, not business logic
Product.objects.all_rows(release)
# Filter by status directly
Product.objects.for_release(release).filter(status="future")
📥 Imports Reference
from django_versioned_models.mixins import VersionedModel, DataStatus
from django_versioned_models.models import Release
from django_versioned_models.services import (
create_release,
lock_release,
get_versioned_models,
get_versioned_models_ordered,
)
🧪 Running Tests
uv run pytest
🤝 Contributing
Contributions are welcome! Please fork the repo, create a branch, and submit a pull request.
📄 License
MIT License — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_versioned_models-1.1.0.tar.gz.
File metadata
- Download URL: django_versioned_models-1.1.0.tar.gz
- Upload date:
- Size: 12.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c13dde3262f295244d9c3e5a914d5abe4388898b8d31708a0bad8fc092d52d20
|
|
| MD5 |
e49b149e241fcd2079a44162a89ea292
|
|
| BLAKE2b-256 |
83759eb3d9f56884fe13f1f428385e1907e56de5ad693bff2c24a31242a89be7
|
File details
Details for the file django_versioned_models-1.1.0-py3-none-any.whl.
File metadata
- Download URL: django_versioned_models-1.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19f7c8331fe16315ab54be0565a27bf645c0e39c8ed3d24f8ed7be76b49872e2
|
|
| MD5 |
3ba884dc8e0bf4c30a3d97fdfd075d47
|
|
| BLAKE2b-256 |
4011860994bdcf8811cc747c004fe3511460520c40b7bd9337044bd543efe2b3
|