Datagrowth common: Supabase Auth (cookie JWT + Admin API), API helpers, structured logging, result pattern, Supabase client, shared models, Jinja2 + Tailwind UI subpackage, prod-readiness QA auditor, observability (Sentry + Prometheus), security middlewares (CSRF, headers, rate-limit) and GDPR self-service router.
Project description
datagrowth-common
Paquete Python (src/datagrowth_common/): auth (Supabase + cookie HMAC),
UI (Jinja2 + CSS + JS), logging, helpers de API y production-readiness.
Se instala vía pip.
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).
El scaffold FastAPI para apps nuevas vive en el repo
datagrowth/app-template(GitHub template con el skeleton en la raíz):gh repo create my-new-app --template datagrowth/app-template --private. Supyproject.tomlpinnea la versión de este paquete.
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,observability,middleware]>=0.10,<1.0"
Las extras [observability] (sentry-sdk + prometheus-fastapi-instrumentator) y [middleware] (slowapi) son opcionales — la app puede importar los helpers sin instalarlas y los inits devuelven False / None (no-op). Útil en dev local cuando no quieres pagar el peso de las deps.
Desde v0.8.1/v0.8.2, el wiring CSRF se entrega como bundle JS del paquete (/static/dg-common/js/csrf.js). En v0.8.1 se cableó en sidebar_shell.html (cubre logout del topbar y forms de cualquier página autenticada); en v0.8.2 se añadió también a empty.html (cubre login/signup/reset). Desde v0.8.8, el mismo csrf.js monkey-patcha window.fetch para inyectar el header X-CSRF-Token en cualquier llamada same-origin con método no-safe (POST/PUT/PATCH/DELETE); ambos shells lo cargan sin defer para que el patch esté activo antes que cualquier <script> inline posterior pueda lanzar un request. Sin esto, todo fetch('/foo', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: ...}) muere con 403 CSRF_TOKEN_MISMATCH. Antes cada host app tenía que copiar el script en su extra_head, y si lo hacía mal (p.ej. document.body.addEventListener en <head> con el body aún no parseado) el form enviaba POST sin token y el middleware respondía 403. Ver CHANGELOG.md [0.8.1]/[0.8.2]/[0.8.8] para detalles.
Desde v0.8.0, el paquete propaga al template las piezas de production-readiness que hasta ahora vivían solo en app-backend: observability.init_sentry (init Sentry con scrubbing de PII), observability.default_instrumentator (Prometheus FastAPI), middlewares RequestIdMiddleware/SecurityHeadersMiddleware/CSRFMiddleware y helper build_limiter (slowapi + Redis), router GDPR self-service make_me_router (export ZIP + delete diferido + cancel) y helper audit.emit para user_audit_log con ip/user_agent. El template trae además health.py (/live, /ready, /healthz), workflow ci.yml con prod-ready-audit, backup.sh placeholder, migración v3_access_event.sql y plantillas docs/PRIVACY.md + docs/RUNBOOK.md. Cada pieza es opt-in: las apps que no las necesiten pueden no instalar las extras [observability]/[middleware] y los inits responden no-op sin romper imports.
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.15, el item activo del sidebar (.sidebar-link-active) es lima sólido + texto navy en lugar del translúcido al 22% que tenía hasta 0.7.14, alineado con el look del backend orquestador. Resuelve via var(--color-brand-primary-rgb)/var(--color-brand-secondary-rgb), así cada cliente lo personaliza vía sus triplets del tokens-override.css. Desde v0.7.14, las apps generadas sirven correctamente sus assets locales (src/static/brand/tokens-override.css, src/static/css/app.css): el main.py del template ahora monta /static/ apuntando al directorio local del repo además del /static/dg-common/ del paquete que ya monta register_ui. Hasta este fix las apps respondían 404 a tokens-override.css aunque el deployer Datagrowth lo hubiera escrito en su filesystem — el bug fue invisible mientras las utility classes Tailwind bakeaban hex literales (pre-0.7.13), pero se hizo crítico con el refactor var(--color-*-rgb) de 0.7.13. Apps existentes heredan el mount via patcher _patch_main_for_local_static_mount del backend Datagrowth al pulsar "Actualizar common". Desde v0.7.13, el paquete deja de imponer la identidad Datagrowth a sus consumidores: tokens.css declara los brand colors en dos formas paralelas — triplet RGB sin envoltura (--color-brand-primary-rgb: 71 85 105) consumido por las utility classes Tailwind con <alpha-value>, y forma funcional (--color-brand-primary) para CSS directo — con defaults gray-based neutrales. El tailwind-preset.cjs pasa de hex literales (brand.lime: '#b5ff82') a rgb(var(--color-brand-primary-rgb) / <alpha-value>) para que bg-brand-lime/15 y similares respeten el tokens-override.css del cliente. sidebar_shell.html añade dos fallbacks request.state.* (dg_user para el topbar email/admin/salir, dg_tokens_override_href para el <link> al CSS por cliente) que permiten a la host app inyectarlos via dep global. Nuevo manifest setup.json + SETUP.md en el skeleton — el dev del paquete declara campos extra a rellenar al instalar la app (prompts runtime, tokens externos, flags) que el orquestador pinta y persiste. Desde v0.7.12, las apps generadas pueden abrirse a desarrolladores externos sin que rompan el rebranding ni el flow de "Actualizar common": nueva skill dg-customize + regla .claude/rules/managed-files.md que codifican el contrato customizable vs gestionado, setup.sh de bootstrap (symlinks .claude/skills + .cursor/skills → .agents/skills) y README rewrite con sección de dev-onboarding. La clase .sidebar-section-label aterriza por fin en el compiled CSS (fix: tailwind.config.js ahora escanea también template/src/templates/**/*.html). Desde v0.7.11, el sidebar admin es consistente en todas las pantallas: la dep populate_template_state del template inyecta request.state.dg_user antes de cada handler HTML, y el partial sidebar_nav.html la lee como fallback cuando el handler concreto no pasa user al context Jinja (caso típico: users_router del paquete). 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 (SESSION_SECRET). Fallback para apps internas que no usan Supabase Auth. |
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/core") 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/core/ |
18 partials Jinja2 con prefijo obligatorio core/...: layouts (sidebar_shell, empty), componentes (badge, button, card, data_table, empty_state, form_field, icon, 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 |
Production-readiness helpers (v0.8.0+)
| Módulo | Contenido |
|---|---|
observability.init_sentry |
Inicializa Sentry si SENTRY_DSN está seteada. component ("api", "worker", ...) como tag. before_send redacta emails crudos, cabeceras sensibles, cookies y keys cuyo nombre contiene password/token/secret. Acepta extra_before_send para scrubbing custom. No-op si sentry-sdk no está instalado. |
observability.default_instrumentator |
Registra prometheus_fastapi_instrumentator.Instrumentator y expone /metrics. Excluye /live, /ready, /health*, /metrics, /static.* por defecto. No-op si la lib no está instalada. |
middleware.RequestIdMiddleware |
Lee/genera X-Request-Id, lo bindea a structlog + Sentry. Devuelve el id en la response. |
middleware.SecurityHeadersMiddleware |
HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP configurable. Flag SECURITY_HEADERS_ENABLED. |
middleware.CSRFMiddleware |
Double-submit cookie. Valida extra_exempt_prefixes (rechaza ""///no-absolutos). /auth/password NO exento — adjunta X-CSRF-Token (HTMX) o csrf_token (form) en el POST. Desde v0.8.6 expone el token como request.state.dg_csrf_token para que los templates Jinja lo rendericen server-side via macro dg/components/csrf.html::csrf_input(request) — sin dependencia de JS. Flag CSRF_ENABLED. |
middleware.MetricsAuthMiddleware |
Basic-auth para /metrics con secrets.compare_digest (sin timing attacks). Default OFF: si METRICS_USER/METRICS_PASS vacíos, responde 404 (no enumera). |
middleware.build_limiter |
Crea limiter de slowapi + handler 429 JSON. Storage Redis si REDIS_URL set, fallback memory. Flag RATE_LIMIT_ENABLED. |
me.make_me_router |
Router GDPR self-service: GET /me/data/export (ZIP), DELETE /me/data (delete diferido), POST /me/data/delete/cancel. Acepta extra_exporters para inyectar blobs propios. trust_proxy=False por default (ignora X-Forwarded-For para evitar IPs falseadas en audit log); pasa True solo detrás de proxy de confianza. |
legal.make_legal_router |
Endpoints públicos /privacy, /terms, /security desde markdown. Parser mínimo (h1-h6, listas, tablas GFM, párrafos, **bold**, `code`, [link](url)) con html.escape(quote=True) antes del inline + whitelist de esquemas (https/http/mailto/tel///#) + rel="noopener noreferrer" en links — inmune a XSS via attribute injection o javascript:. Acepta md_to_html_fn= propio para parser completo. |
retention.make_retention_purger |
Factory que devuelve iteration() -> tuple[stats, Exception|None] para schedulear con APScheduler/arq/cron. Purga user_deletion_request vencidas + emite audit data-purged, retención user_audit_log (365d default), retención opcional de tabla de logs operativos. Valida identificadores SQL con whitelist regex en construct-time (fail-fast). |
audit.emit |
Inserta fila en user_audit_log con ip + user_agent. Patrón de error como valor — un fallo de logging nunca rompe el handler. Scrubea emails del user_agent antes de persistir; valida schema contra whitelist regex. |
env.dg_env / is_dev_env / is_prod_env |
Helper canónico para ENV. Normaliza a prod/dev (default prod, valores desconocidos caen a prod por fail-safe). Acepta legacy production/development por compat con apps ya desplegadas. |
errors.register_error_handlers |
Registra handlers para HTTPException, RequestValidationError y Exception. Para navegaciones HTML renderiza dg/pages/error.html con el shell completo (autenticado) o empty.html (anónimo) + banner inferior-derecho con el mensaje de error. Para HTMX devuelve un fragmento OOB que inyecta el toast en #dg-toasts (el wiring htmx:beforeSwap en htmx-events.js fuerza el swap aunque el status sea 4xx/5xx). Para Accept: application/json o paths /api/*//auth/* mantiene el JSON {"detail": ...}. Mensajes por código (401/403/404/422/429/5xx); >=500 oculta detalles internos al usuario. Llamar después de register_ui(...) porque depende de los templates dg/.... Desde v0.8.6. |
Uso típico (en src/main.py de la app):
from datagrowth_common.legal import make_legal_router
from datagrowth_common.middleware import (
CSRFMiddleware, MetricsAuthMiddleware, RequestIdMiddleware,
SecurityHeadersMiddleware, build_limiter,
is_csrf_enabled, is_metrics_enabled,
is_rate_limit_enabled, is_security_headers_enabled,
)
from datagrowth_common.observability import default_instrumentator, init_sentry
init_sentry(component="api")
app = FastAPI(...)
limiter, rl_handler = build_limiter()
if limiter is not None and is_rate_limit_enabled():
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, rl_handler)
app.add_middleware(SlowAPIMiddleware)
if is_csrf_enabled():
app.add_middleware(CSRFMiddleware)
if is_security_headers_enabled():
app.add_middleware(SecurityHeadersMiddleware)
if is_metrics_enabled():
app.add_middleware(MetricsAuthMiddleware)
app.add_middleware(RequestIdMiddleware)
default_instrumentator(app)
# Páginas legales públicas (GET /privacy, /terms, /security).
app.include_router(make_legal_router(
docs_dir=Path("docs/legal"), templates=templates,
))
Y para schedular el cron de retención GDPR + audit log:
from datagrowth_common.retention import make_retention_purger
purger = make_retention_purger(
session_factory=lambda: Session(engine),
extra_grant_tables=(("user_role", "user_id"),),
)
# Schedular `purger()` cada RETENTION_PURGE_INTERVAL_S segundos
# (APScheduler, arq, cron del SO).
Cuenta self-service + email transaccional (v0.11.0+)
| Módulo | Contenido |
|---|---|
account.make_account_router |
Página /account: perfil (display name), cambio de contraseña re-verificando la actual contra Supabase Auth (sign_in_with_password antes de set_user_password), y acciones GDPR pintadas sobre los endpoints de me. El host inyecta auth_dep + templates + un wrapper account.html que incluye el fragmento core/account/account_panel.html. |
email.send_email |
Email transaccional propio de la app vía SMTP (SMTP_HOST/PORT/USER/PASSWORD/FROM), STARTTLS o SSL implícito (puerto 465), error-como-valor y logging sin PII (solo dominio del destinatario). is_smtp_configured() para degradar (p. ej. /support → mailto). Los emails de AUTH (reset, magic link, invite) NO pasan por aquí: los envía Supabase. |
UX del shell (v0.11.0+): command palette Ctrl+K (core/components/command_palette.html, indexa el sidebar sin endpoints), dgConfirm() + interceptor de hx-confirm (diálogo de confirmación del design system en vez del confirm() nativo), macro modal() sobre <dialog>, validación progresiva de forms (form-validate.js, Constraint Validation API, opt-out data-dg-novalidate), preload de navegación en hover (extensión preload de htmx), View Transitions en swaps, focus ring focus-visible en componentes interactivos y soporte prefers-reduced-motion.
Production-readiness auditor (v0.8.0+)
| Módulo | Contenido |
|---|---|
qa.run_audit(repo_root) |
Auditor determinista. Ejecuta 26 checks sobre el repo (no-print, structlog, .env.example, migrations, /live, /ready, security headers, CSRF, rate-limit, GDPR endpoints, flujo de reset completable, página de cuenta, /support, SMTP/SUPPORT_EMAIL declaradas, plantillas de email Supabase, CI workflow, secrets hardcoded, colores hardcodeados, etc.) y devuelve (report, exc) con blockers + warnings. Cero deps (solo stdlib). |
qa.CheckResult |
Dataclass inmutable con name, passed, details, severity ("blocker" | "warning"). |
Aplicable al backend interno y a cada app generada por el pipeline. Skill heredable prod-ready-checklist en el skeleton (app-template) invoca este auditor desde Claude Code.
from pathlib import Path
from datagrowth_common.qa import run_audit
report, exc = run_audit(Path("."))
if exc is None and report["blockers"] == 0:
print("LISTO PARA RELEASE")
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. El alias auth.local se eliminó en 0.8.0 — importar from datagrowth_common.auth import local lanza ImportError. |
Instalación
Publicado en PyPI: https://pypi.org/project/datagrowth-common/.
pip install "datagrowth-common[supabase,postgres]>=0.10,<1.0"
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.10,<1.0",
]
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/core/* y core/... 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 skeleton oficial (datagrowth/app-template) ya viene con esto cableado — incluido el bootstrap de admin desde APP_ADMIN_EMAIL. Ver src/main.py y src/api/admin_users.py de ese repo 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 |
SUPABASE_JWT_LEEWAY_S |
Segundos de tolerancia de reloj al validar iat/nbf/exp del JWT (leeway de PyJWT). Útil si el reloj del host va por detrás del de Supabase. Fail-closed (valores inválidos → 0). |
0 |
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 |
ENV |
Valor canónico: prod (default) o dev. Acepta legacy production/development por compat. dev permite cookies sin Secure para tests locales sin TLS. |
prod |
Genéricas
| Variable | Descripción | Por defecto |
|---|---|---|
ENV |
Valor canónico: prod (default) o dev. Acepta legacy production/development por compat. dev permite cookies sin Secure para tests locales sin TLS. |
prod |
LOG_LEVEL |
DEBUG, INFO, WARNING, ERROR |
INFO |
APP_SLUG |
Slug de la app para el checker de actualizaciones | — |
TENANT_SLUG |
Slug del tenant para el checker de actualizaciones | — |
APP_VERSION |
Versión actual de la app | 0.0.0 |
RELEASES_URL |
URL base del backend de releases | https://backend.dev.datagrowth.es |
Email transaccional (email, v0.11.0+)
| Variable | Descripción | Por defecto |
|---|---|---|
SMTP_HOST |
Servidor SMTP. Vacío = is_smtp_configured() False y send_email devuelve smtp-not-configured. |
— |
SMTP_PORT |
Puerto. 465 usa SSL implícito; el resto STARTTLS. |
587 |
SMTP_USER |
Usuario SMTP (login solo si está seteado) | — |
SMTP_PASSWORD |
Contraseña SMTP | — |
SMTP_FROM |
Remitente. Si vacío, cae a SMTP_USER. |
— |
Cookie HMAC propia (auth.session_hmac, opcional)
| Variable | Descripción | Por defecto |
|---|---|---|
SESSION_SECRET |
Secreto HMAC para firmar la cookie (≥32 bytes) | — |
SESSION_COOKIE |
Nombre de la cookie | dg_session |
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 |
|---|---|---|
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 DATABASE_URL) |
— |
SUPABASE_DB_HOST |
Host de Postgres | db |
SUPABASE_DB_PORT |
Puerto de Postgres | 5432 |
SUPABASE_DB_USER |
Usuario Postgres | postgres |
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.10.x): cookie HMAC (auth.session_hmac), validación JWT Supabase, router CRUD de usuarios, cookie httpOnly de sesión Supabase, auth_router (login + OAuth con state CSRF + refresh + logout + /api/me whitelist), 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), observability (init_sentry con scrubbing 6-campos PII), middleware (CSRFMiddleware + MetricsAuth + RequestId + SecurityHeaders + build_limiter), wiring CSRF JS (static/js/csrf.js, v0.8.1+, cableado desde sidebar_shell.html), me.make_me_router GDPR self-service, legal.make_legal_router (parser markdown XSS-safe), retention.make_retention_purger (whitelist regex SQL identifiers), audit.emit (scrub UA + schema validation), env.dg_env/is_dev_env/is_prod_env, helper qa.run_audit con 26 checks, postgres.resolve_postgres_url/to_sqlalchemy_url (unificación cross-repo).
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.12.1 |
Patch sobre 0.12.0: leeway de reloj opt-in al validar JWT (SUPABASE_JWT_LEEWAY_S, default 0, fail-closed) — elimina el monkeypatch por app cuando el reloj del host va por detrás del de Supabase; aliases cortos de tokens CSS (--color-fg/-bg/-muted/-accent); check QA no-hardcoded-colors (severidad warning). Fix: apply_migrations Windows-safe (worker async en SelectorEventLoop dedicado en os.name=="nt"; el driver async de psycopg no soporta el ProactorEventLoop); core/legal/page.html tokenizada (var(--color-*) + data-theme en vez de hex + prefers-color-scheme). Sin cambios en la API pública. |
v0.12.0 |
BREAKING: las env vars que la app lee a través del paquete pierden el prefijo DG_ (DG_ENV→ENV, DG_DATABASE_URL→DATABASE_URL, DG_CSRF_ENABLED→CSRF_ENABLED, …), sin aliases de retro-compat. Las apps ya desplegadas reciben el rename de su .env vía "Actualizar common"; hasta entonces deben quedarse en 0.11.x. Sin cambios funcionales — solo cambia la clave de entorno. |
v0.11.0 |
Página de cuenta self-service (account.make_account_router: /account con perfil + cambio de contraseña re-verificando la actual contra Supabase Auth + acciones GDPR) y email transaccional (email.send_email vía SMTP, error-como-valor, sin PII). UX del shell: command palette Ctrl+K, dgConfirm() + interceptor hx-confirm, macro modal() sobre <dialog>, validación progresiva de forms, preload de navegación en hover, View Transitions y prefers-reduced-motion. prod_ready: checks nuevos (password-reset-flow, account-page, support-page, env SMTP_HOST/SUPPORT_EMAIL). |
v0.10.1 |
UX de componentes compartidos: paginated_table muestra spinner (hx-indicator) y deshabilita los botones de paginación durante la petición HTMX; htmx-events.js marca el target de todo swap con aria-busy="true" mientras la petición está en vuelo; el hover de .table-tr pasa a un tinte 8% del color de texto vía color-mix (perceptible en ambos temas y con cualquier override de marca; antes surface-subtle sobre panel blanco era ~1.5:1). Sin cambios de API Python. |
v0.10.0 |
BREAKING (solo para consumidores del directorio template/): el skeleton de apps sale del paquete y vive en el repo propio (GitHub template, contenido en la raíz). El paquete queda solo con la lib pip. El backend clona DG_APP_TEMPLATE_REPO; las apps generadas no importan template/ y no se ven afectadas. Fix: el 404 de rutas no definidas devolvía JSON crudo en navegación — register_error_handlers se registra ahora sobre starlette.exceptions.HTTPException (clase padre), que captura tanto las HTTPException de FastAPI como las del router de Starlette. |
v0.9.6 |
Fix CSRF en uploads nativos: el middleware CSRF solo leía el token de bodies application/x-www-form-urlencoded. Un form multipart/form-data con submit nativo (subir un fichero sin HTMX/fetch) no manda el header X-CSRF-Token; csrf.js inyecta el token como campo hidden, pero viaja en el body multipart que el middleware ignoraba → 403 CSRF_TOKEN_MISMATCH en toda subida, con el JSON crudo a pantalla. _extract_submitted_token ahora parsea también multipart. Además, un fallo CSRF en navegación nativa (Accept: text/html, sin HTMX/XHR) devuelve una página HTML legible de "sesión caducada" con botón de volver, en vez del JSON crudo a pantalla; HTMX/XHR siguen recibiendo el JSON. Solo lib; sin cambios en el template. |
v0.9.5 |
Fix páginas legales 404 en apps: el Dockerfile del template no copiaba docs/ al runtime, así que make_legal_router no se cableaba y los links del footer (/privacy, /terms, /security) daban 404 (mismo patrón que el 503 del backend). Añadido COPY docs/legal/ + ajuste del .dockerignore (docs/* + !docs/legal). Fix UI: footer del shell con texto descentrado — el padding-bottom: env(safe-area-inset-bottom) machacaba el py-2 en desktop; eliminado. Fix CSS perdido: .gitignore ignoraba todo src/static/css/ (donde las skills mandan escribir CSS custom) → assets escritos a mano se perdían (404). Ahora solo se ignora tailwind.compiled.css; el .gitignore pasa a create-only en el update para no revertir ajustes del operador. |
v0.9.4 |
Fix CI security-audit: template/.github/workflows/ci.yml auditaba con pip-audit --strict --skip-editable, que inspecciona todo el entorno del runner (incluido pip). PYSEC-2026-196 (pip 26.1.1) tumbaba el job de toda app generada. Pasa al patrón pip install -e . + pip freeze --exclude-editable + pip-audit --requirement (excluye editables y herramientas del entorno). Solo template; sin cambios en la lib. |
v0.9.3 |
Fix shell reescrito a flex-column: el fix de 0.9.2 no bastaba (el margin-top del wrapper colapsaba con el <body> → sidebar sobre el header + hueco abajo). sidebar_shell.html reescrito al patrón app-shell canónico: <body> columna flex de alto viewport (nunca scrollea), header/footer shrink-0, fila sidebar+contenido flex-1, solo main scrollea. |
v0.9.2 |
Fix footer fijo y doble scrollbar: sidebar_shell.html — el footer estaba fuera del presupuesto de alto del layout: el body desbordaba (doble scrollbar) y la sidebar estática subía por encima del header al scrollear. El wrapper pasa a flex-col con fila interna flex-1 (sidebar + main) y el footer shrink-0; solo main scrollea. |
v0.9.1 |
Fix responsive (varios): CSS recompilado — faltaban md:static/md:translate-x-0/md:w-56/sm:justify-center/sm:pt-0 en el wheel (sidebar oculta en desktop, login sin centrar en sm+). users_panel.html overflow-hidden → overflow-x-auto (scroll horizontal en mobile). paginated_table.html con id dinámico desde target (HTMX apuntaba a id inexistente). Shell: safe-area iOS, z-index overlay drawer, touch target hamburguesa 10×10. roles_panel.html grid sm:grid-cols-2. legal/page.html dark mode + scroll en mobile. Test test_css_coverage.py para prevenir la rotura. Removed: modal.html y command_palette.html eliminados (muertos). |
v0.9.0 |
BREAKING: namespace de UI dg/ → core/ (directorio físico ui/templates/dg/ → ui/templates/core/) y mount estático /static/dg-common → /static/core (name="core-static"). Sin alias de compatibilidad — toda app que use {% extends "dg/..." %} o /static/dg-common/... debe actualizar antes de instalar. Added: responsive shell mobile-first completo: drawer de navegación con mobile-nav.js, trap de foco, lock de scroll, logout en mobile, viewport-fit=cover + env(safe-area-inset-*) en todos los shells. |
v0.8.7 |
Fix register_error_handlers rompía el redirect a /login en sesión expirada: cuando require_auth levantaba HTTPException(303, headers={"Location": "/login"}), el handler global de v0.8.6 lo interceptaba, perdía el Location y renderizaba "Error 303 / See Other" como página. Ahora los códigos [300, 400) se devuelven como RedirectResponse(url=Location, status_code=...) sin tocar; solo [400, 600) activa el flow banner/JSON. 2 tests de regresión cubriendo Location mayúscula y minúscula. |
v0.8.6 |
Banner global de errores (register_error_handlers(app, jinja_env)): reemplaza el JSON crudo sobre pantalla blanca por un toast en #dg-toasts para navegaciones HTML/HTMX; preserva JSON para clientes API (/api/*//auth/* o Accept: application/json). Mensajes por código (401/403/404/422/429/5xx). >=500 oculta detalles internos al usuario y vuelca el stack al logger. Soporte en layouts: empty.html monta #dg-toasts + carga toasts.js; sidebar_shell.html expone bloque toast_initial. htmx-events.js añade htmx:beforeSwap para que HTMX swappee respuestas 4xx/5xx con OOB toast. — Fix CSRF en /auth/password y /logout server-side (sin JS): CSRFMiddleware resuelve/genera el token al inicio del dispatch y lo expone como request.state.dg_csrf_token. Macro nueva dg/components/csrf.html::csrf_input(request) renderiza el hidden input csrf_token directamente en Jinja, eliminando la dependencia de csrf.js para forms server-rendered. template/src/templates/auth/login.html (standalone, no cargaba csrf.js) y dg/layouts/sidebar_shell.html (form de logout) usan el macro. csrf.js sigue cargado para HTMX y como red de seguridad para forms dinámicos. 2 tests nuevos de regresión CSRF + 5 nuevos de errors. |
v0.8.5 |
Fix DG_DATABASE_URL con esquema HTTPS reventaba el lifespan: resolve_postgres_url valida ahora el esquema del valor explícito y levanta ValueError con mensaje claro si no es libpq (postgresql://, postgres://, postgresql+psycopg://, postgresql+psycopg2://). Antes pasaba https://... directo a SQLAlchemy y rebotaba con NoSuchModuleError: sqlalchemy.dialects:https críptico en crash loop. |
v0.8.4 |
Fix pip-audit (era Dependency not found on PyPI: <pkg>): template/.github/workflows/ci.yml::security-audit lee el nombre del paquete del propio pyproject.toml (via tomllib) y lo desinstala tras pip install . para que pip-audit --strict no rechace el paquete privado del cliente. Fix I001 en src/main.py patcheado: _patch_main_for_production_readiness_helpers fusiona health dentro del from src.api import ... existente (sorted) en lugar de añadir línea separada. Backend: update_common_pr.py aplica ruff --fix (+ wrapper de docstrings largos: ruff E501 no rompe """...""" solo, lo hacemos a mano) automáticamente sobre src/main.py (tras los patches encadenados) y sobre todos los .py de src/api/sections/ + tests/ (archivos del agente section_boilerplater) dentro del PR del bot — el dev no necesita ejecutar ruff --fix a mano tras cada bump. |
v0.8.3 |
Fix lint del template (ruff) y CI (pip-audit): template/src/** pasa ahora ruff check limpio tras auto-fix de I001/E501/RUF100 en db.py, main.py, api/auth.py, api/admin_users.py, api/health.py. template/.github/workflows/ci.yml::security-audit cambia pip install -e . por pip install . para que pip-audit --strict --skip-editable no choque con el editable. Apps existentes reciben ambos fixes en el siguiente "Actualizar common" del backend. |
v0.8.2 |
Fix CSRF en empty.html (login/signup/reset): dg/layouts/empty.html ahora carga csrf.js con defer, cubriendo el form POST /auth/password que v0.8.1 dejó fuera. Ambos layouts del paquete (sidebar_shell + empty) tienen wiring CSRF completo. |
v0.8.1 |
Fix CSRF en logout (topbar): nuevo static/js/csrf.js bundleado en el wheel, cargado desde sidebar_shell.html. Lee la cookie dg_csrf e inyecta X-CSRF-Token en cada htmx:configRequest; para forms HTML clásicos inserta campo hidden csrf_token en el submit. Idempotente. Fix template/src/api/health.py: helper público get_engine() lazy en template/src/db.py para que el smoke test del template pase sin POSTGRES_PASSWORD. |
v0.8.0 |
BREAKING: auth.local eliminado (alias deprecated desde v0.3.0 — reemplazar por auth.session_hmac). Security (11 fixes): XSS en parser markdown, OAuth callback sin anti-CSRF, /auth/password exento de CSRF, Sentry scrubbing incompleto, _client_ip confiaba en X-Forwarded-For, SQL identifier injection en retention/purger.py, audit.emit sin scrub de emails en UA. 40 tests de regresión. Added: qa.run_audit (19 checks, 0 deps externas); helpers production-readiness (observability, middleware, me, legal, retention, audit, env); template con compose perfiles redis/worker/backup, ci.yml con prod-ready-audit, plantillas docs/PRIVACY.md + docs/RUNBOOK.md + docs/legal/; módulo env con dg_env()/is_dev_env()/is_prod_env() — canónico prod/dev. |
v0.7.15 |
.sidebar-link-active sólido (lima + texto navy) en lugar de translúcido al 22%: alineado con el look del backend orquestador. Resuelve via var(--color-brand-primary-rgb)/var(--color-brand-secondary-rgb). |
v0.7.14 |
Fix crítico de assets locales en apps generadas: template/src/main.py ahora monta /static/ apuntando a src/static/ del repo (además del /static/dg-common/ que monta register_ui). Sin este mount las apps respondían 404 a /static/brand/tokens-override.css aunque el deployer Datagrowth lo hubiera escrito en su filesystem — bug invisible mientras tailwind.compiled.css bakeaba hex literales (pre-0.7.13), crítico desde 0.7.13 con utilities var(--color-*-rgb). Apps existentes heredan el mount via patcher quirúrgico _patch_main_for_local_static_mount del backend Datagrowth al pulsar "Actualizar common" (idempotente, deja structure-mismatch:no-register-ui si el operador customizó el bootstrap). Encadenado con el patcher _patch_main_for_template_state (0.7.11) en un único commit src/main.py (state dep, local static mount). |
v0.7.13 |
Paquete deja de imponer marca Datagrowth: tokens.css declara brand/semánticos en doble forma (triplet --color-X-rgb para Tailwind con <alpha-value> + alias funcional --color-X para CSS directo) con defaults gray-based neutrales. tailwind-preset.cjs pasa de hex literales a rgb(var(--color-X-rgb) / <alpha-value>) — tailwind.compiled.css recompilado: cero hex Datagrowth bakeados, todas las utility classes resuelven la marca del cliente vía CSS vars. sidebar_shell.html añade dos fallbacks request.state.*: dg_user (topbar email/Admin/Salir consistente en páginas servidas por routers del paquete como make_users_router) y dg_tokens_override_href (<link> al CSS override del cliente sin tener que pasarlo en cada TemplateResponse). Nuevo set canónico de 46 vars en el override del cliente (5 brand-rgb + 5 brand funcionales + 2 on-*, 4 surfaces light/dark, 4 text, 2 link, 2 sidebar-active, 8 semánticos-rgb + 8 funcionales, 3 fonts, 1 ring-focus). Nuevo template/setup.json + template/SETUP.md para manifest declarativo de campos extra al instalar la app (consumido por app-backend/src/packaging/setup_manifest.py). Apps existentes: regenerar tokens-override.css desde la galería tras pullear para que las utility classes con alpha (bg-brand-lime/15) recuperen la marca del cliente — sin regenerar caen a gray. |
v0.7.12 |
Apps generadas listas para desarrolladores externos: nueva skill template/.agents/skills/dg-customize con el contrato customizable vs gestionado (qué archivos regenera el backend y cuáles puede tocar el dev), regla template/.claude/rules/managed-files.md que Claude Code carga automáticamente al editar archivos gestionados (src/static/brand/**, infra/Dockerfile, src/api/{deps,auth,admin_users}.py, etc.), template/setup.sh de bootstrap (symlinks .claude/skills + .cursor/skills → .agents/skills), template/README.md rewrite con sección "Empezar a desarrollar", template/CLAUDE.md con bloque inicial "Esta es una app generada". Fix de rendering del header "Administración" del sidebar: la clase .sidebar-section-label ya viaja en el compiled CSS (era purgada porque tailwind.config.js no escaneaba template/src/templates/**). Header renombrado a "Administrador" con mb-2/my-3 para más jerarquía visual. Apps existentes: setup.sh + regla en _SYNC_FILES, skill via _SYNC_DIRS_RECURSIVE, rename + spacing via patcher quirúrgico _patch_sidebar_admin_section (idempotente). |
v0.7.11 |
Sidebar admin consistente cross-pantalla en apps generadas. Nueva dep template/src/api/deps.py:populate_template_state que inyecta request.state.dg_user antes de cada handler HTML. main.py del template aplica dependencies=[Depends(populate_template_state)] a dashboard.router, users_router y admin_users.router (no a auth.router, que sirve /login público). partials/sidebar_nav.html añade fallback {% set user = user or request.state.dg_user %} para que el bloque "Administración" funcione cuando el handler (caso users_router del paquete) no pasa user al context Jinja. Apps existentes: el backend Datagrowth ofrece 2 patchers quirúrgicos en update_common_pr.py (_patch_main_for_template_state + _patch_sidebar_for_state_fallback) que viajan en el siguiente "Actualizar common". |
v0.7.10 |
Skills heredadas en template/.agents/skills/ viajan en cada repo nuevo: fastapi, supabase, boot, evolution, evolve, find-skills, fix-issue, modular-design, precommit. El backend Datagrowth las sincroniza vía _SYNC_DIRS_RECURSIVE (auto-discovery — añadir una skill nueva al template basta para que se propague sin tocar la lista de archivos en update_common_pr.py). |
v0.7.9 |
Sidebar activo deriva del color de marca: tokens.css añade --color-sidebar-active-bg (color-mix(in srgb, var(--color-brand-primary) 18%, transparent) en light, 22% en dark) y --color-sidebar-active-text (en dark usa var(--color-brand-primary) para legibilidad sobre superficie navy). .sidebar-link-active deja de hardcodear bg-brand-lime/20 text-brand-dark. Apps con primary distinto al lime ven el sidebar adaptado automáticamente. Fallback admin badge tras primer arranque: require_auth de la app fuerza is_admin=True si el email del JWT coincide con APP_ADMIN_EMAIL (cubre el caso de carrera entre el bootstrap admin del lifespan y el JWT viejo en cookies). tmp/ retirado del template (era residuo del app-backend). |
v0.7.8 |
Fixes visibles en la app desplegada: alias POST /logout añadido (antes 404 — el botón del topbar shared apunta a /logout), dashboard.html arregla {{ user.email }} que se renderizaba literal (era string param a macro), bootstrap_admin_from_env ahora sincroniza app_metadata.role en Supabase Auth tras set_user_roles (sin esto el sidebar admin no aparece aunque la tabla app_role lo marque). |
v0.7.7 |
Bundle de marca local en src/static/brand/ (CSS + meta.json + logo) copiado por el deployer al repo de la instance — cero fetch runtime al backend, cero env vars BRAND_*. Marker INSTANCE_TOKENS_OVERRIDE_LINK eliminado del template. auth.py:login_page lee meta.json desde filesystem. Fix crítico: _get_audience() cae al default "authenticated" cuando SUPABASE_JWT_AUDIENCE está empty/whitespace (antes rompía con "Audience doesn't match" aunque el JWT viniera correcto). SUPABASE_JWT_AUDIENCE, BRAND_CLIENT_NAME, BRAND_LOGO_URL, BRAND_PRIMARY_COLOR eliminadas del .env.example. update_common_pr (backend) filtra estas keys obsoletas del .env.example de apps existentes. |
v0.7.6 |
Tokens semánticos de contraste --color-on-primary / --color-on-secondary en tokens.css. Login standalone usa el token en el botón primario (antes color: #fff hardcoded, ilegible sobre lime). El agente design_to_tokens (backend) genera ambos tokens automáticamente con heurística de luminance. Recomendados, no obligatorios — brand designs viejos siguen funcionando. |
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.12.1.tar.gz.
File metadata
- Download URL: datagrowth_common-0.12.1.tar.gz
- Upload date:
- Size: 203.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ded6a8f1bd82def5f35d61ab16a337a8d80d390d8471c4205f1dd72379bab99
|
|
| MD5 |
600bff65281686bfd80546b5755ad0e7
|
|
| BLAKE2b-256 |
672a6f575dee0cad14210fa88538c6dd892cf14a786244ff7c9af89202e3c5ba
|
Provenance
The following attestation bundles were made for datagrowth_common-0.12.1.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.12.1.tar.gz -
Subject digest:
2ded6a8f1bd82def5f35d61ab16a337a8d80d390d8471c4205f1dd72379bab99 - Sigstore transparency entry: 2008598487
- Sigstore integration time:
-
Permalink:
datagrowth/datagrowth-common@6c52314350cc628b123b1a6f63336d50a22c1d8b -
Branch / Tag:
refs/tags/v0.12.1 - Owner: https://github.com/datagrowth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6c52314350cc628b123b1a6f63336d50a22c1d8b -
Trigger Event:
push
-
Statement type:
File details
Details for the file datagrowth_common-0.12.1-py3-none-any.whl.
File metadata
- Download URL: datagrowth_common-0.12.1-py3-none-any.whl
- Upload date:
- Size: 174.6 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 |
43c62e5f85db9ed44b64abdf615c3eca27cff8655bb8316a59fc8e0c0c09a6bf
|
|
| MD5 |
3dd34984f406c6e2cdf5767c870f7930
|
|
| BLAKE2b-256 |
6b7f5e7744eaa2d5ccc94d251578a05438c500fa100677484dcf2e2634afc752
|
Provenance
The following attestation bundles were made for datagrowth_common-0.12.1-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.12.1-py3-none-any.whl -
Subject digest:
43c62e5f85db9ed44b64abdf615c3eca27cff8655bb8316a59fc8e0c0c09a6bf - Sigstore transparency entry: 2008598616
- Sigstore integration time:
-
Permalink:
datagrowth/datagrowth-common@6c52314350cc628b123b1a6f63336d50a22c1d8b -
Branch / Tag:
refs/tags/v0.12.1 - Owner: https://github.com/datagrowth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6c52314350cc628b123b1a6f63336d50a22c1d8b -
Trigger Event:
push
-
Statement type: