← Los programas salen al mundo ETAPA 04 · 30 MIN

Sirve el contrato

Convierte el contrato HTTP en un programa de Python que se ejecuta, con el manejo de las peticiones fácil de probar.

Abrir en el laboratorio ↗

AL TERMINAR, PODRÁS

  • Explicar qué recibe y qué devuelve una aplicación WSGI
  • Enrutar una petición según su método y su ruta, con un código de estado para cada resultado
  • Probar el manejo de las peticiones sin red
El servidor habla HTTP; la aplicación solo convierte una petición en una respuesta, así que una prueba puede llamarla sin red.

Descripción del diagrama: 1. Cliente → Servidor: POST /jobs {"kind": "email"}; 2. Servidor → app(): environ, start_response; 3. app() → Servidor: "202 Accepted", headers, [b'{"id": "1"}']; 4. Servidor → Cliente: 202 Accepted {"id": "1"}; 5. Servidor: una prueba puede reemplazar al servidor

Un servidor y una aplicación

La lección anterior definió el contrato de Relay: POST /jobs acepta un trabajo y GET /jobs/{id} devuelve su estado.

Ahora algo tiene que escuchar en un puerto de red, leer cada petición y enviar una respuesta. Python divide ese trabajo en dos. Un servidor acepta las conexiones e interpreta los bytes de HTTP; una aplicación decide qué significa cada petición.

WSGI (Web Server Gateway Interface, la interfaz entre servidores web y aplicaciones, definida en el PEP 3333) es el acuerdo entre ambos. La aplicación es una función con dos argumentos: environ, un diccionario que describe la petición, y start_response, una función a la que llama con el código de estado y las cabeceras. Devuelve el cuerpo como un iterable de bytes, por ejemplo una lista con un solo objeto bytes.

Las aplicaciones de Flask y de Django también son aplicaciones WSGI, así que esta lección muestra lo que esos frameworks hacen por ti.

Python
import json

def app(environ, start_response):
    body = json.dumps({"status": "ok"}).encode("utf-8")
    start_response("200 OK", [("Content-Type", "application/json")])
    return [body]

Enruta, lee y responde

environ["REQUEST_METHOD"] guarda el método y environ["PATH_INFO"] la ruta, así que enrutar es decidir según esos dos valores.

Para POST /jobs, lee el cuerpo de environ["wsgi.input"], exactamente tantos bytes como diga CONTENT_LENGTH. Esa variable puede faltar o venir vacía, y la aplicación nunca debe leer más allá. Decodifica los bytes como JSON y revisa su forma antes de crear trabajo.

Cada resultado tiene su propio código: 202 Accepted con el id del trabajo nuevo, 400 Bad Request para un cuerpo que no es JSON válido o al que le falta un campo, 404 Not Found para un trabajo desconocido y 405 Method Not Allowed para una ruta conocida con el método equivocado, que además debe nombrar los métodos permitidos en una cabecera Allow. El estado es texto como "404 Not Found", las cabeceras son pares de strings y el cuerpo son bytes.

Python
def app(environ, start_response):
    method = environ["REQUEST_METHOD"]
    path = environ.get("PATH_INFO", "")
    if path == "/jobs" and method == "POST":
        return create_job(environ, start_response)
    if path.startswith("/jobs/") and method == "GET":
        return show_job(path.removeprefix("/jobs/"), start_response)
    return respond(start_response, "404 Not Found", {"error": "not_found"})

Ejecútala y luego pruébala sin red

wsgiref.simple_server, de la biblioteca estándar, sirve la aplicación en tu propia computadora: make_server("", 8000, app).serve_forever() responde en el puerto 8000, y un cliente como curl puede enviarle peticiones.

Su documentación dice con claridad que wsgiref es una implementación de referencia y que no se recomienda para producción. Un despliegue ejecuta la misma aplicación detrás de un servidor hecho para ese trabajo, como Gunicorn o Waitress, que manejan muchas conexiones, tiempos de espera y reinicios. Como la aplicación es solo una función, una prueba (test) puede llamarla directamente: arma un diccionario environ pequeño, pasa un start_response que anote el estado y las cabeceras, y lee los bytes que devuelve.

Por ahora Relay guarda sus trabajos en un diccionario, así que un 202 solo promete memoria; las próximas lecciones los pasan a SQLite.

Python
import io

def request(method, path, body=b""):
    seen = {}
    def start_response(status, headers, exc_info=None):
        seen["status"] = status
    environ = {"REQUEST_METHOD": method, "PATH_INFO": path,
               "CONTENT_LENGTH": str(len(body)), "wsgi.input": io.BytesIO(body)}
    chunks = app(environ, start_response)
    return seen["status"], b"".join(chunks)

ERROR FRECUENTE

Dejar que json.loads lance una excepción con un cuerpo mal formado. El servidor la convierte en un 500, que le dice al cliente que falló el servidor, cuando fue el cliente quien envió una petición mala y necesita un 400 con el que pueda hacer algo.

INTÉNTALO TÚ

Escribe los diccionarios environ de tres peticiones: un POST /jobs válido, un POST con JSON roto y un GET /jobs/missing. Predice el estado de cada una y luego llama a tu aplicación con ellas.

COMPROBACIÓN DE RECUERDO / APLICA LA IDEA

Decide en el laboratorio.

El checkpoint de dos preguntas, tus notas y tu progreso guardado están en el laboratorio. Gratis y sin cuenta.

Abrir en el laboratorio ↗

CONÉCTALO CON RELAY

Sirve el contrato de Relay desde una aplicación WSGI con un almacén en memoria: POST /jobs responde 202 con un id, GET /jobs/{id} devuelve el trabajo, y la entrada inválida, los trabajos desconocidos y los métodos equivocados reciben 400, 404 y 405. Pruébala llamando a la función y luego ejecútala una vez con wsgiref.simple_server y curl.