Construindo uma API Rest com o Bottle Framework

Publicados: 2022-03-11

As APIs REST tornaram-se uma maneira comum de estabelecer uma interface entre back-ends e front-ends da Web e entre diferentes serviços da Web. A simplicidade desse tipo de interface e o suporte onipresente dos protocolos HTTP e HTTPS em diferentes redes e estruturas o tornam uma escolha fácil ao considerar problemas de interoperabilidade.

Bottle é um framework web minimalista em Python. É leve, rápido e fácil de usar e é adequado para a construção de serviços RESTful. Uma comparação básica feita por Andriy Kornatskyy o colocou entre os três principais frameworks em termos de tempo de resposta e taxa de transferência (solicitações por segundo). Em meus próprios testes nos servidores virtuais disponíveis na DigitalOcean, descobri que a combinação da pilha de servidores uWSGI e do Bottle poderia atingir uma sobrecarga de até 140μs por solicitação.

Neste artigo, fornecerei um passo a passo de como criar um serviço de API RESTful usando Bottle.

Bottle: Um framework Web Python rápido e leve

Instalação e configuração

A estrutura Bottle atinge seu desempenho impressionante em parte graças ao seu peso leve. Na verdade, toda a biblioteca é distribuída como um módulo de um arquivo. Isso significa que ele não segura sua mão tanto quanto outros frameworks, mas também é mais flexível e pode ser adaptado para caber em muitas pilhas de tecnologia diferentes. O Bottle é, portanto, mais adequado para projetos em que o desempenho e a personalização são importantes e onde as vantagens de economia de tempo de estruturas mais pesadas são menos importantes.

A flexibilidade do Bottle torna uma descrição detalhada da configuração da plataforma um pouco inútil, pois pode não refletir sua própria pilha. No entanto, uma visão geral rápida das opções e onde aprender mais sobre como configurá-las é apropriada aqui:

Instalação

Instalar o Bottle é tão fácil quanto instalar qualquer outro pacote Python. Suas opções são:

  • Instale em seu sistema usando o gerenciador de pacotes do sistema. Debian Jessie (atual estável) empacota a versão 0.12 como python-bottle .
  • Instale em seu sistema usando o Python Package Index com pip install bottle .
  • Instale em um ambiente virtual (recomendado).

Para instalar o Bottle em um ambiente virtual, você precisará das ferramentas virtualenv e pip . Para instalá-los, consulte a documentação do virtualenv e do pip, embora você provavelmente já os tenha em seu sistema.

No Bash, crie um ambiente com Python 3:

 $ virtualenv -p `which python3` env

Suprimir o parâmetro -p `which python3` levará à instalação do interpretador Python padrão presente no sistema – geralmente Python 2.7. O Python 2.7 é suportado, mas este tutorial pressupõe o Python 3.4.

Agora ative o ambiente e instale o Bottle:

 $ . env/bin/activate $ pip install bottle

É isso. A garrafa está instalada e pronta para uso. Se você não estiver familiarizado com virtualenv ou pip , a documentação deles é de primeira qualidade. Dê uma olhada! Eles valem muito a pena.

Servidor

O Bottle está em conformidade com o padrão Web Server Gateway Interface (WSGI) do Python, o que significa que pode ser usado com qualquer servidor compatível com WSGI. Isso inclui uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine e outros.

A maneira correta de configurá-lo varia um pouco com cada ambiente. Bottle expõe um objeto que está em conformidade com a interface WSGI e o servidor deve ser configurado para interagir com esse objeto.

Para saber mais sobre como configurar seu servidor, consulte a documentação do servidor e a documentação do Bottle aqui.

Base de dados

Bottle é independente de banco de dados e não se importa de onde os dados estão vindo. Se você quiser usar um banco de dados em seu aplicativo, o Python Package Index tem várias opções interessantes, como SQLAlchemy, PyMongo, MongoEngine, CouchDB e Boto for DynamoDB. Você só precisa do adaptador apropriado para fazê-lo funcionar com o banco de dados de sua escolha.

Fundamentos da Estrutura da Garrafa

