Skip to main content

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.

PyPI version Python versions License: MIT

Requirements

  • Python ≥ 3.10
  • Django ≥ 5.0 — uses db_default
  • PostgreSQL — currently the only supported backend; the soft-delete rule and _updated_at trigger 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=False are not carried into a later _save=True call 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_at trigger) live in a migration generated by makeguitarmigrations. By default makemigrations generates it for you (see below); until it's created and you migrate, .delete() permanently deletes the row — the protection isn't wired up yet.

makeguitarmigrations

The triggers and rules don't come from plain Django schema migrations — they live in separate migrations generated by this command, and they're required for soft deletion and the _updated_at trigger to work.

By default you don't run it directly: makemigrations is extended to generate these advanced migrations right after the core ones, so a single command keeps both in sync:

python manage.py makemigrations   # generates core + trigger/rule migrations

Prefer the explicit two-command workflow? Turn the extension off and run the command yourself:

# settings.py
GUITARS_AUTO_MAKE_MIGRATIONS = False   # defaults to True
python manage.py makemigrations
python manage.py makeguitarmigrations

Either way, the command 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"

Both commands accept optional app labels to scope generation, mirroring Django's own makemigrations: makemigrations blog (or makeguitarmigrations blog) only touches blog, and an unknown label is rejected the same way Django's is. With no labels, every app in LOCAL_APPS is scanned.

Cross-app cascade rules: a soft-delete cascade rule (e.g. "deleting a Band cascades to its Albums") is written into the parent model's migration — Band's app here. If that parent's app isn't named in a scoped run, the cascade rule is skipped even when the child's app is; the command prints a warning naming the skipped rule and the app to include. Run without labels, or name the parent's app, to close the gap.

Use --check in CI to fail when advanced migrations are missing. With the extension on, makemigrations --check validates both the core and the trigger/rule migrations; the standalone form still works too:

python manage.py makemigrations --check       # checks both layers
python manage.py makeguitarmigrations --check # checks the trigger/rule layer only

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 :4455)
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.

Releasing

Two interactive helpers in scripts/ drive a release:

./scripts/bump.sh minor   # bump pyproject.toml + seed CHANGELOG, then commit
$EDITOR CHANGELOG.md       # write the release notes
./scripts/release.sh       # git tag + push + GitHub release (via gh)

pyproject.toml is the single source of truth for the version — guitars.__version__ reads it from the installed package metadata. See scripts/README.md for details.

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

django_guitars-0.5.1.tar.gz (29.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_guitars-0.5.1-py3-none-any.whl (21.1 kB view details)

Uploaded Python 3

File details

Details for the file django_guitars-0.5.1.tar.gz.

File metadata

  • Download URL: django_guitars-0.5.1.tar.gz
  • Upload date:
  • Size: 29.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_guitars-0.5.1.tar.gz
Algorithm Hash digest
SHA256 1f2c8eaba7e02f5ba1a7bddd102eafc83d59b056b5568f424e7723bcbd2b7e45
MD5 a85219b600c0bfdcd1b7ef12b5045c17
BLAKE2b-256 6f5b9b42f709c5a2d71394fe29870c8a2151e863ad6004b07de7b3d66304e166

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_guitars-0.5.1.tar.gz:

Publisher: publish.yml on Behnam-RK/django-guitars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_guitars-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: django_guitars-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 21.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_guitars-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 de347f5035e0f85a808712a174a3b7cacfabbb988a8b91bb9dcbbd35da807ff5
MD5 b42b5122a9653869eefa35ab2e37a38f
BLAKE2b-256 60191e36ac731dd49d81a99983a0e390874b831a4e0eb408eec4f02126c8d966

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_guitars-0.5.1-py3-none-any.whl:

Publisher: publish.yml on Behnam-RK/django-guitars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page