Download e descompactando a fonte de dados e Endpoint

4 minute read

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

CGUDOWNLOAD

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}

Untitled

Também será retornado mensagens de erro, graças ao fallback controller, mais uma das cortesias do Phoenix/Elixir.

Untitled

Buscar informações através de http://localhost:4000/api/sources/

Untitled

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.

Untitled

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.