Construyendo una API Rest con Bottle Framework
Publicado: 2022-03-11Las API REST se han convertido en una forma común de establecer una interfaz entre back-end y front-end web, y entre diferentes servicios web. La simplicidad de este tipo de interfaz y el soporte omnipresente de los protocolos HTTP y HTTPS en diferentes redes y marcos lo convierten en una opción fácil cuando se consideran problemas de interoperabilidad.
Bottle es un marco web minimalista de Python. Es ligero, rápido y fácil de usar, y se adapta bien a la creación de servicios RESTful. Una comparación básica realizada por Andriy Kornatskyy lo colocó entre los tres mejores marcos en términos de tiempo de respuesta y rendimiento (solicitudes por segundo). En mis propias pruebas en los servidores virtuales disponibles de DigitalOcean, descubrí que la combinación de la pila del servidor uWSGI y Bottle podía lograr una sobrecarga de tan solo 140 μs por solicitud.
En este artículo, proporcionaré un tutorial sobre cómo crear un servicio de API RESTful usando Bottle.
Instalacion y configuracion
El marco de la botella logra su impresionante rendimiento en parte gracias a su peso ligero. De hecho, toda la biblioteca se distribuye como un módulo de un solo archivo. Esto significa que no es tan útil como otros marcos, pero también es más flexible y se puede adaptar para encajar en muchas pilas tecnológicas diferentes. Por lo tanto, Bottle es más adecuado para proyectos en los que el rendimiento y la personalización son primordiales, y donde las ventajas de ahorro de tiempo de los marcos más resistentes son menos importantes.
La flexibilidad de Bottle hace que una descripción detallada de la configuración de la plataforma sea un poco inútil, ya que es posible que no refleje su propia pila. Sin embargo, es apropiado aquí una descripción general rápida de las opciones y dónde obtener más información sobre cómo configurarlas:
Instalación
Instalar Bottle es tan fácil como instalar cualquier otro paquete de Python. Tus opciones son:
- Instálelo en su sistema usando el administrador de paquetes del sistema. Debian Jessie (estable actual) empaqueta la versión 0.12 como python-bottle .
- Instálelo en su sistema utilizando el índice de paquetes de Python con la
pip install bottle
. - Instalar en un entorno virtual (recomendado).
Para instalar Bottle en un entorno virtual, necesitará las herramientas virtualenv y pip . Para instalarlos, consulte la documentación de virtualenv y pip, aunque probablemente ya los tenga en su sistema.
En Bash, crea un entorno con Python 3:
$ virtualenv -p `which python3` env
La supresión del parámetro -p `which python3`
conducirá a la instalación del intérprete de Python predeterminado presente en el sistema, generalmente Python 2.7. Se admite Python 2.7, pero este tutorial asume Python 3.4.
Ahora activa el entorno e instala Bottle:
$ . env/bin/activate $ pip install bottle
Eso es todo. La botella está instalada y lista para usar. Si no está familiarizado con virtualenv o pip , su documentación es de primera categoría. ¡Echar un vistazo! Valen la pena.
Servidor
Bottle cumple con la interfaz de puerta de enlace del servidor web (WSGI) estándar de Python, lo que significa que se puede usar con cualquier servidor compatible con WSGI. Esto incluye uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine y otros.
La forma correcta de configurarlo varía ligeramente con cada entorno. Bottle expone un objeto que se ajusta a la interfaz WSGI y el servidor debe estar configurado para interactuar con este objeto.
Para obtener más información sobre cómo configurar su servidor, consulte los documentos del servidor y los documentos de Bottle, aquí.
Base de datos
Bottle es independiente de la base de datos y no le importa de dónde provienen los datos. Si desea utilizar una base de datos en su aplicación, Python Package Index tiene varias opciones interesantes, como SQLAlchemy, PyMongo, MongoEngine, CouchDB y Boto para DynamoDB. Solo necesita el adaptador adecuado para que funcione con la base de datos de su elección.
Conceptos básicos del marco de la botella
Ahora, veamos cómo hacer una aplicación básica en Bottle. Para ejemplos de código, asumiré Python >= 3.4. Sin embargo, la mayor parte de lo que escribiré aquí también funcionará en Python 2.7.
Una aplicación básica en Bottle se ve así:
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
Cuando digo básico, me refiero a que este programa ni siquiera te dice "Hello World". (¿Cuándo fue la última vez que accedió a una interfaz REST que respondió "Hello World?") Todas las solicitudes HTTP a 127.0.0.1:8000
recibirán un estado de respuesta 404 No encontrado.
Aplicaciones en botella
Bottle puede tener varias instancias de aplicaciones creadas, pero por conveniencia, la primera instancia se crea para usted; esa es la aplicación predeterminada. Bottle mantiene estas instancias en una pila interna al módulo. Cada vez que hace algo con Bottle (como ejecutar la aplicación o adjuntar una ruta) y no especifica de qué aplicación está hablando, se refiere a la aplicación predeterminada. De hecho, la línea app = application = bottle.default_app()
ni siquiera necesita existir en esta aplicación básica, pero está ahí para que podamos invocar fácilmente la aplicación predeterminada con Gunicorn, uWSGI o algún servidor WSGI genérico.
La posibilidad de múltiples aplicaciones puede parecer confusa al principio, pero añaden flexibilidad a Bottle. Para diferentes módulos de su aplicación, puede crear aplicaciones de Bottle especializadas instanciando otras clases de Bottle y configurándolas con diferentes configuraciones según sea necesario. Se puede acceder a estas diferentes aplicaciones mediante diferentes URL, a través del enrutador de URL de Bottle. No profundizaremos en eso en este tutorial, pero le animamos a echar un vistazo a la documentación de Bottle aquí y aquí.
Invocación del servidor
La última línea del script ejecuta Bottle usando el servidor indicado. Si no se indica ningún servidor, como es el caso aquí, el servidor predeterminado es el servidor de referencia WSGI incorporado de Python, que solo es adecuado para fines de desarrollo. Se puede usar un servidor diferente así:
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
Este es azúcar sintáctico que le permite iniciar la aplicación ejecutando este script. Por ejemplo, si este archivo se llama main.py
, simplemente puede ejecutar python main.py
para iniciar la aplicación. Bottle tiene una lista bastante extensa de adaptadores de servidor que se pueden usar de esta manera.
Algunos servidores WSGI no tienen adaptadores de botella. Estos se pueden iniciar con los propios comandos de ejecución del servidor. En uWSGI, por ejemplo, todo lo que tendría que hacer sería llamar a uwsgi
así:
$ uwsgi --http :8000 --wsgi-file main.py
Una nota sobre la estructura de archivos
Bottle deja la estructura de archivos de su aplicación totalmente en sus manos. Descubrí que mis políticas de estructura de archivos evolucionan de un proyecto a otro, pero tienden a basarse en una filosofía MVC.
Construyendo su API REST
Por supuesto, nadie necesita un servidor que solo devuelva 404 para cada URI solicitado. Te prometí que crearíamos una API REST, así que hagámoslo.
Suponga que desea crear una interfaz que manipule un conjunto de nombres. En una aplicación real, probablemente usaría una base de datos para esto, pero para este ejemplo solo usaremos la estructura de datos del set
en memoria.
El esqueleto de nuestra API podría verse así. Puede colocar este código en cualquier parte del proyecto, pero mi recomendación sería un archivo API separado, como api/names.py
.
from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass
Enrutamiento
Como podemos ver, el enrutamiento en Bottle se realiza mediante decoradores. Los decoradores importados post
, get
, put
y delete
controladores de registros para estas cuatro acciones. Comprender cómo funcionan estos se puede desglosar de la siguiente manera:
- Todos los decoradores anteriores son un acceso directo a los decoradores de enrutamiento
default_app
. Por ejemplo, el decorador@get()
aplicabottle.default_app().get()
al controlador. - Los métodos de enrutamiento en
default_app
son todos atajos pararoute()
. Entoncesdefault_app().get('/')
es equivalente adefault_app().route(method='GET', '/')
.
Entonces @get('/')
es lo mismo que @route(method='GET', '/')
, que es lo mismo que @bottle.default_app().route(method='GET', '/')
, y estos se pueden usar indistintamente.
Una cosa útil sobre el decorador @route
es que si desea, por ejemplo, usar el mismo controlador para tratar tanto las actualizaciones como las eliminaciones de objetos, puede simplemente pasar una lista de métodos que maneja de esta manera:
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
Muy bien, entonces, implementemos algunos de estos controladores.
POST: Creación de recursos
Nuestro controlador POST podría verse así:
import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})
Bueno, eso es bastante. Revisemos estos pasos parte por parte.
análisis corporal
Esta API requiere que el usuario publique una cadena JSON en el cuerpo con un atributo llamado "nombre".

El objeto de request
importado anteriormente de bottle
siempre apunta a la solicitud actual y contiene todos los datos de la solicitud. Su atributo de body
contiene un flujo de bytes del cuerpo de la solicitud, al que se puede acceder mediante cualquier función que pueda leer un objeto de flujo (como leer un archivo).
El método request.json()
verifica los encabezados de la solicitud para el tipo de contenido "aplicación/json" y analiza el cuerpo si es correcto. Si Bottle detecta un cuerpo con formato incorrecto (p. ej., vacío o con un tipo de contenido incorrecto), este método devuelve None
y, por lo tanto, generamos un ValueError
. Si el analizador JSON detecta contenido JSON con formato incorrecto; genera una excepción que detectamos y volvemos a aumentar, nuevamente como ValueError
.
Análisis y validación de objetos
Si no hay errores, hemos convertido el cuerpo de la solicitud en un objeto de Python al que hace referencia la variable de data
. Si recibimos un diccionario con una clave de "nombre", podremos acceder a él a través data['name']
. Si recibimos un diccionario sin esta clave, intentar acceder a él nos llevará a una excepción KeyError
. Si recibimos algo más que un diccionario, obtendremos una excepción TypeError
. Si ocurre alguno de estos errores, una vez más, lo volvemos a generar como ValueError
, lo que indica una entrada incorrecta.
Para verificar si la clave de nombre tiene el formato correcto, debemos probarla con una máscara de expresiones regulares, como la máscara de namepattern
de nombre que creamos aquí. Si el name
de la clave no es una cadena, namepattern.match()
generará un TypeError
y, si no coincide, devolverá None
.
Con la máscara en este ejemplo, un nombre debe ser un ASCII alfanumérico sin espacios en blanco de 1 a 64 caracteres. Esta es una validación simple y no prueba un objeto con datos basura, por ejemplo. Se puede lograr una validación más compleja y completa mediante el uso de herramientas como FormEncode.
Prueba de existencia
La última prueba antes de cumplir con la solicitud es si el nombre de pila ya existe en el conjunto. En una aplicación más estructurada, esa prueba probablemente debería ser realizada por un módulo dedicado y señalada a nuestra API a través de una excepción especializada, pero dado que estamos manipulando un conjunto directamente, tenemos que hacerlo aquí.
Señalamos la existencia del nombre generando un KeyError
.
Respuestas de error
Así como el objeto de solicitud contiene todos los datos de solicitud, el objeto de respuesta hace lo mismo con los datos de respuesta. Hay dos formas de establecer el estado de respuesta:
response.status = 400
y:
response.status = '400 Bad Request'
Para nuestro ejemplo, optamos por la forma más simple, pero la segunda forma puede usarse para especificar la descripción del texto del error. Internamente, Bottle dividirá la segunda cadena y establecerá el código numérico de manera adecuada.
Respuesta de éxito
Si todos los pasos son exitosos, cumplimos con la solicitud agregando el nombre al conjunto _names
, configurando el encabezado de respuesta Content-Type
y devolviendo la respuesta. Cualquier cadena devuelta por la función se tratará como el cuerpo de respuesta de una respuesta 200 Success
, por lo que simplemente generamos una con json.dumps
.
OBTENER: Listado de recursos
Pasando de la creación del nombre, implementaremos el controlador de listado de nombres:
@get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})
Enumerar los nombres fue mucho más fácil, ¿no? En comparación con la creación de nombres, no hay mucho que hacer aquí. Simplemente configure algunos encabezados de respuesta y devuelva una representación JSON de todos los nombres, y listo.
PUT: actualización de recursos
Ahora, veamos cómo implementar el método de actualización. No es muy diferente del método de creación, pero usamos este ejemplo para introducir los parámetros de URI.
@put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})
El esquema del cuerpo para la acción de actualización es el mismo que para la acción de creación, pero ahora también tenemos un nuevo parámetro de oldname
en el URI, como lo define la ruta @put('/names/<oldname>')
.
Parámetros de URI
Como puede ver, la notación de Bottle para los parámetros URI es muy sencilla. Puede crear URI con tantos parámetros como desee. Bottle los extrae automáticamente del URI y los pasa al controlador de solicitudes:
@get('/<param1>/<param2>') def handler(param1, param2): pass
Usando decoradores de rutas en cascada, puede crear URI con parámetros opcionales:
@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass
Además, Bottle permite los siguientes filtros de enrutamiento en URI:
-
int
Hace coincidir solo los parámetros que se pueden convertir a
int
y pasa el valor convertido al controlador:@get('/<param:int>') def handler(param): pass
-
float
Lo mismo que
int
, pero con valores de punto flotante:@get('/<param:float>') def handler(param): pass
-
re
(expresiones regulares)
Coincide solo con parámetros que coinciden con la expresión regular dada:
@get('/<param:re:^[az]+$>') def handler(param): pass
-
path
Coincide con subsegmentos de la ruta URI de una manera flexible:
@get('/<param:path>/id>') def handler(param): pass
Partidos:
/x/id
, pasandox
comoparam
./x/y/id
, pasandox/y
comoparam
.
ELIMINAR: Eliminación de recursos
Al igual que el método GET, el método DELETE nos trae pocas novedades. Solo tenga en cuenta que devolver None
sin establecer un estado devuelve una respuesta con un cuerpo vacío y un código de estado 200.
@delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return
Paso final: activación de la API
Suponiendo que hayamos guardado nuestra API de nombres como api/names.py
, ahora podemos habilitar estas rutas en el archivo principal de la aplicación main.py
import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
Tenga en cuenta que solo hemos importado el módulo de names
. Dado que hemos decorado todos los métodos con sus URI adjuntos a la aplicación predeterminada, no es necesario realizar ninguna configuración adicional. Nuestros métodos ya están en su lugar, listos para ser accedidos.
Puede usar herramientas como Curl o Postman para consumir la API y probarla manualmente. (Si usa Curl, puede usar un formateador JSON para que la respuesta parezca menos desordenada).
Bonificación: Intercambio de recursos de origen cruzado (CORS)
Una razón común para crear una API REST es comunicarse con un front-end de JavaScript a través de AJAX. Para algunas aplicaciones, se debe permitir que estas solicitudes provengan de cualquier dominio, no solo del dominio principal de su API. De forma predeterminada, la mayoría de los navegadores no permiten este comportamiento, así que déjame mostrarte cómo configurar el uso compartido de recursos de origen cruzado (CORS) en Bottle para permitir esto:
from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return
El decorador de hook
nos permite llamar a una función antes o después de cada solicitud. En nuestro caso, para habilitar CORS debemos configurar los encabezados Access-Control-Allow-Origin
, -Allow-Methods
y -Allow-Headers
para cada una de nuestras respuestas. Estos indican al solicitante que atenderemos las solicitudes indicadas.
Además, el cliente puede realizar una solicitud HTTP de OPCIONES al servidor para ver si realmente puede realizar solicitudes con otros métodos. Con este ejemplo general, respondemos a todas las solicitudes de OPCIONES con un código de estado 200 y un cuerpo vacío.
Para habilitar esto, simplemente guárdelo e impórtelo desde el módulo principal.
Envolver
¡Eso es todo al respecto!
Con este tutorial, he tratado de cubrir los pasos básicos para crear una API REST para una aplicación de Python con el marco web de Bottle.
Puede profundizar su conocimiento sobre este marco pequeño pero poderoso visitando su tutorial y los documentos de referencia de la API.