bb_api 0.4.0__tar.gz → 0.5.0__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.4.0 → bb_api-0.5.0}/PKG-INFO +1 -1
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api/__init__.py +12 -4
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api/accountability.py +17 -33
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api/common.py +0 -5
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api/gestao_agil.py +61 -63
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api/sia.py +127 -233
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api.egg-info/PKG-INFO +1 -1
- {bb_api-0.4.0 → bb_api-0.5.0}/pyproject.toml +6 -1
- {bb_api-0.4.0 → bb_api-0.5.0}/LICENSE +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/README.md +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api.egg-info/SOURCES.txt +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api.egg-info/dependency_links.txt +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api.egg-info/requires.txt +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/bb_api.egg-info/top_level.txt +0 -0
- {bb_api-0.4.0 → bb_api-0.5.0}/setup.cfg +0 -0
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
from importlib.metadata import version
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
try:
|
|
5
5
|
__version__ = version("bb_api")
|
|
6
|
-
except:
|
|
7
|
-
__version__ = "0.
|
|
6
|
+
except PackageNotFoundError:
|
|
7
|
+
__version__ = "0.5.0"
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from .common import Ambiente
|
|
11
11
|
from .accountability import AccountabilityV3RepasseAPI, AccountabilityV3ControleAPI
|
|
12
|
-
from .sia import
|
|
12
|
+
from .sia import (
|
|
13
|
+
BBSiaAPI,
|
|
14
|
+
CredenciaisNecessariasError,
|
|
15
|
+
FalhaDownload,
|
|
16
|
+
ResultadoDownloads,
|
|
17
|
+
)
|
|
13
18
|
from .gestao_agil import (
|
|
14
19
|
parse_retorno_abertura_massificada,
|
|
15
20
|
ler_retorno_abertura_massificada,
|
|
@@ -20,6 +25,9 @@ __all__ = [
|
|
|
20
25
|
"AccountabilityV3RepasseAPI",
|
|
21
26
|
"AccountabilityV3ControleAPI",
|
|
22
27
|
"BBSiaAPI",
|
|
28
|
+
"CredenciaisNecessariasError",
|
|
29
|
+
"FalhaDownload",
|
|
30
|
+
"ResultadoDownloads",
|
|
23
31
|
"parse_retorno_abertura_massificada",
|
|
24
32
|
"ler_retorno_abertura_massificada",
|
|
25
33
|
]
|
|
@@ -85,13 +85,9 @@ class _AccountabilityV3BaseAPI:
|
|
|
85
85
|
+ "ambiente 'BB_API_CLIENT_SECRET'."
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
base64_credentials = (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
f"{client_id}:{client_secret}".encode("utf-8")
|
|
92
|
-
)
|
|
93
|
-
.decode("utf-8")
|
|
94
|
-
)
|
|
88
|
+
base64_credentials = base64.b64encode(
|
|
89
|
+
f"{client_id}:{client_secret}".encode("utf-8")
|
|
90
|
+
).decode("utf-8")
|
|
95
91
|
self._access_token = ""
|
|
96
92
|
self._base64_credentials = base64_credentials
|
|
97
93
|
self._last_access_token_request_timestamp = None
|
|
@@ -126,9 +122,7 @@ class _AccountabilityV3BaseAPI:
|
|
|
126
122
|
)
|
|
127
123
|
|
|
128
124
|
if res.status_code != 200:
|
|
129
|
-
raise Exception(
|
|
130
|
-
"Não foi possível adquirir as novas credenciais de acesso."
|
|
131
|
-
)
|
|
125
|
+
raise Exception("Não foi possível adquirir as novas credenciais de acesso.")
|
|
132
126
|
|
|
133
127
|
data = common.parse_json_object(res)
|
|
134
128
|
self._access_token = cast("str", data["access_token"])
|
|
@@ -230,9 +224,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
230
224
|
)
|
|
231
225
|
|
|
232
226
|
if res.status_code != 200:
|
|
233
|
-
raise Exception(
|
|
234
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
235
|
-
)
|
|
227
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
236
228
|
|
|
237
229
|
res = common.parse_json_object(res)
|
|
238
230
|
|
|
@@ -306,9 +298,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
306
298
|
)
|
|
307
299
|
|
|
308
300
|
if res.status_code != 200:
|
|
309
|
-
raise Exception(
|
|
310
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
311
|
-
)
|
|
301
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
312
302
|
|
|
313
303
|
res = common.parse_json_object(res)
|
|
314
304
|
|
|
@@ -439,9 +429,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
439
429
|
)
|
|
440
430
|
|
|
441
431
|
if res.status_code != 200:
|
|
442
|
-
raise Exception(
|
|
443
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
444
|
-
)
|
|
432
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
445
433
|
|
|
446
434
|
res = common.parse_json_object(res)
|
|
447
435
|
|
|
@@ -573,9 +561,7 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
573
561
|
)
|
|
574
562
|
|
|
575
563
|
if res.status_code != 200:
|
|
576
|
-
raise Exception(
|
|
577
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
578
|
-
)
|
|
564
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
579
565
|
|
|
580
566
|
res = common.parse_json_object(res)
|
|
581
567
|
|
|
@@ -762,7 +748,9 @@ class AccountabilityV3RepasseAPI(_AccountabilityV3BaseAPI):
|
|
|
762
748
|
},
|
|
763
749
|
)
|
|
764
750
|
|
|
765
|
-
df["Código Programa Governo"] = cast(
|
|
751
|
+
df["Código Programa Governo"] = cast(
|
|
752
|
+
"common.Scalar", res["codigoProgramaGoverno"]
|
|
753
|
+
)
|
|
766
754
|
df["Nome Programa Governo"] = cast("common.Scalar", res["nomeProgramaGoverno"])
|
|
767
755
|
df["Código SubPrograma Governo"] = cast(
|
|
768
756
|
"common.Scalar", res["codigoSubProgramaGoverno"]
|
|
@@ -1245,9 +1233,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1245
1233
|
)
|
|
1246
1234
|
|
|
1247
1235
|
if res.status_code != 200:
|
|
1248
|
-
raise Exception(
|
|
1249
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
1250
|
-
)
|
|
1236
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
1251
1237
|
|
|
1252
1238
|
res = common.parse_json_object(res)
|
|
1253
1239
|
|
|
@@ -1321,9 +1307,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1321
1307
|
)
|
|
1322
1308
|
|
|
1323
1309
|
if res.status_code != 200:
|
|
1324
|
-
raise Exception(
|
|
1325
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
1326
|
-
)
|
|
1310
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
1327
1311
|
|
|
1328
1312
|
res = common.parse_json_object(res)
|
|
1329
1313
|
|
|
@@ -1454,9 +1438,7 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1454
1438
|
)
|
|
1455
1439
|
|
|
1456
1440
|
if res.status_code != 200:
|
|
1457
|
-
raise Exception(
|
|
1458
|
-
"Não foi possível reaver o extrato do órgão repassador."
|
|
1459
|
-
)
|
|
1441
|
+
raise Exception("Não foi possível reaver o extrato do órgão repassador.")
|
|
1460
1442
|
|
|
1461
1443
|
res = common.parse_json_object(res)
|
|
1462
1444
|
|
|
@@ -1770,7 +1752,9 @@ class AccountabilityV3ControleAPI(_AccountabilityV3BaseAPI):
|
|
|
1770
1752
|
},
|
|
1771
1753
|
)
|
|
1772
1754
|
|
|
1773
|
-
df["Código Programa Governo"] = cast(
|
|
1755
|
+
df["Código Programa Governo"] = cast(
|
|
1756
|
+
"common.Scalar", res["codigoProgramaGoverno"]
|
|
1757
|
+
)
|
|
1774
1758
|
df["Nome Programa Governo"] = cast("common.Scalar", res["nomeProgramaGoverno"])
|
|
1775
1759
|
df["Código SubPrograma Governo"] = cast(
|
|
1776
1760
|
"common.Scalar", res["codigoSubProgramaGoverno"]
|
|
@@ -42,11 +42,6 @@ _sia_domains = {
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def sia_domain_for(ambiente: Ambiente) -> str:
|
|
45
|
-
"""Devolve o domínio do BB Sia (GMT-EDI) correspondente ao ``ambiente``.
|
|
46
|
-
|
|
47
|
-
Lança ``ValueError`` para ambientes que o BB Sia não atende (como o
|
|
48
|
-
``HOMOLOGACAO_ALTERNATIVO``).
|
|
49
|
-
"""
|
|
50
45
|
domain = _sia_domains.get(ambiente)
|
|
51
46
|
if domain is None:
|
|
52
47
|
raise ValueError(
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
"""Parser para arquivos de retorno MCIF470 (abertura de contas massificadas).
|
|
2
|
-
|
|
3
|
-
Esses arquivos são disponibilizados pela API GMT-SIA do Banco do Brasil
|
|
4
|
-
(``gmtedi.bb.com.br/gmt-sia-api``), no contexto do sistema Gestão Ágil, e seguem
|
|
5
|
-
um layout de largura fixa de 150 posições por linha, conforme a aba
|
|
6
|
-
"MCIF470-Abertura RETORNO" da planilha de leiaute de abertura massificada.
|
|
7
|
-
|
|
8
|
-
Cada linha é identificada pelas 5 primeiras posições:
|
|
9
|
-
|
|
10
|
-
HEADER -> "00000"
|
|
11
|
-
TRAILER -> "99999"
|
|
12
|
-
DETALHE -> demais
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
1
|
import os
|
|
2
|
+
import warnings
|
|
16
3
|
from collections.abc import Mapping, Sequence
|
|
4
|
+
from datetime import date
|
|
17
5
|
|
|
18
6
|
import pandas as pd
|
|
19
7
|
|
|
@@ -24,11 +12,9 @@ _HEADER_ID = "00000"
|
|
|
24
12
|
_TRAILER_ID = "99999"
|
|
25
13
|
_LINE_LENGTH = 150
|
|
26
14
|
|
|
27
|
-
# Cada campo: (nome, posição inicial, posição final, formato).
|
|
28
|
-
# Posições 1-based e inclusivas, como na planilha de leiaute.
|
|
29
15
|
_HEADER_LAYOUT = [
|
|
30
16
|
("preenchimento", 1, 5, "N"),
|
|
31
|
-
("data_remessa", 6, 13, "
|
|
17
|
+
("data_remessa", 6, 13, "A"),
|
|
32
18
|
("nome_arquivo", 14, 21, "A"),
|
|
33
19
|
("numero_processo", 22, 26, "N"),
|
|
34
20
|
("sequencial_remessa", 27, 31, "N"),
|
|
@@ -38,21 +24,21 @@ _HEADER_LAYOUT = [
|
|
|
38
24
|
|
|
39
25
|
_DETALHE_LAYOUT = [
|
|
40
26
|
("sequencial", 1, 5, "N"),
|
|
41
|
-
("cpf_cnpj", 6, 19, "
|
|
42
|
-
("data_nascimento", 20, 27, "
|
|
27
|
+
("cpf_cnpj", 6, 19, "ID"),
|
|
28
|
+
("data_nascimento", 20, 27, "A"),
|
|
43
29
|
("nome_cliente", 28, 87, "A"),
|
|
44
30
|
("uso_cliente", 88, 95, "A"),
|
|
45
|
-
("numero_programa_gestao_agil", 96, 104, "
|
|
46
|
-
("agencia_cliente", 105, 108, "
|
|
47
|
-
("dv_agencia_cliente", 109, 109, "
|
|
48
|
-
("grupo_setex", 110, 111, "
|
|
49
|
-
("dv_grupo_setex", 112, 112, "
|
|
50
|
-
("conta", 113, 123, "
|
|
51
|
-
("dv_conta", 124, 124, "
|
|
52
|
-
("ocorrencia_cliente", 125, 127, "
|
|
53
|
-
("ocorrencia_conta", 128, 130, "
|
|
54
|
-
("ocorrencia_limite_credito", 131, 133, "
|
|
55
|
-
("codigo_mci", 134, 142, "
|
|
31
|
+
("numero_programa_gestao_agil", 96, 104, "ID"),
|
|
32
|
+
("agencia_cliente", 105, 108, "ID"),
|
|
33
|
+
("dv_agencia_cliente", 109, 109, "ID"),
|
|
34
|
+
("grupo_setex", 110, 111, "ID"),
|
|
35
|
+
("dv_grupo_setex", 112, 112, "ID"),
|
|
36
|
+
("conta", 113, 123, "ID"),
|
|
37
|
+
("dv_conta", 124, 124, "ID"),
|
|
38
|
+
("ocorrencia_cliente", 125, 127, "ID"),
|
|
39
|
+
("ocorrencia_conta", 128, 130, "ID"),
|
|
40
|
+
("ocorrencia_limite_credito", 131, 133, "ID"),
|
|
41
|
+
("codigo_mci", 134, 142, "ID"),
|
|
56
42
|
("espacos_em_branco", 143, 150, "A"),
|
|
57
43
|
]
|
|
58
44
|
|
|
@@ -62,7 +48,6 @@ _TRAILER_LAYOUT = [
|
|
|
62
48
|
("espacos_em_branco", 15, 150, "A"),
|
|
63
49
|
]
|
|
64
50
|
|
|
65
|
-
# Tabela 1 - Ocorrências do Cliente
|
|
66
51
|
_TABELA_OCORRENCIA_CLIENTE = {
|
|
67
52
|
"001": "tipo pessoa inválido",
|
|
68
53
|
"002": "tipo CPF/CNPJ inválido",
|
|
@@ -82,7 +67,6 @@ _TABELA_OCORRENCIA_CLIENTE = {
|
|
|
82
67
|
"017": "tipo de pessoa não permitido para esse processo/tipo de repasse",
|
|
83
68
|
}
|
|
84
69
|
|
|
85
|
-
# Tabela 2 - Ocorrências da Conta
|
|
86
70
|
_TABELA_OCORRENCIA_CONTA = {
|
|
87
71
|
"001": "ind cheque especial inválido",
|
|
88
72
|
"002": "setex/dv inválido",
|
|
@@ -90,7 +74,6 @@ _TABELA_OCORRENCIA_CONTA = {
|
|
|
90
74
|
"004": "dados pessoa física divergente",
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
# Tabela 3 - Ocorrências do Limite de Crédito
|
|
94
77
|
_TABELA_OCORRENCIA_LIMITE = {
|
|
95
78
|
"001": "cod estado civil inválido",
|
|
96
79
|
"002": "cod natureza ocupação inválido",
|
|
@@ -102,7 +85,6 @@ _TABELA_OCORRENCIA_LIMITE = {
|
|
|
102
85
|
"008": "nao atende ao credit scoring",
|
|
103
86
|
}
|
|
104
87
|
|
|
105
|
-
# Colunas de preenchimento/controle que não interessam ao DataFrame final.
|
|
106
88
|
_COLUNAS_DESCARTADAS = ["tipo", "espacos_em_branco"]
|
|
107
89
|
|
|
108
90
|
_RENAME_DETALHE = {
|
|
@@ -136,7 +118,6 @@ def _parse_fields(
|
|
|
136
118
|
linha: str,
|
|
137
119
|
layout: Sequence[tuple[str, int, int, str]],
|
|
138
120
|
) -> dict[str, str | int]:
|
|
139
|
-
"""Fatia a linha conforme o layout. Campos "N" viram int quando possível."""
|
|
140
121
|
rec: dict[str, str | int] = {}
|
|
141
122
|
for nome, ini, fim, fmt in layout:
|
|
142
123
|
valor = linha[ini - 1:fim].strip()
|
|
@@ -147,8 +128,28 @@ def _parse_fields(
|
|
|
147
128
|
return rec
|
|
148
129
|
|
|
149
130
|
|
|
131
|
+
def _parse_data_ddmmaaaa(valor: object) -> str | None:
|
|
132
|
+
texto = str(valor).strip().zfill(8)
|
|
133
|
+
if texto == "0" * 8 or len(texto) != 8 or not texto.isdigit():
|
|
134
|
+
return None
|
|
135
|
+
dia, mes, ano = texto[:2], texto[2:4], texto[4:]
|
|
136
|
+
try:
|
|
137
|
+
return date(int(ano), int(mes), int(dia)).isoformat()
|
|
138
|
+
except ValueError:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_cpf_cnpj(valor: str) -> dict[str, str] | None:
|
|
143
|
+
digitos = "".join(c for c in valor if c.isdigit())
|
|
144
|
+
if not digitos.strip("0"):
|
|
145
|
+
return None
|
|
146
|
+
significativos = digitos.lstrip("0")
|
|
147
|
+
if len(significativos) <= 11:
|
|
148
|
+
return {"valor": digitos[-11:].zfill(11), "tipo": "cpf"}
|
|
149
|
+
return {"valor": digitos.zfill(14), "tipo": "cnpj"}
|
|
150
|
+
|
|
151
|
+
|
|
150
152
|
def _decode_ocorrencia(codigo: object, tabela: Mapping[str, str]) -> str | None:
|
|
151
|
-
"""Descrição da ocorrência; None para "000"/vazio/sem código."""
|
|
152
153
|
cod = str(codigo).zfill(3) if str(codigo).strip() else ""
|
|
153
154
|
if cod in ("", "000"):
|
|
154
155
|
return None
|
|
@@ -156,7 +157,6 @@ def _decode_ocorrencia(codigo: object, tabela: Mapping[str, str]) -> str | None:
|
|
|
156
157
|
|
|
157
158
|
|
|
158
159
|
def _parse_linha(linha: str, numero: int) -> dict[str, object] | None:
|
|
159
|
-
"""Parseia uma linha e devolve um dict com o tipo de registro e os campos."""
|
|
160
160
|
linha = linha.rstrip("\r\n")
|
|
161
161
|
if not linha.strip():
|
|
162
162
|
return None
|
|
@@ -166,15 +166,27 @@ def _parse_linha(linha: str, numero: int) -> dict[str, object] | None:
|
|
|
166
166
|
|
|
167
167
|
ident = linha[:5]
|
|
168
168
|
if ident == _HEADER_ID:
|
|
169
|
-
|
|
169
|
+
header: dict[str, object] = {
|
|
170
|
+
"tipo": "HEADER",
|
|
171
|
+
"linha": numero,
|
|
172
|
+
**_parse_fields(linha, _HEADER_LAYOUT),
|
|
173
|
+
}
|
|
174
|
+
header["data_remessa"] = _parse_data_ddmmaaaa(header["data_remessa"])
|
|
175
|
+
return header
|
|
170
176
|
if ident == _TRAILER_ID:
|
|
171
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
"tipo": "TRAILER",
|
|
179
|
+
"linha": numero,
|
|
180
|
+
**_parse_fields(linha, _TRAILER_LAYOUT),
|
|
181
|
+
}
|
|
172
182
|
|
|
173
183
|
rec: dict[str, object] = {
|
|
174
184
|
"tipo": "DETALHE",
|
|
175
185
|
"linha": numero,
|
|
176
186
|
**_parse_fields(linha, _DETALHE_LAYOUT),
|
|
177
187
|
}
|
|
188
|
+
rec["cpf_cnpj"] = _parse_cpf_cnpj(str(rec["cpf_cnpj"]))
|
|
189
|
+
rec["data_nascimento"] = _parse_data_ddmmaaaa(rec["data_nascimento"])
|
|
178
190
|
rec["ocorrencia_cliente_desc"] = _decode_ocorrencia(
|
|
179
191
|
rec["ocorrencia_cliente"], _TABELA_OCORRENCIA_CLIENTE)
|
|
180
192
|
rec["ocorrencia_conta_desc"] = _decode_ocorrencia(
|
|
@@ -185,18 +197,6 @@ def _parse_linha(linha: str, numero: int) -> dict[str, object] | None:
|
|
|
185
197
|
|
|
186
198
|
|
|
187
199
|
def parse_retorno_abertura_massificada(conteudo: str) -> pd.DataFrame:
|
|
188
|
-
"""Parseia o conteúdo de um arquivo de retorno MCIF470 (abertura de contas
|
|
189
|
-
massificadas) e devolve um ``DataFrame`` com os registros de DETALHE.
|
|
190
|
-
|
|
191
|
-
Os códigos de ocorrência são decodificados em colunas descritivas e os
|
|
192
|
-
metadados do cabeçalho (número do processo, data e sequencial da remessa)
|
|
193
|
-
são repetidos em todas as linhas para facilitar a rastreabilidade.
|
|
194
|
-
|
|
195
|
-
Parâmetros
|
|
196
|
-
----------
|
|
197
|
-
conteudo: str
|
|
198
|
-
Conteúdo textual completo do arquivo de retorno.
|
|
199
|
-
"""
|
|
200
200
|
registros: list[dict[str, object]] = []
|
|
201
201
|
for numero, linha in enumerate(conteudo.splitlines(), start=1):
|
|
202
202
|
rec = _parse_linha(linha, numero)
|
|
@@ -211,6 +211,16 @@ def parse_retorno_abertura_massificada(conteudo: str) -> pd.DataFrame:
|
|
|
211
211
|
header = registro
|
|
212
212
|
break
|
|
213
213
|
|
|
214
|
+
trailer = next((r for r in registros if r["tipo"] == "TRAILER"), None)
|
|
215
|
+
if trailer is not None:
|
|
216
|
+
esperado = trailer.get("quantidade_registros")
|
|
217
|
+
if isinstance(esperado, int) and esperado != len(registros):
|
|
218
|
+
warnings.warn(
|
|
219
|
+
f"Trailer indica {esperado} registros, mas {len(registros)}"
|
|
220
|
+
+ " foram lidos do arquivo.",
|
|
221
|
+
stacklevel=2,
|
|
222
|
+
)
|
|
223
|
+
|
|
214
224
|
data = {
|
|
215
225
|
"detalhes": detalhes,
|
|
216
226
|
"numero_processo": header.get("numero_processo"),
|
|
@@ -234,18 +244,6 @@ def ler_retorno_abertura_massificada(
|
|
|
234
244
|
caminho: str | os.PathLike[str],
|
|
235
245
|
encoding: str = "latin-1",
|
|
236
246
|
) -> pd.DataFrame:
|
|
237
|
-
"""Lê um arquivo de retorno MCIF470 do disco e o parseia.
|
|
238
|
-
|
|
239
|
-
Atalho para ``parse_retorno_abertura_massificada`` quando o arquivo já está
|
|
240
|
-
salvo localmente.
|
|
241
|
-
|
|
242
|
-
Parâmetros
|
|
243
|
-
----------
|
|
244
|
-
caminho: str | os.PathLike
|
|
245
|
-
Caminho do arquivo de retorno a ser lido.
|
|
246
|
-
encoding: str
|
|
247
|
-
Codificação do arquivo (padrão: ``latin-1``).
|
|
248
|
-
"""
|
|
249
247
|
with open(caminho, "r", encoding=encoding) as arquivo:
|
|
250
248
|
conteudo = arquivo.read()
|
|
251
249
|
return parse_retorno_abertura_massificada(conteudo)
|
|
@@ -1,39 +1,20 @@
|
|
|
1
|
-
"""Encapsulador da API BB Sia (Gestão Ágil) do Banco do Brasil.
|
|
2
|
-
|
|
3
|
-
O BB Sia é acessado pelo domínio do GMT-EDI (``gmtedi.bb.com.br`` em produção) e
|
|
4
|
-
expõe, entre outros, os seguintes grupos de endpoints:
|
|
5
|
-
|
|
6
|
-
* ``gmt-autorizador-api`` -- autorização (usuário/senha e refresh token) e
|
|
7
|
-
revogação de tokens;
|
|
8
|
-
* ``gmt-catalogo-api`` -- catálogo dos uploads possíveis;
|
|
9
|
-
* ``gmt-sia-api`` -- envio (upload) e recebimento (download) de arquivos, além
|
|
10
|
-
da consulta de downloads e dos seus metadados;
|
|
11
|
-
* ``gmt-protocolo-api`` -- consulta de protocolos.
|
|
12
|
-
|
|
13
|
-
Diferente da API ``Accountability``, a autorização do BB Sia não usa o fluxo
|
|
14
|
-
``client_credentials`` do ``oauth.bb.com.br``: ela usa o *grant* ``password``
|
|
15
|
-
(usuário e senha) ou ``refresh_token`` contra o próprio GMT-EDI, devolvendo um
|
|
16
|
-
``access_token`` e um ``refresh_token``.
|
|
17
|
-
|
|
18
|
-
.. note::
|
|
19
|
-
Os nomes das listas e colunas devolvidas pelos endpoints de consulta
|
|
20
|
-
(``listaUploads``, ``listaDownloads``, ``listaProtocolos`` e metadados) são
|
|
21
|
-
inferidos a partir da semântica de cada endpoint, pois não há um leiaute de
|
|
22
|
-
resposta publicado na coleção de referência. Por isso esses métodos não
|
|
23
|
-
renomeiam as colunas: ajuste o tratamento conforme a resposta real da API.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
1
|
import os
|
|
27
2
|
import base64
|
|
28
3
|
import hashlib
|
|
29
4
|
import datetime
|
|
30
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
31
7
|
from typing import cast
|
|
32
8
|
|
|
33
9
|
import pandas as pd
|
|
34
10
|
import requests
|
|
35
11
|
|
|
36
12
|
import bb_api.common as common
|
|
13
|
+
import bb_api.gestao_agil as gestao_agil
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CredenciaisNecessariasError(Exception):
|
|
17
|
+
pass
|
|
37
18
|
|
|
38
19
|
|
|
39
20
|
def _to_dataframe(
|
|
@@ -41,13 +22,6 @@ def _to_dataframe(
|
|
|
41
22
|
main_list: str | None = None,
|
|
42
23
|
rename_dict: Mapping[str, str] | None = None,
|
|
43
24
|
) -> pd.DataFrame:
|
|
44
|
-
"""Constrói um ``DataFrame`` a partir de uma resposta de esquema desconhecido.
|
|
45
|
-
|
|
46
|
-
Usa ``main_list`` quando essa chave existe e aponta para uma lista; caso
|
|
47
|
-
contrário, usa a primeira lista encontrada na resposta e, se não houver
|
|
48
|
-
nenhuma, trata a resposta inteira como um único registro. Assim os endpoints
|
|
49
|
-
de consulta do BB Sia não quebram mesmo sem um leiaute de resposta conhecido.
|
|
50
|
-
"""
|
|
51
25
|
record = cast("Mapping[str, object]", res)
|
|
52
26
|
|
|
53
27
|
if main_list is not None and isinstance(record.get(main_list), list):
|
|
@@ -61,22 +35,11 @@ def _to_dataframe(
|
|
|
61
35
|
|
|
62
36
|
|
|
63
37
|
def _content_md5(conteudo: bytes) -> str:
|
|
64
|
-
"""Calcula o cabeçalho ``Content-MD5`` (digest MD5 em base64, RFC 1864).
|
|
65
|
-
|
|
66
|
-
Usa ``usedforsecurity=False`` porque o MD5 aqui é de integridade, não
|
|
67
|
-
criptográfico -- isso também evita ``ValueError`` em builds FIPS do Python.
|
|
68
|
-
"""
|
|
69
38
|
digest = hashlib.md5(conteudo, usedforsecurity=False).digest()
|
|
70
39
|
return base64.b64encode(digest).decode("utf-8")
|
|
71
40
|
|
|
72
41
|
|
|
73
42
|
def _to_iso_z(value: common.DateLike, *, fim_do_dia: bool = False) -> str:
|
|
74
|
-
"""Formata uma data no padrão ISO-8601 com milissegundos e sufixo ``Z``.
|
|
75
|
-
|
|
76
|
-
Strings são repassadas sem alteração (o chamador controla o formato exato).
|
|
77
|
-
Para ``date``/``datetime`` sem hora, usa ``00:00:00`` ou ``23:59:59`` (quando
|
|
78
|
-
``fim_do_dia``), como nos exemplos de ``dtCriacaoMin``/``dtCriacaoMax``.
|
|
79
|
-
"""
|
|
80
43
|
if isinstance(value, str):
|
|
81
44
|
return value
|
|
82
45
|
|
|
@@ -90,15 +53,20 @@ def _to_iso_z(value: common.DateLike, *, fim_do_dia: bool = False) -> str:
|
|
|
90
53
|
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
91
54
|
|
|
92
55
|
|
|
93
|
-
|
|
94
|
-
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class FalhaDownload:
|
|
58
|
+
id_arquivo: int | str | None
|
|
59
|
+
nome_arquivo: str | None
|
|
60
|
+
erro: str
|
|
95
61
|
|
|
96
|
-
O token de acesso é gerenciado automaticamente: a instância reaproveita o
|
|
97
|
-
token por 10 minutos e gera um novo quando necessário, preferindo o
|
|
98
|
-
``refresh_token`` e caindo para usuário/senha quando ele não está disponível
|
|
99
|
-
ou expirou.
|
|
100
|
-
"""
|
|
101
62
|
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ResultadoDownloads:
|
|
65
|
+
dados: pd.DataFrame
|
|
66
|
+
falhas: list[FalhaDownload]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BBSiaAPI:
|
|
102
70
|
_scope: str
|
|
103
71
|
_sia_domain: str
|
|
104
72
|
_access_token: str
|
|
@@ -116,43 +84,18 @@ class BBSiaAPI:
|
|
|
116
84
|
refresh_token: str | None = None,
|
|
117
85
|
scope: str = "sia:usuario",
|
|
118
86
|
):
|
|
119
|
-
"""Inicia uma instância do encapsulador da API BB Sia do Banco do Brasil.
|
|
120
|
-
|
|
121
|
-
Parâmetros
|
|
122
|
-
----------
|
|
123
|
-
ambiente: common.Ambiente
|
|
124
|
-
Ambiente de execução. O BB Sia não possui ambiente alternativo
|
|
125
|
-
(sandbox).
|
|
126
|
-
username: str | None
|
|
127
|
-
Nome de usuário para o *grant* ``password``. Lido da variável de
|
|
128
|
-
ambiente ``BBS_USERNAME`` quando não informado.
|
|
129
|
-
password: str | None
|
|
130
|
-
Senha para o *grant* ``password``. Lida da variável de ambiente
|
|
131
|
-
``BBS_PASSWORD`` quando não informada.
|
|
132
|
-
refresh_token: str | None
|
|
133
|
-
Refresh token para o *grant* ``refresh_token``. Lido da variável de
|
|
134
|
-
ambiente ``BBS_REFRESH_TOKEN`` quando não informado.
|
|
135
|
-
scope: str
|
|
136
|
-
Escopo solicitado no *grant* ``password`` (padrão ``sia:usuario``).
|
|
137
|
-
|
|
138
|
-
É obrigatório fornecer um ``refresh_token`` ou o par usuário/senha.
|
|
139
|
-
"""
|
|
140
87
|
self._ambiente = ambiente
|
|
141
88
|
self._sia_domain = common.sia_domain_for(ambiente)
|
|
142
89
|
self._scope = scope
|
|
143
90
|
|
|
144
|
-
self._username = username
|
|
145
|
-
self._password = password
|
|
146
|
-
self._refresh_token =
|
|
147
|
-
refresh_token if refresh_token is not None else os.getenv("BBS_REFRESH_TOKEN")
|
|
148
|
-
)
|
|
91
|
+
self._username = username
|
|
92
|
+
self._password = password
|
|
93
|
+
self._refresh_token = refresh_token
|
|
149
94
|
|
|
150
95
|
if not self._refresh_token and not (self._username and self._password):
|
|
151
96
|
raise ValueError(
|
|
152
97
|
"Credenciais inválidas para o BB Sia. Forneça um 'refresh_token'"
|
|
153
|
-
+ "
|
|
154
|
-
+ " usuário/senha (parâmetros 'username'/'password' ou as"
|
|
155
|
-
+ " variáveis de ambiente 'BBS_USERNAME'/'BBS_PASSWORD')."
|
|
98
|
+
+ " ou o par usuário/senha (parâmetros 'username'/'password')."
|
|
156
99
|
)
|
|
157
100
|
|
|
158
101
|
self._access_token = ""
|
|
@@ -161,9 +104,16 @@ class BBSiaAPI:
|
|
|
161
104
|
def _store_access_token(self, data: dict[str, object]) -> dict[str, object]:
|
|
162
105
|
access_token = data.get("access_token")
|
|
163
106
|
if not access_token:
|
|
164
|
-
raise Exception(
|
|
107
|
+
raise Exception(
|
|
108
|
+
"A resposta de autorização do BB Sia não trouxe um 'access_token'."
|
|
109
|
+
)
|
|
165
110
|
|
|
166
111
|
self._access_token = cast("str", access_token)
|
|
112
|
+
|
|
113
|
+
refresh_token = data.get("refresh_token")
|
|
114
|
+
if refresh_token:
|
|
115
|
+
self._refresh_token = cast("str", refresh_token)
|
|
116
|
+
|
|
167
117
|
self._last_access_token_request_timestamp = datetime.datetime.now()
|
|
168
118
|
return data
|
|
169
119
|
|
|
@@ -185,22 +135,26 @@ class BBSiaAPI:
|
|
|
185
135
|
},
|
|
186
136
|
)
|
|
187
137
|
|
|
138
|
+
if res.status_code in (400, 401):
|
|
139
|
+
raise CredenciaisNecessariasError(
|
|
140
|
+
"Usuário ou senha do BB Sia inválidos ou expirados. Forneça"
|
|
141
|
+
+ " credenciais válidas."
|
|
142
|
+
)
|
|
188
143
|
if res.status_code != 200:
|
|
189
|
-
raise Exception(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
refresh_token = data.get("refresh_token")
|
|
193
|
-
if refresh_token:
|
|
194
|
-
self._refresh_token = cast("str", refresh_token)
|
|
144
|
+
raise Exception(
|
|
145
|
+
"Não foi possível autorizar o acesso ao BB Sia com usuário e senha."
|
|
146
|
+
)
|
|
195
147
|
|
|
196
|
-
return self._store_access_token(
|
|
148
|
+
return self._store_access_token(common.parse_json_object(res))
|
|
197
149
|
|
|
198
150
|
def _refresh_grant(
|
|
199
151
|
self,
|
|
200
152
|
validade_refresh_token: int | None = None,
|
|
201
153
|
) -> dict[str, object]:
|
|
202
154
|
if not self._refresh_token:
|
|
203
|
-
raise ValueError(
|
|
155
|
+
raise ValueError(
|
|
156
|
+
"Nenhum refresh token disponível para renovar o acesso ao BB Sia."
|
|
157
|
+
)
|
|
204
158
|
|
|
205
159
|
data = {
|
|
206
160
|
"grant_type": "refresh_token",
|
|
@@ -216,6 +170,12 @@ class BBSiaAPI:
|
|
|
216
170
|
data=data,
|
|
217
171
|
)
|
|
218
172
|
|
|
173
|
+
if res.status_code in (400, 401):
|
|
174
|
+
raise CredenciaisNecessariasError(
|
|
175
|
+
"O refresh token do BB Sia expirou ou é inválido. Informe usuário"
|
|
176
|
+
+ " e senha para reautenticar"
|
|
177
|
+
+ " ('autorizar_novo_token(username, password)')."
|
|
178
|
+
)
|
|
219
179
|
if res.status_code != 200:
|
|
220
180
|
raise Exception("Não foi possível renovar o token de acesso ao BB Sia.")
|
|
221
181
|
|
|
@@ -235,55 +195,43 @@ class BBSiaAPI:
|
|
|
235
195
|
|
|
236
196
|
if self._refresh_token:
|
|
237
197
|
try:
|
|
238
|
-
self._refresh_grant()
|
|
198
|
+
_ = self._refresh_grant()
|
|
239
199
|
return
|
|
240
|
-
except
|
|
200
|
+
except CredenciaisNecessariasError:
|
|
241
201
|
if not (self._username and self._password):
|
|
242
202
|
raise
|
|
243
203
|
|
|
244
|
-
self.
|
|
204
|
+
if not (self._username and self._password):
|
|
205
|
+
raise CredenciaisNecessariasError(
|
|
206
|
+
"Não há refresh token nem usuário/senha para autenticar no BB"
|
|
207
|
+
+ " Sia. Forneça as credenciais"
|
|
208
|
+
+ " ('autorizar_novo_token(username, password)')."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
_ = self._password_grant()
|
|
245
212
|
|
|
246
213
|
def _get_access_token(self) -> str:
|
|
247
214
|
self._check_and_update_access_token()
|
|
248
215
|
return self._access_token
|
|
249
216
|
|
|
250
|
-
def autorizar_novo_token(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
217
|
+
def autorizar_novo_token(
|
|
218
|
+
self,
|
|
219
|
+
username: str | None = None,
|
|
220
|
+
password: str | None = None,
|
|
221
|
+
) -> dict[str, object]:
|
|
222
|
+
if username is not None:
|
|
223
|
+
self._username = username
|
|
224
|
+
if password is not None:
|
|
225
|
+
self._password = password
|
|
256
226
|
return self._password_grant()
|
|
257
227
|
|
|
258
228
|
def renovar_token(
|
|
259
229
|
self,
|
|
260
230
|
validade_refresh_token: int | None = None,
|
|
261
231
|
) -> dict[str, object]:
|
|
262
|
-
"""Obtém um novo **token de acesso** a partir do refresh token.
|
|
263
|
-
|
|
264
|
-
Usa o *grant* ``refresh_token``. O refresh token em si não é renovado
|
|
265
|
-
nem substituído por esta chamada -- apenas um novo ``access_token`` é
|
|
266
|
-
devolvido (e passa a ser usado pela instância).
|
|
267
|
-
|
|
268
|
-
Parâmetros
|
|
269
|
-
----------
|
|
270
|
-
validade_refresh_token: int | None
|
|
271
|
-
Validade, em dias, do refresh token atual. ``0`` deixa o refresh
|
|
272
|
-
token com validade indefinida. Quando ``None``, o parâmetro não é
|
|
273
|
-
enviado e vale o padrão do Banco do Brasil (1 dia).
|
|
274
|
-
"""
|
|
275
232
|
return self._refresh_grant(validade_refresh_token)
|
|
276
233
|
|
|
277
234
|
def revogar_token(self, token: str, token_type_hint: str) -> None:
|
|
278
|
-
"""Revoga um token de acesso ou de refresh.
|
|
279
|
-
|
|
280
|
-
Parâmetros
|
|
281
|
-
----------
|
|
282
|
-
token: str
|
|
283
|
-
Valor do token a ser revogado.
|
|
284
|
-
token_type_hint: str
|
|
285
|
-
``"access_token"`` ou ``"refresh_token"``.
|
|
286
|
-
"""
|
|
287
235
|
res = requests.request(
|
|
288
236
|
"POST",
|
|
289
237
|
f"{self._sia_domain}/gmt-autorizador-api/revogar",
|
|
@@ -298,10 +246,6 @@ class BBSiaAPI:
|
|
|
298
246
|
raise Exception("Não foi possível revogar o token do BB Sia.")
|
|
299
247
|
|
|
300
248
|
def listar_uploads_possiveis(self) -> pd.DataFrame:
|
|
301
|
-
"""Lista os uploads (FTAs) que o usuário pode enviar.
|
|
302
|
-
|
|
303
|
-
Endpoint ``GET /gmt-catalogo-api/listaUploads/``.
|
|
304
|
-
"""
|
|
305
249
|
access_token = self._get_access_token()
|
|
306
250
|
|
|
307
251
|
res = requests.request(
|
|
@@ -315,11 +259,7 @@ class BBSiaAPI:
|
|
|
315
259
|
|
|
316
260
|
return _to_dataframe(common.parse_json_object(res))
|
|
317
261
|
|
|
318
|
-
def
|
|
319
|
-
"""Lista os arquivos disponíveis para download.
|
|
320
|
-
|
|
321
|
-
Endpoint ``GET /gmt-sia-api/listaDownloads``.
|
|
322
|
-
"""
|
|
262
|
+
def _fetch_downloads(self) -> dict[str, object]:
|
|
323
263
|
access_token = self._get_access_token()
|
|
324
264
|
|
|
325
265
|
res = requests.request(
|
|
@@ -331,24 +271,16 @@ class BBSiaAPI:
|
|
|
331
271
|
if res.status_code != 200:
|
|
332
272
|
raise Exception("Não foi possível listar os downloads do BB Sia.")
|
|
333
273
|
|
|
334
|
-
return
|
|
274
|
+
return common.parse_json_object(res)
|
|
275
|
+
|
|
276
|
+
def listar_downloads(self) -> pd.DataFrame:
|
|
277
|
+
return _to_dataframe(self._fetch_downloads())
|
|
335
278
|
|
|
336
279
|
def consultar_metadados(
|
|
337
280
|
self,
|
|
338
281
|
id_arquivo: int | str,
|
|
339
282
|
nome_arquivo: str,
|
|
340
283
|
) -> pd.DataFrame:
|
|
341
|
-
"""Consulta os metadados de um arquivo disponível para download.
|
|
342
|
-
|
|
343
|
-
Endpoint ``GET /gmt-sia-api/listaDownloads/{id_arquivo}/{nome_arquivo}``.
|
|
344
|
-
|
|
345
|
-
Parâmetros
|
|
346
|
-
----------
|
|
347
|
-
id_arquivo: int | str
|
|
348
|
-
Identificador do arquivo, como consta ao consultar os downloads.
|
|
349
|
-
nome_arquivo: str
|
|
350
|
-
Nome do arquivo, como consta ao consultar os downloads.
|
|
351
|
-
"""
|
|
352
284
|
access_token = self._get_access_token()
|
|
353
285
|
|
|
354
286
|
res = requests.request(
|
|
@@ -358,7 +290,9 @@ class BBSiaAPI:
|
|
|
358
290
|
)
|
|
359
291
|
|
|
360
292
|
if res.status_code != 200:
|
|
361
|
-
raise Exception(
|
|
293
|
+
raise Exception(
|
|
294
|
+
"Não foi possível consultar os metadados do arquivo no BB Sia."
|
|
295
|
+
)
|
|
362
296
|
|
|
363
297
|
return _to_dataframe(common.parse_json_object(res))
|
|
364
298
|
|
|
@@ -368,21 +302,6 @@ class BBSiaAPI:
|
|
|
368
302
|
nome_arquivo: str,
|
|
369
303
|
caminho: str | os.PathLike[str] | None = None,
|
|
370
304
|
) -> bytes:
|
|
371
|
-
"""Baixa o conteúdo de um arquivo do BB Sia.
|
|
372
|
-
|
|
373
|
-
Endpoint ``GET /gmt-sia-api/download/{id_arquivo}/{nome_arquivo}``.
|
|
374
|
-
|
|
375
|
-
Parâmetros
|
|
376
|
-
----------
|
|
377
|
-
id_arquivo: int | str
|
|
378
|
-
Identificador do arquivo, como consta ao consultar os downloads.
|
|
379
|
-
nome_arquivo: str
|
|
380
|
-
Nome do arquivo, como consta ao consultar os downloads.
|
|
381
|
-
caminho: str | os.PathLike | None
|
|
382
|
-
Quando informado, o conteúdo também é gravado nesse caminho.
|
|
383
|
-
|
|
384
|
-
Devolve o conteúdo do arquivo em ``bytes``.
|
|
385
|
-
"""
|
|
386
305
|
access_token = self._get_access_token()
|
|
387
306
|
|
|
388
307
|
res = requests.request(
|
|
@@ -396,10 +315,60 @@ class BBSiaAPI:
|
|
|
396
315
|
|
|
397
316
|
if caminho is not None:
|
|
398
317
|
with open(caminho, "wb") as arquivo:
|
|
399
|
-
arquivo.write(res.content)
|
|
318
|
+
_ = arquivo.write(res.content)
|
|
400
319
|
|
|
401
320
|
return res.content
|
|
402
321
|
|
|
322
|
+
def processar_downloads(
|
|
323
|
+
self,
|
|
324
|
+
parser: Callable[[str], pd.DataFrame] = gestao_agil.parse_retorno_abertura_massificada,
|
|
325
|
+
encoding: str = "latin-1",
|
|
326
|
+
) -> ResultadoDownloads:
|
|
327
|
+
listagem = self._fetch_downloads()
|
|
328
|
+
arquivos = listagem.get("arquivos")
|
|
329
|
+
if not isinstance(arquivos, list):
|
|
330
|
+
return ResultadoDownloads(dados=pd.DataFrame(), falhas=[])
|
|
331
|
+
|
|
332
|
+
frames: list[pd.DataFrame] = []
|
|
333
|
+
falhas: list[FalhaDownload] = []
|
|
334
|
+
|
|
335
|
+
for arquivo in cast("list[object]", arquivos):
|
|
336
|
+
meta = cast("Mapping[str, object]", arquivo)
|
|
337
|
+
id_arquivo = cast("int | str | None", meta.get("id"))
|
|
338
|
+
nome_arquivo = cast("str | None", meta.get("nome"))
|
|
339
|
+
cod_fta = cast("int | str | None", meta.get("codFta"))
|
|
340
|
+
|
|
341
|
+
if id_arquivo is None or nome_arquivo is None:
|
|
342
|
+
falhas.append(
|
|
343
|
+
FalhaDownload(
|
|
344
|
+
id_arquivo=id_arquivo,
|
|
345
|
+
nome_arquivo=nome_arquivo,
|
|
346
|
+
erro="Download sem 'id' ou 'nome' na listagem do BB Sia.",
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
conteudo = self.baixar_arquivo(id_arquivo, nome_arquivo)
|
|
353
|
+
df = parser(conteudo.decode(encoding))
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
falhas.append(
|
|
356
|
+
FalhaDownload(
|
|
357
|
+
id_arquivo=id_arquivo,
|
|
358
|
+
nome_arquivo=nome_arquivo,
|
|
359
|
+
erro=str(exc),
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
df.insert(0, "ID Arquivo", id_arquivo)
|
|
365
|
+
df.insert(1, "Nome Arquivo", nome_arquivo)
|
|
366
|
+
df.insert(2, "Código FTA", cod_fta)
|
|
367
|
+
frames.append(df)
|
|
368
|
+
|
|
369
|
+
dados = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
|
370
|
+
return ResultadoDownloads(dados=dados, falhas=falhas)
|
|
371
|
+
|
|
403
372
|
def pre_upload(
|
|
404
373
|
self,
|
|
405
374
|
fta: int | str,
|
|
@@ -408,28 +377,6 @@ class BBSiaAPI:
|
|
|
408
377
|
evento: int | str = 1,
|
|
409
378
|
content_md5: str | None = None,
|
|
410
379
|
) -> requests.Response:
|
|
411
|
-
"""Negocia (HEAD) o envio de um arquivo antes do upload.
|
|
412
|
-
|
|
413
|
-
Endpoint ``HEAD /gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}``.
|
|
414
|
-
|
|
415
|
-
Não levanta exceção em respostas não-2xx nem segue redirecionamentos
|
|
416
|
-
automaticamente: o ``status_code`` e os cabeçalhos da resposta fazem
|
|
417
|
-
parte da negociação (por exemplo, para retomar um envio). Inspecione a
|
|
418
|
-
``requests.Response`` devolvida.
|
|
419
|
-
|
|
420
|
-
Parâmetros
|
|
421
|
-
----------
|
|
422
|
-
fta: int | str
|
|
423
|
-
Número do FTA para o qual o arquivo será enviado.
|
|
424
|
-
nome_arquivo: str
|
|
425
|
-
Nome do arquivo a ser enviado.
|
|
426
|
-
conteudo: bytes
|
|
427
|
-
Conteúdo do arquivo, usado para calcular MD5 e tamanho.
|
|
428
|
-
evento: int | str
|
|
429
|
-
Etapa de recepção (por padrão, ``1``).
|
|
430
|
-
content_md5: str | None
|
|
431
|
-
Sobrescreve o ``Content-MD5`` calculado (digest MD5 em base64).
|
|
432
|
-
"""
|
|
433
380
|
access_token = self._get_access_token()
|
|
434
381
|
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
435
382
|
|
|
@@ -455,35 +402,6 @@ class BBSiaAPI:
|
|
|
455
402
|
total_bytes: int | None = None,
|
|
456
403
|
content_md5: str | None = None,
|
|
457
404
|
) -> requests.Response:
|
|
458
|
-
"""Envia (PUT) um arquivo, ou um trecho dele, para o BB Sia.
|
|
459
|
-
|
|
460
|
-
Endpoint ``PUT /gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}``.
|
|
461
|
-
|
|
462
|
-
Levanta exceção apenas em respostas de erro (``status_code >= 400``).
|
|
463
|
-
Redirecionamentos não são seguidos automaticamente: respostas de
|
|
464
|
-
continuação (como ``308``) são devolvidas para o chamador decidir, o
|
|
465
|
-
que permite envios fracionados.
|
|
466
|
-
|
|
467
|
-
Parâmetros
|
|
468
|
-
----------
|
|
469
|
-
fta: int | str
|
|
470
|
-
Número do FTA para o qual o arquivo será enviado.
|
|
471
|
-
nome_arquivo: str
|
|
472
|
-
Nome do arquivo a ser enviado.
|
|
473
|
-
conteudo: bytes
|
|
474
|
-
Bytes a serem enviados nesta requisição (o arquivo todo ou um trecho).
|
|
475
|
-
evento: int | str
|
|
476
|
-
Etapa de recepção (por padrão, ``1``).
|
|
477
|
-
byte_inicial: int
|
|
478
|
-
Primeiro byte deste trecho, para o cabeçalho ``Content-Range``.
|
|
479
|
-
byte_final: int | None
|
|
480
|
-
Último byte deste trecho. Quando ``None``, assume o último byte de
|
|
481
|
-
``conteudo`` (``byte_inicial + len(conteudo) - 1``).
|
|
482
|
-
total_bytes: int | None
|
|
483
|
-
Tamanho total do arquivo. Quando ``None``, assume ``len(conteudo)``.
|
|
484
|
-
content_md5: str | None
|
|
485
|
-
Sobrescreve o ``Content-MD5`` calculado (digest MD5 em base64).
|
|
486
|
-
"""
|
|
487
405
|
access_token = self._get_access_token()
|
|
488
406
|
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
489
407
|
|
|
@@ -519,30 +437,6 @@ class BBSiaAPI:
|
|
|
519
437
|
dt_criacao_min: common.DateLike | None = None,
|
|
520
438
|
dt_criacao_max: common.DateLike | None = None,
|
|
521
439
|
) -> pd.DataFrame:
|
|
522
|
-
"""Consulta protocolos no BB Sia.
|
|
523
|
-
|
|
524
|
-
Endpoint ``POST /gmt-protocolo-api/listaProtocolos``. Apenas os filtros
|
|
525
|
-
informados (diferentes de ``None``) são enviados no corpo da requisição.
|
|
526
|
-
|
|
527
|
-
Parâmetros
|
|
528
|
-
----------
|
|
529
|
-
pagina: int
|
|
530
|
-
Página da consulta (``metadata.pagina``).
|
|
531
|
-
por_pagina: int
|
|
532
|
-
Quantidade de itens por página (``metadata.porPagina``).
|
|
533
|
-
protocolo: Sequence[int] | None
|
|
534
|
-
Números de protocolo a filtrar.
|
|
535
|
-
cod_fta: Sequence[int] | None
|
|
536
|
-
Códigos de FTA a filtrar.
|
|
537
|
-
cod_estado_protocolo: Sequence[int] | None
|
|
538
|
-
Códigos de estado do protocolo a filtrar.
|
|
539
|
-
dt_criacao_min: common.DateLike | None
|
|
540
|
-
Data de criação mínima. ``date``/``datetime`` viram o início do dia
|
|
541
|
-
em ISO-8601 com sufixo ``Z``; ``str`` é repassada como veio.
|
|
542
|
-
dt_criacao_max: common.DateLike | None
|
|
543
|
-
Data de criação máxima. ``date``/``datetime`` viram o fim do dia em
|
|
544
|
-
ISO-8601 com sufixo ``Z``; ``str`` é repassada como veio.
|
|
545
|
-
"""
|
|
546
440
|
access_token = self._get_access_token()
|
|
547
441
|
|
|
548
442
|
body: dict[str, object] = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bb_api"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Wrapper da API do Banco do Brasil."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -16,9 +16,14 @@ build-backend = "setuptools.build_meta"
|
|
|
16
16
|
[tool.setuptools]
|
|
17
17
|
packages = ["bb_api"]
|
|
18
18
|
|
|
19
|
+
[tool.ruff]
|
|
20
|
+
line-length = 99
|
|
21
|
+
|
|
19
22
|
[dependency-groups]
|
|
20
23
|
dev = [
|
|
24
|
+
"basedpyright>=1.39.8",
|
|
21
25
|
"pandas-stubs>=3.0.3.260530",
|
|
26
|
+
"ruff>=0.15.19",
|
|
22
27
|
]
|
|
23
28
|
|
|
24
29
|
[tool.semantic_release]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|