REST é um "estilo" (e não um padrão) para APIs. Já falei sobre isso algumas vezes, está cheio de material bom por aí; e se você tiver interesse em construir APIs REST em Python, eu recomendaria o Django REST Framework, que tem sido uma das principais ferramentas no meu trabalho nos últimos anos.
Vou assumir a partir de agora que o leitor já tenha uma noção ou passado pela experiência de usar uma API REST exposta por terceiros.
O que mais me incomodou quando precisei usar APIs de terceiros foi construir as URLs. Um dos patterns que vi bastante por aí era utilizar uma string BASE_URL e concatenar o nome do recurso à essa string (eventualmente concatenando também as chaves para views de detail). Sempre usando a soma de strings, que é até aceitável para um número pequeno de objetos.
import requests
BASE_URL = "http://localhost/"
def my_function():
# construir a URL
url = BASE_URL + "resource/"
# chamar a API via requests
response = requests.get(url)
# fazer algo com o resultado
pass
Eu sempre achei isso feio e suscetível a erros, por exemplo a ausência de "/" no final das urls ou a duplicação no meio delas. Um outro pattern um pouco melhor que vi era uma classe que centralizava a construção dessas URLs.
import requests
class APIConnector:
def __init__(self, base_url):
self.base_url = base_url
def get_resource_url(self):
return self.base_url + "resource/"
def get_resource_detail_url(self, key):
return self.base_url + "resource/%s/" % key
def get_another_resource_url(self):
return self.base_url + "another-resource/"
def get_resource_detail_url(self, key):
return self.base_url + "another-resource/%s/" % key
def my_function():
# instanciar o conector
connector = APIConnector("http://localhost")
# pedir uma URL específica
url = connector.get_resource_detail_url(123)
# chamar a API via requests
response = requests.get(url)
# fazer algo com o resultado
pass
É um padrão melhor, no meu ponto de vista. Centralizar a criação diminui a possibilidade de erros porque diminui o número de trechos do código em que precisamos somar strings.
Baseei-me neste último pattern pra tentar algo mais elegante ainda. A primeira mudança que eu faria era parar de somar strings e usar o módulo os
para construir as URLs de modo mais seguro, sem risco de errar a manipulação das barras.
Resolvi também não implementar um método para cada tipo de recurso. O nome do recurso seria passado como parâmetro, de preferência como um atributo da classe (no estilo objeto.atributo
). E uma vez "dentro" do recurso, expor os métodos no estilo do Django REST Framework: list
, retrieve
, create
, update
, partial_update
, destroy
.
Segue minha implementação.
import os
import requests
class InvalidParamsException(Exception):
pass
class ResourceDriver:
def __init__(self, resource_url, urls_only=False):
self.resource_url = resource_url
self.urls_only = urls_only
def _base_resource_url(self):
return "%s/" % (self.resource_url,)
def _detail_resource_url(self, key):
return os.path.join(self.resource_url, str(key), "")
def list(self, params=None):
url = self._base_resource_url()
if self.urls_only:
return url
else:
if params:
return requests.get(url, params=params)
else:
return requests.get(url)
def create(self, data=None):
url = self._base_resource_url()
if self.urls_only:
return url
else:
return requests.post(url, data=data)
def retrieve(self, key):
url = self._detail_resource_url(key)
if self.urls_only:
return url
else:
return requests.get(url)
def partial_update(self, key, data=None):
url = self._detail_resource_url(key)
if self.urls_only:
return url
else:
return requests.patch(url, data=data)
def update(self, key, data):
url = self._detail_resource_url(key)
if self.urls_only:
return url
else:
return requests.put(url, data=data)
def destroy(self, key):
url = self._detail_resource_url(key)
if self.urls_only:
return url
else:
return requests.delete(url)
class RestClient:
def __init__(self, base_url=None, urls_only=False):
if not base_url:
raise InvalidParamsException("base_url is mandatory")
else:
self.base_url = base_url
self.urls_only = urls_only
def _get_url(self, resource):
return os.path.join(self.base_url, resource)
def __getattr__(self, item):
return ResourceDriver(self._get_url(item), urls_only=self.urls_only)
def use_example():
# instanciar um client
rest_client = RestClient(base_url="http://localhost/")
# chamar diretamente os recursos
list_response = rest_client.resource.list()
detail_response = rest_client.resource.retrieve(123)
post_response = rest_client.resource.create(data={"field": "data", "other_field": "more_data"})
patch_response = rest_client.resource.partial_update(123, data={"field": "data"})
update_response = rest_client.resource.update(123, data={"field": "data", "other_field": "more_data"})
delete_response = rest_client.resource.destroy(123)
# resource pode ser qualquer coisa, o método __getattr__ tratará o que vier
list_response = rest_client.users.list()
# caso o nome do recurso tenha caracteres não-usuais (como um sinal de menos)
list_response = getattr(rest_client, "algo-bizarro").list()
O código é bem simples! Usei o método especial __getattr__
para, qualquer que seja o atributo pedido, retornar um ResourceDriver
que manipule aquele nome como o nome de um recurso. Esta classe mapeia em seguida os métodos pedidos para os métodos da lib requests
, construindo as URLs conforme necessário usando o módulo os
e assim evitando o risco de uma URL mal-formada.
Criei uma opção urls_only
que não faz as chamadas, apenas retorna as URLs construídas.
Coloquei este código num repo do github, onde também estão incluídos testes, que abordam alguns casos variados como recursos com hífen no nome e url base com ou sem barra no final.
Provavelmente usarei esse código em alguns projetinhos futuros. E pra isso ainda tem muita coisa pra fazer nele. Um caso que já vem na cabeça é recurso dentro de recurso, como uma URL http://localhost/recurso/1/outro-recurso/3
para acessar detalhes de atributos, por exemplo. Talvez um método __getattr__
dentro da própria classe ResourceDriver
sirva para esse trabalho.
Aceito contribuições e palpites!