Metadata-Version: 2.4
Name: datagrowth-common
Version: 0.4.1
Summary: Datagrowth common: Supabase Auth (cookie JWT + Admin API), API helpers, structured logging, result pattern, Supabase client, shared models and Jinja2 + Tailwind UI subpackage.
Author-email: Datagrowth <pablo.ramos@datagrowth.es>
License: MIT
Keywords: auth,datagrowth,fastapi,jinja2,structlog,supabase,tailwind
Classifier: Framework :: FastAPI
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.28
Requires-Dist: jinja2>=3.1
Requires-Dist: pydantic[email]>=2.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: structlog>=24.4
Provides-Extra: dev
Requires-Dist: httpx>=0.28; extra == 'dev'
Requires-Dist: pyjwt>=2.9; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: supabase
Requires-Dist: pyjwt>=2.9; extra == 'supabase'
Requires-Dist: supabase>=2.0; extra == 'supabase'
Description-Content-Type: text/markdown

# datagrowth-common

Repo de doble propósito:

1. **Paquete Python** (`src/datagrowth_common/`): auth (Supabase + cookie HMAC), UI (Jinja2 + CSS + JS), logging, helpers de API. Se instala vía pip.
2. **GitHub template** (`template/`): scaffold FastAPI listo para Datagrowth. Se instancia con `gh repo create --template datagrowth/datagrowth-common`.

Utilidades compartidas entre las apps del ecosistema Datagrowth (app-backend, ai-automation, fable, chatbot y las apps de cliente generadas por el pipeline multi-agente).

## Quickstart como template (nuevo repo de app)

```bash
gh repo create my-new-app --template datagrowth/datagrowth-common --private
gh repo clone datagrowth/my-new-app
cd my-new-app
cp infra/.env.example .env  # rellena las claves Supabase
docker compose -f infra/docker-compose.yml up -d --wait
curl http://localhost:8000/healthz
```

Tras esto, abre Cursor / Claude Code en el repo y pega el prompt de onboarding del `README.md` del propio repo recién creado.

## Quickstart como dependencia pip

```bash
pip install "datagrowth-common[supabase]~=0.3.1"
```

A partir de `v0.3.0`, el **único modelo de autenticación** es **Supabase Auth** (cookie httpOnly con JWT). Los helpers contra Authentik (forward-auth y OIDC propio) se eliminaron — ver `CHANGELOG.md` para la guía de migración. La cookie HMAC propia se conserva en `auth.session_hmac` como fallback para apps internas que no usan Supabase Auth.

## Qué incluye

### Supabase Auth (v0.3.0+)

| Módulo | Contenido |
|---|---|
| `auth.supabase` | `verify_supabase_jwt(token)` — valida HS256 + `exp` + `aud`. `SupabaseUser` (modelo Pydantic con `id`, `email`, `role`). `require_supabase_jwt` (FastAPI dep que lee `Authorization: Bearer`). `SupabaseUserDep` (alias tipado). `require_role(role)` (factory de dep con check de `app_metadata.role`). |
| `auth.session` | Cookie httpOnly server-rendered: `set_session(response, access_token, refresh_token)`, `clear_session`, `get_session_tokens`, `require_session` (lee cookie + valida JWT), `SessionUserDep`, `refresh_session_tokens` (canjea refresh → nuevos tokens). Pensado para apps Jinja+HTMX donde el cliente NO maneja JWTs en JS. |
| `auth.router` | `make_auth_router(post_login_redirect, post_logout_redirect)` y `auth_router` por defecto. Endpoints reutilizables: `POST /auth/password`, `POST /auth/register`, `POST /auth/magic-link/request`, `POST /auth/password/reset`, `GET /auth/oauth/{provider}/start`, `GET /auth/oauth/{provider}/callback`, `POST /auth/refresh`, `POST /logout`, `GET /api/me`. La página `/login` HTML la sirve cada app con su template propio (branding). |
| `auth.supabase_admin` | Wrappers async sobre Supabase Admin API con patrón `Result`: `list_users`, `invite_user_by_email`, `delete_user`, `update_user_role`, `create_user_with_password`, `send_password_reset`. |
| `auth.session_hmac` | Cookie de sesión firmada con HMAC propio (`DG_SESSION_SECRET`). Fallback para apps internas que no usan Supabase Auth. Antes de v0.3.0 se llamaba `auth.local`. |
| `api.users` | `users_router` — APIRouter con `GET /api/users` (paginado, admin), `POST /api/users/invite` (admin), `DELETE /api/users/{id}` (admin), `GET /api/users/me`. |
| `supabase` | `get_supabase_client(schema)` (anon key) y `get_supabase_admin_client(schema)` (service_role key). |

### Genérico

