latest

Diseño y Desarrollo de una API REST en Python. Parte 1.

Con esta receta pretendemos introducirte en el maravilloso mundo de las Web APIs. Hoy en día, consultar determinada información de un servidor remoto o el propio proceso de intercambio de datos entre diferentes servidores se realiza comúnmente empleando Web APIs. Prácticamente la totalidad de las Web apps y aplicaciones móviles que usas cada día utiliza una Web API en su backend para implementar sus funcionalidades, desde añadir y guardar productos en el carrito de compra en Amazon, hasta consultar el mapa de tu ciudad. Las Web APIs son un mecanismo fundamental para permitir que diferente software corriendo en la misma o en plataformas distintas pueda comunicarse entre sí.    

Al final de esta receta serás capaz de:

  • Comprender los conceptos de Web API y Web Service.
  • Distinguir las principales diferencias entre un Web Service de tipo SOAP y de tipo REST.
  • Contrastar los principios de diseño de una Web API de tipo REST frente a las RPCs.
  • Ventajas de la programación Web usando frameworks.
  • Diseñar y desarrollar tu propia API REST en Python.

Antes de entrar en profundidad en el diseño y desarrollo de tu flamante Web API debemos hacer un inciso para introducir qué es y en qué consiste un Web Service.

Los Web Services, o servicios Web en español, son esencialmente puntos de conexión que permiten la integración entre aplicaciones alojadas en plataformas que pueden ser distintas (o no), correr diferentes sistemas operativos y estar implementadas en diferentes lenguajes de programación. Los servicios Web se comunican entre sí a través de la WWW y requieren dos partes:    

  1. Una parte (servidor) que pone al descubierto una serie de Web APIs.
  2. Y una segunda parte (cliente) que invoca/consume estas Web APIs del servidor.

Los servicios Web son independientes de la implementación de la parte cliente, que puede ser un navegador Web, una aplicación móvil o una aplicación de escritorio capaz de hacer llamadas a una/varias Web APIs. Los servicios Web se pueden clasificar atendiendo a los protocolos de comunicación utilizados:

  • SOAP (Simple Object Access Protocol).
  • HTTP REST (Representation State Transfer).
Arquitectura Orientada a Servicios (SOA). Tipos principales.

Los servicios de tipo SOAP funcionan sobre protocolos HTTP, FTP, POP3, TCP, colas de mensajería (JMS, MQ, etc.), si bien, normalmente se invocan sobre protocolo HTTP. Otra particularidad de los servicios SOAP es que sólo permiten el intercambio de datos empleando el metalenguaje XML.

Por su parte, los servicios de tipo REST transportan datos únicamente por medio del protocolo HTTP, pero permiten utilizar los diversos métodos que proporciona HTTP para comunicarse (GET, POST, PUT, PATCH, DELETE) y a la vez, utilizan los códigos de respuesta nativos de HTTP (404, 200, 204, 409). REST es más flexible que SOAP y permite transmitir varios tipo de datos (no sólo XML). El tipo de datos está definido por la cabecera de HTTP Content-Type, lo que nos permite intercambiar, XML, JSON, datos binarios, texto plano, etc.  En base a esto, los formatos de los mensajes de intercambio usados con más frecuencia por los servicios Web son:

  • JSON (Javascript Object Notation) -> No disponible en los servicios SOAP.
  • XML (eXtensible Markup Language).

En esta receta nos centraremos en las Web APIs de tipo (HTTP) REST, aprendiendo  a desarrollar un conjunto de servicios RESTful (que respetan la arquitectura REST) empleando el lenguaje Python. Las Web APIs de tipo REST (o REST APIs) han conseguido una gran popularidad en la comunidad de desarrollo convirtiéndose en la arquitectura predominante para diseñar e implementar servicios Web.

Los servicios Web componen el núcleo básico de funcionamiento de las aplicaciones Web. Las Web APIs son los mecanismos de integración entre dichos servicios Web, estando desplegadas sobre aplicaciones del mismo o de otros servidores/servicios; y a su vez pudiendo éstos actuar como clientes consumiendo información de otras Web APIs.

Se requiere que la implementación de servicios Web sea escalable, al tiempo que los servicios proporcionan un funcionamiento adecuado y fiable.


Arquitectura REST

