Metadata-Version: 2.4
Name: consentflow
Version: 1.0.0
Summary: Official ConsentFlow SDK — record, read, withdraw and enforce consent, and verify webhooks. Zero required dependencies.
License: MIT
Keywords: consent,dpdp,gdpr,privacy,consentflow,webhooks
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == "fastapi"
Provides-Extra: django
Requires-Dist: django>=4.0; extra == "django"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Dynamic: license-file

# consentflow (Python)

Official Python SDK for [ConsentFlow](../../README.md). Record, read, withdraw and
enforce consent, and verify webhooks. **Zero required dependencies** — the client
uses only the standard library.

```bash
pip install consentflow
# framework helpers pull in their framework:
pip install "consentflow[fastapi]"   # or [django]
```

## Quick start

```python
from consentflow import ConsentFlow

cf = ConsentFlow(api_key="cf_…", host="https://cmp.your-org.io")

# Record (idempotent — a retry never double-writes)
cf.record(identifier={"type": "EXTERNAL_ID", "value": "user_42"},
          contact="person@example.com",
          decisions={"marketing": True, "analytics": False})

# Enforce before processing — fails closed
if cf.check(external_user_id="user_42", purpose_code="marketing"):
    send_newsletter()

cf.withdraw(identifier={"value": "user_42"}, purpose_codes=["marketing"])
cf.renew(identifier={"value": "user_42"}, purpose_code="marketing")
cf.state({"value": "user_42"})
cf.changes(since=0)   # reconciliation feed
```

## `check()` fails closed — by design

`check()` returns a plain `bool` and **never raises**. Any error — network,
timeout, non-2xx, malformed body — is a **DENY**. There is no failure mode that
permits processing. Use `check_detailed()` for the DENY reason; it also fails
closed.

## Webhooks

```python
from consentflow import verify_webhook, WebhookVerificationError

try:
    event = verify_webhook(raw_body, request.headers, "whsec_…")
except WebhookVerificationError:
    return 401   # never process an unverified event
```

Verify over the **raw** body, before parsing. The signature header is a
space-delimited list; any member verifying is accepted, so a secret rotation
needs no code change.

## Framework helpers

```python
# FastAPI
from consentflow.fastapi import RequireConsent
@app.post("/newsletter", dependencies=[Depends(RequireConsent(cf, "marketing", get_user_id))])
async def newsletter(): ...

# Django
from consentflow.django import require_consent
@require_consent(cf, "marketing", lambda req: getattr(req.user, "external_id", None))
def newsletter(request): ...
```

Both block on DENY and on any error (403), fail-closed by construction.

## Development

```bash
python -m pytest
```
