Skip to main content

sinpapel-drf

v0.4.1 — DRF HTTP layer for sinpapel.

Auto-generated REST endpoints (workflow + signature + metadata + predicates + SLA + preview + flow portability) on top of @workflow_enabled Django models. Reusable across SEP, FONDESO, and any sinpapel consumer that needs a functional HTTP API without hand-writing ViewSets, serializers, URLs, or permission classes.

🇪🇸 Leer en Español


Table of Contents

  1. What is sinpapel-drf?
  2. Installation
  3. Settings
  4. Quickstart
  5. Workflow endpoints
  6. Signature backends + dual FIEL mode
  7. Admin endpoints (predicates + SLAs)
  8. Flow portability endpoints
  9. Permissions
  10. Security checklist
  11. Testing
  12. Known limitations & Roadmap
  13. License & Contributing

1. What is sinpapel-drf?

sinpapel-drf is the HTTP layer of sinpapel (workflow + audit + signature engine).

For every Django model decorated with @workflow_enabled(expose_endpoints=True), sinpapel-drf auto-publishes:

Endpoint Verb Purpose
/<slug>/<pk>/available-transitions/ GET List valid target states from current state
/<slug>/<pk>/transition/ POST Execute a transition (with optional signature)
/<slug>/<pk>/history/ GET Paginated audit trail. HistoricalRecords if declared; otherwise falls back to SeguimientoWorkflow transitions
/<slug>/<pk>/preview-transition/ POST v0.2.0 — Impact report without mutation
/<slug>/<pk>/metadatos/ GET / PATCH v0.2.0 — Structured metadata schema + partial update
/<slug>/<pk>/sla-status/ POST v0.2.0 — Evaluate SLA per instance
/<slug>/<pk>/documentos/ GET / POST v0.3.0 — List / upload documents (InstanciaDocumento, multipart)
/<slug>/<pk>/documentos/<doc_id>/ DELETE v0.3.0 — Remove a document from the instance
/<slug>/<pk>/requisitos/ GET v0.3.0 — Document requirements of the current state + fulfillment. v0.4.0: each requisito_documento also carries tipo_documento_id + documentos_disponibles ([{id, nombre}]) for dependent client selects

Plus admin-scoped top-level resources:

Endpoint Verb Purpose
/condiciones/ CRUD v0.2.0 — Transition predicates (CondicionTransicion)
/slas/ CRUD v0.2.0 — State timers (SLAConfiguracion)
/slas/verificar/ POST v0.2.0 — Bulk SLA evaluation across instances
/flujos/<pk>/export/ GET Flow as portable JSON
/flujos/import/ POST Import a flow JSON into a new VersionFlujo

Design pillars:

  • Zero boilerplate in the consumer. No ViewSets, no serializers, no URL definitions. Just decorate the model.
  • Polymorphic signature dispatch. A single POST /transition/ endpoint accepts FIEL client-side (recommended), FIEL server-side (gated), manual, or fake signatures.
  • Dynamic serializers for metadata. MetaFormFactory.build_serializer() constructs DRF serializers at request time from SCHEMA_METADATOS; cached per model with functools.lru_cache.
  • Error mapping consistency. PermissionError → 403, ValueError → 400, SignatureValidationError → 400, DjangoValidationError → 400.

2. Installation

pip install sinpapel-drf

Or via pyproject.toml:

dependencies = [
    "sinpapel-drf>=0.4.1",
]

Transitively pulls sinpapel>=0.7.0. Requires Python 3.10+, Django 5.0+, DRF 3.14+.

Add to INSTALLED_APPS

# settings.py
INSTALLED_APPS = [
    # ...
    "sinpapel",
    "sinpapel_drf",
    # ...
]

Mount the URLs

# project/urls.py
from django.urls import include, path

urlpatterns = [
    path("sinpapel/api/", include("sinpapel_drf.urls")),
]

sinpapel_drf.urls exposes:

  • The dynamic SinpapelRouter (per-model actions) for every @workflow_enabled(expose_endpoints=True) registered model.
  • A DefaultRouter for the admin resources /condiciones/ and /slas/.
  • The /flujos/<pk>/export/ and /flujos/import/ endpoints.

3. Settings

