cuducos/calculadora-do-cidadao

Implementar IGP-M

cuducos opened this issue · 23 comments

Implementar adaptador para o IGP-M.

Vou pegar isso. Por favor, me coloca como assignee. :)

Só para documentar (e lembrar no futuro): a fonte de IGP-M é a FGV, mas eles não liberam série histórica —pelo menos eu não achei.

Mas achei no Banco Central. Por isso, a fonte é esta: https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?method=downLoad&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=[SGSFW2301]

Com isso se baixa um csv com os dados atualizados.

Só para documentar (e lembrar no futuro): a fonte de IGP-M é a FGV, mas eles não liberam série histórica —pelo menos eu não achei.

Achei nessa URL aqui: http://www14.fgv.br/fgvdados20/visualizaconsulta.aspx

image

Aqui pede login:

image

Clicando no "Acesse aqui" e, depois em "Séries institucionais" parece funcionar (falei em off isso, mas registrando aqui).

Parece que este bloco funciona:

import requests

ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS

session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
print(r.text)

Mais tarde vou usar BS4 para parsear o html e pegar apenas table.

Parece que este bloco funciona [...]

Hell yeah! Funcionou aqui sim : )

Mais tarde vou usar BS4 para parsear o html e pegar apenas table.

Posso sugerir outro caminho? Instale a rows com pip install rows[html,xls] (ao invés de só rows[xls] como está hoje) e vamos testar o rows.import_from_html. A vantagem seria código mais em alto nível, uma dependência direta a menos, e, se funcionar direitinho, podemos deixar a serializaçào dos dados a cargo da rows ; )

Cheguei bêbado e testei rows. Estamos quase lá...

import requests
from io import BytesIO

import rows

ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS

session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table:
    print(row)

(Tive um probleminha de encoding, mas contornei...)

Sensacional.

A parte do código tb. Hahaha...

Pensando aqui em como encapsular isso no Adapter e no Download, rascunhei esse pseudo código pq me empolguei. Escrevi aqui mesmo, não rodei nem nada! Compartilho só para tentar ajudar. Mas pode ignorar se quiser!

class Adapter(meta...):

    @property
    def cookies(self) -> dict:
        return getattr(self, "COOKIES", {})

Aé rola passar essas infos na hora de instanciar o Download. E, lá no Downloads, algo como:

    def http(self, path -> Path) -> Path:
        session = Session()
        cookie_jar = cookiejar_from_dict(self.cookies)
        session.cookies = cookie_jar
        response = session.get(self.url)
        path.write_bytes(response.content)
        return path

