Web Services REST con Python y Flask
Vamos a ver un ejemplo de cómo crear un Web Service REST con python. Con los módulos por defecto de python no es posible hacerlo, así que usaremos algún módulo adicional. Hay varios disponbles, cada uno con sus características. Para el ejemplo usaremos Flask.
Tienes los ejemplos de código de este tutorial en python-rest-flask. Ante cualquier duda o sugerencia, suelo contestar en este foro de python
Instalación de Flask[editar]
Instalar Flask es sencillo. Desde un terminal de comandos de Windows o Linux, donde tengamos acceso a los ejecutables que vienen con la instalación de python, basta ejecutar el siguiente comando pip
pip install Flask
Lo básico de Flask[editar]
Para facilitar la explicación de los web services, vamos primero con algunas cosas básicas de Flask.
Hola Mundo con Flask[editar]
El código python con Flask para el Hola Mundo sería como el siguiente
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
Primero importamos Flask
del módulo flask
. Se instancia Flask
pasándole como parámetro el nombre de nuestra aplicación. Puede ser un nombre cualquiera. Suele ser habitual poner __name__
que es una variable especial de python que contiene el nombre de nuestro módulo. Finalmente, declaramos una función que devuelva lo que queramos que muestre nuestra web. Para indicar con Flask cuándo se debe llamar a esa función en concreto, la "anotamos" con @app.route()
pasando como parámetro el path dentro de nuestra Web. En nuestro ejemplo, sería el directorio raíz de nuestra web "/"
El arranque de esta aplicación es un poco especial. Se arranca desde una ventana de comandos con el siguiente comando
python -m flask --app hello-world-flask run
Este comando básicamente dice que arranaquemos el módulo flask -m flask
, que el fichero con la aplicación es hello-world-flask.py --app hello-world-flask
(sin la extensión) y que queremos ejecutarlo (run). La salida al arrancar este comando es
python.exe -m flask --app hello-world-flask run * Serving Flask app 'hello-world-flask' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit
Por defecto se abre en el puerto 5000. Podemos ver el resultado con el navegador web de nuestra elección y escribiendo la URL http://127.0.0.1:5000
Recoger path, parámetros y body content de la petición[editar]
En los web services necesitaremos recoger ciertos valores tanto del path de la url, posibles parámetros que se pasen así como el contenido del cuerpo de la petición HTTP. Para ver cómo se hace, usamos el siguiente código de ejemplo
from flask import Flask, request
@app.route("/<path>", methods=['GET', 'POST'])
def show_path(path):
content = f"<p>{path}</p>"
if request.args:
content += "<ul>"
for key in request.args.keys():
content += f"<li>{key} = {request.args[key]}</li>"
content += "</ul>"
if request.data:
content += f"<p>data = {request.data}</p>"
return content
Para recoger parte del path, en $app.route()
ponemos entre símbolos < y > un nombre de variable que queramos. Esta variable estará accesible com párametro de la función que definamos para esta URL. En el código de ejemplo, la variable es path
.
En $app.route()
hemos añadido además un segundo parámetro. Es methods=[]
. Ahí ponemos un array con los posibles tipos de peticiones HTTP que debe poder atender esta función. Si no ponemos nada, por defecto sólo atiende peticiones GET. Pero las peticiones GET no admite contenido en el cuerpo de la petición HTTP, así que en este ejemplo añadimo ademas que atienda peticiones POST. Las peticiones POST sí permiten añadir contenido en el cuerpo de la petición HTTP y así podemos probar nuestro ejemplo.
Los parámetros de la petición HTTP van en la misma URL de la siguiente forma http:127.0.0.1:5000/path?param1=value1¶m2=value2. Es decir, son param1
y param2
. Los valores son value1
y value2
. Para hacer accesibles estos parámetros y también el cuerpo de la petición HTTP, necesitamos el objeto request
del módulo Flask. Por ello, si te fijas, verás que en el import
incial hemos metido tanto Flask
como request
.
Los parámetros de la URL está accesibles en request.args
. Esta variable es un diccionario dict de python con las claves y valores de la URL. En el código hemos puesto un if
para saber si hay o no parámetros y en caso de que los haya, sacarlos en HTML con una lista.
En cuanto al cuerpo de la petición HTTP, está disponible en request.data
, por defecto como bytes. En el código hemos puesto un if
para ver si el cuerpo tiene contenido y mostrarlo en un párrafo HTML.
El código completo de este ejemplo lo tienes en hello-world-flask.py por si quieres arrancarlo. Para hacer la petición POST tendrás que instalarte algún plugin en el navegador que te permita hacerlo. En mi caso, con el navegador Google Chrome, utilizo el plugin Tabbed Postman - REST client.
La siguiente figura muestra la petición y la respuesta. Fíjate que es una petición POST, que tiene parámetros y body y cómo muestra el resultado HTML.
Web Service REST con JSON[editar]
Ahora que sabemos lo básico de Flask, vamos a hacer el web service. La aplicación Flask es básicamente la misma, pero las funciones que debemos definir cambian un poco, según qué web services queramos publicar. El ejemplo completo lo tienes en rest-flask.py
Aunque aquí no lo vamos a mencionar, porque no es necesario instalarlo, en el código de ejemplo se ha instalado y usa también el módulo flask-cors. Si te bajas el ejemplo tal cual y quieres arrancarlo, necesitarás instalar flask-cors
pip install -U flask-cors
o bien, sin instalarlo, que borres el import de CORS y la llamada a CORS(app) en el código.
import necesarios y arrancar la aplicación Flask[editar]
Primero hacemos una serie de import
que vamos a necesitar y luego instanciamos la aplicación Flask.
from flask import Flask, abort, request
import json
app = Flask(__name__)
Del módulo flask necesitaremos la clase Flask
, la función abort
y los datos request
. Flask
, como hemos visto, para iniciar la aplicación y tener la anotaciones necesarias de las funciones que creemos. abort
para poder devolver códigos de error HTTP en las llamadas a nuestros web services si algo va mal. request
para acceder a los datos que nos envíen al hacer la petición como hemos comentado anteriormente.
Por otro lado, como queremos que nuestro servicio REST tenga los datos en JSON, necesitamos importar el módulo estándar de python json
para poder parsear y desparsear JSON.
Preparamos algunos datos[editar]
Para darle un poco de sentido al ejemplo, creamos una clase UserData
que tenga un par de datos: nombre y edad. Y luego una lista de usuarios con tres usuarios de ejemplo
# Datos de usuario
class UserData:
def __init__(self, name, age):
self.name = name
self.age = age
# Lista de usuarios
users = [
UserData("Juan", 11),
UserData("Pedro", 22),
UserData("Maria", 33)
]
En una aplicación más en serio, estos datos seguramente estarían en una base de datos y UserData
tendría más campos. Pero no queremos complicar el ejemplo metiendo la complejidad de una base de datos.
Petición HTTP GET[editar]
En un API REST bien construido, una petición GET a una url como "/users" debería devolvernos la lista de usuarios. El código para conseguir esto es el siguiente
# Metodo GET para obtener la lista de todos los usuarios.
@app.get("/users")
def get_users():
encoder = json.JSONEncoder()
result = []
for user in users:
result.append(user.__dict__)
return encoder.encode(result)
Hemos creado la función get_users()
. Para que Flask la levante como web service, le ponemos la anotración @app.get()
. Con get()
indicamos que la petición es de tipo GET y como parámetro pasamos la url donde queremos que la publique "/users". En este caso hemos usado $app.get()
en vez de $app.request()
como habíamos visto en los ejemplos básicos.
La función debe devolver un JSON válido que represente la lista de usuarios. Lo devolvemos usando JSONEncoder
para convertir nuestra lista de usuarios user_data
en un texto JSON. JSONEncoder
sabe convertir a JSON listas y diccionarios de valores simples como string, enteros, etc. JSONEncoder
no es capaz de convertir a JSON una clase, en concreto UserData
. Afortunadamente, user.__dict__
tiene una representación de la misma como diccionario dict que JSONEncoder
sí sabe convertir.
Así que creamos una lista temporal result
vacía. Hacemos un bucle para recorrer todos los usuarios y para cada usuario, cogemos su diccionario user.__dict__
y lo añadimos a la lista temporal result
. Una vez hecho esto, con encoder.encode()
la convertimos en JSON y la devolvemos.
Para probar esto, si una vez arrancada la aplicación, escribimos en el navegador la url http://127.0.0.1:5000/users, obtendríamos el siguiente resultado
[{"name": "Juan", "age": 11}, {"name": "Pedro", "age": 22}, {"name": "Maria", "age": 33}]
Petición HTTP GET con un identificador en el path[editar]
Vamos ahora a obtener un usuario concreto. En una petición REST suele ser habitual hacerlo con una petición GET y usar la URL de la lista añadiendo el identificador del usuario concreto. Algo como "/users/<idx>" donde <idx>
es el identificador único del usuario. En nuestro caso, será un entero y será el índice de la lista users
. El código es el siguiente
# Metodo GET para obtener un usuario concreto por indice
@app.get("/users/<idx>")
def get_user(idx):
try:
user = users[int(idx)]
return user.__dict__
except IndexError:
traceback.print_exc()
abort(404)
except ValueError:
traceback.print_exc()
abort(400)
except Exception:
traceback.print_exc()
raise
En la anotación @app.get()
ponemos de URL "/users/<idx>". Como hemos visto en el ejemplo sencillo, esto hará que tengamos accesible como parámetro de nuestra función una variable idx
con el identificador. Si la petición dice "/users/2", en idx
tendríamos un string "2". Flask permite que lo pongamos así $app.get("/users/<int:idx>")
y de esta forma haría él la conversión autmática a entero.
Cogemos el usuario de la lista users
usando idx
como índice de la lista. Hemos comentado que idx
viene como un string "2", por eso debemos convertirlo a entero con int(idx)
.
Puesto que puede haber varios errores, como que el indice idx
que nos pasan estén fuera de rango o incluso que escriban "/users/abc" que no se puede convertir a entero, ponemos todo este código en un try
para tratar los posibles errores.
IndexError
es que el índice está fuera de rango. Debemos devolver un eror 404 que quiere decir que ese usuario no existe. La funciónabort()
se encarga de devolver el código de error que le digamos y termina la ejecución de esta función.ValueError
es si el índice que nos pasan no es un entero. Se devuelve un error 400 que quiere decir que la petición que nos hacen es incorrecta.Excepcionz
si ocurre cualquier otra excepción. Como no parece que sea algo procedente la petición que nos han hecho, sacamos la excepción por pantalla y relanzamos la excepción conraise
. Esto hará que Flask la capture y devuelva un error 500: error interno del servidor.
En todos ellos hacemos una llamada a traceback.print_exc()
. Esta llamada al estar dentro de un except
coge la excepción que se haya producido y saca por pantalla toda la traza del error. Esto nos ayudará a depurar errores.
Por supuesto, todo este tratamiento de errores es solo un ejemplo tratando de cumplir las buenas costumbres REST. Tú puedes implementarlo según tus necesidades o gustos.
Petición HTTP DELETE[editar]
Para borar un usuario, las buenas costumbres REST indican que debemos poner la URL concreta del usuario "/users/<idx>" y hacer una petición HTTP DELETE. El código sería este
# Metodo DELETE para borrar un usuario concreto por indice
@app.delete("/users/<idx>")
def delete_user(idx):
try:
del users[int(idx)]
return "", 204
except (IndexError, ValueError):
traceback.print_exc()
abort(400)
except Exception:
traceback.print_exc()
raise
Poco hay que no hayamos comentado ya en la petición GET anterior. No obatante, comentamos un par de detalles. Por supuesto, la anotación es @app.delete()
en vez de @app.get()
. También hemos juntado las excepciones IndexError
y ValueError
en el mismo tratamiento. Devolvemos ahí un error 400 que indica que la petición es incorrecta.
Y sí hay una nueva cosa de interés. Una petición DELETE correcta debe devolver un código 204 que significa que la petición ha ido bien, pero no devuelve ningún dato como resultado. La forma de hacer esto es con return "", 204
. El contenido que se devuelve es "" y además el código 204. Podemos usar esta forma en cualquier return de estas funciones web service si el código por defecto no nos vale.
Si quieres probar esta llamada, tendrás que hacerlo desde la extension Postman que hemos comentado antes, ya que un navegador estándar no permite hacer peticiones HTTP DELETE fácilmente.
Petición HTTP POST[editar]
Para crear un usuario nuevo, las buenas costumbres REST indican que debemos usar una petición POST y en el cuerpo de la petición poner un JSON con los datos del usuario. El código sería el siguiente:
# Peticion POST para añadir un nuevo usuario
@app.post("/users")
def add_user():
try:
new_user_dict = request.get_json()
new_user = UserData(new_user_dict["name"], int(new_user_dict["age"]))
users.append(new_user)
return new_user.__dict__, 201
except json.JSONDecodeError:
traceback.print_exc()
abort(400)
except Exception:
traceback.print_exc()
raise
La anotación es @app.post()
y pasamos como url la de la lista de usuario "/users". En request.data
tenemos el contenido de la petición que nos haya hecho el navegador. Este contenido debería ser el JSON del nuevo usuario que se quiere añadir. Viene como array de bytes, pero la llamada request.get_json()
nos devuelve un dict con el contenido del JSON. Para que esta llamada funcione correctamente, es imprescindible que la cabecera de la petición HTTP indique que el contenido es JSON con Content-Type: application/json
. Guardamos el dict obtenido en la variable new_user_dict
. Instanciamos la clase UserData
pasando nombre y edad como parámetros. Metemos este usuario recién creado en la lista users
usando el método append()
que lo añade al final. Y si todo va bien devolvemos el usuario recién creado como JSON y el código 201. El código 201 indica que se ha creado.
Por supuesto, hay cosas que puede ir mal, así que hacemos tratamiento de excepciones. json.JSONDecodeError
saltará si el JSON que nos pasan desde el navegador no es correcto. Devolvemos un error 400 que significa petición incorrecta. Cualquier otro error consideramos que es un error 500 interno del servidor.
Nuevamente, para probar este ejemplo, deberás hacerlo con la extensión Postman de Google Chrome. En el body de la petición deberás meter un JSON válido con los datos del usuario que quieras crear. Esta sería la captura del ejemplo cuando lo he probado
Petición HTTP PUT[editar]
Para actualizar los datos de un usuario se debe hacer una petición HTTP PUT poniendo como datos de la petición el usuario a poner. La URL debe ser la del usuario que se quiere actualizar "/users/<idx>". El código es el siguiente
# Peticion PUT para modificar un usuario por indice.
@app.put("/users/<idx>")
def put_user(idx):
try:
new_user_dict = request.get_json()
new_user = UserData(new_user_dict["name"], int(new_user_dict["age"]))
users[int(idx)] = new_user
return new_user.__dict__, 200
except (IndexError, ValueError, json.JSONDecodeError):
traceback.print_exc()
abort(400)
except Exception:
traceback.print_exc()
raise
El código es muy similar al de la petición POST. Devolvemos el usuario modificado con un código 200 que significa que todo ha ido bien. Como tratmaiento de errores, si el índice está fuera de rango IndexError
, si lo que ponen no se puede convertir a índice entero ValueError
o el JSON que nos pasan no es correcto JSONDecodeError
, devolvemos un error 400. Cualquier otro error, un error 500 internos del servidor.
La prueba, ya sabes, con la extensión Postman de Google Chrome.