Agora, vamos ver como fazer um aplicativo básico no Bottle. Para exemplos de código, assumirei Python >= 3.4. No entanto, a maior parte do que escreverei aqui também funcionará no Python 2.7.

Um aplicativo básico no Bottle se parece com isso:

 import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Quando digo básico, quero dizer que este programa nem mesmo “Hello World” você. (Quando foi a última vez que você acessou uma interface REST que respondeu “Hello World?”) Todas as solicitações HTTP para 127.0.0.1:8000 receberão um status de resposta 404 Not Found.

Aplicativos em garrafa

O Bottle pode ter várias instâncias de aplicativos criadas, mas por conveniência, a primeira instância é criada para você; esse é o aplicativo padrão. Bottle mantém essas instâncias em uma pilha interna ao módulo. Sempre que você faz algo com o Bottle (como executar o aplicativo ou anexar uma rota) e não especifica de qual aplicativo você está falando, ele se refere ao aplicativo padrão. Na verdade, a linha app = application = bottle.default_app() nem precisa existir neste aplicativo básico, mas está lá para que possamos invocar facilmente o aplicativo padrão com Gunicorn, uWSGI ou algum servidor WSGI genérico.

A possibilidade de vários aplicativos pode parecer confusa no início, mas adiciona flexibilidade ao Bottle. Para diferentes módulos de seu aplicativo, você pode criar aplicativos Bottle especializados instanciando outras classes Bottle e configurando-as com configurações diferentes conforme necessário. Esses diferentes aplicativos podem ser acessados ​​por diferentes URLs, através do roteador de URL do Bottle. Não vamos nos aprofundar nisso neste tutorial, mas você é encorajado a dar uma olhada na documentação do Bottle aqui e aqui.

Invocação do servidor

A última linha do script executa o Bottle usando o servidor indicado. Se nenhum servidor for indicado, como é o caso aqui, o servidor padrão é o servidor de referência WSGI integrado do Python, que é adequado apenas para fins de desenvolvimento. Um servidor diferente pode ser usado assim:

 bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

Este é o açúcar sintático que permite iniciar o aplicativo executando este script. Por exemplo, se esse arquivo for chamado main.py , você pode simplesmente executar python main.py para iniciar o aplicativo. O Bottle carrega uma lista bastante extensa de adaptadores de servidor que podem ser usados ​​dessa maneira.

Alguns servidores WSGI não possuem adaptadores Bottle. Eles podem ser iniciados com os próprios comandos de execução do servidor. No uWSGI, por exemplo, tudo o que você precisa fazer é chamar o uwsgi assim:

 $ uwsgi --http :8000 --wsgi-file main.py

Uma Nota sobre a Estrutura de Arquivos

Bottle deixa a estrutura de arquivos do seu aplicativo inteiramente por sua conta. Descobri que minhas políticas de estrutura de arquivos evoluem de projeto para projeto, mas tendem a se basear em uma filosofia MVC.

Construindo sua API REST

Claro, ninguém precisa de um servidor que retorne apenas 404 para cada URI solicitada. Eu prometi a você que construiríamos uma API REST, então vamos fazer isso.

Suponha que você queira construir uma interface que manipule um conjunto de nomes. Em um aplicativo real, você provavelmente usaria um banco de dados para isso, mas para este exemplo usaremos apenas a estrutura de dados set na memória.

O esqueleto da nossa API pode ser assim. Você pode colocar esse código em qualquer lugar do projeto, mas minha recomendação seria um arquivo de 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

Roteamento

Como podemos ver, o roteamento em Bottle é feito usando decoradores. Os decoradores importados post , get , put e delete handlers de registro para essas quatro ações. Entenda como esses trabalhos podem ser divididos da seguinte forma:

  • Todos os decoradores acima são um atalho para os decoradores de roteamento default_app . Por exemplo, o decorador @get @get() aplica bottle.default_app().get() ao manipulador.
  • Os métodos de roteamento em default_app são todos atalhos para route() . Então default_app().get('/') é equivalente a default_app().route(method='GET', '/') .

Então @get('/') é o mesmo que @route(method='GET', '/') , que é o mesmo que @bottle.default_app().route(method='GET', '/') , e estes podem ser usados ​​alternadamente.

