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_enabledDjango models. Reusable across SEP, FONDESO, and any sinpapel consumer that needs a functional HTTP API without hand-writing ViewSets, serializers, URLs, or permission classes.
Table of Contents
- What is sinpapel-drf?
- Installation
- Settings
- Quickstart
- Workflow endpoints
- Signature backends + dual FIEL mode
- Admin endpoints (predicates + SLAs)
- Flow portability endpoints
- Permissions
- Security checklist
- Testing
- Known limitations & Roadmap
- 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 fromSCHEMA_METADATOS; cached per model withfunctools.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
DefaultRouterfor 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 defaultactivo=Falseon 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-spectacularschema generation produces warnings for the polymorphicSignatureRequestSerializer(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-spectacularpolish (post-1.0).- Built-in
UserRateThrottleopt-in via setting. - Public PyPI release after stable adoption in SEP + FONDESO.
13. License & Contributing
GPL-3.0-or-later — see LICENSE.
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:
- sinpapel — engine core (workflow + signature + audit + predicates + SLA + metadata)
- sinpapel-webhooks — event-driven HTTP communication
- sinpapel-designer — visual workflow editor
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 sinpapel_drf-0.4.1.tar.gz.
File metadata
- Download URL: sinpapel_drf-0.4.1.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd1749143436d21f7c89f1c28836ec10f442c705d3199b745b222fcf65877768
|
|
| MD5 |
96e6a4f8ea33b0943ad7bb9f8d4fbf03
|
|
| BLAKE2b-256 |
6e917e26f08cca323f7507cbd8d0e2134ea7e74682c808dc1e8be07d2565a755
|
File details
Details for the file sinpapel_drf-0.4.1-py3-none-any.whl.
File metadata
- Download URL: sinpapel_drf-0.4.1-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
759045873085c5d2171e3ba71b2520737ea73c3c1fbb2453c13d068b360633bd
|
|
| MD5 |
5bb86b975c53fd0b781e686f5153168e
|
|
| BLAKE2b-256 |
a567a2914f5239d5098182f50caf452bc8d7e74551ce59d494af397b640f3815
|