bb_api 0.5.3__tar.gz → 0.5.5__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.3
3
+ Version: 0.5.5
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.3"
7
+ __version__ = "0.5.5"
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",
@@ -22,22 +22,73 @@ _ERRO_INTERVALO_LONGO = "maior do que 31 dias"
22
22
  _STATUS_SEM_MOVIMENTO = 404
23
23
  _SEM_MOVIMENTO_PADRAO = "não há movimento no período consultado"
24
24
 
25
+ _LIMITE_DO_CORPO = 500
25
26
 
26
- class ExtratoError(Exception):
27
- """Erro devolvido pela API ao consultar o extrato do órgão repassador.
28
27
 
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.
28
+ class RespostaError(Exception):
29
+ """Resposta de erro devolvida pela API do Banco do Brasil.
30
+
31
+ Carrega o status HTTP e a mensagem originais junto da ação que falhou. Sem
32
+ eles, um ``404`` de "não há o que devolver", um ``403`` de escopo que falta
33
+ e um ``400`` de parâmetro inválido chegam ao chamador iguais.
34
+
35
+ Quando recebe a ``resposta``, guarda também o corpo cru e o
36
+ ``x-request-id``: o corpo traz o ``codigo`` e a ``ocorrencia`` do BB, que a
37
+ mensagem sozinha perde, e é com eles que o suporte do banco rastreia a falha.
31
38
  """
32
39
 
33
40
  status_code: int
34
41
  mensagem: str
42
+ corpo: str
43
+ request_id: str | None
35
44
 
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}")
45
+ def __init__(
46
+ self,
47
+ acao: str,
48
+ status_code: int,
49
+ mensagem: str,
50
+ *,
51
+ resposta: requests.Response | None = None,
52
+ ) -> None:
39
53
  self.status_code = status_code
40
54
  self.mensagem = mensagem
55
+ self.corpo = ""
56
+ self.request_id = None
57
+ if resposta is not None:
58
+ self.corpo = resposta.text.strip()
59
+ self.request_id = resposta.headers.get("x-request-id")
60
+ super().__init__(_descreve(acao, status_code, mensagem, self.corpo, self.request_id))
61
+
62
+
63
+ def _descreve(
64
+ acao: str, status_code: int, mensagem: str, corpo: str, request_id: str | None
65
+ ) -> str:
66
+ partes = [f"Não foi possível {acao} (HTTP {status_code}): {mensagem}"]
67
+ if corpo and corpo != mensagem:
68
+ encurtado = corpo[:_LIMITE_DO_CORPO] + ("…" if len(corpo) > _LIMITE_DO_CORPO else "")
69
+ partes.append(f"resposta do BB: {encurtado}")
70
+ if request_id:
71
+ partes.append(f"x-request-id: {request_id}")
72
+ return " | ".join(partes)
73
+
74
+
75
+ class ExtratoError(RespostaError):
76
+ """Erro devolvido pela API ao consultar o extrato do órgão repassador.
77
+
78
+ Carrega o status HTTP e a mensagem originais do Banco do Brasil, porque a
79
+ API usa ``400`` tanto para falha de verdade quanto para situações normais.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ status_code: int,
85
+ mensagem: str,
86
+ *,
87
+ resposta: requests.Response | None = None,
88
+ ) -> None:
89
+ super().__init__(
90
+ "reaver o extrato do órgão repassador", status_code, mensagem, resposta=resposta
91
+ )
41
92
 
42
93
 
43
94
  class SemLancamentosError(ExtratoError):
@@ -99,14 +150,19 @@ def _mensagem_da_lista_de_erros(campos: dict[str, object]) -> str:
99
150
  return ""
100
151
 
101
152
 
153
+ def _erro(acao: str, res: requests.Response) -> RespostaError:
154
+ """Erro de uma resposta não-200, com a ação que falhou e o que o BB disse."""
155
+ return RespostaError(acao, res.status_code, _mensagem_de_erro(res), resposta=res)
156
+
157
+
102
158
  def _erro_extrato(res: requests.Response) -> ExtratoError:
103
159
  mensagem = _mensagem_de_erro(res)
104
160
  normalizada = mensagem.casefold()
105
161
  if _ERRO_SEM_LANCAMENTOS in normalizada:
106
- return SemLancamentosError(res.status_code, mensagem)
162
+ return SemLancamentosError(res.status_code, mensagem, resposta=res)
107
163
  if _ERRO_INTERVALO_LONGO in normalizada:
108
- return IntervaloLongoDemaisError(res.status_code, mensagem)
109
- return ExtratoError(res.status_code, mensagem)
164
+ return IntervaloLongoDemaisError(res.status_code, mensagem, resposta=res)
165
+ return ExtratoError(res.status_code, mensagem, resposta=res)
110
166
 
111
167
 
112
168
  def _erro_extrato_aplicacao(res: requests.Response) -> ExtratoError:
@@ -118,7 +174,7 @@ def _erro_extrato_aplicacao(res: requests.Response) -> ExtratoError:
118
174
  """
119
175
  if res.status_code == _STATUS_SEM_MOVIMENTO:
120
176
  mensagem = _mensagem_de_erro(res) or _SEM_MOVIMENTO_PADRAO
121
- return SemLancamentosError(res.status_code, mensagem)
177
+ return SemLancamentosError(res.status_code, mensagem, resposta=res)
122
178
  return _erro_extrato(res)
123
179
 
124
180
 
@@ -235,7 +291,7 @@ class _AccountabilityV3BaseAPI:
235
291
  )
236
292
 
237
293
  if res.status_code != 200:
238
- raise Exception("Não foi possível adquirir as novas credenciais de acesso.")
294
+ raise _erro("adquirir as novas credenciais de acesso", res)
239
295
 
240
296
  data = common.parse_json_object(res)
241
297
  self._access_token = cast("str", data["access_token"])
@@ -266,9 +322,7 @@ class _AccountabilityV3BaseAPI:
266
322
  )
267
323
 
268
324
  if res.status_code != 200:
269
- raise Exception(
270
- "Não foi possível listar as categorias do programa de governo."
271
- )
325
+ raise _erro("listar as agências próximas", res)
272
326
 
273
327
  res = common.parse_json_object(res)
274
328
  return common.handle_results(
@@ -411,7 +465,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
411
465
  )
412
466
 
413
467
  if res.status_code != 200:
414
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
468
+ raise _erro("reaver o documento de despesa do programa de governo", res)
415
469
 
416
470
  res = common.parse_json_object(res)
417
471
 
@@ -542,7 +596,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
542
596
  )
543
597
 
544
598
  if res.status_code != 200:
545
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
599
+ raise _erro("reaver o documento de despesa da prestação de contas", res)
546
600
 
547
601
  res = common.parse_json_object(res)
548
602
 
@@ -674,7 +728,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
674
728
  )
675
729
 
676
730
  if res.status_code != 200:
677
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
731
+ raise _erro("reaver o extrato de subtransações do programa de governo", res)
678
732
 
679
733
  res = common.parse_json_object(res)
680
734
 
@@ -974,9 +1028,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
974
1028
  )
975
1029
 
976
1030
  if res.status_code != 200:
977
- raise Exception(
978
- "Não foi possível listar as categorias do programa de governo."
979
- )
1031
+ raise _erro("listar os lançamentos atualizados do programa de governo", res)
980
1032
 
981
1033
  res = common.parse_json_object(res)
982
1034
  return common.handle_results(
@@ -1017,9 +1069,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1017
1069
  )
1018
1070
 
1019
1071
  if res.status_code != 200:
1020
- raise Exception(
1021
- "Não foi possível listar as categorias do programa de governo."
1022
- )
1072
+ raise _erro("listar os sublançamentos atualizados do programa de governo", res)
1023
1073
 
1024
1074
  res = common.parse_json_object(res)
1025
1075
  return common.handle_results(
@@ -1053,9 +1103,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1053
1103
  )
1054
1104
 
1055
1105
  if res.status_code != 200:
1056
- raise Exception(
1057
- "Não foi possível listar as categorias do programa de governo."
1058
- )
1106
+ raise _erro("listar as categorias do programa de governo", res)
1059
1107
 
1060
1108
  res = common.parse_json_object(res)
1061
1109
  return common.handle_results(
@@ -1086,9 +1134,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1086
1134
  )
1087
1135
 
1088
1136
  if res.status_code != 200:
1089
- raise Exception(
1090
- "Não foi possível listar as categorias do programa de governo."
1091
- )
1137
+ raise _erro("reaver o saldo das aplicações financeiras", res)
1092
1138
 
1093
1139
  res = common.parse_json_object(res)
1094
1140
  return common.handle_results(
@@ -1125,9 +1171,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1125
1171
  )
1126
1172
 
1127
1173
  if res.status_code != 200:
1128
- raise Exception(
1129
- "Não foi possível listar as categorias do programa de governo."
1130
- )
1174
+ raise _erro("reaver o saldo da conta corrente", res)
1131
1175
 
1132
1176
  res = common.parse_json_object(res)
1133
1177
 
@@ -1173,9 +1217,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1173
1217
  )
1174
1218
 
1175
1219
  if res.status_code not in [200, 201]:
1176
- raise Exception(
1177
- "Não foi possível listar as categorias do programa de governo."
1178
- )
1220
+ raise _erro("categorizar a despesa do lançamento a crédito", res)
1179
1221
 
1180
1222
  res = common.parse_json_object(res)
1181
1223
  return common.handle_results(
@@ -1222,9 +1264,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1222
1264
  )
1223
1265
 
1224
1266
  if res.status_code not in [200, 201]:
1225
- raise Exception(
1226
- "Não foi possível listar as categorias do programa de governo."
1227
- )
1267
+ raise _erro("identificar o lançamento a crédito", res)
1228
1268
 
1229
1269
  res = common.parse_json_object(res)
1230
1270
  return common.handle_results(
@@ -1255,9 +1295,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1255
1295
  )
1256
1296
 
1257
1297
  if res.status_code not in [200, 201]:
1258
- raise Exception(
1259
- "Não foi possível listar as categorias do programa de governo."
1260
- )
1298
+ raise _erro("excluir a identificação do lançamento a crédito", res)
1261
1299
 
1262
1300
  res = common.parse_json_object(res)
1263
1301
  return common.handle_results(
@@ -1286,9 +1324,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
1286
1324
  )
1287
1325
 
1288
1326
  if res.status_code != 200:
1289
- raise Exception(
1290
- "Não foi possível listar as categorias do programa de governo."
1291
- )
1327
+ raise _erro("listar as identificações dos lançamentos a débito", res)
1292
1328
 
1293
1329
  res = common.parse_json_object(res)
1294
1330
  return common.handle_results(
@@ -1431,7 +1467,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1431
1467
  )
1432
1468
 
1433
1469
  if res.status_code != 200:
1434
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
1470
+ raise _erro("reaver o documento de despesa do programa de governo", res)
1435
1471
 
1436
1472
  res = common.parse_json_object(res)
1437
1473
 
@@ -1562,7 +1598,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1562
1598
  )
1563
1599
 
1564
1600
  if res.status_code != 200:
1565
- raise Exception("Não foi possível reaver o extrato do órgão repassador.")
1601
+ raise _erro("reaver o documento de despesa da prestação de contas", res)
1566
1602
 
1567
1603
  res = common.parse_json_object(res)
1568
1604
 
@@ -1688,9 +1724,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1688
1724
  )
1689
1725
 
1690
1726
  if res.status_code != 200:
1691
- raise Exception(
1692
- "Não foi possível listar as categorias do programa de governo."
1693
- )
1727
+ raise _erro("reaver o extrato de subtransações do programa de governo", res)
1694
1728
 
1695
1729
  res = common.parse_json_object(res)
1696
1730
  return common.handle_results(
@@ -1978,9 +2012,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
1978
2012
  )
1979
2013
 
1980
2014
  if res.status_code != 200:
1981
- raise Exception(
1982
- "Não foi possível listar as categorias do programa de governo."
1983
- )
2015
+ raise _erro("listar as contas correntes do órgão de controle", res)
1984
2016
 
1985
2017
  res = common.parse_json_object(res)
1986
2018
  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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.5.3
3
+ Version: 0.5.5
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.3"
3
+ version = "0.5.5"
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