Skip to content

Repository files navigation

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.

PyPI version Python versions License: MIT

Requirements

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.sql names generated migrations depend on, and GUITARS_* settings — is stable since 1.0.0; breaking changes now require a major version. See CHANGELOG.md.

Installation

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",
]

Where to find what

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/

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): 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's DutarModelTarModel, SetarModelDutarModel, GuitarModelSetarModel (behaviour-identical); GuitarModel now means "SetarModel + tenancy". See CHANGELOG.md.

from django.db import models
from guitars.models import SetarModel

class Article(SetarModel):
    title = models.CharField(max_length=200)

Quick taste

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_at trigger live in a migration generated by makeguitarmigrations — by default makemigrations generates it for you. Until it's created and you migrate, .delete() permanently deletes the row. See docs/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.

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 :4455)
uv run pytest            # run the test suite

The 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.

License

MIT © 2026 Behnam RK

About

Reusable Django utilities with database-enforced object metadata — soft deletes and timestamps that survive bulk updates, raw SQL, and queryset deletes.

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages