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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Wrapper da API do Banco do Brasil.
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -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.0"
7
+ __version__ = "0.5.2"
8
8
 
9
9
 
10
10
  from .common import Ambiente
11
- from .accountability import AccountabilityV3RepasseAPI, AccountabilityV3ControleAPI
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 Exception("Não foi possível reaver o extrato do órgão repassador.")
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 Exception(
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
- "digitoVerificadorContaRecebedora",
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
- "digitoVerificadorContaRecebedora": "Dígito Verificador Conta Recebedora",
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 Exception(
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 Exception("Não foi possível reaver o extrato do órgão repassador.")
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 Exception(
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
- "digitoVerificadorContaRecebedora",
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
- "digitoVerificadorContaRecebedora": "Dígito Verificador Conta Recebedora",
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 Exception(
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[insertable])
94
+ df[insertable] = cast("Scalar", record.get(insertable))
95
95
 
96
96
  if explodeables is not None:
97
97
  for explodeable in explodeables:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Wrapper da API do Banco do Brasil.
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bb_api"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Wrapper da API do Banco do Brasil."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes
File without changes
File without changes
File without changes
File without changes