bb_api 0.5.2__tar.gz → 0.5.4__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.2
3
+ Version: 0.5.4
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.2"
7
+ __version__ = "0.5.4"
8
8
 
9
9
 
10
10
  from .common import Ambiente
@@ -14,6 +14,7 @@ from .accountability import (
14
14
  AccountabilityV3ControleAPI,
15
15
  ExtratoError,
16
16
  IntervaloLongoDemaisError,
17
+ RespostaError,
17
18
  SemLancamentosError,
18
19
  )
19
20
  from .sia import (
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "CredenciaisNecessariasError",
37
38
  "ExtratoError",
38
39
  "IntervaloLongoDemaisError",
40
+ "RespostaError",
39
41
  "SemLancamentosError",
40
42
  "FalhaDownload",
41
43
  "ResultadoDownloads",
@@ -23,23 +23,34 @@ _STATUS_SEM_MOVIMENTO = 404
23
23
  _SEM_MOVIMENTO_PADRAO = "não há movimento no período consultado"
24
24
 
25
25
 
26
- class ExtratoError(Exception):
27
- """Erro devolvido pela API ao consultar o extrato do órgão repassador.
26
+ class RespostaError(Exception):
27
+ """Resposta de erro devolvida pela API do Banco do Brasil.
28
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.
29
+ Carrega o status HTTP e a mensagem originais junto da ação que falhou. Sem
30
+ eles, um ``404`` de "não o que devolver", um ``403`` de escopo que falta
31
+ e um ``400`` de parâmetro inválido chegam ao chamador iguais.
31
32
  """
32
33
 
33
34
  status_code: int
34
35
  mensagem: str
35
36
 
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}")
37
+ def __init__(self, acao: str, status_code: int, mensagem: str) -> None:
38
+ super().__init__(f"Não foi possível {acao} (HTTP {status_code}): {mensagem}")
39
39
  self.status_code = status_code
40
40
  self.mensagem = mensagem
41
41
 
42
42
 
43
+ class ExtratoError(RespostaError):
44
+ """Erro devolvido pela API ao consultar o extrato do órgão repassador.
45
+
46
+ Carrega o status HTTP e a mensagem originais do Banco do Brasil, porque a
47
+ API usa ``400`` tanto para falha de verdade quanto para situações normais.
48
+ """
49
+
50
+ def __init__(self, status_code: int, mensagem: str) -> None:
51
+ super().__init__("reaver o extrato do órgão repassador", status_code, mensagem)
52
+
53
+
43
54
  class SemLancamentosError(ExtratoError):
44
55
  """A conta não tem lançamento nenhum no período consultado.
45
56
 
@@ -99,6 +110,11 @@ def _mensagem_da_lista_de_erros(campos: dict[str, object]) -> str:
99
110
  return ""
100
111
 
101
112
 
113
+ def _erro(acao: str, res: requests.Response) -> RespostaError:
114
+ """Erro de uma resposta não-200, com a ação que falhou e o que o BB disse."""
115
+ return RespostaError(acao, res.status_code, _mensagem_de_erro(res))
116
+
117
+
102
118
  def _erro_extrato(res: requests.Response) -> ExtratoError:
103
119
  mensagem = _mensagem_de_erro(res)
104
120
  normalizada = mensagem.casefold()
@@ -235,7 +251,7 @@ class _AccountabilityV3BaseAPI:
235
251
  )
236
252
 
237
253
  if res.status_code != 200:
238
- raise Exception("Não foi possível adquirir as novas credenciais de acesso.")
254
+ raise _erro("adquirir as novas credenciais de acesso", res)
239
255
 
240
256
  data = common.parse_json_object(res)
241
257
  self._access_token = cast("str", data["access_token"])
@@ -266,9 +282,7 @@ class _AccountabilityV3BaseAPI:
266
282
  )
267
283
 
268
284
  if res.status_code != 200:
269
- raise Exception(
270
- "Não foi possível listar as categorias do programa de governo."
271
- )
285
+ raise _erro("listar as agências próximas", res)
272
286
 
273
287
  res = common.parse_json_object(res)
274
288
  return common.handle_results(
@@ -411,7 +425,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
411
425
  )
412
426
 
413
427
  if res.status_code != 200:
414
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
428
+ raise _erro("reaver o documento de despesa do programa de governo", res)
415
429
 
416
430
  res = common.parse_json_object(res)
417
431
 
@@ -542,7 +556,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
542
556
  )
543
557
 
544
558
  if res.status_code != 200:
545
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
559
+ raise _erro("reaver o documento de despesa da prestação de contas", res)
546
560
 
547
561
  res = common.parse_json_object(res)
548
562
 
@@ -674,7 +688,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
674
688
  )
675
689
 
676
690
  if res.status_code != 200:
677
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
691
+ raise _erro("reaver o extrato de subtransações do programa de governo", res)
678
692
 
679
693
  res = common.parse_json_object(res)
680
694
 
@@ -761,6 +775,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
761
775
  df = common.handle_results(
762
776
  extrato,
763
777
  main_list="listaLancamentosExtrato",
778
+ keep_header_when_empty=True,
764
779
  insertables=[
765
780
  "numeroAgenciaRecebedora",
766
781
  "digitoVerificadoraContaRecebedora",
@@ -913,6 +928,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
913
928
  return common.handle_results(
914
929
  res,
915
930
  main_list="listaLancamentos",
931
+ keep_header_when_empty=True,
916
932
  insertables=[
917
933
  "codigoProgramaGoverno",
918
934
  "nomeProgramaGoverno",
@@ -972,9 +988,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
972
988
  )
973
989
 
974
990
  if res.status_code != 200:
975
- raise Exception(
976
- "Não foi possível listar as categorias do programa de governo."
977
- )
991
+ raise _erro("listar os lançamentos atualizados do programa de governo", res)
978
992
 
979
993
  res = common.parse_json_object(res)
980
994
  return common.handle_results(
@@ -1015,9 +1029,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1015
1029
  )
1016
1030
 
1017
1031
  if res.status_code != 200:
1018
- raise Exception(
1019
- "Não foi possível listar as categorias do programa de governo."
1020
- )
1032
+ raise _erro("listar os sublançamentos atualizados do programa de governo", res)
1021
1033
 
1022
1034
  res = common.parse_json_object(res)
1023
1035
  return common.handle_results(
@@ -1051,9 +1063,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1051
1063
  )
1052
1064
 
1053
1065
  if res.status_code != 200:
1054
- raise Exception(
1055
- "Não foi possível listar as categorias do programa de governo."
1056
- )
1066
+ raise _erro("listar as categorias do programa de governo", res)
1057
1067
 
1058
1068
  res = common.parse_json_object(res)
1059
1069
  return common.handle_results(
@@ -1084,9 +1094,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1084
1094
  )
1085
1095
 
1086
1096
  if res.status_code != 200:
1087
- raise Exception(
1088
- "Não foi possível listar as categorias do programa de governo."
1089
- )
1097
+ raise _erro("reaver o saldo das aplicações financeiras", res)
1090
1098
 
1091
1099
  res = common.parse_json_object(res)
1092
1100
  return common.handle_results(
@@ -1123,9 +1131,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1123
1131
  )
1124
1132
 
1125
1133
  if res.status_code != 200:
1126
- raise Exception(
1127
- "Não foi possível listar as categorias do programa de governo."
1128
- )
1134
+ raise _erro("reaver o saldo da conta corrente", res)
1129
1135
 
1130
1136
  res = common.parse_json_object(res)
1131
1137
 
@@ -1171,9 +1177,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1171
1177
  )
1172
1178
 
1173
1179
  if res.status_code not in [200, 201]:
1174
- raise Exception(
1175
- "Não foi possível listar as categorias do programa de governo."
1176
- )
1180
+ raise _erro("categorizar a despesa do lançamento a crédito", res)
1177
1181
 
1178
1182
  res = common.parse_json_object(res)
1179
1183
  return common.handle_results(
@@ -1220,9 +1224,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1220
1224
  )
1221
1225
 
1222
1226
  if res.status_code not in [200, 201]:
1223
- raise Exception(
1224
- "Não foi possível listar as categorias do programa de governo."
1225
- )
1227
+ raise _erro("identificar o lançamento a crédito", res)
1226
1228
 
1227
1229
  res = common.parse_json_object(res)
1228
1230
  return common.handle_results(
@@ -1253,9 +1255,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1253
1255
  )
1254
1256
 
1255
1257
  if res.status_code not in [200, 201]:
1256
- raise Exception(
1257
- "Não foi possível listar as categorias do programa de governo."
1258
- )
1258
+ raise _erro("excluir a identificação do lançamento a crédito", res)
1259
1259
 
1260
1260
  res = common.parse_json_object(res)
1261
1261
  return common.handle_results(
@@ -1284,9 +1284,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1284
1284
  )
1285
1285
 
1286
1286
  if res.status_code != 200:
1287
- raise Exception(
1288
- "Não foi possível listar as categorias do programa de governo."
1289
- )
1287
+ raise _erro("listar as identificações dos lançamentos a débito", res)
1290
1288
 
1291
1289
  res = common.parse_json_object(res)
1292
1290
  return common.handle_results(
@@ -1429,7 +1427,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1429
1427
  )
1430
1428
 
1431
1429
  if res.status_code != 200:
1432
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
1430
+ raise _erro("reaver o documento de despesa do programa de governo", res)
1433
1431
 
1434
1432
  res = common.parse_json_object(res)
1435
1433
 
@@ -1560,7 +1558,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1560
1558
  )
1561
1559
 
1562
1560
  if res.status_code != 200:
1563
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
1561
+ raise _erro("reaver o documento de despesa da prestação de contas", res)
1564
1562
 
1565
1563
  res = common.parse_json_object(res)
1566
1564
 
@@ -1686,9 +1684,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1686
1684
  )
1687
1685
 
1688
1686
  if res.status_code != 200:
1689
- raise Exception(
1690
- "Não foi possível listar as categorias do programa de governo."
1691
- )
1687
+ raise _erro("reaver o extrato de subtransações do programa de governo", res)
1692
1688
 
1693
1689
  res = common.parse_json_object(res)
1694
1690
  return common.handle_results(
@@ -1773,6 +1769,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1773
1769
  df = common.handle_results(
1774
1770
  extrato,
1775
1771
  main_list="listaLancamentosExtrato",
1772
+ keep_header_when_empty=True,
1776
1773
  insertables=[
1777
1774
  "numeroAgenciaRecebedora",
1778
1775
  "digitoVerificadoraContaRecebedora",
@@ -1922,6 +1919,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1922
1919
  return common.handle_results(
1923
1920
  res,
1924
1921
  main_list="listaLancamentos",
1922
+ keep_header_when_empty=True,
1925
1923
  insertables=[
1926
1924
  "codigoProgramaGoverno",
1927
1925
  "nomeProgramaGoverno",
@@ -1974,9 +1972,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1974
1972
  )
1975
1973
 
1976
1974
  if res.status_code != 200:
1977
- raise Exception(
1978
- "Não foi possível listar as categorias do programa de governo."
1979
- )
1975
+ raise _erro("listar as contas correntes do órgão de controle", res)
1980
1976
 
1981
1977
  res = common.parse_json_object(res)
1982
1978
  return common.handle_results(
@@ -60,9 +60,27 @@ def handle_numeric_string_with_symbols(v: str) -> str:
60
60
  return re.sub(r"\D", "", v)
61
61
 
62
62
 
63
+ def _data_de_texto(texto: str) -> datetime:
64
+ """Interpreta uma data em ISO ou em dd/mm/aaaa.
65
+
66
+ A API devolve os dois formatos, e o valor de uma resposta costuma virar
67
+ parâmetro da consulta seguinte: o ``bookingDate`` do extrato chega em
68
+ dd/mm/aaaa e é o que identifica o documento de despesa daquela transação.
69
+ """
70
+ formatos = ("%Y-%m-%d", "%d/%m/%Y")
71
+ for formato in formatos:
72
+ try:
73
+ return datetime.strptime(texto, formato)
74
+ except ValueError:
75
+ continue
76
+
77
+ aceitos = " ou ".join(formatos)
78
+ raise ValueError(f"A data '{texto}' não está em {aceitos}.")
79
+
80
+
63
81
  def handle_dates(v: DateLike) -> str:
64
82
  if isinstance(v, str):
65
- dt = datetime.strptime(v, "%Y-%m-%d")
83
+ dt = _data_de_texto(v)
66
84
  elif isinstance(v, datetime):
67
85
  dt = v
68
86
  else:
@@ -81,11 +99,25 @@ def handle_results(
81
99
  insertables: Sequence[str] | None = None,
82
100
  explodeables: Sequence[str] | None = None,
83
101
  rename_dict: Mapping[str, str] | None = None,
102
+ *,
103
+ keep_header_when_empty: bool = False,
84
104
  ) -> pd.DataFrame:
105
+ """Monta o DataFrame da resposta: uma linha por item de ``main_list``.
106
+
107
+ Os campos de ``insertables`` ficam no nível de cima da resposta, fora da
108
+ lista, e são repetidos em todas as linhas.
109
+
110
+ Com ``keep_header_when_empty``, uma lista vazia devolve uma linha só com
111
+ esses campos, em vez de um DataFrame sem linha nenhuma. É o que preserva o
112
+ cabeçalho das respostas em que ele carrega o dado principal, como o
113
+ rendimento do mês nos extratos de aplicação.
114
+ """
85
115
  record = cast("Mapping[str, object]", data)
86
116
 
87
117
  if main_list is not None:
88
- df = pd.DataFrame(cast("list[dict[Hashable, object]]", record[main_list]))
118
+ itens = cast("list[dict[Hashable, object]]", record.get(main_list) or [])
119
+ so_cabecalho = not itens and keep_header_when_empty
120
+ df = pd.DataFrame(index=[0]) if so_cabecalho else pd.DataFrame(itens)
89
121
  else:
90
122
  df = pd.DataFrame([dict(record)])
91
123
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.5.2
3
+ Version: 0.5.4
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.2"
3
+ version = "0.5.4"
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