Setting Default Purpose
SINPAPEL_ALLOW_SERVER_SIGNING False Enable FIEL server-side mode (uploaded .key + password). Requires legal review — see ADR-012.
REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES — Standard DRF auth (Token, JWT, Session, etc.)

The package does not impose authentication. The consumer chooses (Session, Token, JWT, etc.) via the standard DRF setting.


4. Quickstart

1. Decorate the model

# myapp/models.py
from django.db import models
from sinpapel import workflow_enabled
from sinpapel.mixins import CampoMetadato, MetadatosCapturables

@workflow_enabled(
    state_field="estado",
    expose_endpoints=True,
    endpoint_slug="tramites",
)
class Tramite(MetadatosCapturables, models.Model):
    estado = models.ForeignKey("sinpapel.Estado", on_delete=models.PROTECT)
    monto = models.DecimalField(max_digits=12, decimal_places=2)

    SCHEMA_METADATOS = [
        CampoMetadato(nombre="rfc", tipo=str, requerido=True, etiqueta="RFC"),
        CampoMetadato(nombre="nivel", tipo=str, choices=["A", "B", "C"]),
    ]

2. Seed Estado + ConfiguracionTransicion (Django admin, fixture, or migration)

3. Call the endpoints

# List available transitions
curl -H "Authorization: Token <…>" \
     https://host/sinpapel/api/tramites/42/available-transitions/

# Preview a transition without mutating
curl -X POST -H "Authorization: Token <…>" \
     -H "Content-Type: application/json" \
     -d '{"target_state": "Aprobado"}' \
     https://host/sinpapel/api/tramites/42/preview-transition/

# Execute the transition
curl -X POST -H "Authorization: Token <…>" \
     -H "Content-Type: application/json" \
     -d '{"target_state": "Aprobado", "comentarios": "OK"}' \
     https://host/sinpapel/api/tramites/42/transition/

5. Workflow endpoints

Per-instance, auto-registered via SinpapelRouter. All require IsAuthenticated by default.

GET .../available-transitions/

Returns the list of valid target Estado objects from the current state.

[
  {"id": 5, "nombre": "Aprobado", "color": "#4DEFE2"},
  {"id": 7, "nombre": "Rechazado", "color": "#FF0000"}
]

POST .../transition/

Executes a transition. Request body validated by TransitionRequestSerializer:

{
  "target_state": "Aprobado",
  "comentarios": "Documentation complete",
  "condiciones": null,
  "signature": { "backend": "fiel", "mode": "client-side", "firma_b64": "...", "certificado_cer_b64": "..." }
}

Response 201:

{
  "success": true,
  "instance_id": 42,
  "estado_anterior": "Revisión",
  "estado_nuevo": "Aprobado",
  "seguimiento_id": 1834
}

GET .../history/

Paginated audit trail (page_size=10, max 100).

Source: if the model declares HistoricalRecords (django-simple-history), it serves instance.history. If it only inherits from Trazable (no HistoricalRecords), it falls back to the transition audit trail from SeguimientoWorkflow (most recent first), mapped to the same response shape — history_type is '+' for the creation entry / '~' for transitions, and history_change_reason is "PREVIOUS → NEW" plus the transition comments.

{
  "count": 12,
  "next": "...page=2",
  "previous": null,
  "results": [
    {"history_id": 1834, "history_type": "~", "history_date": "...", "history_user": "alice", "history_change_reason": "..."}
  ]
}

POST .../preview-transition/ (v0.2.0)

Simulates a transition and returns an impact report. Does not mutate state, does not execute side-effects, does not sign anything.

curl -X POST -d '{"target_state": "Aprobado"}' .../preview-transition/

Response 200:

{
  "permitido": true,
  "razones_bloqueo": [],
  "documentos_faltantes": [],
  "predicados_fallidos": [],
  "aprobadores_requeridos": [],
  "side_effects": [],
  "historial_reciente": [{"fecha": "...", "transicion": "Borrador → Revisión", "usuario": "alice", "comentarios": "..."}]
}

If permitido is false, razones_bloqueo contains at least one {tipo, mensaje} object with tipo ∈ {estado, transicion, documento, permiso, predicado}.

GET / PATCH .../metadatos/ (v0.2.0)

For models inheriting MetadatosCapturables:

GET returns the schema + current values:

