Download e descompactando a fonte de dados e Endpoint
Utilizando HTTPoison para fazer download da fonte de dados
Fazer o download do arquivo no site da transparência felizmente foi uma tarefa fácil, pois não há captchas nem registros prévios. Como mostrado abaixo, basta selecionar o ano e mês e o download é disponibilizado no endpoint /{ano_mes}.
O HTTPoison, um cliente em conjunto com o uso de pattern matching foram mais que o suficiente para cumprir essa tarefa
defp get_zip(ano_mes) do
url = "https://transparencia.gov.br/download-de-dados/pep/" <> ano_mes
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{body: zip_body, status_code: 200}} ->
{:ok, zip_body}
{:ok, %HTTPoison.Response{body: _zip_body, status_code: 404}} ->
{:error, Error.build(:not_found, "Arquivo nao encontrado para o ano_mes informado")}
{:ok, %HTTPoison.Response{body: _zip_body, status_code: _}} ->
{:error, Error.build(:bad_request, "Resposta inesperada do servidor da transparencia")}
end
end
O ultimo match, sem status_code especifico, foi criado pra lidar com a inconsistência no site da Transparência, que com certa frequência tem ficado offline ou devolvendo status que não fazem muito sentido.
Agora basta escrever o conteúdo do zip localmente utilizando o modulo File do Elixir
defp write_zip(zip_body, ano_mes) do
path = "priv/downloaded/zip/" <> ano_mes <> ".zip"
case File.write(path, zip_body) do
:ok -> {:ok, path}
{:error, _reason} -> {:error, Error.build(:bad_request, "Erro ao salvar zip")}
end
end
Descompactando o arquivo baixado utilizando Erlang
Com acesso ao arquivo compactado, basta descompacta-lo para termos acesso ao CSV. Confesso que essa parte me preocupava um pouco, mas com um pouco de pesquisa encontrei o módulo :zip do Erlang (Que pode ser utilizado no Elixir) que possui a função unzip.
defp unzip(source_path) do
report_folder = "priv/reports/"
erl_source_path = String.to_charlist(source_path)
erl_report_folder = String.to_charlist(report_folder)
case :zip.unzip(erl_source_path, [{:cwd, erl_report_folder}]) do
{:ok, [report_path]} -> {:ok, to_string(report_path)}
{:error, reason} -> {:error, reason}
end
end
Como Strings no Elixir são Binary e no Erlang são charlist, é necessário fazer conversões ao utilizar o módulo :zip.
Adicionando as informações do arquivo ao banco de dados
Achei importante manter no banco de dados informações relativas a fonte dos dados.
Migration:
defmodule Pep.Repo.Migrations.Sources do
use Ecto.Migration
def change do
create table(:sources) do
add :ano_mes, :string
add :report_path, :string
add :source_path, :string
timestamps()
end
create unique_index(:sources, [:ano_mes])
create unique_index(:sources, [:report_path])
create unique_index(:sources, [:source_path])
end
end
Schema e changeset:
defmodule Pep.Source do
use Ecto.Schema
import Ecto.Changeset
@required_fields [:ano_mes, :report_path, :source_path]
schema "sources" do
field :ano_mes, :string
field :report_path, :string
field :source_path, :string
timestamps()
end
def changeset(params) do
%__MODULE__{}
|> cast(params, @required_fields)
|> validate_required(@required_fields)
|> validate_length(:ano_mes, is: 6)
end
end
A informação que mais será utilizada será a inserted_at, que o Ecto convenientemente disponibiliza para nós através do timestamps().
Disponibilizando endpoints para importar fonte de dados e informações sobre fonte de dados
Agora basta disponibilizar uma forma para os usuários atualizarem as informações quando o CGU disponibilizar novas versões do relatório e também buscar dados sobre os imports.
Controller:
defmodule PepWeb.SourcesController do
use PepWeb, :controller
alias Pep.Source
alias PepWeb.FallbackController
action_fallback FallbackController
def create(conn, %{"ano_mes" => ano_mes} = _params) do
with {:ok, %Source{} = source} <- Pep.create_source(ano_mes) do
conn
|> put_status(:created)
|> render("create.json", source: source)
end
end
def show(conn, _params) do
with source_list <- Pep.list_sources() do
conn
|> put_status(:ok)
|> render("show.json", source: source_list)
end
end
end
# Views e router omitidas. Se você se interessar, poderá checar depois no código fonte
Agora podemos atualizar as informações através do endpoint POST http://localhost:4000/api/sources/{ano_mes}
Também será retornado mensagens de erro, graças ao fallback controller, mais uma das cortesias do Phoenix/Elixir.
Buscar informações através de http://localhost:4000/api/sources/
Uso de recursos
Essa parte não está diretamente relacionada ao desenvolvimento da API, mas o Elixir/Phoenix me surpreendeu com a quantidade minima de recursos que utiliza. Mesmo após fazer as chamadas para baixar e descompactar as fontes dos dados, ele continua utilizando pouquissimo recurso.
E o melhor é que não tenho que me preocupar com o “fechamento de recursos”, pois a BEAM mata o processo assim que ele não é mais necessário, ou seja, nenhum processo fica consumindo recurso atoa.