PyScoped
Tenant scoping and transactional audit history for the Django application you already have.
Keep your models, tables, primary keys, authentication, and business logic. Add PyScoped one model at a time. Free and MIT licensed, with no account, API key, paid tier, telemetry, or hosted-service dependency.
2.0.0 is the stable Django release. It is a
breaking redesign of 1.x. The new import is pyscoped; the old scoped framework is
preserved in legacy/v1/ and excluded from the package.
Add it to an existing model
Install the stable release:
python -m pip install pyscoped==2.0.0
Add "pyscoped" to INSTALLED_APPS, then run python manage.py migrate.
Only PyScoped's history tables are added; your model needs no extra columns.
from django.db import models
from pyscoped import scoped
from pyscoped.query import ScopedManager
@scoped(scope_field="organization", fields=["amount", "status"])
class Invoice(models.Model):
organization = models.ForeignKey("Organization", on_delete=models.PROTECT)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=32, default="draft")
objects = ScopedManager()
Use your existing authenticated user and organization after checking membership:
from pyscoped import scope
with scope(actor=request.user, scope=organization.pk):
invoices = list(Invoice.objects.filter(status="draft"))
Invoice.objects.filter(pk=invoice_id).update(status="approved")
Reads through the scoped manager are filtered before aggregation or pagination. Supported writes and audit records commit or roll back together. Missing context or an instance write to another scope raises. An explicit field allowlist controls what is recorded; it is your responsibility to exclude sensitive fields.
scope() is an application-level boundary: your application must authenticate the
actor and authorize scope membership. It does not grant a user permission merely
because a tenant ID was supplied by a client.
Adopt gradually
- Begin with
mode="audit"to retain existing read behavior while adding attributed write history. Writes still require actor context and the supported manager. - Use
@scoped(...)on a current model, orregister(Invoice, ...)inAppConfig.ready(). - If convenient, inherit
pyscoped.models.ScopedModel; it adds a manager, no columns. The registration decorator is still required. - Compose existing queryset methods with
ScopedQuerySet. SupplyScopedManagerfor every public manager on a registered model. - Baseline existing data, then enable enforcement for that model when its workflows and relationship integrity have been checked.
# Preview first; actor and scope are explicit operator assertions.
python manage.py pyscoped_backfill billing.Invoice --actor migration-operator --scope acme
python manage.py pyscoped_backfill billing.Invoice --actor migration-operator --scope acme --apply
The baseline preserves existing IDs and records the state observed now. It does not invent historical events. Applying is idempotent and resumable per row.
For request context, add pyscoped.middleware.ScopedMiddleware after your existing
session/authentication middleware and configure PYSCOPED_CONTEXT_RESOLVER with a
trusted callable returning ScopeContext or None. For existing service functions,
use @in_scope(resolver) from pyscoped.integration.
History and restoration
from pyscoped.history import history, verify_history, restore
with scope(actor=request.user, scope=organization.pk):
events = history(Invoice, invoice_id)
verified = verify_history(Invoice, invoice_id)
invoice = restore(Invoice, invoice_id, revision=1, expected_revision=3)
Restoration updates selected scalar fields on a live row and records a new event. It checks the expected head revision and detects divergence from recorded state. It does not resurrect deleted rows, restore external effects, or change ownership, primary keys, or relationships. History is hash-linked per resource; rewriting the entire chain and head with database-administrator privileges is outside that guarantee.
Supported scope
Django 5.2 and 6.0, on their supported Python versions. SQLite for local development; PostgreSQL for concurrent transactional use. There are no non-Django adapters in 2.0.
| Operation | Initial 2.0 behavior |
|---|---|
| Model save, partial save, expression save | Attributed and audited atomically |
| Scoped query reads, counts, aggregates, pagination | Scope predicate enforced at the root queryset |
| Queryset update | Native SQL semantics, row locks and per-row audit snapshots in one transaction |
| Instance/queryset/cascade delete | Django deletion semantics; retained audit events |
| Async ORM operations | Same contract via Django's async methods |
Ordinary bulk_create, bulk_update |
Native Django signal semantics, transactional audit snapshots |
| Conflict-handling bulk insert, duplicate bulk-update IDs, raw fixture save | Explicitly rejected |
select_related, joins into enforced models |
Scope predicates on joined tables; outer-join semantics preserved |
| FK / one-to-one reads between enforced models, prefetch | Scoped target queries and context checks |
| Implicit many-to-many, multi-table inheritance, proxies | Outside initial support; configuration fails |
Raw SQL, RawSQL, private ORM APIs, historical migration models |
Outside enforcement; never use for ordinary registered operations |
This is an ORM integration, not database row-level security or a sandbox against untrusted Python code. Unregistered models, external database writers, direct private/base-manager access, and already-returned data do not acquire automatic access controls. Preserve valid same-scope relationships and use scoped querysets for each protected read boundary.
See the full guarantees and limitations, the adoption guide, and the executable existing-app example.
Develop and contribute
python -m pip install -e '.[dev,postgres]'
python -m pytest -q
python -m django check --settings=tests.settings
python -m django makemigrations --check --dry-run --settings=tests.settings
ruff check pyscoped tests examples
python -m build
See CONTRIBUTING.md for PostgreSQL tests, the phased development plan for scope and progress, and the release record for validation evidence.
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 pyscoped-2.0.0.tar.gz.
File metadata
- Download URL: pyscoped-2.0.0.tar.gz
- Upload date:
- Size: 44.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24261012843dfd7f24aea46468fbef471397db35f9e9d04bfc098ca7438e8d57
|
|
| MD5 |
35fc95e09bd8b1a352c8ae3600952caa
|
|
| BLAKE2b-256 |
9eca3a73cc9ad184e3bc9c1df5d861ddaef0034fc30381e09898bf793cdb5a30
|
Provenance
The following attestation bundles were made for pyscoped-2.0.0.tar.gz:
Publisher:
publish.yml on kwip-info/pyscoped
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyscoped-2.0.0.tar.gz -
Subject digest:
24261012843dfd7f24aea46468fbef471397db35f9e9d04bfc098ca7438e8d57 - Sigstore transparency entry: 2752521156
- Sigstore integration time:
-
Permalink:
kwip-info/pyscoped@74cb0a8cd9f86481c9e839bf7a218989bc336f6a -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/kwip-info
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@74cb0a8cd9f86481c9e839bf7a218989bc336f6a -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyscoped-2.0.0-py3-none-any.whl.
File metadata
- Download URL: pyscoped-2.0.0-py3-none-any.whl
- Upload date:
- Size: 23.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e657c04347daafa444e92f29e8f90bb81e7df3cc6be08f9d2e6b17ad27da90d2
|
|
| MD5 |
b3ce3316937d13a4ffe57a8ead814bae
|
|
| BLAKE2b-256 |
9e413b970f533a354d4fb0dc85904d90406d93e444e5964e7a2d59f1377fbb22
|
Provenance
The following attestation bundles were made for pyscoped-2.0.0-py3-none-any.whl:
Publisher:
publish.yml on kwip-info/pyscoped
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyscoped-2.0.0-py3-none-any.whl -
Subject digest:
e657c04347daafa444e92f29e8f90bb81e7df3cc6be08f9d2e6b17ad27da90d2 - Sigstore transparency entry: 2752521168
- Sigstore integration time:
-
Permalink:
kwip-info/pyscoped@74cb0a8cd9f86481c9e839bf7a218989bc336f6a -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/kwip-info
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@74cb0a8cd9f86481c9e839bf7a218989bc336f6a -
Trigger Event:
release
-
Statement type: