Metadata-Version: 2.4
Name: stapel-gdpr
Version: 0.8.0
Summary: GDPR data export and account deletion Django app for the Stapel framework
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-gdpr
Project-URL: Repository, https://github.com/usestapel/stapel-gdpr
Project-URL: Documentation, https://github.com/usestapel/stapel-gdpr#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-gdpr/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-gdpr/issues
Keywords: django,stapel,gdpr,privacy,data-export,right-to-erasure
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
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: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.26.0
Requires-Dist: celery>=5.3
Provides-Extra: s3
Requires-Dist: boto3>=1.34; extra == "s3"
Provides-Extra: all
Requires-Dist: stapel-gdpr[s3]; extra == "all"
Dynamic: license-file

<!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->

# stapel-gdpr

[![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-gdpr/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-gdpr/actions/workflows/ci.yml?query=branch%3Amain)
[![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-gdpr?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-gdpr)
[![pypi](https://img.shields.io/pypi/v/stapel-gdpr?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-gdpr/)
[![downloads](https://static.pepy.tech/badge/stapel-gdpr/month)](https://pepy.tech/project/stapel-gdpr)
[![python](https://img.shields.io/pypi/pyversions/stapel-gdpr?logo=python&logoColor=white)](https://pypi.org/project/stapel-gdpr/)
[![license](https://img.shields.io/github/license/usestapel/stapel-gdpr)](https://github.com/usestapel/stapel-gdpr/blob/main/LICENSE)
[![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-gdpr/blob/main/docs/llms.txt)

> GDPR compliance: staged async data export (Art. 15/20) with status polling and download; subject-scoped erasure (Art. 17) — account, workspace, meeting, recording, document, file — each proven by one receipt per data owner that claims the subject type, with a purge SLA, timeout sweep and a subprocessor ledger behind `fully_erased_by`; account closure keeps its cancellable 30-day grace and produces an ErasureRequest at grace end; DSAR intake (Art. 12) with automated acknowledgement and both statutory clocks; data-owner liveness probing; inactivity closure, retention cleanup, LegalHold and ReRegistrationHash; orchestrated through an in-process GDPRProvider registry (stapel_core.gdpr.gdpr_registry) plus comm confirmation (gdpr.section.erased) for remote owners.

Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.

## Install

```bash
pip install stapel-gdpr
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.8.0` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 15 |
| Config axes | 3 |
| Usage surface | 18 |
| Extension points | 4 |
| Error codes | 60 |
| Fleet dependencies | [`stapel-core`](https://github.com/usestapel/stapel-core) |

## Documentation

[OpenAPI](https://github.com/usestapel/stapel-gdpr/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-gdpr/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-gdpr/blob/main/docs/llms.txt)

## Quick start

```python
# settings.py
INSTALLED_APPS = [
    ...
    'stapel_gdpr',
]

MIDDLEWARE = [
    ...
    # After authentication: refuses every request of an account being erased,
    # whatever a still-valid token claims.
    'stapel_gdpr.guards.AccountClosureGuardMiddleware',
]

STAPEL_GDPR = {
    # Every store holding personal data, mapped to the subjects it holds it
    # about. Erasure is only ever reported complete when each of these
    # returned a deletion receipt, so an owner missing here is a store that
    # quietly keeps the data. `manage.py check` fails while this is empty.
    #
    # These are the names the LIBRARIES declare, not app labels: the `cdn`
    # app owns `media`, the `profiles` app owns `profile`. A name no
    # installed library declares is inferred remote and times out in
    # silence, so `manage.py check` refuses it (gdpr.E009), and an installed
    # owner missing from this map — a store no erasure ever waits for — is
    # gdpr.E010.
    'DATA_OWNERS': {
        'auth': ['account'],
        'profile': ['account'],
        'media': {'subject_types': ['account', 'workspace', 'file'],
                  'kind': 'remote'},
    },
    'DATA_OWNERS_VERSION': '2026-09-07.1',
    # How the user's sessions are revoked at closure. Auto-detected when
    # stapel-auth is installed; without any seam, closure is refused rather
    # than performed with live tokens left behind.
    'SESSION_REVOKER': 'stapel_auth.sessions.services.SessionService.revoke_all',
}
```

Run `manage.py check` after wiring: a missing or stale data-owner inventory,
an owner name no installed library declares, an installed owner the inventory
omits, hash rows written outside `store_hashes`, and every open escape hatch
are reported there rather than discovered in an audit.

## Closing an account, from the client's side

`POST user/account/close` revokes every session of the subject — including the
one that made the call. Its 202 therefore carries `closure_token`, the
capability that survives the revocation:

```jsonc
// 202 Accepted
{
  "status": "grace",
  "grace_ends_at": "2026-10-07T09:12:00Z",
  "can_cancel": true,
  // Issued ONCE. Store it; there is no way to ask for it again.
  "closure_token": "eyJjaWQiOjQyfQ:1uL9Wq:0S3n..."
}
```

Send it back as a header — never in a URL — to poll or to undo:

```http
GET  /gdpr/api/v1/user/account/close/status
POST /gdpr/api/v1/user/account/cancel-close
X-Closure-Token: eyJjaWQiOjQyfQ:1uL9Wq:0S3n...
```

The token is signed with the project `SECRET_KEY`, nothing is stored, it is
scoped to that one closure, and it expires with the grace period
(`error.401.gdpr.closure_token_expired` after that). A live session still works
wherever the host's auth backend authenticates a deactivated user; Django's
default backend does not, which is what the token is for.

## Bus events

### Emits
| `user.deleted` | [schema](schemas/emits/user.deleted.json) | All user PII permanently deleted after grace period. Every package storing user  |
| `user.deletion_cancelled` | [schema](schemas/emits/user.deletion_cancelled.json) | Account closure cancelled during the grace period; every reversible reaction to `user.deletion_initiated` must be lifted. |
| `user.deletion_initiated` | [schema](schemas/emits/user.deletion_initiated.json) | Account closure started. 30-day grace period begins; account is deactivated. |
| `user.export_ready` | [schema](schemas/emits/user.export_ready.json) | Data export archive is ready for download. |
| `user.sessions_revoked` | [schema](schemas/emits/user.sessions_revoked.json) | Closure revoked every session and access JTI of the user. |

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-gdpr/blob/main/LICENSE).

---

<sub>This page is assembled by `stapel-readme` from `docs/readme.md` plus the contract artifacts in `docs/`. Edit the prose in `docs/readme.md`; the badges, facts and links above and below it are generated — do not hand-edit `README.md`.</sub>
