Metadata-Version: 2.5
Name: django-guitars
Version: 2.5.1
Summary: A kit of reusable Django utilities: base models, PostgreSQL soft deletion, and more.
Project-URL: Homepage, https://github.com/Behnam-RK/django-guitars
Project-URL: Repository, https://github.com/Behnam-RK/django-guitars
Project-URL: Issues, https://github.com/Behnam-RK/django-guitars/issues
Project-URL: Changelog, https://github.com/Behnam-RK/django-guitars/blob/main/CHANGELOG.md
Author-email: Behnam RK <behnam.rk47@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: django,guitars,reusable-app
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: django>=5.0
Provides-Extra: psycopg
Requires-Dist: psycopg[c]>=3.2; extra == 'psycopg'
Description-Content-Type: text/markdown

# 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](https://img.shields.io/pypi/v/django-guitars.svg)](https://pypi.org/project/django-guitars/) [![Python versions](https://img.shields.io/pypi/pyversions/django-guitars.svg)](https://pypi.org/project/django-guitars/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

## 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`](CHANGELOG.md).

## Installation

```bash
pip install django-guitars
# or, for psycopg[c] built against your system libpq (psycopg's own production recommendation):
pip install django-guitars[psycopg]
```

```python
INSTALLED_APPS = [
    # ...
    "guitars",
]
```

## Where to find what

| If you want to… | Read |
| --- | --- |
| pick a base model | [Pick your instrument](#pick-your-instrument), below |
| understand soft deletion, cascades and `hard_delete` | [`docs/soft-deletion.md`](docs/soft-deletion.md) |
| scope rows to a tenant | [`docs/tenancy.md`](docs/tenancy.md) |
| use multi-table inheritance | [`docs/mti.md`](docs/mti.md) |
| know how the triggers, rules and policies get into your database | [`docs/migrations.md`](docs/migrations.md) |
| look up a setting, command flag, or the frozen `guitars.sql` names | [`docs/api-reference.md`](docs/api-reference.md) |
| know *why* something was built this way | [`docs/adr/`](docs/adr/README.md) |

## 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](docs/tenancy.md): 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 `DutarModel`→`TarModel`, `SetarModel`→`DutarModel`, `GuitarModel`→`SetarModel` (behaviour-identical); `GuitarModel` now means "`SetarModel` + tenancy". See [`CHANGELOG.md`](CHANGELOG.md).

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

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

### Quick taste

```python
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`](docs/migrations.md).

Multi-tenancy adds a scope requirement on top:

```python
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`](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](https://docs.astral.sh/uv/) and Docker (for PostgreSQL).

```bash
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`](CLAUDE.md) for the full command reference and [`scripts/README.md`](scripts/README.md) for releasing.

## License

[MIT](LICENSE) © 2026 Behnam RK
