Generar documentación de API completa y precisa es una parte crítica pero a menudo tediosa del desarrollo de software. La Especificación OpenAPI (anteriormente conocida como Swagger) ha surgido como el estándar de la industria para definir APIs RESTful. Proporciona un formato legible por máquina que permite tanto a humanos como a computadoras descubrir y comprender las capacidades de un servicio sin acceso al código fuente, la documentación o mediante la inspección del tráfico de red.1
Si bien muchos frameworks ofrecen plugins para generar especificaciones OpenAPI a partir de anotaciones de código (como docstrings), hay escenarios en los que podría necesitar un control más directo y programático sobre la creación de la especificación. Esto podría deberse a que está trabajando con un sistema heredado, un framework no estándar, o necesita generar una especificación para una API compuesta por múltiples microservicios.
Aquí es donde entra pyswagger
. Es una potente biblioteca de Python que funciona como un conjunto de herramientas para OpenAPI. Si bien a menudo se utiliza como cliente de API para consumir servicios definidos por una especificación OpenAPI, su verdadero poder reside en su modelo de objetos, que le permite construir, manipular y validar una especificación de forma programática.
En este tutorial completo, recorreremos el proceso de uso de pyswagger
para generar, de forma manual pero automática, una especificación OpenAPI 3.0 completa para una sencilla aplicación web Python construida con Flask. Construiremos la especificación desde cero, pieza por pieza, demostrando cómo los objetos de pyswagger
se mapean directamente a los componentes del estándar OpenAPI. Al final, no solo tendrá un archivo openapi.json
generado, sino también una interfaz de usuario de documentación interactiva y en vivo servida directamente desde su aplicación.
¿Quiere una plataforma integrada y todo en uno para que su equipo de desarrolladores trabaje junto con máxima productividad?
Apidog cumple todas sus demandas, y reemplaza a Postman a un precio mucho más asequible!
Parte 1: Configuración del Entorno del Proyecto
Antes de poder empezar a generar nuestra especificación, necesitamos configurar un entorno de desarrollo adecuado. Esto implica crear un entorno Python aislado para gestionar nuestras dependencias e instalar las bibliotecas necesarias.
Creando Su Espacio de Trabajo ⚙️
Primero, vamos a crear un directorio para nuestro proyecto. Abra su terminal o símbolo del sistema y ejecute los siguientes comandos:Bash
# Crear un nuevo directorio para nuestro proyecto
mkdir pyswagger-tutorial
cd pyswagger-tutorial
# Crear un entorno virtual de Python
# En macOS/Linux
python3 -m venv venv
# En Windows
python -m venv venv
Un entorno virtual es un árbol de directorios autocontenido que incluye una instalación de Python y una serie de archivos de soporte. Usar un entorno virtual asegura que los paquetes que instalamos para este proyecto no entren en conflicto con los paquetes instalados para otros proyectos.
Ahora, active el entorno virtual:Bash
# En macOS/Linux
source venv/bin/activate
# En Windows
.\venv\Scripts\activate
Una vez activado, el indicador de su terminal debería cambiar para mostrar el nombre del entorno virtual (por ejemplo, (venv)
), indicando que ahora está trabajando dentro de él.
Instalación de las Bibliotecas Necesarias
Con nuestro entorno activo, podemos instalar las bibliotecas de Python que necesitaremos para este tutorial. Necesitamos pyswagger
para construir la especificación, Flask
para crear nuestra sencilla API web, y PyYAML
porque pyswagger
lo utiliza para operaciones YAML.Bash
pip install "pyswagger[utils]" Flask PyYAML
La parte [utils]
al instalar pyswagger
es una buena práctica ya que incluye utilidades útiles, como un validador que usaremos más adelante para verificar la corrección de nuestra especificación generada.
Para una buena gestión del proyecto, es conveniente fijar sus dependencias en un archivo requirements.txt
.Bash
pip freeze > requirements.txt
Su archivo requirements.txt
ahora contendrá las bibliotecas y sus versiones específicas, haciendo que su proyecto sea fácilmente reproducible por otros.
Creación de la Aplicación Básica con Flask
Ahora, vamos a crear una aplicación mínima con Flask. Esto servirá como base de la API que vamos a documentar.
En el directorio de su proyecto, cree un nuevo archivo llamado app.py
y añada el siguiente código:Python
# app.py
from flask import Flask, jsonify
# Inicializar la aplicación Flask
app = Flask(__name__)
@app.route("/")
def index():
""" Un endpoint simple para verificar si la aplicación está funcionando. """
return jsonify({"message": "¡La API está activa y funcionando!"})
if __name__ == "__main__":
# Ejecuta la aplicación Flask en http://127.0.0.1:5000
app.run(debug=True)
Este código configura un servidor web muy simple con un único endpoint. Para ejecutarlo, asegúrese de que su entorno virtual aún esté activo y ejecute el siguiente comando en su terminal:Bash
python app.py
Debería ver una salida que indica que el servidor está funcionando, algo como esto:
* Serving Flask app 'app'
* Debug mode: on
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
Ahora puede abrir su navegador web o usar una herramienta como curl
para visitar http://127.0.0.1:5000
. Debería ver la respuesta JSON: {"message": "¡La API está activa y funcionando!"}
.
Con nuestro entorno básico y el esqueleto de la aplicación listos, ahora podemos sumergirnos en los conceptos centrales de pyswagger
.
Parte 2: Entendiendo el Modelo de Objetos de pyswagger
Para usar pyswagger
de manera efectiva en la generación de una especificación, primero necesita comprender cómo su modelo de objetos se corresponde con la estructura de un documento OpenAPI. Una especificación OpenAPI es esencialmente un gran objeto JSON o YAML con un esquema específico. pyswagger
proporciona clases y objetos de Python que reflejan este esquema, permitiéndole construir la especificación de una manera más intuitiva y orientada a objetos.
Conceptos Centrales de la Especificación OpenAPI 3.0 📜
Un documento OpenAPI 3.0 tiene algunos campos clave de nivel superior:
openapi
: Una cadena que especifica la versión de la Especificación OpenAPI (por ejemplo,'3.0.0'
).info
: Un objeto que proporciona metadatos sobre la API. Esto incluye eltitle
(título),version
(versión),description
(descripción) e información de contacto.servers
: Un array de objetos de servidor, que definen las URLs base para la API.paths
: El campo más importante. Este objeto contiene todos los endpoints de API disponibles (rutas) y las operaciones HTTP (GET, POST, PUT, DELETE, etc.) que se pueden realizar sobre ellos.components
: Un objeto que contiene un conjunto de objetos reutilizables para diferentes partes de la especificación. Esto es clave para mantener su especificación DRY (Don't Repeat Yourself - No se repita). Puede definirschemas
(modelos de datos) reutilizables,responses
(respuestas),parameters
(parámetros),examples
(ejemplos) y más.
Mapeo de OpenAPI a Objetos de pyswagger
pyswagger
proporciona un mapeo limpio de estos conceptos de OpenAPI a objetos de Python. Exploremos los principales que utilizaremos.
El objeto central en pyswagger
es App
. Puede pensar en una instancia de App
como la raíz de su documento OpenAPI.Python
from pyswagger import App
# La raíz del documento de especificación
# Lo inicializamos con una versión, pero también puede cargar desde una URL o archivo
root_app = App(version='3.0.0')
Una vez que tenga su objeto App
, puede empezar a poblar sus atributos, que se corresponden directamente con los campos de OpenAPI. pyswagger
utiliza un patrón builder, permitiendo una sintaxis fluida y legible.
Info y Servers
Las secciones info
y servers
son fáciles de poblar.Python
# Poblando el objeto 'info'
root_app.info.title = "API de Usuario"
root_app.info.version = "1.0.0"
root_app.info.description = "Una API simple para gestionar usuarios, utilizada para el tutorial de pyswagger."
# Poblando el array 'servers'
# Se crea un objeto Server y se añade
server = root_app.prepare_obj('Server', {'url': 'http://127.0.0.1:5000', 'description': 'Servidor de desarrollo local'})
root_app.servers.append(server)
Paths y Operations
Las rutas (Paths) se definen en el objeto App
. Se añaden nuevas rutas y luego se definen las operaciones (métodos HTTP) dentro de ellas. Cada operación se configura con detalles como un summary
(resumen), description
(descripción), parameters
(parámetros), un requestBody
(cuerpo de la solicitud) y responses
(respuestas).Python
# Definiendo una ruta y una operación
# Esto no ejecuta nada; simplemente construye la estructura del objeto.
path_item = root_app.define_path('/users')
get_op = path_item.define_op('get')
get_op.summary = "Recuperar una lista de todos los usuarios"
Components: Schemas, Parameters y Responses
El verdadero poder de una especificación OpenAPI bien estructurada proviene de los componentes reutilizables. En lugar de definir la estructura de un objeto "Usuario" cada vez que aparece en una respuesta, lo define una vez en components/schemas
y luego lo referencia utilizando un puntero $ref
. pyswagger
maneja esto de manera elegante.
Schema
: Un objeto Schema
define un modelo de datos. Puede especificar su tipo (object
, string
, integer
) y sus propiedades.Python
# Preparando un objeto Schema para un Usuario
user_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'id': {'type': 'integer', 'format': 'int64'},
'username': {'type': 'string'},
'email': {'type': 'string', 'format': 'email'}
},
'required': ['id', 'username', 'email']
})
# Añadirlo a los componentes reutilizables
app.components.schemas['User'] = user_schema
Parameter
: Un objeto Parameter
define un único parámetro de operación. Especifica su nombre, dónde se encuentra (in
: 'path'
, 'query'
, 'header'
, o 'cookie'
), y su esquema.Python
# Preparando un objeto Parameter para un ID de usuario en la ruta
user_id_param = app.prepare_obj('Parameter', {
'name': 'user_id',
'in': 'path',
'description': 'ID del usuario a recuperar',
'required': True,
'schema': {'type': 'integer'}
})
Response
: Un objeto Response
define la estructura de una respuesta para un código de estado HTTP específico. Incluye una description
(descripción) y el content
(contenido), que especifica el tipo de medio (por ejemplo, application/json
) y su esquema.Python
# Preparando un objeto Response para una respuesta 200 OK que devuelve un único usuario
# Note el uso de '$ref' para apuntar a nuestro esquema User reutilizable
ok_user_response = app.prepare_obj('Response', {
'description': 'Recuperación exitosa de un usuario',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/User'}
}
}
})
Comprender este mapeo es la clave para construir su especificación. Esencialmente, está construyendo un grafo de objetos de Python que pyswagger
serializará más tarde en un archivo JSON o YAML válido de OpenAPI.
Parte 3: Construyendo una API Simple con Flask
Para hacer práctico nuestro ejercicio de documentación, necesitamos una API real para documentar. Ampliaremos nuestra sencilla aplicación Flask de la Parte 1 en una API REST mínima para gestionar una lista de usuarios. Esta API servirá como la "fuente de verdad" que describiremos con pyswagger
.
Diseñando una API Simple de "Usuario" 📝
Implementaremos cuatro endpoints básicos que representan operaciones CRUD (Crear, Leer, Actualizar, Eliminar) comunes:
GET /users
: Recupera una lista de todos los usuarios.POST /users
: Crea un nuevo usuario.GET /users/{user_id}
: Obtiene un único usuario por su ID.DELETE /users/{user_id}
: Elimina un usuario por su ID.
Para simplificar, utilizaremos un simple diccionario de Python como nuestra "base de datos" en memoria. En una aplicación del mundo real, esto sería una conexión a una base de datos como PostgreSQL o MongoDB.
Implementación de los Endpoints de Flask
Vamos a actualizar nuestro app.py
archivo para incluir la lógica de estos endpoints. Reemplace el contenido de app.py
con lo siguiente:Python
# app.py
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# --- Base de Datos en Memoria ---
# Un diccionario simple para almacenar nuestros usuarios.
# La clave es el user_id (entero), y el valor son los datos del usuario (dict).
USERS_DB = {
1: {"username": "alice", "email": "alice@example.com"},
2: {"username": "bob", "email": "bob@example.com"},
3: {"username": "charlie", "email": "charlie@example.com"},
}
# Un contador para simular IDs auto-incrementales para nuevos usuarios
LAST_INSERT_ID = 3
# --- Endpoints de la API ---
@app.route("/users", methods=["GET"])
def get_users():
""" Devuelve una lista de todos los usuarios. """
# Necesitamos convertir el diccionario a una lista de objetos de usuario, incluyendo sus IDs.
users_list = []
for user_id, user_data in USERS_DB.items():
user = {'id': user_id}
user.update(user_data)
users_list.append(user)
return jsonify(users_list)
@app.route("/users", methods=["POST"])
def create_user():
""" Crea un nuevo usuario. """
global LAST_INSERT_ID
if not request.json or 'username' not in request.json or 'email' not in request.json:
abort(400, description="Falta nombre de usuario o correo electrónico en el cuerpo de la solicitud.")
LAST_INSERT_ID += 1
new_user_id = LAST_INSERT_ID
new_user = {
"username": request.json["username"],
"email": request.json["email"],
}
USERS_DB[new_user_id] = new_user
# La respuesta debe incluir el ID del usuario recién creado
response_user = {'id': new_user_id}
response_user.update(new_user)
return jsonify(response_user), 201
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
""" Devuelve un único usuario por su ID. """
if user_id not in USERS_DB:
abort(404, description=f"Usuario con ID {user_id} no encontrado.")
user_data = USERS_DB[user_id]
user = {'id': user_id}
user.update(user_data)
return jsonify(user)
@app.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
""" Elimina un usuario por su ID. """
if user_id not in USERS_DB:
abort(404, description=f"Usuario con ID {user_id} no encontrado.")
del USERS_DB[user_id]
# Una respuesta 204 No Content es estándar para eliminaciones exitosas
return '', 204
if __name__ == "__main__":
app.run(debug=True, port=5000)
Ahora, si ejecuta python app.py
de nuevo, tendrá una API completamente funcional (aunque simple). Puede probarla con curl
o una herramienta similar:
- Obtener todos los usuarios:
curl http://127.0.0.1:5000/users
- Obtener un usuario específico:
curl http://127.0.0.1:5000/users/1
- Crear un usuario:
curl -X POST -H "Content-Type: application/json" -d '{"username": "david", "email": "david@example.com"}' http://127.0.0.1:5000/users
- Eliminar un usuario:
curl -X DELETE http://127.0.0.1:5000/users/3
Con nuestra API implementada, tenemos un objetivo concreto para nuestros esfuerzos de documentación. El siguiente paso es usar pyswagger
para describir cada uno de estos endpoints en detalle.
Parte 4: Auto-Generando la Especificación OpenAPI con pyswagger
Este es el núcleo de nuestro tutorial. Ahora crearemos un script Python separado que importa pyswagger
, define la estructura de nuestra API utilizando su modelo de objetos y luego serializa esa estructura en un archivo openapi.json
completo. Este enfoque desacopla la generación de la especificación de la lógica de la aplicación, lo que puede ser un patrón muy limpio y mantenible.
Creando el Generador de Especificaciones
En el directorio de su proyecto, cree un nuevo archivo llamado generate_spec.py
. Este script será responsable de construir y guardar nuestra especificación OpenAPI.
Construyendo la Especificación, Paso a Paso
Vamos a construir el script generate_spec.py
pieza por pieza.
1. Importaciones e Inicialización de la App
Primero, necesitamos importar el objeto App
de pyswagger
y PyYAML
para ayudar a volcar el archivo final. Crearemos nuestro objeto raíz App
y poblaremos las secciones básicas info
y servers
, tal como discutimos en la Parte 2.Python
# generate_spec.py
import json
from pyswagger import App
from pyswagger.contrib.client.requests import Client
# --- 1. Inicializar el objeto raíz App ---
app = App(version='3.0.0')
# --- 2. Población de las secciones Info y Servers ---
app.info.title = "API de Usuario"
app.info.version = "1.0.0"
app.info.description = "Una API simple para gestionar usuarios, para el tutorial de pyswagger."
server = app.prepare_obj('Server', {
'url': 'http://127.0.0.1:5000',
'description': 'Servidor de desarrollo local'
})
app.servers.append(server)
2. Definiendo Componentes Reutilizables (Schemas)
Un buen diseño de API evita la repetición. Definiremos nuestro modelo de datos User
una vez y lo reutilizaremos. También definiremos un esquema Error
genérico para nuestras respuestas de error (como 404 Not Found).
Añada el siguiente código a generate_spec.py
:Python
# --- 3. Definiendo Componentes Reutilizables (Schemas) ---
# Esquema para la respuesta de Error
error_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'code': {'type': 'integer', 'format': 'int32'},
'message': {'type': 'string'}
}
})
app.components.schemas['Error'] = error_schema
# Esquema para un único Usuario. Note que las propiedades coinciden con nuestra estructura USERS_DB.
user_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'id': {
'type': 'integer',
'description': 'Identificador único para el usuario.',
'readOnly': True # El cliente no puede establecer este valor
},
'username': {
'type': 'string',
'description': 'El nombre de usuario elegido por el usuario.'
},
'email': {
'type': 'string',
'description': 'La dirección de correo electrónico del usuario.',
'format': 'email'
}
},
'required': ['id', 'username', 'email']
})
app.components.schemas['User'] = user_schema
# Esquema para crear un usuario (no incluye el campo 'id')
new_user_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'username': {
'type': 'string',
'description': 'El nombre de usuario elegido por el usuario.'
},
'email': {
'type': 'string',
'description': 'La dirección de correo electrónico del usuario.',
'format': 'email'
}
},
'required': ['username', 'email']
})
app.components.schemas['NewUser'] = new_user_schema
3. Documentando los Endpoints /users
Ahora definiremos la ruta /users
y sus dos operaciones: GET
y POST
.
- Para
GET
, la respuesta es un array de objetosUser
. - Para
POST
, el cuerpo de la solicitud esperará un objetoNewUser
, y la respuesta exitosa será un único objetoUser
(incluyendo el nuevo ID).
Python
# --- 4. Documentando las Rutas (Paths) ---
# -- Ruta: /users --
path_users = app.define_path('/users')
# Operación: GET /users
op_get_users = path_users.define_op('get')
op_get_users.summary = "Listar todos los usuarios"
op_get_users.description = "Devuelve un array JSON de todos los objetos de usuario."
op_get_users.tags.append('Usuarios')
op_get_users.responses.A('200').description = "Una lista de usuarios."
op_get_users.responses.A('200').content.A('application/json').schema.A(
'array', items={'$ref': '#/components/schemas/User'}
)
# Operación: POST /users
op_post_users = path_users.define_op('post')
op_post_users.summary = "Crear un nuevo usuario"
op_post_users.description = "Añade un nuevo usuario a la base de datos."
op_post_users.tags.append('Usuarios')
op_post_users.requestBody.description = "Objeto de usuario que necesita ser añadido."
op_post_users.requestBody.required = True
op_post_users.requestBody.content.A('application/json').schema.set_ref('#/components/schemas/NewUser')
op_post_users.responses.A('201').description = "Usuario creado exitosamente."
op_post_users.responses.A('201').content.A('application/json').schema.set_ref('#/components/schemas/User')
op_post_users.responses.A('400').description = "Entrada inválida proporcionada."
op_post_users.responses.A('400').content.A('application/json').schema.set_ref('#/components/schemas/Error')
Observe el uso de set_ref
y A
(que significa "acceso") para una sintaxis más concisa al construir la estructura de objetos anidados.
4. Documentando los Endpoints /users/{user_id}
A continuación, documentaremos la ruta para interactuar con un único usuario. Esta ruta incluye un parámetro de ruta, {user_id}
.
- Para
GET
, necesitaremos definir este parámetro de ruta. La respuesta es un único objetoUser
o un error404
. - Para
DELETE
, también necesitamos el parámetro de ruta. La respuesta exitosa es un204 No Content
.
Python
# -- Ruta: /users/{user_id} --
path_user_id = app.define_path('/users/{user_id}')
# Podemos definir el parámetro una vez y reutilizarlo para todas las operaciones en esta ruta.
user_id_param = app.prepare_obj('Parameter', {
'name': 'user_id',
'in': 'path',
'description': 'ID del usuario',
'required': True,
'schema': {'type': 'integer'}
})
path_user_id.parameters.append(user_id_param)
# Operación: GET /users/{user_id}
op_get_user_id = path_user_id.define_op('get')
op_get_user_id.summary = "Buscar usuario por ID"
op_get_user_id.description = "Devuelve un único usuario."
op_get_user_id.tags.append('Usuarios')
op_get_user_id.responses.A('200').description = "Operación exitosa."
op_get_user_id.responses.A('200').content.A('application/json').schema.set_ref('#/components/schemas/User')
op_get_user_id.responses.A('404').description = "Usuario no encontrado."
op_get_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')
# Operación: DELETE /users/{user_id}
op_delete_user_id = path_user_id.define_op('delete')
op_delete_user_id.summary = "Elimina un usuario"
op_delete_user_id.description = "Elimina un único usuario de la base de datos."
op_delete_user_id.tags.append('Usuarios')
op_delete_user_id.responses.A('204').description = "Usuario eliminado exitosamente."
op_delete_user_id.responses.A('404').description = "Usuario no encontrado."
op_delete_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')
5. Validando y Guardando la Especificación
Finalmente, el paso más satisfactorio. Le pediremos a pyswagger
que valide nuestro grafo de objetos construido contra el esquema OpenAPI 3.0. Si es válido, lo volcaremos a un archivo JSON.
Añada este bloque final de código a generate_spec.py
:Python
# --- 5. Validar y Guardar la Especificación ---
if __name__ == '__main__':
try:
# Validar la especificación generada
app.validate()
print("La especificación es válida.")
# Guardar la especificación en un archivo JSON
with open('openapi.json', 'w') as f:
f.write(app.dump_json(indent=2))
print("openapi.json generado exitosamente")
except Exception as e:
print(f"Error de Validación: {e}")
Su archivo generate_spec.py
ahora está completo. Ejecútelo desde su terminal:Bash
python generate_spec.py
Si todo es correcto, verá la siguiente salida:
La especificación es válida.
openapi.json generado exitosamente
Ahora tendrá un nuevo archivo, openapi.json
, en el directorio de su proyecto. Ábralo y explore su contenido. Verá un documento OpenAPI 3.0 perfectamente estructurado que describe su API Flask con todo detalle.
Parte 5: Sirviendo la Documentación
Tener un archivo openapi.json
es excelente para las máquinas y para generar SDKs de cliente, pero para los desarrolladores humanos, una interfaz de usuario interactiva es mucho más útil. En esta parte final, integraremos nuestra especificación generada en nuestra aplicación Flask y la