Uma coisa útil sobre o decorador @route é que, se você quiser, por exemplo, usar o mesmo manipulador para lidar com atualizações e exclusões de objetos, basta passar uma lista de métodos que ele manipula assim:

 @route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

Tudo bem, vamos implementar alguns desses manipuladores.

Crie sua API REST perfeita com o Bottle Framework.

APIs RESTful são um grampo do desenvolvimento web moderno. Sirva aos seus clientes de API uma mistura potente com um back-end do Bottle.
Tweet

POST: Criação de recursos

Nosso manipulador POST pode ficar assim:

 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})

Bem, isso é bastante. Vamos revisar essas etapas parte por parte.

Análise do corpo

Esta API requer que o usuário POSTe uma string JSON no corpo com um atributo chamado “name”.

O objeto de request importado anteriormente da bottle sempre aponta para a solicitação atual e contém todos os dados da solicitação. Seu atributo body contém um fluxo de bytes do corpo da solicitação, que pode ser acessado por qualquer função que seja capaz de ler um objeto de fluxo (como ler um arquivo).

O método request.json() verifica os cabeçalhos da solicitação para o tipo de conteúdo “application/json” e analisa o corpo se estiver correto. Se Bottle detecta um corpo malformado (por exemplo: vazio ou com tipo de conteúdo errado), esse método retorna None e, assim, geramos um ValueError . Se o conteúdo JSON malformado for detectado pelo analisador JSON; ele gera uma exceção que capturamos e re-aumentamos, novamente como um ValueError .

Análise e validação de objetos

Se não houver erros, convertemos o corpo da solicitação em um objeto Python referenciado pela variável de data . Se recebemos um dicionário com uma chave “name”, poderemos acessá-lo via data['name'] . Se recebemos um dicionário sem essa chave, tentar acessá-lo nos levará a uma exceção KeyError . Se recebermos algo que não seja um dicionário, receberemos uma exceção TypeError . Se algum desses erros ocorrer, mais uma vez, nós o re-aumentamos como um ValueError , indicando uma entrada incorreta.

Para verificar se a chave de nome tem o formato correto, devemos testá-la em uma máscara regex, como a máscara namepattern que criamos aqui. Se o name da chave não for uma string, namepattern.match() gerará um TypeError e, se não corresponder, retornará None .

Com a máscara neste exemplo, um nome deve ser alfanumérico ASCII sem espaços em branco de 1 a 64 caracteres. Esta é uma validação simples e não testa um objeto com dados de lixo, por exemplo. Uma validação mais complexa e completa pode ser obtida através do uso de ferramentas como FormEncode.

Teste de existência

O último teste antes de atender a solicitação é se o nome dado já existe no conjunto. Em um aplicativo mais estruturado, esse teste provavelmente deve ser feito por um módulo dedicado e sinalizado para nossa API por meio de uma exceção especializada, mas como estamos manipulando um conjunto diretamente, temos que fazer isso aqui.

Sinalizamos a existência do nome levantando um KeyError .

Respostas de erro

Assim como o objeto de solicitação contém todos os dados de solicitação, o objeto de resposta faz o mesmo com os dados de resposta. Há duas maneiras de definir o status da resposta:

 response.status = 400

e:

 response.status = '400 Bad Request'

Para nosso exemplo, optamos pelo formulário mais simples, mas o segundo formulário pode ser usado para especificar a descrição do texto do erro. Internamente, o Bottle dividirá a segunda string e definirá o código numérico adequadamente.

Resposta de sucesso

Se todas as etapas forem bem-sucedidas, atenderemos à solicitação adicionando o nome ao set _names , definindo o cabeçalho de resposta Content-Type e retornando a resposta. Qualquer string retornada pela função será tratada como o corpo da resposta de uma resposta 200 Success , então simplesmente geramos uma com json.dumps .

GET: Listagem de Recursos

Passando da criação do nome, implementaremos o manipulador de listagem de nomes:

 @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)})

Listar os nomes foi muito mais fácil, não foi? Comparado com a criação de nomes, não há muito o que fazer aqui. Basta definir alguns cabeçalhos de resposta e retornar uma representação JSON de todos os nomes e pronto.

