Desenvolvendo APIs em R com plumber e Docker
Confere seu saldo no banco. Esse tipo de comportamento é comum, especialmente entre aqueles que experienciaram o confisco das poupanças que foi feito durante o Governo Collor. Essa ordem de acontecimentos é comum, talvez você - leitor - tenha uma rotina semelhante, talvez não. Todavia, o que talvez nem você e nem Dona Maria saibam é a infinidade de pedidos e processamentos que ocorrem no instante em que ela abriu a aplicação do banco.
No Brasil, em que o principal marcador temporal é a presidência de Jair M. Bolsonaro, Alice é desenvolvedora back-end, e trabalha em uma empresa de serviços financeiros. Seu trabalho? Desenvolver a interface que faz a ligação do celular de sua mãe com os servidores da companhia em que trabalha. Entregar de forma rápida, segura e confiável o saldo para outras pessoas como ela e sua mãe, que entram em panico ao ouvir a palavra confisco, ou ao receberem o menor sinal de que seu suado dinheiro pode ter sido tomado.
A forma que essa entrega é feita é por meio de uma API, uma Interface de Programação de Aplicações, que basicamente é um programa de computador que está constantemente em execução. O que a API faz responder a pedidos que são feitos por usuários. Quando Dona Maria abre o aplicativo do banco, o que a API que Alice desenhou faz é processar:
Quem está fazendo o pedido?
Qual pedido de informação está sendo feito?
Esse usuário tem permissão para fazer esse pedido?
Caso as respostas sejam:
Dona Maria.
Obter o saldo da conta de Dona Maria.
Sim.
É retornado ao celular dela o valor do saldo da conta. Se as respostas não forem estas é retornada uma mensagem de acesso negado - por convenção nós chamamos essa mensagem de status 403.
Como toda pessoa de coração puro, Alice decidiu escrever sua API em R, e para isso, faz uso da {plumber}
, a principal biblioteca do R para o desenvolvimento desse tipo de interface. Os motivos para usar a plumber
são muitos, mas o principal é sua simplicidade. Um endpoint é levantado fazendo o uso de comentários especiais, que seguem uma sintaxe parecida com a que usamos para escrever documentações com o {roxygen2}.
Digamos que o objetivo é construir uma simples API que recebe um nome e um ID. A partir disso, o sistema deve consultar uma banco de dados que de usuários procurando aquele nome e ID. Caso não encontre, deve retornar um erro. Todavia, caso encontre devemos retornar um JSON com o saldo daquele usuário. Comecemos construindo uma tabela de SQLite
com esses dados…
O Inicio
Essa tarefa será executada com auxilio da DBI, que pode ser uma biblioteca que alguns não conhecem, mas ela é a principal forma de conectar em bancos de dados usando R. Ela suporta uma infinidade de tipos de banco do MS SQL Server
, passando por MongoDB
, SQLite
e indo até o Cassandra
.
library(DBI)
library(uuid)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(purrr)
library(tibble)
library(plumber)
library(randomNames)
# Criando um DB temporário na memória RAM.
con <- DBI::dbConnect(
RSQLite::SQLite(),
":memory:")
# Tabela dummy com nossos dados
tbl <- tibble::tibble(
names = randomNames::randomNames(
n = 10,
name.order = "first.last",
name.sep = " "),
id = uuid::UUIDgenerate(n = 10),
balance = purrr::rdunif(10, 1, 1000)) %>%
dplyr::bind_rows(
tibble::tibble(names = "Maria",
id = "123456789t",
balance = 420.69))
Perfeito, tudo certo com ele, nossos dados fantasia estão aí, agora vamos envia-los para nosso DB e conferir se o envio das informações foi um sucesso.
Para isso, ainda usando a DBI, usamos a função DBI::dbWriteTable
, que irá receber 3 argumentos: uma conexão ao DB - que foi estabelecida usando a DBI::dbConnect
no bloco de código acima -, o nome que você deseja dar a tabela que irá criar, e a tibble que deve ser enviada para o banco.
DBI::dbWriteTable(con, "accounts", tbl)
Essa função não possui retorno, para verificar se a operação foi um sucesso devemos no atentar a erros. Caso nenhum tenha ocorrido tudo parece estar certo. Iremos agora utilizar a combinação de DBI::dbSendQuery
e DBI::dbFetch
para verificar se o nosso upload foi um sucesso.
DBI::dbSendQuery(
con,
"SELECT * from accounts") %>%
DBI::dbFetch()
## names id balance
## 1 Cameron Lopez-Garcia 668ef02a-311d-4994-a168-c214aaf1c0fd 881.00
## 2 Mika Baker 867a6348-80ec-47ef-8152-2b706a341e04 182.00
## 3 Antonio Blueeyes 8f68a78f-b3dc-4221-9955-7576b78b7a64 603.00
## 4 Katherine Lazo 7f561f83-cb91-478c-bc15-37961a695507 807.00
## 5 Sabrina Luevano Chavez ff8e7432-c8af-47e8-8fef-bf2ad9ad0839 608.00
## 6 Thao Kim Reilly 0c51eab9-a3ad-4516-821a-0a41e3b52b52 689.00
## 7 Gabriela Adame Escobar 983f1f6c-bd2c-45eb-ba79-65c57aa092a8 947.00
## 8 Joselyn Tafoya 65d6c8dd-a712-4ac3-b379-ac4609b2b4d4 434.00
## 9 Katherine Morgan 282b5ab9-4196-444c-97e2-d7c76e897fac 206.00
## 10 Surya Talusani d587e0b4-2a7c-4e22-8e19-aa1230360bf0 265.00
## 11 Maria 123456789t 420.69
Como o retorno da operação foi um sucesso, temos um banco de dados. Passamos agora para a construção da API, e começamos carregando o plumber e fazendo um desenho básico. O usuário irá perceber que essa definição de endpoint é feita por meio de um tipo especial de comentário, que é o sustenido seguido de um asterisco (#*). Essa sintaxe é única do plumber e indica que a função seguinte é uma API.
O tipo de endpoint é definido pelos verbos @get, @post, @delete, @patch ou, por fim, @put. Essas tags definem o que o nosso endpoint fará. Como esse não se propõe a ser um tutorial sobre como APIs funcionam, deixo aqui uma pequena explicação sobre o que faz cada tipo de verbo HTTP.
library(plumber)
#* Consulta o Banco
#*
#* @param name nome do usuario
#* @param id id do usuario
#*
#* @get /balance
function(name, id) {
con %>%
DBI::dbSendQuery(
glue::glue("SELECT *
FROM accounts
WHERE id = '{id}'
AND names = '{name}'")) %>%
DBI::dbFetch() %>%
dplyr::pull(balance)
}
## function(name, id) {
##
## con %>%
## DBI::dbSendQuery(
## glue::glue("SELECT *
## FROM accounts
## WHERE id = '{id}'
## AND names = '{name}'")) %>%
## DBI::dbFetch() %>%
## dplyr::pull(balance)
## }
Agora iremos iniciar um contêiner para podermos deixar nossa API no ar de forma isolada das outras aplicações que estão em execução no nosso computador. Para isso precisamos ter o Docker instalado - mais instruções sobre como fazer isso na documentação do Docker - , tendo isso é só executar o seguinte código no seu terminal:
$ docker pull rstudio/plumber
O que ele fará é conectar aos servidores do Docker e fazer o download da imagem Docker que possui o R e o Plumber
instalados, isso vai nos ganhar um tempo.
Criaremos então, um arquivo chamado api.R que irá conter todo código que digitamos até agora, ficará assim:
install.packages( c("tibble", "dplyr", "randomNames", "purrr", "DBI", "uuid"))
library(DBI)
library(uuid)
library(dplyr)
library(purrr)
library(tibble)
library(plumber)
library(randomNames)
# Criando um DB temporário na memória RAM.
con <- DBI::dbConnect(
RSQLite::SQLite(),
":memory:")
# Tabela dummy com nossos dados
tbl <- tibble::tibble(
names = randomNames::randomNames(
n = 10,
name.order = "first.last",
name.sep = " "),
id = uuid::UUIDgenerate(n = 10),
balance = purrr::rdunif(10, 1, 1000)) %>%
dplyr::bind_rows(
tibble::tibble(names = "Maria",
id = "123456789t",
balance = 420.69))
DBI::dbWriteTable(con, "accounts", tbl)
#---------------
#* Consulta o Banco
#*
#* @param name nome do usuario
#* @param id id do usuario
#*
#* @get /balance
function(name, id) {
con %>%
DBI::dbSendQuery(
glue::glue("SELECT *
FROM accounts
WHERE id = '{id}'
AND names = '{name}'")) %>%
DBI::dbFetch() %>%
dplyr::pull(balance)
}
E a partir de então iremos iniciar nossa API, para isso precisamos voltar ao terminal e executar o seguinte código, que dirá ao Docker que deve ser inicializado um contêiner usando a imagem rstudio/plumber
e dentro deste deve ser executada a API que está definida em api.R
, que lá dentro será renomeada para plumber.R
.
$ docker run --rm -p 8000:8000 -v `pwd`/api.R:/plumber.R rstudio/plumber /plumber.R
Essa operação de levantar a API pode demorar alguns minutos, isso ocorrerá pois nosso contêiner opera como se fosse um computador separado do nosso, por conta disso, se faz necessário a instalação de todas as bibliotecas.
O importante é que no final dela, poderemos requisitar o saldo de Maria por meio de uma simples consulta HTTP por meio da cURL, um programa de linha de comando usado para esse tipo de requisição.
$ curl -X GET "<http://127.0.0.1:8000/balance?name=Maria&id=123456789t>" -H "accept: */*"
O terminal irá ter como retorno o valor: 420.69
, que é o saldo de Maria. Maria continuará tendo medo de um eventual confisco, e não há nada que possamos fazer para resolver isso, mas pelo menos agora sabemos como Alice escreveu uma API.
Vale notar que acessando: http://127.0.0.1:8000/__docs__/ encontramos uma documentação da sua API e uma forma de fazer uma consulta rápida. A documentação é gerada a partir do comentário escrito na hora de definir o endpoint.