Não usei o encoding de propósito, pois acho que isso ficaria no Adapter, como kwarg na hora de abrir o arquivo. Também teria que mudar uma linha para que o protocolo https seja lido como http`, mas deixemos esses detalhes pro PR : )

Só vim dar m toque aqui pois com o d628e8e:

  • O Adapter base agora suporta URL com HTTP e também HTTP com cookies!
  • No README.md tem um mini-guia para criar novos adaptadores ; )

Opa. Deixei essa issue "descansando" dada a minha inaptidão para OOP. rs...
Vou ver o README.md e ver se sigo. :)

@cuducos , acho que preciso de uma ajuda...

O IGP-M vem com aquele encoding zoado. É preciso decode de ISO-8859-1 e, em seguida, encode de UTF-8. Quando fiz o rascunho, ficou assim:

import requests
from io import BytesIO

import rows

ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS

session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table:
    print(row)

Tentei reproduzir isso em download.py[a partir da linha 44], ou não conseguimos ler. Fiz isso:

    def http(self, path: Path) -> Path:
        session = Session()
        if self.cookies:
            session.cookies = cookiejar_from_dict(self.cookies)
        response = session.get(self.url)
        if response.encoding != 'utf-8':
            raw_encoding = response.content
            decoded = raw_encoding.decode('iso-8859-1').encode('utf-8')
            path.write_bytes(decoded)
        else:
            path.write_bytes(response.content)
        return path

Ainda assim, não consegui. Deu UnicodeDecodeError:

In [14]: igpm = Igpm()
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-14-a5c7534a4bc0> in <module>()
----> 1 igpm = Igpm()

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in __init__(self)
     31             raise AdapterNoImportMethod(msg)
     32 
---> 33         self.data = {key: value for key, value in self.download()}
     34         if self.data:
     35             self.most_recent_date = max(self.data.keys())

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in <dictcomp>(.0)
     31             raise AdapterNoImportMethod(msg)
     32 
---> 33         self.data = {key: value for key, value in self.download()}
     34         if self.data:
     35             self.most_recent_date = max(self.data.keys())

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in download(self)
    108         with download() as path:
    109             for kwargs in self.import_kwargs:
--> 110                 for data in self.read_from(path, **kwargs):
    111                     yield from (row for row in self.serialize(data) if row)

/home/rodolfo/.local/lib/python3.7/site-packages/rows/plugins/plugin_html.py in import_from_html(filename_or_fobj, encoding, index, ignore_colspan, preserve_html, properties, table_tag, row_tag, column_tag, *args, **kwargs)
     77     """Return rows.Table from HTML file."""
     78     filename, fobj = get_filename_and_fobj(filename_or_fobj, mode="rb")
---> 79     html = fobj.read().decode(encoding)
     80     html_tree = document_fromstring(html)
     81     tables = html_tree.xpath("//{}".format(table_tag))

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xea in position 6160: invalid continuation byte

Tem alguma sugestão?

Sugestão: você pode passar um encoding para a função import_from_html, dessa forma não precisa fazer o decode (e depois o re-encode para UTF-8) do HTML. Além disso, é mais robusto usar response.apparent_encoding em vez de response.encoding. Sugiro essa alteração:

# ...
response = session.get(URL)
html = response.content
table = rows.import_from_html(BytesIO(html), encoding=response.apparent_encoding, index=4)
for row in table:
    print(row)

Caso queiram detectar o encoding de maneira mais rápida e confiável, vejam esse vídeo que criei.

@turicas , valeu pela sugestão. :)

Em download.py, porém, não chegamos a chamar a função import_from_html; isso fica em base.py, no __init__ da classe:

class Adapter(metaclass=ABCMeta):
    def __init__(self) -> None:
        functions = {"html": import_from_html, "xls": import_from_xls}
        try:
            self.read_from = functions[self.file_type]
        except KeyError:
            msg = (
                f"Invalid file type {self.file_type}. "
                f"Valid file types are: {', '.join(functions)}."
            )
            raise AdapterNoImportMethod(msg)

        self.data = {key: value for key, value in self.download()}
        if self.data:
            self.most_recent_date = max(self.data.keys())
        if self.should_aggregate:
            self.aggregate()

Então ficaria "estranho" checar o encoding em download.py e fazer qualquer alteração de encoding em base.py, não?

( Na verdade, eu não faço ideia de como implementar isso ou mesmo o io.BytesIO necessário...)

Talvez seja o caso de alguém mais experiente pegar o IGP-M para eu não cagar tudo... :/

@turicas não rolou. Setei no IMPORT_KWARGS, mas ele vira NoneType e não pode ser iterado.

Rola mostrar como tá teu adapter e essa linha do IMPORT_KWARGS? Seria esse o caminho mesmo: download faz tudo em bytes sem se preocupar com encoding, que seria configurado no IMPORT_KWARGS… mas posso ter deixado algum bug sem querer ¯_(ツ)_/¯

@cuducos , para mim parece ser o seguinte caso: download.py está ok; é possível, em igpm.py, indicar o encoding de entrada; como não há conversão para utf-8, fica NoneType.

Repare que, antes de criar igpm.py, quando estava apenas testando o link, tive de usar io.BytesIO:

import requests
from io import BytesIO

import rows

ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS

session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table[:-1]:
    value = getattr(row, "field_189_monthly_var")
    date = getattr(row, "date_monthyyyy")
    month = date[0:3]
    year = date[4:]
    print(year)

Esse processo i/o não é repetido na função serialize(). Não sendo repetido, não é possível iterar.

Mas como está tem igpm.py? Estou acompanhando do celular, será que perdi no fio?

@cuducos , conforme falamos, aqui os arquivos em que mexi.

adapters/igpm.py:

from datetime import date
from typing import NamedTuple

from calculadora_do_cidadao.base import Adapter
from calculadora_do_cidadao.fields import PercentField
from calculadora_do_cidadao.months import MONTHS
from calculadora_do_cidadao.typing import MaybeIndexesGenerator


class Igpm(Adapter):
    ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
    SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
    ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
    url = ROOT + SERIE + ARGS
    COOKIES = {'dtcookie': 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ'}
    file_type = "html"

    SHOULD_AGGREGATE = True
    IMPORT_KWARGS = tuple({
        "index": index,
        "encoding": 'iso-8859-1'
    } for index in range(1, 4))

    def serialize(self, row: NamedTuple) -> MaybeIndexesGenerator:
        print(row)
        #value = getattr(i, "field_189_monthly_var")
        #date = getattr(i, "date_monthyyyy")
        #month = date[0:3]
        #year = date[4:]
        # if value is None:
        #    continue
        # yield self.round_date(date(year, month, 1)), value

adapters/__init__.py:

from calculadora_do_cidadao.adapters.ipca import Ipca  # noqa
from calculadora_do_cidadao.adapters.selic import Selic  # noqa
from calculadora_do_cidadao.adapters.igpm import Igpm  # noqa

E aqui, o TypeError:

In [1]: from datetime import date

In [2]: from decimal import Decimal

In [3]: from calculadora_do_cidadao.adapters import Igpm

In [4]: igpm = Igpm()
Row(field_0='Period', csv_file='Function')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-a5c7534a4bc0> in <module>()
----> 1 igpm = Igpm()

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in __init__(self)
     31             raise AdapterNoImportMethod(msg)
     32 
---> 33         self.data = {key: value for key, value in self.download()}
     34         if self.data:
     35             self.most_recent_date = max(self.data.keys())

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in <dictcomp>(.0)
     31             raise AdapterNoImportMethod(msg)
     32 
---> 33         self.data = {key: value for key, value in self.download()}
     34         if self.data:
     35             self.most_recent_date = max(self.data.keys())

/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in download(self)
    109             for kwargs in self.import_kwargs:
    110                 for data in self.read_from(path, **kwargs):
--> 111                     yield from (row for row in self.serialize(data) if row)

TypeError: 'NoneType' object is not iterable

Teu IMPORT_KWARGS é uma tupla, ou seja, é uma sequência com alguns dicionários:

({'index': 1, 'encoding': 'iso-8859-1'},
 {'index': 2, 'encoding': 'iso-8859-1'},
 {'index': 3, 'encoding': 'iso-8859-1'})

Como explicado no README.md, isso faz com que o import_from_html seja chamo 3 vezes, cada uma um com desses dicionários como kwargs. Nos teus exemplos aqui você não precisa chamar o import_from_html 3 vezes, só precisa chamar uma com {'index': 4, 'encoding': 'iso-8859-1'}.

Ou seja, tu precisa de um IMPORT_KWARGS mais simples. Teste aí com:

IMPORT_KWARGS = {"encoding": "iso-8859-1", "index": 4}

E nos conte se rola progresso ; )