Acessando APIs REST com Python

Um serviço web (web service) é um mecanismo utilizado para comunicação entre dois ou mais programas através da infraestrutura da internet. Apesar do que o nome pode sugerir, um serviço web não oferece funcionalidades diretamente para o usuário final, mas sim para outros programas que precisam de sua ajuda para realizar alguma tarefa, seja para computar algo ou para fornecer dados úteis.

Você conhece o serviço de busca de endereços postais por CEP? O site dos correios fornece aos cidadãos um serviço que permite que estes façam consultas à base de dados de endereços através de uma página web. Esse não é um exemplo de web service, pois tem como foco servir diretamente ao usuário final. Um web service equivalente poderia ser um serviço oferecido pelos próprios correios para que outros programas, como por exemplo o software de alguma loja virtual, possam verificar qual é o endereço do CEP preenchido pelo usuário. Nesse caso, o software faz uma requisição ao web service, aguarda a resposta, decodifica a mesma e então inclui a resposta obtida na página mostrada ao usuário final.

Um web service pode ser fornecido de várias maneiras. Entretanto, foram definidos alguns padrões para facilitar a interoperabilidade entre programas de diferentes origens, sendo REST e SOAP os mais conhecidos. Este post irá mostrar como utilizar APIs web baseadas no padrão REST.

APIs REST

Uma API (application programming interface) é uma especificação que define como componentes de software devem interagir entre si (thanks, wikipedia!). APIs REST se utilizam do protocolo HTTP para fornecer determinadas funcionalidades aos seus clientes. Essas funcionalidades são descritas por conjuntos de recursos que podem ser acessados remotamente pelos clientes do serviço, através de requisições HTTP comuns.

Em uma API REST existem dois conceitos principais: os recursos (resources) e as coleções (collections). Um recurso é uma unidade que representa um objeto (composto por dados, relacionamentos com outros recursos e métodos). Já uma coleção é um conjunto de recursos que pode ser obtido acessando uma URL. Tal coleção poderia representar a coleção de todos os registros de determinado tipo, ou então, todos os registros que possuem relacionamento com determinado objeto, ou todos os registros que atendem à determinada condição, etc.

A API do twitter, por exemplo, fornece acesso a alguns recursos como os tweets enviados pelos usuários. Com ela, nossa aplicação pode enviar uma requisição HTTP ao twitter solicitando os últimos tweets de um determinado usuário ou até mesmo postar uma mensagem em nome do usuário autenticado.

Por exemplo, para obter uma lista com os últimos 10 tweets postados por mim, basta enviar uma requisição HTTP do tipo GET ao endereço https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=stummjr&count=10. Para que isso funcione, porém, é preciso de autenticação utilizando o mecanismo descrito na documentação da API.

Outro exemplo, com a API do próprio twitter, é o serviço de busca de tweets. Se quisermos buscar os últimos 20 tweets que contenham a hashtag #python, podemos enviar uma requisição à URL: https://api.twitter.com/1.1/search/tweets.json?q=#python&count=20. Como resposta às requisições, receberemos coleções de dados estruturados em formato JSON. A resposta dada por uma chamada a uma API REST normalmente é composta por dados em algum formato estruturado, como JSON ou XML. Esses formatos permitem a interoperabilidade entre plataformas e linguagens diferentes, pois o conteúdo é nada mais do que texto puro codificado com algum esquema de caracteres.

Muitos sites famosos (como twitter, facebook, reddit, etc) fornecem APIs REST para que terceiros possam escrever aplicativos que utilizem os dados armazenados nesses sites. Com essas APIs, fica fácil criar programas que interajam com redes sociais, lendo e postando dados para as mesmas.

Como você pôde ver, uma API REST nada mais é do que uma API que fornece acesso remoto a recursos via HTTP. Para podermos entender melhor e fazer requisições HTTP a um serviço REST, precisamos conhecer um pouquinho mais sobre o protocolo HTTP e como seus métodos são utilizados em uma API REST.

