bb_api 0.5.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bb_api-0.5.0 → bb_api-0.5.2}/PKG-INFO +1 -1
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api/__init__.py +13 -2
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api/accountability.py +151 -22
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api/common.py +1 -1
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api.egg-info/PKG-INFO +1 -1
- {bb_api-0.5.0 → bb_api-0.5.2}/pyproject.toml +1 -1
- {bb_api-0.5.0 → bb_api-0.5.2}/LICENSE +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/README.md +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api/gestao_agil.py +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api/sia.py +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api.egg-info/SOURCES.txt +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api.egg-info/dependency_links.txt +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api.egg-info/requires.txt +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/bb_api.egg-info/top_level.txt +0 -0
- {bb_api-0.5.0 → bb_api-0.5.2}/setup.cfg +0 -0
|
@@ -4,11 +4,18 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
4
4
|
try:
|
|
5
5
|
__version__ = version("bb_api")
|
|
6
6
|
except PackageNotFoundError:
|
|
7
|
-
__version__ = "0.5.
|
|
7
|
+
__version__ = "0.5.2"
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from .common import Ambiente
|
|
11
|
-
from .accountability import
|
|
11
|
+
from .accountability import (
|
|
12
|
+
MAX_DIAS_EXTRATO,
|
|
13
|
+
AccountabilityV3RepasseAPI,
|
|
14
|
+
AccountabilityV3ControleAPI,
|
|
15
|
+
ExtratoError,
|
|
16
|
+
IntervaloLongoDemaisError,
|
|
17
|
+
SemLancamentosError,
|
|
18
|
+
)
|
|
12
19
|
from .sia import (
|
|
13
20
|
BBSiaAPI,
|
|
14
21
|
CredenciaisNecessariasError,
|
|
@@ -21,11 +28,15 @@ from .gestao_agil import (
|
|
|
21
28
|
)
|
|
22
29
|
|
|
23
30
|
__all__ = [
|
|
31
|
+
"MAX_DIAS_EXTRATO",
|
|
24
32
|
"Ambiente",
|
|
25
33
|
"AccountabilityV3RepasseAPI",
|
|
26
34
|
"AccountabilityV3ControleAPI",
|
|
27
35
|
"BBSiaAPI",
|
|
28
36
|
"CredenciaisNecessariasError",
|
|
37
|
+
"ExtratoError",
|
|
38
|
+
"IntervaloLongoDemaisError",
|
|
39
|
+
"SemLancamentosError",
|
|
29
40
|
"FalhaDownload",
|
|
30
41
|
"ResultadoDownloads",
|
|
31
42
|
"parse_retorno_abertura_massificada",
|
|
@@ -9,6 +9,119 @@ import requests
|
|
|
9
9
|
import bb_api.common as common
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
MAX_DIAS_EXTRATO = 31
|
|
13
|
+
"""Maior intervalo, em dias corridos, aceito numa consulta de extrato.
|
|
14
|
+
|
|
15
|
+
``startDate`` e ``endDate`` podem distar no máximo esse tanto: 31 dias passam,
|
|
16
|
+
32 devolvem ``400``. Períodos maiores precisam ser fatiados pelo chamador.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_ERRO_SEM_LANCAMENTOS = "não existem lançamentos"
|
|
20
|
+
_ERRO_INTERVALO_LONGO = "maior do que 31 dias"
|
|
21
|
+
|
|
22
|
+
_STATUS_SEM_MOVIMENTO = 404
|
|
23
|
+
_SEM_MOVIMENTO_PADRAO = "não há movimento no período consultado"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExtratoError(Exception):
|
|
27
|
+
"""Erro devolvido pela API ao consultar o extrato do órgão repassador.
|
|
28
|
+
|
|
29
|
+
Carrega o status HTTP e a mensagem originais do Banco do Brasil, porque a
|
|
30
|
+
API usa ``400`` tanto para falha de verdade quanto para situações normais.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
status_code: int
|
|
34
|
+
mensagem: str
|
|
35
|
+
|
|
36
|
+
def __init__(self, status_code: int, mensagem: str) -> None:
|
|
37
|
+
detalhe = f"(HTTP {status_code}): {mensagem}"
|
|
38
|
+
super().__init__(f"Não foi possível reaver o extrato do órgão repassador {detalhe}")
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
self.mensagem = mensagem
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SemLancamentosError(ExtratoError):
|
|
44
|
+
"""A conta não tem lançamento nenhum no período consultado.
|
|
45
|
+
|
|
46
|
+
A API responde ``400`` nesse caso, e não ``200`` com uma lista vazia, então
|
|
47
|
+
esse resultado normal chega como erro e precisa ser separado de uma falha —
|
|
48
|
+
senão uma conta parada vira indistinguível de uma consulta quebrada.
|
|
49
|
+
|
|
50
|
+
Nos extratos de aplicação (fundos de investimento e poupança) a mesma
|
|
51
|
+
situação chega como ``404`` de corpo vazio, e cobre tanto o mês sem
|
|
52
|
+
movimento quanto a aplicação que a conta não possui.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IntervaloLongoDemaisError(ExtratoError):
|
|
57
|
+
"""O intervalo pedido passa dos ``MAX_DIAS_EXTRATO`` dias corridos."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _mensagem_de_erro(res: requests.Response) -> str:
|
|
61
|
+
try:
|
|
62
|
+
corpo = cast("object", res.json())
|
|
63
|
+
except ValueError:
|
|
64
|
+
return res.text.strip()
|
|
65
|
+
|
|
66
|
+
if not isinstance(corpo, dict):
|
|
67
|
+
return res.text.strip()
|
|
68
|
+
|
|
69
|
+
campos = cast("dict[str, object]", corpo)
|
|
70
|
+
for chave in ("error", "message", "mensagem", "erro"):
|
|
71
|
+
valor = campos.get(chave)
|
|
72
|
+
if isinstance(valor, str) and valor:
|
|
73
|
+
return valor
|
|
74
|
+
|
|
75
|
+
detalhada = _mensagem_da_lista_de_erros(campos)
|
|
76
|
+
if detalhada:
|
|
77
|
+
return detalhada
|
|
78
|
+
return res.text.strip()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _mensagem_da_lista_de_erros(campos: dict[str, object]) -> str:
|
|
82
|
+
"""Extrai a mensagem de um corpo no formato ``{"erros": [{...}]}``.
|
|
83
|
+
|
|
84
|
+
``menssagem``, com dois esses, é como o Banco do Brasil escreve a chave
|
|
85
|
+
nesse formato de erro.
|
|
86
|
+
"""
|
|
87
|
+
for chave in ("erros", "errors"):
|
|
88
|
+
itens = campos.get(chave)
|
|
89
|
+
if not isinstance(itens, list) or not itens:
|
|
90
|
+
continue
|
|
91
|
+
primeiro = cast("list[object]", itens)[0]
|
|
92
|
+
if not isinstance(primeiro, dict):
|
|
93
|
+
continue
|
|
94
|
+
item = cast("dict[str, object]", primeiro)
|
|
95
|
+
for interna in ("menssagem", "mensagem", "message", "descricao"):
|
|
96
|
+
valor = item.get(interna)
|
|
97
|
+
if isinstance(valor, str) and valor:
|
|
98
|
+
return valor
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _erro_extrato(res: requests.Response) -> ExtratoError:
|
|
103
|
+
mensagem = _mensagem_de_erro(res)
|
|
104
|
+
normalizada = mensagem.casefold()
|
|
105
|
+
if _ERRO_SEM_LANCAMENTOS in normalizada:
|
|
106
|
+
return SemLancamentosError(res.status_code, mensagem)
|
|
107
|
+
if _ERRO_INTERVALO_LONGO in normalizada:
|
|
108
|
+
return IntervaloLongoDemaisError(res.status_code, mensagem)
|
|
109
|
+
return ExtratoError(res.status_code, mensagem)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _erro_extrato_aplicacao(res: requests.Response) -> ExtratoError:
|
|
113
|
+
"""Classifica o erro dos extratos de fundo de investimento e de poupança.
|
|
114
|
+
|
|
115
|
+
Esses extratos usam ``404`` de corpo vazio para "não há o que devolver" —
|
|
116
|
+
mês sem movimento ou aplicação que a conta não tem —, então esse status
|
|
117
|
+
vira :class:`SemLancamentosError` em vez de falha.
|
|
118
|
+
"""
|
|
119
|
+
if res.status_code == _STATUS_SEM_MOVIMENTO:
|
|
120
|
+
mensagem = _mensagem_de_erro(res) or _SEM_MOVIMENTO_PADRAO
|
|
121
|
+
return SemLancamentosError(res.status_code, mensagem)
|
|
122
|
+
return _erro_extrato(res)
|
|
123
|
+
|
|
124
|
+
|
|
12
125
|
class _AccountabilityV3BaseAPI:
|
|
13
126
|
_app_key: str
|
|
14
127
|
_client_id: str
|
|
@@ -224,7 +337,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
224
337
|
)
|
|
225
338
|
|
|
226
339
|
if res.status_code != 200:
|
|
227
|
-
raise
|
|
340
|
+
raise _erro_extrato(res)
|
|
228
341
|
|
|
229
342
|
res = common.parse_json_object(res)
|
|
230
343
|
|
|
@@ -617,6 +730,15 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
617
730
|
mes: int,
|
|
618
731
|
ano: int,
|
|
619
732
|
) -> pd.DataFrame:
|
|
733
|
+
"""Reaver o extrato mensal de um fundo de investimento da conta.
|
|
734
|
+
|
|
735
|
+
``fundo_investimento_id`` é o ``grupoAtivo`` que
|
|
736
|
+
:meth:`get_saldo_aplicacoes_financeiras` devolve em cada operação — não
|
|
737
|
+
o ``codigo`` nem a ``modalidade``, que respondem ``404``.
|
|
738
|
+
|
|
739
|
+
Levanta :class:`SemLancamentosError` quando o mês não tem movimento ou
|
|
740
|
+
a conta não tem esse fundo.
|
|
741
|
+
"""
|
|
620
742
|
access_token = self._get_access_token()
|
|
621
743
|
|
|
622
744
|
res = requests.request(
|
|
@@ -631,9 +753,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
631
753
|
)
|
|
632
754
|
|
|
633
755
|
if res.status_code != 200:
|
|
634
|
-
raise
|
|
635
|
-
"Não foi possível listar as categorias do programa de governo."
|
|
636
|
-
)
|
|
756
|
+
raise _erro_extrato_aplicacao(res)
|
|
637
757
|
|
|
638
758
|
res = common.parse_json_object(res)
|
|
639
759
|
extrato = cast("dict[str, object]", res["extrato"])
|
|
@@ -643,7 +763,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
643
763
|
main_list="listaLancamentosExtrato",
|
|
644
764
|
insertables=[
|
|
645
765
|
"numeroAgenciaRecebedora",
|
|
646
|
-
"
|
|
766
|
+
"digitoVerificadoraContaRecebedora",
|
|
647
767
|
"numeroContaCorrenteRecebedora",
|
|
648
768
|
"numeroDigitoVerificadorContaCorrenteRecebedora",
|
|
649
769
|
"nomeClienteRecebedor",
|
|
@@ -651,7 +771,6 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
651
771
|
"CNPJFundoInvestimento",
|
|
652
772
|
"valorCotaExtrato",
|
|
653
773
|
"dataAfericaoValorCota",
|
|
654
|
-
"ultimaCotacaoCota",
|
|
655
774
|
"dataUltimaCotacaoCota",
|
|
656
775
|
"sinalRentabilidadeMes",
|
|
657
776
|
"valorRentabilidadeMes",
|
|
@@ -690,7 +809,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
690
809
|
],
|
|
691
810
|
rename_dict={
|
|
692
811
|
"numeroAgenciaRecebedora": "Número Agência Recebedora",
|
|
693
|
-
"
|
|
812
|
+
"digitoVerificadoraContaRecebedora": "Dígito Verificador Conta Recebedora",
|
|
694
813
|
"numeroContaCorrenteRecebedora": "Número Conta Corrente Recebedora",
|
|
695
814
|
"numeroDigitoVerificadorContaCorrenteRecebedora": "Número Dígito Verificador Conta Corrente Recebedora",
|
|
696
815
|
"nomeClienteRecebedor": "Nome Cliente Recebedor",
|
|
@@ -698,7 +817,6 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
698
817
|
"CNPJFundoInvestimento": " CNPJ Fundo Investimento",
|
|
699
818
|
"valorCotaExtrato": "Valor Cota Extrato",
|
|
700
819
|
"dataAfericaoValorCota": "Data Afericão Valor Cota",
|
|
701
|
-
"ultimaCotacaoCota": "Última Cotação Cota",
|
|
702
820
|
"dataUltimaCotacaoCota": "Data Última Cotação Cota",
|
|
703
821
|
"sinalRentabilidadeMes": "Sinal Rentabilidade Mês",
|
|
704
822
|
"valorRentabilidadeMes": "Valor Rentabilidade Mês",
|
|
@@ -769,6 +887,12 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
769
887
|
mes: int,
|
|
770
888
|
ano: int,
|
|
771
889
|
) -> pd.DataFrame:
|
|
890
|
+
"""Reaver o extrato mensal de uma variação de poupança da conta.
|
|
891
|
+
|
|
892
|
+
Levanta :class:`SemLancamentosError` quando não há o que devolver e
|
|
893
|
+
:class:`ExtratoError` quando a variação não existe — nesse caso a API
|
|
894
|
+
responde ``400`` dizendo que o número da variação não é válido.
|
|
895
|
+
"""
|
|
772
896
|
access_token = self._get_access_token()
|
|
773
897
|
|
|
774
898
|
res = requests.request(
|
|
@@ -783,9 +907,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
783
907
|
)
|
|
784
908
|
|
|
785
909
|
if res.status_code != 200:
|
|
786
|
-
raise
|
|
787
|
-
"Não foi possível listar as categorias do programa de governo."
|
|
788
|
-
)
|
|
910
|
+
raise _erro_extrato_aplicacao(res)
|
|
789
911
|
|
|
790
912
|
res = common.parse_json_object(res)
|
|
791
913
|
return common.handle_results(
|
|
@@ -1233,7 +1355,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1233
1355
|
)
|
|
1234
1356
|
|
|
1235
1357
|
if res.status_code != 200:
|
|
1236
|
-
raise
|
|
1358
|
+
raise _erro_extrato(res)
|
|
1237
1359
|
|
|
1238
1360
|
res = common.parse_json_object(res)
|
|
1239
1361
|
|
|
@@ -1621,6 +1743,14 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1621
1743
|
mes: int,
|
|
1622
1744
|
ano: int,
|
|
1623
1745
|
) -> pd.DataFrame:
|
|
1746
|
+
"""Reaver o extrato mensal de um fundo de investimento da conta.
|
|
1747
|
+
|
|
1748
|
+
``fundo_investimento_id`` é o ``grupoAtivo`` da operação no saldo de
|
|
1749
|
+
aplicações financeiras, não o ``codigo`` nem a ``modalidade``.
|
|
1750
|
+
|
|
1751
|
+
Levanta :class:`SemLancamentosError` quando o mês não tem movimento ou
|
|
1752
|
+
a conta não tem esse fundo.
|
|
1753
|
+
"""
|
|
1624
1754
|
access_token = self._get_access_token()
|
|
1625
1755
|
|
|
1626
1756
|
res = requests.request(
|
|
@@ -1635,9 +1765,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1635
1765
|
)
|
|
1636
1766
|
|
|
1637
1767
|
if res.status_code != 200:
|
|
1638
|
-
raise
|
|
1639
|
-
"Não foi possível listar as categorias do programa de governo."
|
|
1640
|
-
)
|
|
1768
|
+
raise _erro_extrato_aplicacao(res)
|
|
1641
1769
|
|
|
1642
1770
|
res = common.parse_json_object(res)
|
|
1643
1771
|
extrato = cast("dict[str, object]", res["extrato"])
|
|
@@ -1647,7 +1775,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1647
1775
|
main_list="listaLancamentosExtrato",
|
|
1648
1776
|
insertables=[
|
|
1649
1777
|
"numeroAgenciaRecebedora",
|
|
1650
|
-
"
|
|
1778
|
+
"digitoVerificadoraContaRecebedora",
|
|
1651
1779
|
"numeroContaCorrenteRecebedora",
|
|
1652
1780
|
"numeroDigitoVerificadorContaCorrenteRecebedora",
|
|
1653
1781
|
"nomeClienteRecebedor",
|
|
@@ -1655,7 +1783,6 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1655
1783
|
"CNPJFundoInvestimento",
|
|
1656
1784
|
"valorCotaExtrato",
|
|
1657
1785
|
"dataAfericaoValorCota",
|
|
1658
|
-
"ultimaCotacaoCota",
|
|
1659
1786
|
"dataUltimaCotacaoCota",
|
|
1660
1787
|
"sinalRentabilidadeMes",
|
|
1661
1788
|
"valorRentabilidadeMes",
|
|
@@ -1694,7 +1821,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1694
1821
|
],
|
|
1695
1822
|
rename_dict={
|
|
1696
1823
|
"numeroAgenciaRecebedora": "Número Agência Recebedora",
|
|
1697
|
-
"
|
|
1824
|
+
"digitoVerificadoraContaRecebedora": "Dígito Verificador Conta Recebedora",
|
|
1698
1825
|
"numeroContaCorrenteRecebedora": "Número Conta Corrente Recebedora",
|
|
1699
1826
|
"numeroDigitoVerificadorContaCorrenteRecebedora": "Número Dígito Verificador Conta Corrente Recebedora",
|
|
1700
1827
|
"nomeClienteRecebedor": "Nome Cliente Recebedor",
|
|
@@ -1702,7 +1829,6 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1702
1829
|
"CNPJFundoInvestimento": " CNPJ Fundo Investimento",
|
|
1703
1830
|
"valorCotaExtrato": "Valor Cota Extrato",
|
|
1704
1831
|
"dataAfericaoValorCota": "Data Afericão Valor Cota",
|
|
1705
|
-
"ultimaCotacaoCota": "Última Cotação Cota",
|
|
1706
1832
|
"dataUltimaCotacaoCota": "Data Última Cotação Cota",
|
|
1707
1833
|
"sinalRentabilidadeMes": "Sinal Rentabilidade Mês",
|
|
1708
1834
|
"valorRentabilidadeMes": "Valor Rentabilidade Mês",
|
|
@@ -1772,6 +1898,11 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1772
1898
|
variacao_poupanca: str,
|
|
1773
1899
|
codigo_variacao: int,
|
|
1774
1900
|
) -> pd.DataFrame:
|
|
1901
|
+
"""Reaver o extrato de uma variação de poupança da conta.
|
|
1902
|
+
|
|
1903
|
+
Levanta :class:`SemLancamentosError` quando não há o que devolver e
|
|
1904
|
+
:class:`ExtratoError` quando a variação não existe.
|
|
1905
|
+
"""
|
|
1775
1906
|
access_token = self._get_access_token()
|
|
1776
1907
|
|
|
1777
1908
|
res = requests.request(
|
|
@@ -1785,9 +1916,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1785
1916
|
)
|
|
1786
1917
|
|
|
1787
1918
|
if res.status_code != 200:
|
|
1788
|
-
raise
|
|
1789
|
-
"Não foi possível listar as categorias do programa de governo."
|
|
1790
|
-
)
|
|
1919
|
+
raise _erro_extrato_aplicacao(res)
|
|
1791
1920
|
|
|
1792
1921
|
res = common.parse_json_object(res)
|
|
1793
1922
|
return common.handle_results(
|
|
@@ -91,7 +91,7 @@ def handle_results(
|
|
|
91
91
|
|
|
92
92
|
if insertables is not None:
|
|
93
93
|
for insertable in insertables:
|
|
94
|
-
df[insertable] = cast("Scalar", record
|
|
94
|
+
df[insertable] = cast("Scalar", record.get(insertable))
|
|
95
95
|
|
|
96
96
|
if explodeables is not None:
|
|
97
97
|
for explodeable in explodeables:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|