API REST pública con los datos reales del Torneo Volleyball 2026: la liga (7 equipos, 21 partidos y 63 sets) y la fase final con su podio. Calcula la tabla de posiciones con puntos FIVB y explica cada desempate. También tiene un endpoint para simular resultados hipotéticos.
Hecha con Python 3.14 + FastAPI + Pydantic. 100 pruebas y 99 % de cobertura.
| Documentación interactiva | api.davidameth.dev/docs, para llamar a cada ruta desde el navegador |
| Swagger UI | /swagger |
| Referencia | /redoc |
| Contrato OpenAPI 3.1 | /openapi.json |
curl https://api.davidameth.dev/v1/standings
curl "https://api.davidameth.dev/v1/matches?team=shalom-2&limit=3"
curl -X POST https://api.davidameth.dev/v1/standings/simulate \
-H "Content-Type: application/json" \
-d '{"results":[{"home":"shalom-1","away":"generacion-de-fe","sets":[{"home":25,"away":20},{"home":26,"away":24},{"home":25,"away":18}]}]}'| Método | Ruta | Qué hace |
|---|---|---|
| GET | /v1/teams · /v1/teams/{id} |
Equipos, en orden de siembra |
| GET | /v1/jornadas · /v1/jornadas/{n} |
Fechas de juego; el estado se deriva de sus partidos |
| GET | /v1/matches |
Partidos set a set. Filtros jornada, team, status; paginación limit/offset |
| GET | /v1/matches/{id} |
Un partido (j3-p2 = jornada 3, posición 2) |
| GET | /v1/standings?through_jornada=n |
La tabla actual, o como quedó al cerrar la jornada n |
| POST | /v1/standings/simulate |
Recalcula la tabla con resultados hipotéticos. No guarda nada |
| GET | /v1/final-phase |
Semifinales, 3.er puesto, final y el podio que sale de ellos |
| GET | /health · /problems/{tipo} |
Salud del servicio y catálogo de errores |
- La tabla no se guarda, se calcula. Sale de los parciales de cada set en cada petición, así que no hay posiciones almacenadas que puedan desincronizarse. La app del torneo aprendió eso en producción: el conteo de sets guardado vino mal una vez, y desde entonces se deriva de los parciales. Esta API hace lo mismo.
- Las reglas viven aparte del HTTP.
app/domain.pyson funciones puras (sets a 25 con 2 de ventaja, 3 sets fijos, puntos FIVB 3-2-1-0, desempate por partidos ganados, ratio de sets y ratio de puntos). Se prueban sin levantar el servidor. - Un marcador imposible no entra por ningún lado. Las mismas reglas validan el archivo de datos al
arrancar (con un 25-24 el servidor no arranca) y cada resultado simulado (devuelve
422). - Errores en formato RFC 9457 (
application/problem+json), contype,title,status,detail,instanceyrequest_id. Cadatypese documenta en/problems/{tipo}. - Validación estricta. Los campos que no existen se rechazan (
extra="forbid") y no hay conversiones silenciosas:0no se acepta comofalseni"25"como número. - Caché HTTP. Cada GET lleva
ETagyCache-Control: private; conIf-None-Matchla respuesta es304sin cuerpo.privatey nopublic: la respuesta lleva unX-Request-IDpor petición, y una caché compartida (el CDN) la repetiría a otros clientes. - Trazabilidad. Todas las respuestas, también los 500, llevan
X-Request-ID. Si el cliente manda uno, se reutiliza. Un error inesperado no expone detalles internos. - Versionada bajo
/v1. - Una búsqueda sin resultados es
200con lista vacía, no404. El404es para recursos concretos que no existen.
tests/
├── test_dominio.py reglas del voleibol + propiedades con Hypothesis
├── test_api.py rutas, filtros, paginación, errores y cabeceras HTTP
├── test_contrato.py Schemathesis genera peticiones desde el OpenAPI
├── test_datos.py la carga se niega a arrancar con datos imposibles
└── test_oraculo_produccion.py la tabla y el podio coinciden con los de la app en producción
-
Oráculo de producción. La app del torneo (TypeScript) y esta API (Python) calculan la tabla por separado con los mismos datos. La prueba exige que coincidan en cada número de cada fila, y que el podio sea el mismo.
-
Pruebas de propiedades. Hypothesis genera cientos de temporadas al azar y comprueba invariantes que tienen que cumplirse siempre. Cada partido reparte exactamente 3 puntos, los sets ganados suman lo mismo que los perdidos, la tabla sale ordenada y el orden de los partidos no la cambia.
-
Pruebas de contrato. Schemathesis lee el OpenAPI y bombardea cada ruta con entradas válidas e inválidas. En su primera corrida encontró cinco defectos reales, todos corregidos y cubiertos por pruebas:
- Los
405no llevaban la cabeceraAllow(la exige RFC 9110). - El OpenAPI documentaba los errores como
application/json, pero salían comoapplication/problem+json. - Los
500perdían elX-Request-ID, porque los atiende un middleware externo. include_season: 0se aceptaba comofalse, y el contrato diceboolean.- Un cuerpo que no era JSON devolvía
404en vez de400.
Un sexto salió al probar a mano el despliegue:
HEADrespondía405(RFC 9110 obliga a aceptarlo donde se aceptaGET). Schemathesis no lo cubre porqueHEADno figura en el OpenAPI. Ahora tiene su prueba.Un séptimo apareció en producción el 2026-10-02: las respuestas salían con
Cache-Control: public, así que el CDN de Vercel las guardaba cinco minutos con todas sus cabeceras y servía el mismoX-Request-IDa todos, ignorando el que mandara cada cliente. Ninguna prueba lo veía, porque corren contra la aplicación sin el CDN en medio, y la delX-Request-IDsolo miraba/health, la única ruta que no se cachea. Ahora esprivatey hay pruebas sobre todas las rutas cacheables.Solo en la simulación se desactiva el chequeo de "datos válidos aceptados". JSON Schema no puede expresar "a 25 con 2 de ventaja", así que un 0-0 cumple el esquema y aun así debe rechazarse.
- Los
El CI (GitHub Actions) corre el linter, el formato y toda la batería en cada push, con Python 3.12 (el de producción) y 3.14. También publica la cobertura y el informe JUnit como artefacto. Si la cobertura baja del 95 %, falla.
Las dependencias están en pyproject.toml, fijadas en uv.lock. Hace falta uv.
uv sync
uv run uvicorn app.main:app --reload # http://127.0.0.1:8000/docs
uv run pytest --cov=appdata/temporada-2026.json se genera con scripts/export_seed.py a partir de la temporada congelada de la app
del torneo (src/data/archivo-2026.json), que quedó fija al terminar la temporada. Solo exporta equipos,
jornadas, partidos, parciales y la fase final.
La fase final no tiene marcador. Se jugó sin anotar los puntos, así que de cada partido solo se sabe quién
ganó. La API lo dice así (score_recorded: false) en vez de inventar un resultado. Al arrancar comprueba que el
cuadro cuadre con la liga: los semifinalistas son los cuatro primeros, por el 3.er puesto juegan los perdedores de
semifinal y la final, los ganadores. La fase final no suma a la tabla de la liga. No incluye jugadores ni ningún
otro dato personal (Ley 81 de Panamá): la API es pública y esas personas no aceptaron salir en ella.
David Ameth Martínez Sánchez, ingeniero de control de calidad (QA): davidameth.dev.
Licencia MIT.