Datagrowth common: Supabase Auth (cookie JWT + Admin API), API helpers, structured logging, result pattern, Supabase client, shared models and Jinja2 + Tailwind UI subpackage.
Project description
datagrowth-common
Repo de doble propósito:
- Paquete Python (
src/datagrowth_common/): auth (Supabase + cookie HMAC), UI (Jinja2 + CSS + JS), logging, helpers de API. Se instala vía pip. - GitHub template (
template/): scaffold FastAPI listo para Datagrowth. Se instancia congh 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)
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
pip install "datagrowth-common[supabase,postgres]>=0.7,<0.8"
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.
Desde v0.7, las apps generadas aplican sus migrations/*.sql automáticamente en el lifespan via apply_migrations(...). Desde v0.5.0, la gestión de usuarios + RBAC + audit log se monta con make_users_router(catalog, deps, ...) (módulo datagrowth_common.users). Desde v0.3.1, la UI compartida (Jinja2 + Tailwind + JS) se inyecta con register_ui(app, jinja_env) (subpaquete datagrowth_common.ui).
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). |
UI compartida (v0.3.1+)
| Módulo | Contenido |
|---|---|
ui |
register_ui(app, jinja_env, mount_path="/static/dg-common") añade un PackageLoader al ChoiceLoader y monta los assets (tokens.css, tailwind.compiled.css, theme.js, toasts.js, htmx-events.js). UI_VERSION alineado con __version__. |
ui/templates/dg/ |
20 partials Jinja2 con prefijo obligatorio dg/...: layouts (sidebar_shell, empty), componentes (badge, button, card, command_palette, data_table, empty_state, form_field, icon, modal, page_header, paginated_table, sidebar_link, skeleton, theme_toggle, toast), admin (users_panel, roles_panel, audit_panel). |
ui/tailwind-preset.cjs |
Preset Tailwind empaquetado en el wheel: paleta brand, semantic, surfaces, fonts, radii, shadows + safelist con responsive variants (md:flex, lg:grid-cols-3, …). Importable via importlib.resources para builds de frontend dockerizados. |
Gestión de usuarios + RBAC (v0.5.0+)
| Módulo | Contenido |
|---|---|
users |
make_users_router(catalog, deps, mount_prefix, schema, templates) — APIRouter con 13 endpoints: HTML del panel, listado paginado, invite, create con password, set password, update roles, password reset, delete, permisos efectivos, CRUD de roles, audit log. |
users.permissions |
PermissionCatalog({"runs.read": "Ver runs", ...}) declarado en código (la BD solo guarda asignaciones, no el catálogo). make_require_permission(perm) — FastAPI dep con cache por request, deniega por defecto. |
users.bootstrap |
bootstrap_roles(db, system_roles, catalog) idempotente: upserts roles del sistema + poda role_permission huérfanos. Refusa borrar si el catalog está vacío (defensa contra bug de import). |
users/migrations/001_users_rbac.sql |
Crea role, user_role, role_permission, user_audit_log con FKs ON DELETE CASCADE e índices. |
Auto-migration (v0.7+)
| Módulo | Contenido |
|---|---|
migrations |
`apply_migrations(migrations_dir, *, db_url=None) -> tuple[list[str], Exception |
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/.
pip install "datagrowth-common[supabase,postgres]>=0.7,<0.8"
Extras disponibles:
[supabase]—supabase>=2.0+pyjwt>=2.9(necesario paraverify_supabase_jwt,users_routery el módulousers).[postgres]—psycopg[binary]>=3.2(necesario paraapply_migrationsy para el módulousers, que habla Postgres directo via SQLAlchemy).[dev]— toolchain de tests (pytest,pytest-asyncio,pytest-cov,httpx,pyjwt,psycopg).
En pyproject.toml de otra app
[project]
dependencies = [
"datagrowth-common[supabase,postgres]>=0.7,<0.8",
]
Editable (desarrollo local del propio paquete)
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)
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):
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.
Uso rápido — UI compartida + RBAC + auto-migration
Una app generada por el template monta los tres módulos en src/main.py:
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from datagrowth_common import (
apply_migrations, get_logger, make_auth_router, register_ui, setup_logging,
)
from datagrowth_common.users import (
PermissionCatalog, make_users_router, bootstrap_roles,
)
from src.db import get_session
setup_logging()
log = get_logger(__name__)
_MIGRATIONS_DIR = Path(__file__).resolve().parent.parent / "migrations"
CATALOG = PermissionCatalog({
"users.manage": "Gestionar usuarios",
"runs.read": "Ver runs",
})
SYSTEM_ROLES = {
"admin": list(CATALOG.keys()), # todo
"viewer": [],
}
@asynccontextmanager
async def lifespan(app: FastAPI):
applied, exc = await apply_migrations(_MIGRATIONS_DIR)
if exc is not None:
log.error("migrations_failed", error=str(exc)[:500])
raise exc
if applied:
log.info("migrations_applied", count=len(applied), files=applied)
with next(get_session()) as db:
bootstrap_roles(db, SYSTEM_ROLES, CATALOG)
db.commit()
yield
app = FastAPI(lifespan=lifespan)
templates = Jinja2Templates(directory="src/templates")
register_ui(app, jinja_env=templates.env) # monta /static/dg-common/* y dg/... templates
app.include_router(make_auth_router(post_login_redirect="/", post_logout_redirect="/login"))
app.include_router(make_users_router(
catalog=CATALOG,
deps={"db": get_session},
mount_prefix="/admin/users",
schema="public",
templates=templates,
))
El template oficial (template/) ya viene con esto cableado — incluido el bootstrap de admin desde APP_ADMIN_EMAIL. Ver template/src/main.py y template/src/api/admin_users.py como referencia.
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 |
Postgres (apply_migrations, módulo users, v0.6.0+)
| Variable | Descripción | Por defecto |
|---|---|---|
DG_DATABASE_URL |
URL completa Postgres (postgresql+psycopg://user:pass@host:port/db). Si está presente, las piezas individuales se ignoran. |
— |
POSTGRES_PASSWORD |
Password del usuario Postgres (alternativa a DG_DATABASE_URL) |
— |
DG_SUPABASE_DB_HOST |
Host de Postgres | db |
DG_SUPABASE_DB_PORT |
Puerto de Postgres | 5432 |
DG_SUPABASE_DB_USER |
Usuario Postgres | postgres |
DG_SUPABASE_DB_NAME |
Nombre de la BD | postgres |
APP_ADMIN_EMAIL |
Email del admin inicial. El bootstrap resuelve el user_id en Supabase Auth y le asigna el rol admin (aditivo, no revoca otros). |
— |
Patrón Result
Evita excepciones flotantes retornando el error como valor:
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
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
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
pip install -e ".[dev,supabase]"
pytest -q
Cobertura actual (v0.7.x): cookie HMAC (auth.session_hmac), validación JWT Supabase, router CRUD de usuarios (legacy), cookie httpOnly de sesión Supabase, auth_router (login + OAuth + refresh + logout), deprecation warning de auth.local, smoke tests del subpaquete ui (20 templates + assets), módulo users (catálogo, RBAC, make_users_router, audit log, validaciones SQL), apply_migrations (idempotencia + tracking en dg_internal._dg_applied_migrations).
Versionado y releases
Sigue SemVer. 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.7.5 |
Nueva skill template/.claude/skills/modular-design: contrato de modularidad para todos los elementos de diseño en apps generadas. Cualquier asset visual (colores, logo, tipos, copy de marca) debe ser sustituible sin tocar código vía tokens-override.css del cliente o env vars BRAND_*. Defaults Datagrowth en template/infra/.env.example (BRAND_CLIENT_NAME=Datagrowth, logo apuntando a datagrowth.es) — el control plane sobrescribe con valores del cliente al deployar. Color primario migrado de env var a CSS token (var(--color-brand-primary)). |
v0.7.4 |
apply_migrations blindado contra race conditions cross-process: adquiere pg_advisory_lock antes de leer la tabla de control. Sin esto, dos workers uvicorn entrando al lifespan en paralelo se cargaban en CREATE TYPE con duplicate key ... pg_type_typname_nsp_index. Template infra/Dockerfile baja a --workers 1 como defensa en profundidad. template/src/api/deps.py:require_auth redirige a /login (303) cuando el navegador entra sin sesión, en vez de devolver JSON 401. |
v0.7.2 |
Nuevo datagrowth_common.migrations.apply_migrations(). El template invoca la función en el lifespan: las apps generadas aplican sus migrations/*.sql solas (sin que el control plane tenga que pegar Postgres del cliente). Nueva optional dep [postgres]. Template ajustado: Dockerfile copia migrations/, docker-compose.yml declara env_file: ../.env para cargar el .env que Easypanel materializa en la raíz del repo clonado. |
v0.6.x |
Template adopta el panel RBAC nuevo (make_users_router). Endpoint POST /admin/users/{id}/password/set. Compose del template: build local + red n8n_supabase_default external + sin labels Traefik. Botones admin con bypass para is_admin + fall-through en DG_ENV != production. |
v0.5.0 |
Nuevo módulo datagrowth_common.users: make_users_router, PermissionCatalog, make_require_permission, bootstrap_roles, audit log, migración 001_users_rbac.sql. UI panel reescrita (3 tabs, multi-rol, vanilla JS sin HTMX). Dep nueva sqlalchemy>=2.0. |
v0.4.x |
Tailwind preset distribuido (tailwind-preset.cjs) empaquetado dentro del wheel. Tokens semánticos text-fg/bg-surface-subtle + dark mode con FOUC guard + sync entre pestañas. Token link/link-hover para anchors legibles en dark. |
v0.3.x |
BREAKING (v0.3.0): elimina authentik, auth_fastapi, integración Authentik OIDC. Renombra auth.local → auth.session_hmac. Modelo único: Supabase Auth. v0.3.1: subpaquete datagrowth_common.ui (register_ui + 20 templates + tokens). v0.3.2+: repo dual (PyPI + GitHub template). |
v0.2.x |
Añade auth.supabase (verify JWT), auth.supabase_admin (Admin API), auth.session (cookie httpOnly), auth.router (make_auth_router con OAuth Microsoft/Google + refresh). |
v0.1.0 |
Primera release pública: auth.local, authentik, auth_fastapi, result, logger, supabase, shared_models, updater. |
Project details
Release history Release notifications | RSS feed
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 datagrowth_common-0.7.5.tar.gz.
File metadata
- Download URL: datagrowth_common-0.7.5.tar.gz
- Upload date:
- Size: 82.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac23640f81237c6f935727848fe3e4d03d0da2d7a50274310e057dfade6f5736
|
|
| MD5 |
84930215b40c509b88bb980178569625
|
|
| BLAKE2b-256 |
5bc0b87c897b9a017fe31f7a78657cd898d6ddb993bbec9a539983948208f56c
|
Provenance
The following attestation bundles were made for datagrowth_common-0.7.5.tar.gz:
Publisher:
release.yml on datagrowth/datagrowth-common
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
datagrowth_common-0.7.5.tar.gz -
Subject digest:
ac23640f81237c6f935727848fe3e4d03d0da2d7a50274310e057dfade6f5736 - Sigstore transparency entry: 1554019241
- Sigstore integration time:
-
Permalink:
datagrowth/datagrowth-common@85a123acbe053d1d39a720ed686f45e775ed489d -
Branch / Tag:
refs/tags/v0.7.5 - Owner: https://github.com/datagrowth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@85a123acbe053d1d39a720ed686f45e775ed489d -
Trigger Event:
push
-
Statement type:
File details
Details for the file datagrowth_common-0.7.5-py3-none-any.whl.
File metadata
- Download URL: datagrowth_common-0.7.5-py3-none-any.whl
- Upload date:
- Size: 89.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36099342aee18b1950c7f89065400de3cf8c12f9b7bc57ac2a3332d4a0e35023
|
|
| MD5 |
be3fe6e182b507366a8b81e544c076dc
|
|
| BLAKE2b-256 |
1145ebb6f8ef4d984a08c295cf46e4d7717e3b408304e21d8ed706c9f047b06f
|
Provenance
The following attestation bundles were made for datagrowth_common-0.7.5-py3-none-any.whl:
Publisher:
release.yml on datagrowth/datagrowth-common
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
datagrowth_common-0.7.5-py3-none-any.whl -
Subject digest:
36099342aee18b1950c7f89065400de3cf8c12f9b7bc57ac2a3332d4a0e35023 - Sigstore transparency entry: 1554019281
- Sigstore integration time:
-
Permalink:
datagrowth/datagrowth-common@85a123acbe053d1d39a720ed686f45e775ed489d -
Branch / Tag:
refs/tags/v0.7.5 - Owner: https://github.com/datagrowth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@85a123acbe053d1d39a720ed686f45e775ed489d -
Trigger Event:
push
-
Statement type: