bb_api 0.5.1__tar.gz → 0.5.3__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.1
3
+ Version: 0.5.3
4
4
  Summary: Wrapper da API do Banco do Brasil.
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ from importlib.metadata import PackageNotFoundError, version
4
4
  try:
5
5
  __version__ = version("bb_api")
6
6
  except PackageNotFoundError:
7
- __version__ = "0.5.1"
7
+ __version__ = "0.5.3"
8
8
 
9
9
 
10
10
  from .common import Ambiente
@@ -19,6 +19,9 @@ MAX_DIAS_EXTRATO = 31
19
19
  _ERRO_SEM_LANCAMENTOS = "não existem lançamentos"
20
20
  _ERRO_INTERVALO_LONGO = "maior do que 31 dias"
21
21
 
22
+ _STATUS_SEM_MOVIMENTO = 404
23
+ _SEM_MOVIMENTO_PADRAO = "não há movimento no período consultado"
24
+
22
25
 
23
26
  class ExtratoError(Exception):
24
27
  """Erro devolvido pela API ao consultar o extrato do órgão repassador.
@@ -43,6 +46,10 @@ class SemLancamentosError(ExtratoError):
43
46
  A API responde ``400`` nesse caso, e não ``200`` com uma lista vazia, então
44
47
  esse resultado normal chega como erro e precisa ser separado de uma falha —
45
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.
46
53
  """
47
54
 
48
55
 
@@ -64,9 +71,34 @@ def _mensagem_de_erro(res: requests.Response) -> str:
64
71
  valor = campos.get(chave)
65
72
  if isinstance(valor, str) and valor:
66
73
  return valor
74
+
75
+ detalhada = _mensagem_da_lista_de_erros(campos)
76
+ if detalhada:
77
+ return detalhada
67
78
  return res.text.strip()
68
79
 
69
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
+
70
102
  def _erro_extrato(res: requests.Response) -> ExtratoError:
71
103
  mensagem = _mensagem_de_erro(res)
72
104
  normalizada = mensagem.casefold()
@@ -77,6 +109,19 @@ def _erro_extrato(res: requests.Response) -> ExtratoError:
77
109
  return ExtratoError(res.status_code, mensagem)
78
110
 
79
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
+
80
125
  class _AccountabilityV3BaseAPI:
81
126
  _app_key: str
82
127
  _client_id: str
@@ -685,6 +730,15 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
685
730
  mes: int,
686
731
  ano: int,
687
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
+ """
688
742
  access_token = self._get_access_token()
689
743
 
690
744
  res = requests.request(
@@ -699,9 +753,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
699
753
  )
700
754
 
701
755
  if res.status_code != 200:
702
- raise Exception(
703
- "Não foi possível listar as categorias do programa de governo."
704
- )
756
+ raise _erro_extrato_aplicacao(res)
705
757
 
706
758
  res = common.parse_json_object(res)
707
759
  extrato = cast("dict[str, object]", res["extrato"])
@@ -709,9 +761,10 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
709
761
  df = common.handle_results(
710
762
  extrato,
711
763
  main_list="listaLancamentosExtrato",
764
+ keep_header_when_empty=True,
712
765
  insertables=[
713
766
  "numeroAgenciaRecebedora",
714
- "digitoVerificadorContaRecebedora",
767
+ "digitoVerificadoraContaRecebedora",
715
768
  "numeroContaCorrenteRecebedora",
716
769
  "numeroDigitoVerificadorContaCorrenteRecebedora",
717
770
  "nomeClienteRecebedor",
@@ -719,7 +772,6 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
719
772
  "CNPJFundoInvestimento",
720
773
  "valorCotaExtrato",
721
774
  "dataAfericaoValorCota",
722
- "ultimaCotacaoCota",
723
775
  "dataUltimaCotacaoCota",
724
776
  "sinalRentabilidadeMes",
725
777
  "valorRentabilidadeMes",
@@ -758,7 +810,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
758
810
  ],
759
811
  rename_dict={
760
812
  "numeroAgenciaRecebedora": "Número Agência Recebedora",
761
- "digitoVerificadorContaRecebedora": "Dígito Verificador Conta Recebedora",
813
+ "digitoVerificadoraContaRecebedora": "Dígito Verificador Conta Recebedora",
762
814
  "numeroContaCorrenteRecebedora": "Número Conta Corrente Recebedora",
763
815
  "numeroDigitoVerificadorContaCorrenteRecebedora": "Número Dígito Verificador Conta Corrente Recebedora",
764
816
  "nomeClienteRecebedor": "Nome Cliente Recebedor",
@@ -766,7 +818,6 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
766
818
  "CNPJFundoInvestimento": " CNPJ Fundo Investimento",
767
819
  "valorCotaExtrato": "Valor Cota Extrato",
768
820
  "dataAfericaoValorCota": "Data Afericão Valor Cota",
769
- "ultimaCotacaoCota": "Última Cotação Cota",
770
821
  "dataUltimaCotacaoCota": "Data Última Cotação Cota",
771
822
  "sinalRentabilidadeMes": "Sinal Rentabilidade Mês",
772
823
  "valorRentabilidadeMes": "Valor Rentabilidade Mês",
@@ -837,6 +888,12 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
837
888
  mes: int,
838
889
  ano: int,
839
890
  ) -> pd.DataFrame:
891
+ """Reaver o extrato mensal de uma variação de poupança da conta.
892
+
893
+ Levanta :class:`SemLancamentosError` quando não há o que devolver e
894
+ :class:`ExtratoError` quando a variação não existe — nesse caso a API
895
+ responde ``400`` dizendo que o número da variação não é válido.
896
+ """
840
897
  access_token = self._get_access_token()
841
898
 
842
899
  res = requests.request(
@@ -851,14 +908,13 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
851
908
  )
852
909
 
853
910
  if res.status_code != 200:
854
- raise Exception(
855
- "Não foi possível listar as categorias do programa de governo."
856
- )
911
+ raise _erro_extrato_aplicacao(res)
857
912
 
858
913
  res = common.parse_json_object(res)
859
914
  return common.handle_results(
860
915
  res,
861
916
  main_list="listaLancamentos",
917
+ keep_header_when_empty=True,
862
918
  insertables=[
863
919
  "codigoProgramaGoverno",
864
920
  "nomeProgramaGoverno",
@@ -1689,6 +1745,14 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1689
1745
  mes: int,
1690
1746
  ano: int,
1691
1747
  ) -> pd.DataFrame:
1748
+ """Reaver o extrato mensal de um fundo de investimento da conta.
1749
+
1750
+ ``fundo_investimento_id`` é o ``grupoAtivo`` da operação no saldo de
1751
+ aplicações financeiras, não o ``codigo`` nem a ``modalidade``.
1752
+
1753
+ Levanta :class:`SemLancamentosError` quando o mês não tem movimento ou
1754
+ a conta não tem esse fundo.
1755
+ """
1692
1756
  access_token = self._get_access_token()
1693
1757
 
1694
1758
  res = requests.request(
@@ -1703,9 +1767,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1703
1767
  )
1704
1768
 
1705
1769
  if res.status_code != 200:
1706
- raise Exception(
1707
- "Não foi possível listar as categorias do programa de governo."
1708
- )
1770
+ raise _erro_extrato_aplicacao(res)
1709
1771
 
1710
1772
  res = common.parse_json_object(res)
1711
1773
  extrato = cast("dict[str, object]", res["extrato"])
@@ -1713,9 +1775,10 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1713
1775
  df = common.handle_results(
1714
1776
  extrato,
1715
1777
  main_list="listaLancamentosExtrato",
1778
+ keep_header_when_empty=True,
1716
1779
  insertables=[
1717
1780
  "numeroAgenciaRecebedora",
1718
- "digitoVerificadorContaRecebedora",
1781
+ "digitoVerificadoraContaRecebedora",
1719
1782
  "numeroContaCorrenteRecebedora",
1720
1783
  "numeroDigitoVerificadorContaCorrenteRecebedora",
1721
1784
  "nomeClienteRecebedor",
@@ -1723,7 +1786,6 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1723
1786
  "CNPJFundoInvestimento",
1724
1787
  "valorCotaExtrato",
1725
1788
  "dataAfericaoValorCota",
1726
- "ultimaCotacaoCota",
1727
1789
  "dataUltimaCotacaoCota",
1728
1790
  "sinalRentabilidadeMes",
1729
1791
  "valorRentabilidadeMes",
@@ -1762,7 +1824,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1762
1824
  ],
1763
1825
  rename_dict={
1764
1826
  "numeroAgenciaRecebedora": "Número Agência Recebedora",
1765
- "digitoVerificadorContaRecebedora": "Dígito Verificador Conta Recebedora",
1827
+ "digitoVerificadoraContaRecebedora": "Dígito Verificador Conta Recebedora",
1766
1828
  "numeroContaCorrenteRecebedora": "Número Conta Corrente Recebedora",
1767
1829
  "numeroDigitoVerificadorContaCorrenteRecebedora": "Número Dígito Verificador Conta Corrente Recebedora",
1768
1830
  "nomeClienteRecebedor": "Nome Cliente Recebedor",
@@ -1770,7 +1832,6 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1770
1832
  "CNPJFundoInvestimento": " CNPJ Fundo Investimento",
1771
1833
  "valorCotaExtrato": "Valor Cota Extrato",
1772
1834
  "dataAfericaoValorCota": "Data Afericão Valor Cota",
1773
- "ultimaCotacaoCota": "Última Cotação Cota",
1774
1835
  "dataUltimaCotacaoCota": "Data Última Cotação Cota",
1775
1836
  "sinalRentabilidadeMes": "Sinal Rentabilidade Mês",
1776
1837
  "valorRentabilidadeMes": "Valor Rentabilidade Mês",
@@ -1840,6 +1901,11 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1840
1901
  variacao_poupanca: str,
1841
1902
  codigo_variacao: int,
1842
1903
  ) -> pd.DataFrame:
1904
+ """Reaver o extrato de uma variação de poupança da conta.
1905
+
1906
+ Levanta :class:`SemLancamentosError` quando não há o que devolver e
1907
+ :class:`ExtratoError` quando a variação não existe.
1908
+ """
1843
1909
  access_token = self._get_access_token()
1844
1910
 
1845
1911
  res = requests.request(
@@ -1853,14 +1919,13 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1853
1919
  )
1854
1920
 
1855
1921
  if res.status_code != 200:
1856
- raise Exception(
1857
- "Não foi possível listar as categorias do programa de governo."
1858
- )
1922
+ raise _erro_extrato_aplicacao(res)
1859
1923
 
1860
1924
  res = common.parse_json_object(res)
1861
1925
  return common.handle_results(
1862
1926
  res,
1863
1927
  main_list="listaLancamentos",
1928
+ keep_header_when_empty=True,
1864
1929
  insertables=[
1865
1930
  "codigoProgramaGoverno",
1866
1931
  "nomeProgramaGoverno",
@@ -81,17 +81,31 @@ def handle_results(
81
81
  insertables: Sequence[str] | None = None,
82
82
  explodeables: Sequence[str] | None = None,
83
83
  rename_dict: Mapping[str, str] | None = None,
84
+ *,
85
+ keep_header_when_empty: bool = False,
84
86
  ) -> pd.DataFrame:
87
+ """Monta o DataFrame da resposta: uma linha por item de ``main_list``.
88
+
89
+ Os campos de ``insertables`` ficam no nível de cima da resposta, fora da
90
+ lista, e são repetidos em todas as linhas.
91
+
92
+ Com ``keep_header_when_empty``, uma lista vazia devolve uma linha só com
93
+ esses campos, em vez de um DataFrame sem linha nenhuma. É o que preserva o
94
+ cabeçalho das respostas em que ele carrega o dado principal, como o
95
+ rendimento do mês nos extratos de aplicação.
96
+ """
85
97
  record = cast("Mapping[str, object]", data)
86
98
 
87
99
  if main_list is not None:
88
- df = pd.DataFrame(cast("list[dict[Hashable, object]]", record[main_list]))
100
+ itens = cast("list[dict[Hashable, object]]", record.get(main_list) or [])
101
+ so_cabecalho = not itens and keep_header_when_empty
102
+ df = pd.DataFrame(index=[0]) if so_cabecalho else pd.DataFrame(itens)
89
103
  else:
90
104
  df = pd.DataFrame([dict(record)])
91
105
 
92
106
  if insertables is not None:
93
107
  for insertable in insertables:
94
- df[insertable] = cast("Scalar", record[insertable])
108
+ df[insertable] = cast("Scalar", record.get(insertable))
95
109
 
96
110
  if explodeables is not None:
97
111
  for explodeable in explodeables:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.5.1
3
+ Version: 0.5.3
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.1"
3
+ version = "0.5.3"
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