HTTP e seus métodos

O protocolo HTTP define que uma requisição de um cliente para um servidor é composta por:

  1. Uma linha descrevendo a requisição, composta por:
    1. Método: indica o que desejamos fazer com o recurso. Pode ser: GET, POST, PUT, DELETE, além de outros menos utilizados.
    2. URL: o endereço do recurso que se deseja acessar.
    3. Versão: a versão do protocolo a ser usada (1.1, atualmente).
  2. O corpo da requisição, que pode conter informações como o nome do host de onde desejamos obter o recurso solicitado, dados a serem enviados do cliente para o servidor, etc.

O exemplo abaixo mostra uma requisição HTTP do tipo GET para um recurso na web:

GET /r/programming/.json HTTP/1.1
Host: http://www.reddit.com

O método de uma requisição HTTP indica a ação que pretendemos realizar com aquela requisição, e cada método tem um significado próprio:

  • GET: utilizado para a obtenção de dados. É o tipo de requisição que o navegador faz a um servidor quando você digita uma URL ou clica em um link.
  • POST: utilizada na web para o envio de dados do navegador para o servidor, principalmente quando esses dados vão gerar alguma alteração no último. É o tipo de requisição que o navegador faz a um servidor quando você preenche um formulário na web e clica em “Enviar” (embora existam formulários na web que utilizem outros tipos de requisição, como GET).
  • PUT: serve para solicitar a criação de objetos no servidor caso esses ainda não existirem. Na prática, a maioria das páginas utiliza o método POST para isso também.
  • DELETE: serve para indicar que o usuário (emissor da requisição em questão) deseja apagar determinado recurso do servidor.

Após executar a requisição do cliente, o serviço responde com uma mensagem de resposta HTTP. O protocolo HTTP define que as mensagens de resposta devem possuir um campo indicando o status da requisição. O status mais conhecido na web é o 404 (not found – recurso não encontrado), mas existem vários, como: 200 (OK), 401 (not authorized – indicando falta de autenticação), 500 (internal server error – erro no servidor), dentre outros. Por ser baseado em HTTP, o padrão REST define que as mensagens de resposta devem conter um código de status, para que o cliente do serviço web possa verificar o que aconteceu com a sua requisição.

A seguir veremos como emitir requisições HTTP “programaticamente” em Python, acessando uma API REST disponível na web.

Acessando uma API REST

Para entender melhor, vamos utilizar como exemplo a API REST JSONPlaceHolder, disponível em jsonplaceholder.typicode.com, que é uma API fake criada para ser usada por quem estiver usando REST em seu programa e precisando de dados falsos (dummy data) para testes.

O JSONPlaceHolder disponibiliza acesso a alguns recursos, como: posts, comments, albums, photos, todos e users. Cada um dos recursos está disponível em uma URL específica:

Em nosso exemplo, vamos usar somente o recurso comments, mas o exemplo será válido para qualquer um dos recursos acima.

Como já foi mencionado anteriormente, as APIs REST fornecem suas funcionalidades através dos métodos existentes no protocolo HTTP (GET, POST, PUT e DELETE). Por exemplo, para listar todos os comments existentes, basta enviar uma requisição HTTP do tipo GET à URL http://jsonplaceholder.typicode.com/comments. Para listar algum registro comment em específico, basta enviar um GET à mesma URL, passando como parâmetro o id do comment que queremos obter: http://jsonplaceholder.typicode.com/comments/1. Uma requisição HTTP usando o método DELETE à URL http://jsonplaceholder.typicode.com/comments/1 irá remover o objeto comment em questão. Também é possível alterar um objeto através do método HTTP PUT ou incluir um novo objeto com o método POST.

Podemos resumir a semântica dos métodos HTTP em uma API REST da seguinte forma:

  • GET: obtenção de dados (seja um conjunto de objetos ou um em específico).
  • POST: criação de dados.
  • PUT: alteração de dados existentes.
  • DELETE: remoção de dados.

Obviamente, as APIs REST utilizam mecanismos de autenticação para evitar que alguém altere ou acesse dados de forma indevida.

Mãos na massa

Atenção: esta seção supõe que você tem uma certa familiaridade com JSON. Caso não conheça o formato, veja aqui um post anterior sobre o assunto.

Agora que já temos uma ideia sobre como uma API REST funciona, vamos ver na prática como nosso programa poderia utilizar uma API desse tipo para obtenção e manipulação de dados externos. Para fazer as requisições HTTP ao serviço, vamos utilizar a biblioteca requests (instalável via pip install requests) e para manipular o JSON retornado pelo serviço, vamos usar a biblioteca json (inclusa na biblioteca padrão).

Primeiramente, vamos importar as bibliotecas necessárias:

>>> import json, requests

Obtendo dados

Vamos começar testando a leitura de registros usando o método HTTP GET, que está disponível na requests através do método get().

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments")
>>> print response.status_code
200
>>> print response.content
   [
    {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem     quasi\nreiciendis et nam sapiente accusantium"
    },
    {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
    }
    ...
    ,
    {
    "postId": 100,
    "id": 500,
    "name": "ex eaque eum natus",
    "email": "Emma@joanny.ca",
    "body": "perspiciatis quis doloremque\nveniam nisi eos velit sed\nid totam inventore voluptatem laborum et eveniet\naut aut aut maxime quia temporibus ut omnis"
    }
   ]

Agora que vimos que nossa requisição HTTP foi executada com sucesso (código 200) e que a string retornada como resposta está em formato JSON, vamos empacotar o resultado em um objeto Python para que possamos manipular os dados com maior facilidade:

>>> comments = json.loads(response.content)

A função json.loads() transformou a string JSON em um objeto Python de tipo correspondente, em nosso caso, um objeto list contendo vários dict dentro, onde cada dict representará um dos registros existentes no servidor.

>>> print comments[0]
    {u'body': u'laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium', u'email': u'Eliseo@gardner.biz', u'postId': 1, u'id': 1, u'name': u'id labore ex et quam laborum'}

>>> print comments[0]['body']
     laudantium enim quasi est quidem magnam voluptate ipsam eos
     tempora quo necessitatibus
     dolor quam autem quasi
     reiciendis et nam sapiente accusantium

Listando os nomes dos 10 primeiros comments:

>>> for comment in comments[0:10]:
        print comment['name']
     labore ex et quam laborum
     quo vero reiciendis velit similique earum
     odio adipisci rerum aut animi
     alias odio sit
     vero eaque aliquid doloribus et culpa
     et fugit eligendi deleniti quidem qui sint nihil autem
     repellat consequatur praesentium vel minus molestias voluptatum
     et omnis dolorem
     provident voluptas
     eaque et deleniti atque tenetur ut quo ut

Além da listagem de todos os objetos, também podemos obter um objeto em específico:

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments/1")
>>> response.content
     '{\n  "postId": 1,\n  "id": 1,\n  "name": "id labore ex et quam laborum",\n  "email": "Eliseo@gardner.biz",\n  "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium"\n}'
>>> comment = json.loads(response.content)
>>> print comment['name']
     labore ex et quam laborum

Se quisermos descobrir a qual post que o comment acima se refere, basta fazer uma requisição GET à http://jsonplaceholder.typicode.com/posts/X, sendo X o valor do campo postId do comment retornado. Veja:

>>> post = requests.get("http://jsonplaceholder.typicode.com/posts/%d" % comment['postId'])
>>> post.content
     '{\n  "userId": 1,\n  "id": 1,\n  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",\n  "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"\n}'
>>> post = json.loads(post.content)
>>> post
     {u'body': u'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
     u'id': 1,
     u'title': u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
     u'userId': 1}
>>> post['title']
     u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'

Dessa forma, podemos navegar entre registros, obter objetos relacionados, etc.