{
  "schema": [
    {"nombre": "rfc", "tipo": "str", "requerido": true, "default": null,
     "choices": null, "etiqueta": "RFC", "ayuda": ""}
  ],
  "values": {"rfc": "ABCD010101ABC"}
}

PATCH validates the partial update through a DRF serializer built dynamically with MetaFormFactory.build_serializer() (cached per model). Keys outside SCHEMA_METADATOS are rejected with 400.

curl -X PATCH -d '{"rfc": "ABCD010101ABC"}' .../metadatos/

POST .../sla-status/ (v0.2.0)

Evaluates SLA for a specific instance via SLAEngine.evaluar_instancia(). POST because the alertar action can mutate instance fields. Returns the list of actions executed (or [] if no SLA is active or none have expired).

Requires IsAdminUser.


6. Signature backends + dual FIEL mode

POST /transition/ accepts an optional signature body discriminated by (backend, mode). Four variants, validated by SignatureRequestSerializer.to_internal_value().

Mode A — FIEL client-side (recommended, default)

Client signs locally with the SAT-supplied tools (e.g. firma.gob.mx or local libraries). The server never sees the private key.

{
  "signature": {
    "backend": "fiel",
    "mode": "client-side",
    "firma_b64": "...",
    "certificado_cer_b64": "..."
  }
}

Mode B — FIEL server-side (gated)

Server receives .cer + .key + password via multipart and signs internally. Requires SINPAPEL_ALLOW_SERVER_SIGNING=True and legal review (ADR-012).

curl -X POST \
     -F "target_state=Aprobado" \
     -F "signature[backend]=fiel" \
     -F "signature[mode]=server-side" \
     -F "signature[cer_file]=@firma.cer" \
     -F "signature[key_file]=@firma.key" \
     -F "signature[password]=••••" \
     .../transition/

After signing, the key bytes are scrubbed via _with_secure_key_buffer (del + gc.collect() in finally).

Manual

Scanned-signature workflow with witness.

{
  "signature": {
    "backend": "manual",
    "scanned_image_path": "/media/firmas/123.png",
    "witness_name": "Juan Pérez"
  }
}

Fake (tests only)

{ "signature": { "backend": "fake" } }

7. Admin endpoints (predicates + SLAs)

Both ModelViewSets. All routes require IsAdminUser (is_staff=True).

CondicionTransicion CRUD — transition predicates

A predicate runs before the transition is permitted. Three backends are supported: python_path, json_logic, django_orm.

# List + filter
curl ".../condiciones/?transicion=7&activo=true"

# Create
curl -X POST -H "Content-Type: application/json" \
     -d '{
       "transicion": 7,
       "tipo": "json_logic",
       "configuracion": {"logic": {">": [{"var": "monto"}, 0]}},
       "mensaje_error": "Amount must be positive",
       "orden": 1,
       "activo": true
     }' \
     .../condiciones/

# Update / Delete
curl -X PATCH -d '{"activo": false}' .../condiciones/123/
curl -X DELETE .../condiciones/123/

SLAConfiguracion CRUD — state timers

A SLA defines a max-days-in-state limit and the action to execute on expiry. Actions: notificar, escalar, rechazar, alertar.

# List + filter
curl ".../slas/?estado=3"

# Create
curl -X POST -H "Content-Type: application/json" \
     -d '{
       "estado": 3,
       "dias_maximos": 7,
       "accion_vencimiento": "notificar",
       "configuracion_accion": {"grupo_id": 1, "template": "expiration.html"},
       "activo": true
     }' \
     .../slas/

# Bulk evaluation (equivalent to the sinpapel_verificar_slas management command)
curl -X POST .../slas/verificar/
# → 200 {"ejecutadas": {"notificar": 3, "escalar": 1}}

A POST that creates a duplicate (estado, accion_vencimiento) (violating unique_together) is mapped from IntegrityError → 400.


8. Flow portability endpoints

Admin-gated (IsAdminUser). Useful to move flow definitions across environments without touching the DB directly.

GET /flujos/<pk>/export/

Downloads the flow as JSON v0.2 (Content-Disposition: attachment). Schema v0.2 includes condiciones + slas (added in sinpapel v0.4.x).

POST /flujos/import/[?dry_run=true][&activo=true]

Imports a flow JSON. Atomic, rejects missing references, validates the schema version.

  • ?dry_run=true — validates without persisting; returns {"dry_run": true, "would_create": {...}}.
  • ?activo=true — overrides the safe default activo=False on import.