| Módulo | Contenido |
|---|---|
| `result` | `Result[T]`, `ok()`, `err()` — patrón de error como valor |
| `logger` | `setup_logging()`, `get_logger()` — structlog preconfigurado (JSON en prod, consola en dev) |
| `api.errors` | `ApiResponse`, `ErrorBody`, `ErrorCode`, `ok_response`, `err_response`, `HTTP_STATUS_FOR_CODE` |
| `api.pagination` | `Paginated[T]`, `PageMeta`, `paginate_params`, `build_meta` |
| `shared_models` | `ClientRead`, `ProjectRead`, `ContactRead` — modelos Pydantic de lectura |
| `updater.checker` | `check_for_updates()`, `run_periodic_check()` — verificador de versiones async |
| `testing.fixtures` | `signed_session_cookie`, `mock_supabase_client` y otros fixtures pytest |

### Eliminado en v0.3.0 (breaking)

| Módulo | Reemplazo |
|---|---|
| `authentik` | `auth.supabase` (validación JWT) |
| `auth_fastapi` | `auth.session.require_session` (cookie JWT) o `auth.supabase.require_supabase_jwt` (Bearer) |
| `auth.local` | `auth.session_hmac` (rename). Las funciones OIDC se eliminan; usa `auth.router.make_auth_router()` para login + OAuth + magic link + reset via Supabase |

`auth.local` se conserva como alias deprecated (DeprecationWarning) hasta v0.4.0. Ver `CHANGELOG.md` para la guía de migración completa.

## Instalación

Publicado en PyPI: <https://pypi.org/project/datagrowth-common/>.

```bash
pip install "datagrowth-common[supabase]>=0.3.2,<0.4"
```

El extra `supabase` añade `supabase>=2.0` y `pyjwt>=2.9` (necesarios para `verify_supabase_jwt` y `users_router`).

### En `pyproject.toml` de otra app

```toml
[project]
dependencies = [
    "datagrowth-common[supabase]>=0.3.2,<0.4",
]
```

### Editable (desarrollo local del propio paquete)

```bash
pip install -e /ruta/a/datagrowth-common
```

## Uso rápido — Supabase Auth en una app FastAPI

### Modo API (cliente JS o app móvil envía `Authorization: Bearer`)

```python
from fastapi import FastAPI

from datagrowth_common import (
    setup_logging, get_logger,
    SupabaseUserDep, require_role,
)
from datagrowth_common.api.users import users_router

setup_logging()
log = get_logger(__name__)

app = FastAPI()

# Endpoints CRUD de usuarios sobre Supabase Admin API:
#   GET    /api/users         (admin, paginado)
#   POST   /api/users/invite  (admin)
#   DELETE /api/users/{id}    (admin)
#   GET    /api/users/me      (cualquier user autenticado)
app.include_router(users_router)


@app.get("/leads")
async def list_leads(user: SupabaseUserDep) -> dict:
    log.info("leads", user=user.id, role=user.role)
    return {"data": [...]}


@app.delete("/leads/{lead_id}")
async def delete_lead(lead_id: str, _: object = require_role("admin")):
    return {"deleted": lead_id}
```

El cliente envía `Authorization: Bearer <jwt>` donde el JWT lo emitió la propia Supabase de la app (login local, magic link u OIDC). `verify_supabase_jwt` valida firma HS256 contra `SUPABASE_JWT_SECRET`, comprueba `exp` y `aud`, y extrae `sub`, `email` y `app_metadata.role`.

### Modo server-rendered (Jinja + HTMX + cookie httpOnly)

Para apps donde el navegador navega entre páginas HTML (no SPA):

```python
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

from datagrowth_common import (
    SessionUserDep, make_auth_router, setup_logging, get_logger,
)

setup_logging()
log = get_logger(__name__)
app = FastAPI()

# Router con login propio + OAuth + refresh + logout. La página HTML de
# /login la sirves tú con tu propio branding; el router solo expone los
# endpoints API.
app.include_router(make_auth_router(
    post_login_redirect="/",
    post_logout_redirect="/login",
))


@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: str | None = None):
    return templates.TemplateResponse(request, "login.html", {"error": error})


@app.get("/")
async def home(user: SessionUserDep) -> dict:
    return {"email": user.email}
```

`set_session` guarda dos cookies (`dg_session` con el access_token JWT, `dg_session_refresh` con el refresh_token) httpOnly+Secure+SameSite=Lax. `require_session` valida la cookie en cada request. Si caduca, llamas a `POST /auth/refresh` para renovarla.

## Variables de entorno

### Supabase Auth (v0.2.1+)