La arquitectura REST está pensada para ajustarse al protocolo HTTP empleando sustantivos/recursos (denominados URIs - Identificador de Recursos Uniforme)  y un conjunto reducido de verbos que se corresponde con los métodos de HTTP (GET, POST, PUT, PATCH, DELETE). Este enfoque difiere notablemente del adoptado por otros mecanismos de comunicación entre máquinas para la ejecución de código remoto, como es el caso de las RPCs (Remote Procedure Calls - Llamadas a Procedimientos Remotos) que ponen énfasis en la diversidad de operaciones (verbos) del protocolo. Por ejemplo, una aplicación basada en RPC podría definir operaciones como las expuestas a continuación para gestionar un conjunto de usuarios y localizaciones/ubicaciones:

    findUser(), getUser(), addUser(), 
    removeUser(), updateUser(), getLocation(), 		
    addLocation(), removeLocation(), updateLocation(), 
    listUsers(), listLocations(), findLocation()

En cambio, una API REST, definiría los tipos de recurso siguientes:

    user {}
    location {}

Cada recurso particular tiene su propio identificador (e.j., http://www.example.org/locations/es/madrid/getafe) lo que facilita el seguimiento de dicho recurso.

Las aplicaciones cliente consumen los recursos mediante los métodos estándar de HTTP, como GET para descargar una copia de un recurso en particular. POST se utiliza, por lo  general, para acciones con efectos laterales, como enviar una orden de compra o añadir nuevos recursos a una colección.

Partiendo del mismo ejemplo, un usuario podría representarse como el recurso expuesto abajo. Donde los datos, para este caso particular, están en formateados en XML.

  <user>  
      <name>Frida Cruz</name>
      <gender>femenino</gender>  
      <location href="http://www.example.org/locations/es/madrid/getafe">
            Getafe, Madrid, ES
      </location> 
  </user>

Para actualizar la localización de este usuario, una aplicación cliente de la API REST podría primero descargar el registro XML anterior usando GET. Después, modificaría el fichero para cambiar la localización y lo subiría al servidor utilizando el método PUT.

En este punto, cabe señalar que los métodos/verbos HTTP no proporcionan mecanismos estándar para descubrir recursos, es decir, no hay ninguna operación LIST o FIND en HTTP similar a las operaciones list*() y find*() del ejemplo RPC anterior.  En su lugar, las APIs de tipo REST resuelven este problema tratando una colección de resultados de búsqueda como otro tipo de recurso, lo que requiere conocer las URLs adicionales para mostrar o buscar cada tipo de recurso concreto.

Por ejemplo, una petición GET HTTP sobre la URL http://www.example.org/locations/es/madrid/ podría devolver un enlace a una lista en XML con todas las localizaciones posibles en Madrid, mientras que una petición GET a la URL http://www.example.org/users?surname=Rodríguez podría devolver una lista de enlaces a todos los usuarios con apellido "Rodríguez".


Creando tu propia API REST

Para desplegar la API REST vamos a usar un micro framework escrito en Python denominado Flask, concebido para facilitar el desarrollo de aplicaciones web bajo el patrón MVC (Modelo-Vista-Controlador) de manera sencilla (con una curva de aprendizaje mucho menor que la de otros frameworks mayores como Django) y empleando un número de módulos/extensiones/plugins mínimo (no full-stack).

Por si te encuentras un poco despistado sobre qué es un framework, para qué se usa o qué ventajas proporciona te damos unas breves nociones:

  • En el desarrollo moderno de aplicaciones Web se utilizan diferentes frameworks que son herramientas que ofrecen un esquema de trabajo (generalmente basado en el patrón MVC) y una serie de utilidades y funciones para facilitar y abstraer de tareas de bajo nivel en la construcción de páginas web dinámicas.
  • Estos frameworks están asociados a un lenguaje de programación: Symphony (PHP), Ruby on Rails (Ruby), Django (Python) o en el caso que nos ocupa, Flask (Python).
  • El proyecto asociado a la aplciación Web mantiene una estructura homogénea (mismos elementos, mismos ficheros).
  • Facilita la colaboración entre desarrolladores y diseñadores.
  • Fácil encontrar plugins y otras librerías adaptadas al framework usado.
  • Ventajas particulares de usar Flask:
    • Partes de un desarrollo mínimo con los módulos/extensiones esenciales (no full-stack, si requieres nuevas funcionalidades se van añadiendo según se necesiten).
    • Incluye un servidor web de desarrollo (no se necesita una infraestructura con un servidor web para las pruebas antes del despliegue).
    • Depurador incluido y soporte para pruebas unitarias.
    • Gestión de rutas eficiente. Determinar que ruta está solicitando el cliente a través de su petición para ejecutar el codigo necesario.
    • Soporte nativo para el uso de cookies seguras.
    • Open Source.
    • Documentación abundante.

Tras este breve paseo para remarcar las virtudes de los frameworks destinados al desarrollo Web y tras señalar algunas de las virtudes de Flask, por fin, vamos a pasar a la acción...

Pasemos a diseñar e implementar un servicio Web para la gestión de usuarios, que es sin duda un requisito necesario en el contexto de uso de cualquier aplicación Web o cliente móvil con servicios en la Nube.

  1. Lo primero es modelar el recurso asociado a un "usuario". Al tratarse de un ejemplo conceptual sólo vamos a considerar los siguientes atributos en esta primera parte de la receta:

    • id: Identificador único para el usuario.
    • nombreUsuario: El nombre de usuario usado en la aplicación Web, móvil, etc.
    • email: La cuenta de e-mail del usuario para tareas de notificación.
    • status/activo: Por ejemplo, para conocer si el usuario está online/offline.
  2. El diseño de una API REST implica identificar los recursos URIs y los verbos (métodos HTTP) asociados que intervienen para modelar el recurso de tipo "usuario". Se requieren acciones como: crear un nuevo usuario, actualizar atributos de un usuario existente, obtener la información de un determinado usuario, listar todos los usuarios o borrar un usuario determinado. Por tanto, hay que relacionar las acciones anteriores con los métodos HTTP (GET, POST, PUT, PATCH, DELETE), definiendo las operaciones CRUD (Create, Read, Update, Delete) para nuestro modelo de "usuario". La tabla siguiente ejemplifica esto de manera visual:

    URI Método HTTP Acción
    /v1/usuarios/ GET Obtener una lista de todos los usuarios
    /v1/usuarios/ POST Crear un nuevo usuario
    /v1/usuarios/1 GET Obtener la información de un usuario existente cuyo identificador es 1
    /v1/usuarios/1 PUT/DELETE Actualizar o eliminar el usuario cuyo identificador es 1
  3. Para comenzar con la implementación del servicio primero vamos a configurar un entorno virtual con la herramienta virtualenv. Un entorno virtual nos permite crear una configuración aislada de trabajo con un determinado intérprete Python (podemos elegir una versión concreta), junto con una serie de módulos de Python que consideremos oportuno. Se pueden tener varios entornos distintos en la misma máquina, instalando en cada entorno los módulos necesarios sin que unos afecten a los otros.
    Usar un entorno virtual evita contaminar o llenar de basura la instalación global de Python y los conflictos de diferentes versiones del mismo módulo entre aplicaciones, así como otros problemas relacionados con permisos. Podemos instalar la herramienta virtualenv con el gestor de paquetes en distribuciones Linux o de manera más general con la herramienta pip:

     $ pip install virtualenv

    virtualenv guarda cada entorno virtual en un directorio con el nombre de ese entorno. Para crearlo ejecutamos:

     $ virtualenv DIRECTORIO-DEL-ENTORNO-VIRTUAL

    Por ejemplo:

     $ virtualenv /home/gilmour/entornoVirtual1
  4. Una vez instalado virtualenv, se activa ejecutando el script "activate" dentro del directorio bin del entorno virtual:

     $ source /home/gilmour/entornoVirtual1/bin/activate
     
     (entornoVirtual1):~/entornoVirtual1/bin$
  5. Ahora instalamos Flask en el entorno virtual configurado ("entornoVirtual1") empleando pip:

     (entornoVirtual1):~/entornoVirtual1$ pip install flask

    Si observas el registro de instalación que se muestra en la terminal puedes comprobar que Flask se instala junto con Jinja2, un motor de plantillas (templates) escrito en Python, el cual permite insertar datos procesados (contexto) como texto predeterminado dentro de los archivos/plantillas HTML que conforman la vista del patrón MVC.
    También se instala Werkzeug, una colección de varias utilidades para aplicaciones WSGI (Web Server Gateway Interface) sobre la que funciona Flask. WSGI establece una convención para los servidores Web acerca de cómo manejar las peticiones y redireccionar las respuestas hacia aplicaciones Web y frameworks escritos en Python. Entre otras utilidades, incluye un debugger, un manejador de cookies y sesiones, un sistema para el enrutado (routing) de URLs, una utilidad para gestionar las subidas de archivos, herramientas para el control de las opciones de caché, de modo que se pueda especificar al navegador las políticas de caching a seguir, tanto para las peticiones de los clientes como para las respuestas del servidor. Para el caching, Werkzeug utiliza los parámetros especificados en las cabeceras HTTP de cache-control y las HTTP entity tags (Etag).
    Werkzeug es independiente del motor de templates usado, del adaptador para la base de datos, etc. No especifica o fuerza una determinada manera de manejar las peticiones de los clientes y deja esto completamente en manos del desarrollador.

    Collecting flask
       Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
         100% |████████████████████████████████| 92kB 3.7MB/s 
     Collecting Werkzeug>=0.14 (from flask)
       Downloading https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
         100% |████████████████████████████████| 327kB 13.1MB/s 
     Collecting click>=5.1 (from flask)
       Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
         100% |████████████████████████████████| 81kB 25.0MB/s 
     Collecting Jinja2>=2.10 (from flask)
       Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
         100% |████████████████████████████████| 133kB 12.0MB/s 
     Collecting itsdangerous>=0.24 (from flask)
       Downloading https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz (46kB)
         100% |████████████████████████████████| 51kB 19.3MB/s 
     Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->flask)
       Downloading https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz
     Building wheels for collected packages: itsdangerous, MarkupSafe
       Running setup.py bdist_wheel for itsdangerous ... done
       Stored in directory: /home/pinkfloyd/.cache/pip/wheels/2c/4a/61/5599631c1554768c6290b08c02c72d7317910374ca602ff1e5
       Running setup.py bdist_wheel for MarkupSafe ... done
       Stored in directory: /home/pinkfloyd/.cache/pip/wheels/33/56/20/ebe49a5c612fffe1c5a632146b16596f9e64676768661e4e46
     Successfully built itsdangerous MarkupSafe
     Installing collected packages: Werkzeug, click, MarkupSafe, Jinja2, itsdangerous, flask
     Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 click-7.0 flask-1.0.2 itsdangerous-0.24
    
  1. Con Flask perfectamente instalado y configurado en nuestro entorno virtual podemos implementar nuestra primera pequeña aplicación Web en Flask que denominaremos aplicacion.py (guardar el archivo en la raiz del entorno virtual). El código que tienes que incluir es el siguiente:

         from flask import Flask
         app = Flask(__name__)
         
         @app.route('/')
         def index():
             return "Hola soy tu primera app Web Flask"
             
         if __name__ == '__main__':
             app.run(debug=True)
  2. Al ejecutar python aplicacion.py podrás comprobar como el servidor de Flask se pone en marcha escuchando en el puerto 5000 (por defecto):

          WARNING: Do not use the development server in a production environment.
          Use a production WSGI server instead.
          * Debug mode: on
          * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
          * Restarting with stat
          * Debugger is active!
          * Debugger PIN: 439-951-258
  3. Si acccedes a la URL http://127.0.0.1:5000 comprobarás que se visualiza lo esperado, fruto de la respuesta con éxito a la petición HTTP GET con la ruta('/'):

    Aplicación Web corriendo en puerto 5000
  4. De acuerdo, vamos a avanzar un poco más para implementar la API REST. Empecemos por la petición HTTP GET para recuperar todos los usuarios. La URI asociada a ese recurso es /v1/usuarios. Necesitamos implementar una ruta para la URI que disparará la ejecución de la función getUsuarios(). Dicha función, devolverá todos los usuarios en formato JSON a partir de una estructura que es una combinación de Listas y Diccionarios en Python. Nuestro script queda de la siguiente manera:

         from flask import Flask
         from flask import jsonify
         app = Flask(__name__)
         
         usuarios = [
                 {'id':1,
                 'nombreUsuario': u'David Gilmour',
                 'email': u'gilmour@pinkfloyd.com',
                 'activo': True
                 },
                 {'id':2,
                 'nombreUsuario': u'Richard Wright',
                 'email': u'wright@pinkfloyd.com',
                 'activo': False
                 },
                 {'id':3,
                 'nombreUsuario': u'Roger Waters',
                 'email': u'waters@pinkfloyd.com',
                 'activo': True
                 }
         ]
         
         @app.route('/v1/usuarios/', methods=['GET'])
         def getUsuarios():
             return jsonify({'usuarios': usuarios})
             
         @app.route('/')
         def index():
             return "Hola soy tu primera app Web Flask"
             
         if __name__ == '__main__':
             app.run(debug=True)

    Si relanzamos la aplicación con python aplicacion.py ahora es posible realizar una petición HTTP GET a /v1/usuarios desde el mismo navegador como se muestra en la imagen inferior. Acabamos de implementar una parte de la API REST de nuestro servicio Web de gestión de usuarios.

    GET /v1/usuarios
    Si inspeccionamos la petición con la herramienta para desarrolladores del navegador podemos ver:
    • El Content-Type es de tipo application/JSON.
    • El servidor es Werkzeug (Flask está basado en Werkzeug).
    • La petición GET fue correcta por lo que se devuelve el código 200.
    • ...
    HTTP headers
  5. Vamos a continuar con otro recurso para obtener un usuario a partir de su identificador (id). Añadimos la siguiente ruta a nuestro script:

       #...
       from flask import abort
       from flask import make_response
       
       @app.route('/v1/usuarios/<int:id>/', methods=['GET'])
       def getUsuario(id):
           for usuario in usuarios:
               if usuario.get("id") == id:
                   return jsonify({'usuarios':usuario})
           abort(404)
           
       #Definimos la respuesta para el codigo de error 404
       @app.errorhandler(404)
       def not_found(error):
           return make_response(jsonify({'error': 'No encontrado'}),404)
  6. Con la ruta definida anteriormente invocamos la función getUsuario(id) pasando como argumento el identificador del usuario del cual queremos obtener información. Cuando hacemos la petición HTTP GET en la URI anterior, internamente, se recorren todos los usuarios hasta localizar el usuario con el identificador buscado. Si es encontrado se devuelve su información en formato JSON, en caso negativo, el servidor envía una respuesta HTTP 404. Si invocamos con la URL http://127.0.0.1:5000/v1/usuarios/4 obtendremos la respuesta de error HTTP 404 para los datos del ejemplo.

