Web Services REST con Python y Flask

De ChuWiki

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&param2=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

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ón abort() 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 con raise. 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.