PUT: Atualização de recursos

Agora, vamos ver como implementar o método de atualização. Não é muito diferente do método create, mas usamos este exemplo para introduzir 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})

O esquema do corpo para a ação de atualização é o mesmo da ação de criação, mas agora também temos um novo parâmetro oldname no URI, conforme definido pela rota @put('/names/<oldname>') .

Parâmetros de URI

Como você pode ver, a notação de Bottle para parâmetros de URI é muito direta. Você pode criar URIs com quantos parâmetros desejar. O Bottle os extrai automaticamente do URI e os passa para o manipulador de solicitações:

 @get('/<param1>/<param2>') def handler(param1, param2): pass

Usando decoradores de rotas em cascata, você pode construir URIs com parâmetros opcionais:

 @get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

Além disso, Bottle permite os seguintes filtros de roteamento em URIs:

  • int

Corresponde apenas aos parâmetros que podem ser convertidos em int e passa o valor convertido para o manipulador:

 @get('/<param:int>') def handler(param): pass
  • float

O mesmo que int , mas com valores de ponto flutuante:

 @get('/<param:float>') def handler(param): pass
  • re (expressões regulares)

Corresponde apenas aos parâmetros que correspondem à expressão regular fornecida:

 @get('/<param:re:^[az]+$>') def handler(param): pass
  • path

Corresponde a subsegmentos do caminho de URI de maneira flexível:

 @get('/<param:path>/id>') def handler(param): pass

Partidas:

  • /x/id , passando x como param .
  • /x/y/id , passando x/y como param .

DELETE: Exclusão de Recurso

Assim como o método GET, o método DELETE nos traz poucas novidades. Apenas observe que retornar None sem definir um status retorna uma resposta com um corpo vazio e um código de status 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

Etapa final: ativando a API

Supondo que salvamos nossa API de nomes como api/names.py , agora podemos habilitar essas rotas no arquivo principal do aplicativo 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)

Observe que importamos apenas o módulo de names . Como decoramos todos os métodos com seus URIs anexados ao aplicativo padrão, não há necessidade de fazer nenhuma configuração adicional. Nossos métodos já estão em vigor, prontos para serem acessados.

Nada deixa um front-end feliz como uma API REST bem feita. Funciona como um encanto!

Você pode usar ferramentas como Curl ou Postman para consumir a API e testá-la manualmente. (Se você estiver usando o Curl, poderá usar um formatador JSON para tornar a resposta menos confusa.)

Bônus: Compartilhamento de Recursos de Origem Cruzada (CORS)

Um motivo comum para criar uma API REST é se comunicar com um front-end JavaScript por meio de AJAX. Para alguns aplicativos, essas solicitações devem ter permissão para vir de qualquer domínio, não apenas do domínio inicial da sua API. Por padrão, a maioria dos navegadores não permite esse comportamento, então deixe-me mostrar como configurar o compartilhamento de recursos de origem cruzada (CORS) no Bottle para permitir isso:

 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

O decorador de hook nos permite chamar uma função antes ou depois de cada solicitação. No nosso caso, para habilitar o CORS, devemos definir os cabeçalhos Access-Control-Allow-Origin , -Allow-Methods e -Allow-Headers para cada uma de nossas respostas. Estes indicam ao solicitante que atenderemos as solicitações indicadas.

Além disso, o cliente pode fazer uma solicitação HTTP OPTIONS ao servidor para ver se ele realmente pode fazer solicitações com outros métodos. Com este exemplo abrangente, respondemos a todas as solicitações OPTIONS com um código de status 200 e corpo vazio.

Para habilitar isso, basta salvá-lo e importá-lo do módulo principal.

Embrulhar

Isso é tudo o que há para isso!

Com este tutorial, tentei abordar as etapas básicas para criar uma API REST para um aplicativo Python com a estrutura da Web Bottle.

Você pode aprofundar seu conhecimento sobre essa estrutura pequena, mas poderosa, visitando o tutorial e os documentos de referência da API.

Relacionado: Como criar uma API REST Node.js/TypeScript, Parte 1: Express.js