| Variable | Descripción | Por defecto |
|---|---|---|
| `SUPABASE_URL` | URL del proyecto Supabase (cloud o self-hosted) | — |
| `SUPABASE_KEY` | Anon/publishable key (lectura+RLS) | — |
| `SUPABASE_SERVICE_ROLE_KEY` | Service role key (Admin API). **NO exponer al frontend** | — |
| `SUPABASE_JWT_SECRET` | Secreto HS256 (Project Settings → API). Usado por `verify_supabase_jwt` | — |
| `SUPABASE_JWT_AUDIENCE` | Audience esperada en el JWT | `authenticated` |
| `DG_APP_URL` | URL pública de la app (sin slash final). Usada por `make_auth_router` para construir el `redirect_to` del callback OAuth | derivada de `request.base_url` si vacío |
| `AUTH_REGISTRATION` | `open` permite que cualquiera se registre via `POST /auth/register`; cualquier otro valor lo bloquea (403) | `invite_only` |
| `AUTH_MAGIC_LINK` | `true` habilita `POST /auth/magic-link/request` (OTP via email) | `false` |
| `DG_ENV` | `development` permite cookies sin `Secure` para tests locales sin TLS | `production` |

### Genéricas

| Variable | Descripción | Por defecto |
|---|---|---|
| `DG_ENV` | `development` permite cookies sin `Secure` para tests locales sin TLS | `production` |
| `LOG_LEVEL` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | `INFO` |
| `DG_APP_SLUG` | Slug de la app para el checker de actualizaciones | — |
| `DG_TENANT_SLUG` | Slug del tenant para el checker de actualizaciones | — |
| `DG_APP_VERSION` | Versión actual de la app | `0.0.0` |
| `DG_RELEASES_URL` | URL base del backend de releases | `https://backend.dev.datagrowth.es` |

### Cookie HMAC propia (`auth.session_hmac`, opcional)

| Variable | Descripción | Por defecto |
|---|---|---|
| `DG_SESSION_SECRET` | Secreto HMAC para firmar la cookie (≥32 bytes) | — |
| `DG_SESSION_COOKIE` | Nombre de la cookie | `dg_session` |
| `DG_SESSION_TTL_SEC` | TTL de la sesión en segundos | `86400` |

## Patrón Result

Evita excepciones flotantes retornando el error como valor:

```python
from datagrowth_common import ok, err, Result

async def fetch_data(id: str) -> Result[dict]:
    try:
        data = await some_call(id)
        return ok(data)
    except Exception as e:
        return err(e)

data, error = await fetch_data("123")
if error:
    log.error("fallo fetch", error=str(error))
else:
    print(data)
```

Todas las funciones `auth.supabase_admin.*` y `verify_supabase_jwt` siguen este patrón: nunca lanzan a través de la frontera de módulo.

## Respuesta API estándar

```python
from datagrowth_common.api.errors import ApiResponse, ErrorCode, ok_response, err_response

@app.post("/items")
async def create(body: ItemCreate) -> ApiResponse:
    item, exc = await service.create(body)
    if exc:
        return err_response(ErrorCode.INTERNAL_ERROR, "create-failed")
    return ok_response(item)
```

`ApiResponse` enforza XOR entre `data` y `error` (no se puede tener ambos). El cliente decide éxito leyendo `body.error == null`.

## Checker de actualizaciones

```python
from datagrowth_common.updater.checker import run_periodic_check

def notify(update: dict) -> None:
    print(f"Nueva version disponible: {update['latest']}")

# Llamar en el startup de la app o en un background task
await run_periodic_check(notify, app_slug="fable", tenant_slug="acme")
```

## Tests

```bash
pip install -e ".[dev,supabase]"
pytest -q
```

Cobertura actual (v0.3.0): tests sobre cookie HMAC (`auth.session_hmac`), validación JWT Supabase, router CRUD de usuarios, cookie httpOnly de sesión Supabase, `auth_router` (login + OAuth + refresh + logout) y deprecation warning de `auth.local`.

## Versionado y releases

Sigue [SemVer](https://semver.org/). Cambios mayores que rompen API requieren bump major. La release se publica via GitHub Action al hacer `git tag vX.Y.Z && git push --tags` (ver `RELEASING.md`).

| Versión | Cambios principales |
|---|---|
| `v0.3.0` | **BREAKING**. Elimina `authentik`, `auth_fastapi` y toda la integración con Authentik OIDC dentro de `auth.local`. Renombra `auth.local` → `auth.session_hmac` (alias deprecated). Modelo único: Supabase Auth. Ver `CHANGELOG.md` para la guía de migración. |
| `v0.2.2` | Añade `auth.supabase_admin.create_user_with_password` y `send_password_reset` para flujos de gestión end-to-end. |
| `v0.2.1` | Añade `auth.session` (cookie httpOnly server-rendered con refresh) y `auth.router` (`auth_router` + `make_auth_router` con login propio + OAuth Microsoft/Google via Supabase + refresh + logout). |
| `v0.2.0` | Añade `auth.supabase` (verify JWT), `auth.supabase_admin` (Admin API), `api.users.users_router`, `get_supabase_admin_client`. `auth.local` queda como legacy. |
| `v0.1.0` | Primera release pública: `auth.local`, `authentik`, `auth_fastapi`, `result`, `logger`, `supabase`, `shared_models`, `updater`. |