Inserindo dados

Para a inserção de dados em um serviço que oferece uma API REST, precisamos utilizar o método POST do HTTP, disponível através da função post da requests. Como queremos inserir um novo registro no servidor, é necessário que passemos esse registro junto à requisição HTTP. Isso pode ser feito passando um dicionário com os dados ao parâmetro data:

>>> dados = data={"postId": 1, "name": "John Doe", "email": "john@doe.com", "body": "This is it!"}
>>> response = requests.post("http://jsonplaceholder.typicode.com/comments/", data=dados)

Feito isso, agora podemos verificar qual a resposta do serviço para a nossa requisição. O valor esperado depende de quem projetou a API, pois ele pode enviar uma resposta contendo o novo registro, a URL de acesso ao registro recém-criado, ou outra informação. Além disso, o código de resposta HTTP também pode variar (alguns serviços respondem com 200 — OK, outros com 201 – Created, embora o último faça muito mais sentido). O serviço que estamos usando para os exemplos envia uma resposta com o código 200 e o registro recém inserido como conteúdo.

Como ocorreu tudo dentro do esperado, o serviço respondeu com o registro criado (repare que foi adicionado um campo id que não havia nos dados que enviamos):

>>> print response.status_code
     200
>>> print response.content
     {
     "body": "This is it!",
     "postId": "1",
     "name": "John Doe",
     "email": "john@doe.com",
     "id": 501
     }

Para ter certeza sobre o funcionamento da API que você estiver usando, é preciso ler a especificação dela para descobrir o que esperar como resultado em caso de sucesso ou de erro. Como o protocolo HTTP já possui um conjunto pré-definido de códigos de status, os serviços baseados em REST devem usar tais códigos para indicar o status da requisição.

Alterando registros

O padrão REST define que o método HTTP PUT deve ser utilizado sobre um determinado recurso quando desejarmos alterá-lo. A biblioteca requests fornece a função put para o envio de requisições HTTP que utilizam o método PUT. Vamos a um exemplo, onde vamos alterar o campo email do comentário de id 10:

>>> dados = {"email": "john@doe.com"}
>>> response = requests.put("http://jsonplaceholder.typicode.com/comments/10", data=dados)

Ou seja, enviamos uma requisição PUT à URL que representa o comentário que queremos alterar, e passamos também um dicionário contendo o novo valor para o campo que desejamos alterar. Como resposta, obtivemos o recurso alterado.

Removendo registros

Para apagar um registro, o padrão REST define que uma requisção HTTP usando o método DELETE deve ser enviada ao serviço, passando como recurso o registro que deve ser removido. Para apagar o comment de id 10, utilizamos a função delete da requests:

>>> response = requests.delete("http://jsonplaceholder.typicode.com/comments/10")

Acessando recursos aninhados

Como já vimos pela estrutura dos dados retornados para os comments, cada registro desse tipo está associado a um registro post. Assim, uma necessidade que surge naturalmente é a de obter todos os comments pertencentes a um determinado post. O web service que estamos usando permite consultas a recursos relacionados. Para obter todos os comments relacionados ao post de id 2, fazemos:

>>> response = requests.get("http://jsonplaceholder.typicode.com/posts/2/comments")

Enfim

Apesar de o exemplo que seguimos ter focado em um web service específico, cada serviço possui uma interface de acesso própria. Ou seja, algumas APIs podem não permitir acesso a recursos aninhados, ou não permitir a remoção de registros, etc. É importante que você, antes de utilizar uma API REST, leia a documentação da mesma para saber o que é possível fazer com ela.

Apesar dessas diferenças entre uma API e outra, o mecanismo de acesso às mesmas não muda. Você vai precisar de uma biblioteca para emitir requisições HTTP (requests ou urllib2) e uma biblioteca para fazer a decodificação dos dados retornados (json, simplejson ou alguma biblioteca para manipulação de XML).

Obrigado ao Elias Dorneles pela revisão!

Anúncios