A kit of reusable Django utilities: base models, PostgreSQL soft deletion, and more.
Project description
django-guitars
🎸 Django object-metadata the database enforces — not your .save() method.
Most Django soft-delete and timestamp libraries live in Python: a signal here, a
save() override there. It holds up right until a bulk_update, a raw UPDATE,
or a queryset.delete() strolls straight past your code — and leaves the
metadata lying.
django-guitars pushes that work down into PostgreSQL itself — rules and
triggers, not signals. So _created_at / _updated_at / _deleted_at stay
honest no matter how a row gets touched: ORM, bulk, raw SQL, all of it. The
database keeps score; you just write models. Use only the pieces you need.
Requirements
- Python ≥ 3.10
- Django ≥ 5.0 — uses
db_default - PostgreSQL — currently the only supported backend; the soft-delete rule and
_updated_attrigger live in the database itself. Other backends are on the roadmap.
Status: early days (alpha). The API may still shift between minor versions.
Installation
pip install django-guitars
Add the app to your settings:
INSTALLED_APPS = [
# ...
"guitars",
]
Pick your instrument
The base models are named after string instruments, fewest strings to most — and
the strings are the feature ladder. (du = two, se = three in Persian; tar
= "string". A guitar has six. Django Reinhardt, the jazz guitarist this whole
package winks at, would approve.)
| Base | Strings | What you get |
|---|---|---|
DutarModel |
2 | .update() / .aupdate() and cached-property invalidation on refresh_from_db(). The featherweight — adds no columns. |
SetarModel |
3 | Everything in DutarModel plus DB-managed _created_at / _updated_at (default NOW(); _updated_at is ridden by a statement trigger, so it's right even under bulk/raw updates) and app_label() / model_name() / class_name() helpers. |
GuitarModel |
6 | Everything in SetarModel plus PostgreSQL soft deletion. The full kit. |
Prefer to tune your own chord? Each capability is a standalone mixin in
guitars.models: UpdatableModel, HasCachedPropertyModel, DatedModel, and
SoftDeletableModel.
from django.db import models
from guitars.models import GuitarModel
class Article(GuitarModel):
title = models.CharField(max_length=200)
.update() — set and save in one strum
Available on every rung (it comes from DutarModel):
article.update(title="New title") # set fields + save (only changed fields)
article.update(title="x", _save=False) # change in memory only, no DB write
await article.aupdate(title="async") # async variant
Note: attributes set with
_save=Falseare not carried into a later_save=Truecall unless you also pass_save_all_fields=True.
Soft deletion
For models inheriting SoftDeletableModel (or GuitarModel), .delete() becomes
a soft delete: the row stays and _deleted_at is set. Because a PostgreSQL
rule does the work, it holds even for queryset bulk deletes and raw SQL — there's
no .save() to skip. Three managers expose the data:
Article.objects.all() # live rows only (the default manager)
Article._archives.all() # soft-deleted rows only
Article._all_objects.all() # everything
article.delete() # soft delete — sets _deleted_at
article.is_deleted # True
article.is_alive # False
# Actually want it gone? hard_delete bypasses the rule (and takes CASCADE kids with it):
article.hard_delete() # this row + CASCADE children
Article._all_objects.filter(...).hard_delete() # in bulk
Soft-deleting a row also soft-deletes rows related by on_delete=CASCADE.
⚠️ Required setup. The soft-delete rule (and the
_updated_attrigger) live in a migration generated bymakeguitarmigrations. Until you run that command andmigrate,.delete()permanently deletes the row — the protection isn't wired up yet. Re-run it whenever you add or change a model that uses these bases.
makeguitarmigrations
makemigrations does not create the triggers and rules — they live in
separate migrations generated by this command, and it's required for soft
deletion and the _updated_at trigger to work. After your usual makemigrations,
run:
python manage.py makeguitarmigrations
It scans your first-party apps for models with _updated_at / _deleted_at
and writes the matching trigger/rule migrations. Tell it which apps are yours:
# settings.py
LOCAL_APPS = ["blog", "shop"] # apps the command scans
# Optional: which app hosts the shared trigger-function migration.
# Defaults to LOCAL_APPS[0].
# TRIGGER_FUNCTION_APP = "blog"
Use --check in CI to fail when advanced migrations are missing:
python manage.py makeguitarmigrations --check
DisableSignals
A context manager that temporarily disconnects Django signals — handy for bulk imports or silent saves:
from django.db.models.signals import post_save
from guitars.signals import DisableSignals
with DisableSignals(): # all default signals
instance.save() # nothing fires
with DisableSignals(signals=[post_save]): # only the listed signals
instance.save()
Development
Requires uv and Docker (for PostgreSQL).
uv sync # install dependencies + the package (editable)
docker compose up -d # start PostgreSQL (skip if you already run one on :5432)
uv run pytest # run the test suite
uv run pytest --cov=guitars --cov-report=term-missing
The test suite defines concrete models in tests/testapp (the shipped package is
abstract-only) and runs against a real PostgreSQL database, so the rules and
triggers are actually exercised — not mocked.
License
MIT © 2026 Behnam RK
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_guitars-0.3.0.tar.gz.
File metadata
- Download URL: django_guitars-0.3.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88245853e67209fd590e6d9f87b9ed6f8c0fb79d436ef93dda172157f798aeed
|
|
| MD5 |
6f30f7ceb18ea10a463ad15d5b747090
|
|
| BLAKE2b-256 |
4502760b1bf6cffc39a3a8d674d2987c286e9c045f82e3fbe5c57108e05567ab
|
Provenance
The following attestation bundles were made for django_guitars-0.3.0.tar.gz:
Publisher:
publish.yml on Behnam-RK/django-guitars
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_guitars-0.3.0.tar.gz -
Subject digest:
88245853e67209fd590e6d9f87b9ed6f8c0fb79d436ef93dda172157f798aeed - Sigstore transparency entry: 1791202883
- Sigstore integration time:
-
Permalink:
Behnam-RK/django-guitars@52ec0cfa2cae2ab1bab844b8bab2ad5044bafbc4 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Behnam-RK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@52ec0cfa2cae2ab1bab844b8bab2ad5044bafbc4 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_guitars-0.3.0-py3-none-any.whl.
File metadata
- Download URL: django_guitars-0.3.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3df3c8c06a1116e80e2854d336dc00ea84c2050397a84924a5d0a237e37a45e8
|
|
| MD5 |
cf0043b44be0c1c8ef68c4661d248ec4
|
|
| BLAKE2b-256 |
c422852674411085c2831a895c554bd673628d1d12be91fe78e1e87d3d70057e
|
Provenance
The following attestation bundles were made for django_guitars-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on Behnam-RK/django-guitars
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_guitars-0.3.0-py3-none-any.whl -
Subject digest:
3df3c8c06a1116e80e2854d336dc00ea84c2050397a84924a5d0a237e37a45e8 - Sigstore transparency entry: 1791203634
- Sigstore integration time:
-
Permalink:
Behnam-RK/django-guitars@52ec0cfa2cae2ab1bab844b8bab2ad5044bafbc4 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Behnam-RK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@52ec0cfa2cae2ab1bab844b8bab2ad5044bafbc4 -
Trigger Event:
release
-
Statement type: