🎸 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.
Python ≥ 3.10 · Django 5.0–6.0 (uses db_default; CI samples 5.0/5.2/6.0 against Python 3.10/3.12/3.14) · PostgreSQL ≥ 14, currently the only supported backend since the soft-delete rule and _updated_at trigger live in the database itself (CI verifies 14 and 18).
Status: the public API — base models, managers, the
guitars.sqlnames generated migrations depend on, andGUITARS_*settings — is stable since 1.0.0; breaking changes now require a major version. SeeCHANGELOG.md.
pip install django-guitars
# or, for psycopg[c] built against your system libpq (psycopg's own production recommendation):
pip install django-guitars[psycopg]INSTALLED_APPS = [
# ...
"guitars",
]| If you want to… | Read |
|---|---|
| pick a base model | Pick your instrument, below |
understand soft deletion, cascades and hard_delete |
docs/soft-deletion.md |
| scope rows to a tenant | docs/tenancy.md |
| use multi-table inheritance | docs/mti.md |
| know how the triggers, rules and policies get into your database | docs/migrations.md |
look up a setting, command flag, or the frozen guitars.sql names |
docs/api-reference.md |
| know why something was built this way | docs/adr/ |
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): TarModel (.update()/.aupdate(), cached-property invalidation, no columns) → DutarModel (+ DB-managed _created_at/_updated_at, app_label()/model_name()/class_name()) → SetarModel (+ PostgreSQL soft deletion — the one to reach for by default) → GuitarModel (+ multi-tenancy: a tenant FK, tenant-scoped managers, an RLS policy — the full kit). Each capability is also a standalone mixin in guitars.models: UpdatableModel, HasCachedPropertyModel, DatedModel, SoftDeletableModel.
⚠️ Renamed in 1.0.0 — 0.7'sDutarModel→TarModel,SetarModel→DutarModel,GuitarModel→SetarModel(behaviour-identical);GuitarModelnow means "SetarModel+ tenancy". SeeCHANGELOG.md.
from django.db import models
from guitars.models import SetarModel
class Article(SetarModel):
title = models.CharField(max_length=200)article.update(title="New title") # set fields + save (only changed fields)
article.delete() # soft delete: sets _deleted_at, row stays
Article.objects.all() # live rows only
Article._archives.all() # soft-deleted rows only
article.hard_delete() # actually gone, CASCADE children too
⚠️ Required setup. The soft-delete rule and_updated_attrigger live in a migration generated bymakeguitarmigrations— by defaultmakemigrationsgenerates it for you. Until it's created and youmigrate,.delete()permanently deletes the row. Seedocs/migrations.md.
Multi-tenancy adds a scope requirement on top:
from guitars.tenancy import tenant, tenancy_bypassed
with tenant(org=acme):
Invoice.objects.all() # acme's invoices only
Invoice.objects.all() # TenantScopeMissing — no scope, no rows
with tenancy_bypassed(): # the one explicit cross-tenant path
Invoice.objects.count()Full detail — settings, rollout onto a populated database, auditing, connection pooling — is in docs/tenancy.md.
guitars.signals.DisableSignals() temporarily disconnects Django's signals — with DisableSignals(): instance.save() fires nothing, handy for bulk imports or silent saves.
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 :4455)
uv run pytest # run the test suiteThe suite defines concrete models in tests/testapp (the shipped package is abstract-only) and runs against a real PostgreSQL database as a deliberately non-superuser role, since a superuser bypasses RLS unconditionally. An old checkout needs docker compose down -v && docker compose up -d --wait once. See CLAUDE.md for the full command reference and scripts/README.md for releasing.
MIT © 2026 Behnam RK