HTTP headers
  1. Para poder gestionar altas de usuarios en una futura aplicación cliente (e.j., aplicación Web, aplicación móvil, etc.) requerimos una nueva ruta para crear usuarios empleando el método HTTP POST. Añadimos el siguiente código en nuestro script:

       #...
       from flask import request 
       
       @app.route('/v1/usuarios/', methods=['POST'])
       def crearUsuario():
           if not request.json or not 'email' in request.json:
               abort(404)
           id = usuarios[-1].get('id') + 1
           nombreUsuario = request.json.get('nombreUsuario')
           email = request.json.get('email')
           activo = False
           usuario = {'id': id, 'nombreUsuario': nombreUsuario, 'email': email, 'activo': activo}
           usuarios.append(usuario)
           return jsonify({'usuario':usuario}),201
  2. Por ejemplo, podemos usar el comando curl en Linux para probar la petición POST pasando la información necesaria del nuevo usuario en formato JSON. Abajo se facilite un ejemplo.

     $ curl --header "Content-Type: application/json" --request POST --data '{"nombreUsuario":"Nick Mason","email":"mason@pinkfloyd.com"}' http://127.0.0.1:5000/v1/usuarios/
    

    Como puede comprobarse en el código de la función crearUsuario() el atributo email es obligatorio y, por defecto, los nuevos usuarios aparacen como "inactivos".

  3. Implementemos ahora el recurso necesario para modificar información de un usuario. Añadir en el script aplicacion.py la ruta siguiente:

       @app.route('/v1/usuarios/<int:id>/', methods=['PUT'])
       def actualizarUsuario(id):
           usuario = [usuario for usuario in usuarios if usuario['id'] == id]
           usuario[0]['nombreUsuario'] = request.json.get('nombreUsuario', usuario[0]['nombreUsuario'])
           usuario[0]['email'] = request.json.get('email', usuario[0]['email'])
           usuario[0]['activo'] = request.json.get('activo', usuario[0]['activo'])
           return jsonify({'usuarios':usuario[0]})

    Si ejecutamos la sentencia curl de abajo podemos ver como los datos del usuario con id=3 se modifican.

     $ curl --header "Content-Type: application/json" --request PUT --data '{"nombreUsuario":"Roger Waters","email":"rogerWaters@pinkfloyd.com"}' http://127.0.0.1:5000/v1/usuarios/3/
    
  4. Por último, el recurso necesario para borrar un determinado usuario a través de su identificador. Añadir en el script aplicacion.py la ruta siguiente:

        @app.route('/v1/usuarios/<int:id>/', methods=['DELETE'])
        def borrarUsuario(id):
            usuario = [usuario for usuario in usuarios if usuario['id'] == id]
            usuarios.remove(usuario[0])
            return jsonify({}), 204 # No contenido 
    

    Si ejecutamos la sentencia curl de abajo enviamos una petición HTTP para borrar el usuario con id=3:

     $ curl --request DELETE  http://127.0.0.1:5000/v1/usuarios/3/ 
    

Ejercicio:

Te animamos a implementar esta API REST para el servicio Web de gestión de usuarios.  Además, te proponemos que diseñes (e implementes) un servicio Web sencillo con otro contexto de aplicación diferente (por ejemplo, para simular la recogida de datos de una pulsera de actividad). Puedes integrar la API REST de gestión de usuarios en el nuevo servicio, si así lo deseas.

Seguiremos trabajando sobre API REST y servicios Web en las pŕoximas publicaciones de manera incremental.  

Author image
Ciudad Real