9. Permissions

Endpoint Permission Notes
available-transitions, transition, history IsAuthenticated Group-level filtering handled by ConfiguracionTransicion.grupos_permitidos inside the engine. Engine raises PermissionError → 403.
preview-transition, metadatos IsAuthenticated
documentos, requisitos IsAuthenticated Upload/list/delete documents; read current-state requirements.
sla-status IsAdminUser Mutation possible (alertar action).
/condiciones/*, /slas/*, /slas/verificar/ IsAdminUser All admin resources.
/flujos/<pk>/export/, /flujos/import/ IsAdminUser

Per-transition group filtering is not a custom DRF permission class. It lives in WorkflowEngine.puede_cambiar_estado, which raises PermissionError. The viewset maps it to 403 (ADR-007).


10. Security checklist

Reference: ADR-012 (FIEL dual-mode signing).

Item Status Notes
SINPAPEL_ALLOW_SERVER_SIGNING=False default ✅ Server-side mode is opt-in only
key_file + password write_only=True ✅ Never leaked in responses
del key_bytes; del password; gc.collect() in finally ✅ _with_secure_key_buffer ensures cleanup
Conservative logging ✅ caplog tests verify no key/password material in log capture
Audit RegistroFirma.backend_metadata.mode="server-side" ✅ For post-incident forensics
E2E tests both modes + setting on/off ✅ 28+ tests
Rate limiting mode B (UserRateThrottle) ⏸️ Deferred — user must enable in production
HTTPS-only enforcement 📄 Documented; user responsibility

11. Testing

# Install dev deps
pip install -e ".[dev]"

# Run the suite
pytest

# Run only install-smoke tests (slow, skipped by default)
pytest -m install_smoke

The package ships 90+ unit + E2E tests covering routers, viewsets, serializers, signature dispatch, metadata factory, predicate viewsets, SLA viewsets, and flow portability endpoints. The smoke suite verifies pip-installability into a fresh venv.


12. Known limitations & Roadmap

Limitations:

  • drf-spectacular schema generation produces warnings for the polymorphic SignatureRequestSerializer (discriminated union). Functional but the OpenAPI shape is not perfect.
  • Rate limiting for FIEL server-side mode is the consumer's responsibility.
  • No API versioning (/v1/) until 1.0.

Roadmap:

  • drf-spectacular polish (post-1.0).
  • Built-in UserRateThrottle opt-in via setting.
  • Public PyPI release after stable adoption in SEP + FONDESO.

13. License & Contributing

Copyright (C) 2024-2026 Julio Adrián.

sinpapel-drf is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

sinpapel-drf is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

Issues + PRs at https://github.com/aprendomx/sinpapel-drf/issues. Tag with area/sinpapel-drf.

Architecture decisions:

  • ADR-010 — Two-package split sinpapel + sinpapel-drf
  • ADR-011 — Cache layer in sinpapel core
  • ADR-012 — FIEL dual-mode signing

Related projects:

Metadata

Release files for sinpapel-drf 0.4.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sinpapel-drf 0.4.6
File Size Uploaded
sinpapel_drf-0.4.6.tar.gz 41.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sinpapel-drf 0.4.6
File Interpreter ABI Platform
sinpapel_drf-0.4.6-py3-none-any.whl Python 3 none any Details

Total release size: 79.3 kB

Release files / sinpapel_drf-0.4.6.tar.gz

Download URL sinpapel_drf-0.4.6.tar.gz
Size 41.4 kB
Tags Source
SHA-256 checksum
How to use checksums
5f30e7e39ac67fce18cd7c9cb37f25a31341f828c45ed62942105b575294ec3e
BLAKE2b-256 checksum
How to use checksums
701e5ce972e9b643e23736385dc44039ec52288fb732946ac0b6f95d583036a2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release files / sinpapel_drf-0.4.6-py3-none-any.whl

Download URL sinpapel_drf-0.4.6-py3-none-any.whl
Size 37.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
61a51da253b7620fd609318257d9e8216f92465bec2d7346a826dbc47fcdf322
BLAKE2b-256 checksum
How to use checksums
155b913a4b4dcd56e3c4de82c1397f30e7b4b0e93bf61871ebf30b9fff79b281
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.4.6 This release

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page