bb_api 0.3.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.3.0 → bb_api-0.5.0}/PKG-INFO +1 -1
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api/__init__.py +13 -3
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api/accountability.py +17 -33
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api/common.py +20 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api/gestao_agil.py +61 -63
- bb_api-0.5.0/bb_api/sia.py +469 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api.egg-info/PKG-INFO +1 -1
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api.egg-info/SOURCES.txt +1 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/pyproject.toml +6 -1
- {bb_api-0.3.0 → bb_api-0.5.0}/LICENSE +0 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/README.md +0 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api.egg-info/dependency_links.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api.egg-info/requires.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/bb_api.egg-info/top_level.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.5.0}/setup.cfg +0 -0
|
@@ -1,14 +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 (
|
|
13
|
+
BBSiaAPI,
|
|
14
|
+
CredenciaisNecessariasError,
|
|
15
|
+
FalhaDownload,
|
|
16
|
+
ResultadoDownloads,
|
|
17
|
+
)
|
|
12
18
|
from .gestao_agil import (
|
|
13
19
|
parse_retorno_abertura_massificada,
|
|
14
20
|
ler_retorno_abertura_massificada,
|
|
@@ -18,6 +24,10 @@ __all__ = [
|
|
|
18
24
|
"Ambiente",
|
|
19
25
|
"AccountabilityV3RepasseAPI",
|
|
20
26
|
"AccountabilityV3ControleAPI",
|
|
27
|
+
"BBSiaAPI",
|
|
28
|
+
"CredenciaisNecessariasError",
|
|
29
|
+
"FalhaDownload",
|
|
30
|
+
"ResultadoDownloads",
|
|
21
31
|
"parse_retorno_abertura_massificada",
|
|
22
32
|
"ler_retorno_abertura_massificada",
|
|
23
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"]
|
|
@@ -16,6 +16,10 @@ homo_api_domain = "https://api.hm.bb.com.br"
|
|
|
16
16
|
homo_alt_api_domain = "https://api.sandbox.bb.com.br"
|
|
17
17
|
prod_api_domain = "https://api.bb.com.br"
|
|
18
18
|
|
|
19
|
+
dese_sia_domain = "https://gmtedi.desenv.bb.com.br"
|
|
20
|
+
homo_sia_domain = "https://gmtedi.hm.bb.com.br"
|
|
21
|
+
prod_sia_domain = "https://gmtedi.bb.com.br"
|
|
22
|
+
|
|
19
23
|
time_between_access_token_requests = timedelta(minutes=10)
|
|
20
24
|
|
|
21
25
|
|
|
@@ -30,6 +34,22 @@ class Ambiente(Enum):
|
|
|
30
34
|
PRODUCAO = 3
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
_sia_domains = {
|
|
38
|
+
Ambiente.DESENVOLVIMENTO: dese_sia_domain,
|
|
39
|
+
Ambiente.HOMOLOGACAO: homo_sia_domain,
|
|
40
|
+
Ambiente.PRODUCAO: prod_sia_domain,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sia_domain_for(ambiente: Ambiente) -> str:
|
|
45
|
+
domain = _sia_domains.get(ambiente)
|
|
46
|
+
if domain is None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"O ambiente '{ambiente.name}' não é suportado pela API BB Sia."
|
|
49
|
+
)
|
|
50
|
+
return domain
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
def get_headers(access_token: str) -> dict[str, str]:
|
|
34
54
|
return {
|
|
35
55
|
"Authorization": f"Bearer {access_token}",
|
|
@@ -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)
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import datetime
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
import bb_api.common as common
|
|
13
|
+
import bb_api.gestao_agil as gestao_agil
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CredenciaisNecessariasError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _to_dataframe(
|
|
21
|
+
res: object,
|
|
22
|
+
main_list: str | None = None,
|
|
23
|
+
rename_dict: Mapping[str, str] | None = None,
|
|
24
|
+
) -> pd.DataFrame:
|
|
25
|
+
record = cast("Mapping[str, object]", res)
|
|
26
|
+
|
|
27
|
+
if main_list is not None and isinstance(record.get(main_list), list):
|
|
28
|
+
return common.handle_results(res, main_list=main_list, rename_dict=rename_dict)
|
|
29
|
+
|
|
30
|
+
for key, value in record.items():
|
|
31
|
+
if isinstance(value, list):
|
|
32
|
+
return common.handle_results(res, main_list=key, rename_dict=rename_dict)
|
|
33
|
+
|
|
34
|
+
return common.handle_results(res, rename_dict=rename_dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _content_md5(conteudo: bytes) -> str:
|
|
38
|
+
digest = hashlib.md5(conteudo, usedforsecurity=False).digest()
|
|
39
|
+
return base64.b64encode(digest).decode("utf-8")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _to_iso_z(value: common.DateLike, *, fim_do_dia: bool = False) -> str:
|
|
43
|
+
if isinstance(value, str):
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
if isinstance(value, datetime.datetime):
|
|
47
|
+
dt = value
|
|
48
|
+
elif fim_do_dia:
|
|
49
|
+
dt = datetime.datetime.combine(value, datetime.time(23, 59, 59))
|
|
50
|
+
else:
|
|
51
|
+
dt = datetime.datetime.combine(value, datetime.time())
|
|
52
|
+
|
|
53
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class FalhaDownload:
|
|
58
|
+
id_arquivo: int | str | None
|
|
59
|
+
nome_arquivo: str | None
|
|
60
|
+
erro: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ResultadoDownloads:
|
|
65
|
+
dados: pd.DataFrame
|
|
66
|
+
falhas: list[FalhaDownload]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BBSiaAPI:
|
|
70
|
+
_scope: str
|
|
71
|
+
_sia_domain: str
|
|
72
|
+
_access_token: str
|
|
73
|
+
_username: str | None
|
|
74
|
+
_password: str | None
|
|
75
|
+
_refresh_token: str | None
|
|
76
|
+
_ambiente: common.Ambiente
|
|
77
|
+
_last_access_token_request_timestamp: datetime.datetime | None
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
ambiente: common.Ambiente = common.Ambiente.HOMOLOGACAO,
|
|
82
|
+
username: str | None = None,
|
|
83
|
+
password: str | None = None,
|
|
84
|
+
refresh_token: str | None = None,
|
|
85
|
+
scope: str = "sia:usuario",
|
|
86
|
+
):
|
|
87
|
+
self._ambiente = ambiente
|
|
88
|
+
self._sia_domain = common.sia_domain_for(ambiente)
|
|
89
|
+
self._scope = scope
|
|
90
|
+
|
|
91
|
+
self._username = username
|
|
92
|
+
self._password = password
|
|
93
|
+
self._refresh_token = refresh_token
|
|
94
|
+
|
|
95
|
+
if not self._refresh_token and not (self._username and self._password):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Credenciais inválidas para o BB Sia. Forneça um 'refresh_token'"
|
|
98
|
+
+ " ou o par usuário/senha (parâmetros 'username'/'password')."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self._access_token = ""
|
|
102
|
+
self._last_access_token_request_timestamp = None
|
|
103
|
+
|
|
104
|
+
def _store_access_token(self, data: dict[str, object]) -> dict[str, object]:
|
|
105
|
+
access_token = data.get("access_token")
|
|
106
|
+
if not access_token:
|
|
107
|
+
raise Exception(
|
|
108
|
+
"A resposta de autorização do BB Sia não trouxe um 'access_token'."
|
|
109
|
+
)
|
|
110
|
+
|
|
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
|
+
|
|
117
|
+
self._last_access_token_request_timestamp = datetime.datetime.now()
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
def _password_grant(self) -> dict[str, object]:
|
|
121
|
+
if not (self._username and self._password):
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Usuário e senha são necessários para o grant 'password' do BB Sia."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
res = requests.request(
|
|
127
|
+
"POST",
|
|
128
|
+
f"{self._sia_domain}/gmt-autorizador-api/autoriza",
|
|
129
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
130
|
+
data={
|
|
131
|
+
"grant_type": "password",
|
|
132
|
+
"username": self._username,
|
|
133
|
+
"password": self._password,
|
|
134
|
+
"scope": self._scope,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
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
|
+
)
|
|
143
|
+
if res.status_code != 200:
|
|
144
|
+
raise Exception(
|
|
145
|
+
"Não foi possível autorizar o acesso ao BB Sia com usuário e senha."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return self._store_access_token(common.parse_json_object(res))
|
|
149
|
+
|
|
150
|
+
def _refresh_grant(
|
|
151
|
+
self,
|
|
152
|
+
validade_refresh_token: int | None = None,
|
|
153
|
+
) -> dict[str, object]:
|
|
154
|
+
if not self._refresh_token:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
"Nenhum refresh token disponível para renovar o acesso ao BB Sia."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
data = {
|
|
160
|
+
"grant_type": "refresh_token",
|
|
161
|
+
"refresh_token": self._refresh_token,
|
|
162
|
+
}
|
|
163
|
+
if validade_refresh_token is not None:
|
|
164
|
+
data["validade_refresh_token"] = str(validade_refresh_token)
|
|
165
|
+
|
|
166
|
+
res = requests.request(
|
|
167
|
+
"POST",
|
|
168
|
+
f"{self._sia_domain}/gmt-autorizador-api/autoriza",
|
|
169
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
170
|
+
data=data,
|
|
171
|
+
)
|
|
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
|
+
)
|
|
179
|
+
if res.status_code != 200:
|
|
180
|
+
raise Exception("Não foi possível renovar o token de acesso ao BB Sia.")
|
|
181
|
+
|
|
182
|
+
return self._store_access_token(common.parse_json_object(res))
|
|
183
|
+
|
|
184
|
+
def _check_and_update_access_token(self) -> None:
|
|
185
|
+
now = datetime.datetime.now()
|
|
186
|
+
last_request = self._last_access_token_request_timestamp
|
|
187
|
+
|
|
188
|
+
is_token_valid = (
|
|
189
|
+
self._access_token != ""
|
|
190
|
+
and last_request is not None
|
|
191
|
+
and now - last_request <= common.time_between_access_token_requests
|
|
192
|
+
)
|
|
193
|
+
if is_token_valid:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if self._refresh_token:
|
|
197
|
+
try:
|
|
198
|
+
_ = self._refresh_grant()
|
|
199
|
+
return
|
|
200
|
+
except CredenciaisNecessariasError:
|
|
201
|
+
if not (self._username and self._password):
|
|
202
|
+
raise
|
|
203
|
+
|
|
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()
|
|
212
|
+
|
|
213
|
+
def _get_access_token(self) -> str:
|
|
214
|
+
self._check_and_update_access_token()
|
|
215
|
+
return self._access_token
|
|
216
|
+
|
|
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
|
|
226
|
+
return self._password_grant()
|
|
227
|
+
|
|
228
|
+
def renovar_token(
|
|
229
|
+
self,
|
|
230
|
+
validade_refresh_token: int | None = None,
|
|
231
|
+
) -> dict[str, object]:
|
|
232
|
+
return self._refresh_grant(validade_refresh_token)
|
|
233
|
+
|
|
234
|
+
def revogar_token(self, token: str, token_type_hint: str) -> None:
|
|
235
|
+
res = requests.request(
|
|
236
|
+
"POST",
|
|
237
|
+
f"{self._sia_domain}/gmt-autorizador-api/revogar",
|
|
238
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
239
|
+
data={
|
|
240
|
+
"token": token,
|
|
241
|
+
"token_type_hint": token_type_hint,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if res.status_code not in (200, 204):
|
|
246
|
+
raise Exception("Não foi possível revogar o token do BB Sia.")
|
|
247
|
+
|
|
248
|
+
def listar_uploads_possiveis(self) -> pd.DataFrame:
|
|
249
|
+
access_token = self._get_access_token()
|
|
250
|
+
|
|
251
|
+
res = requests.request(
|
|
252
|
+
"GET",
|
|
253
|
+
f"{self._sia_domain}/gmt-catalogo-api/listaUploads/",
|
|
254
|
+
headers=common.get_headers(access_token),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if res.status_code != 200:
|
|
258
|
+
raise Exception("Não foi possível listar os uploads possíveis no BB Sia.")
|
|
259
|
+
|
|
260
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
261
|
+
|
|
262
|
+
def _fetch_downloads(self) -> dict[str, object]:
|
|
263
|
+
access_token = self._get_access_token()
|
|
264
|
+
|
|
265
|
+
res = requests.request(
|
|
266
|
+
"GET",
|
|
267
|
+
f"{self._sia_domain}/gmt-sia-api/listaDownloads",
|
|
268
|
+
headers=common.get_headers(access_token),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if res.status_code != 200:
|
|
272
|
+
raise Exception("Não foi possível listar os downloads do BB Sia.")
|
|
273
|
+
|
|
274
|
+
return common.parse_json_object(res)
|
|
275
|
+
|
|
276
|
+
def listar_downloads(self) -> pd.DataFrame:
|
|
277
|
+
return _to_dataframe(self._fetch_downloads())
|
|
278
|
+
|
|
279
|
+
def consultar_metadados(
|
|
280
|
+
self,
|
|
281
|
+
id_arquivo: int | str,
|
|
282
|
+
nome_arquivo: str,
|
|
283
|
+
) -> pd.DataFrame:
|
|
284
|
+
access_token = self._get_access_token()
|
|
285
|
+
|
|
286
|
+
res = requests.request(
|
|
287
|
+
"GET",
|
|
288
|
+
f"{self._sia_domain}/gmt-sia-api/listaDownloads/{id_arquivo}/{nome_arquivo}",
|
|
289
|
+
headers=common.get_headers(access_token),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if res.status_code != 200:
|
|
293
|
+
raise Exception(
|
|
294
|
+
"Não foi possível consultar os metadados do arquivo no BB Sia."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
298
|
+
|
|
299
|
+
def baixar_arquivo(
|
|
300
|
+
self,
|
|
301
|
+
id_arquivo: int | str,
|
|
302
|
+
nome_arquivo: str,
|
|
303
|
+
caminho: str | os.PathLike[str] | None = None,
|
|
304
|
+
) -> bytes:
|
|
305
|
+
access_token = self._get_access_token()
|
|
306
|
+
|
|
307
|
+
res = requests.request(
|
|
308
|
+
"GET",
|
|
309
|
+
f"{self._sia_domain}/gmt-sia-api/download/{id_arquivo}/{nome_arquivo}",
|
|
310
|
+
headers=common.get_headers(access_token),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if res.status_code != 200:
|
|
314
|
+
raise Exception("Não foi possível baixar o arquivo do BB Sia.")
|
|
315
|
+
|
|
316
|
+
if caminho is not None:
|
|
317
|
+
with open(caminho, "wb") as arquivo:
|
|
318
|
+
_ = arquivo.write(res.content)
|
|
319
|
+
|
|
320
|
+
return res.content
|
|
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
|
+
|
|
372
|
+
def pre_upload(
|
|
373
|
+
self,
|
|
374
|
+
fta: int | str,
|
|
375
|
+
nome_arquivo: str,
|
|
376
|
+
conteudo: bytes,
|
|
377
|
+
evento: int | str = 1,
|
|
378
|
+
content_md5: str | None = None,
|
|
379
|
+
) -> requests.Response:
|
|
380
|
+
access_token = self._get_access_token()
|
|
381
|
+
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
382
|
+
|
|
383
|
+
return requests.request(
|
|
384
|
+
"HEAD",
|
|
385
|
+
f"{self._sia_domain}/gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}",
|
|
386
|
+
headers={
|
|
387
|
+
**common.get_headers(access_token),
|
|
388
|
+
"Content-MD5": md5,
|
|
389
|
+
"x-gmt-content-length": str(len(conteudo)),
|
|
390
|
+
},
|
|
391
|
+
allow_redirects=False,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def upload(
|
|
395
|
+
self,
|
|
396
|
+
fta: int | str,
|
|
397
|
+
nome_arquivo: str,
|
|
398
|
+
conteudo: bytes,
|
|
399
|
+
evento: int | str = 1,
|
|
400
|
+
byte_inicial: int = 0,
|
|
401
|
+
byte_final: int | None = None,
|
|
402
|
+
total_bytes: int | None = None,
|
|
403
|
+
content_md5: str | None = None,
|
|
404
|
+
) -> requests.Response:
|
|
405
|
+
access_token = self._get_access_token()
|
|
406
|
+
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
407
|
+
|
|
408
|
+
total = total_bytes if total_bytes is not None else len(conteudo)
|
|
409
|
+
fim = byte_final if byte_final is not None else byte_inicial + len(conteudo) - 1
|
|
410
|
+
|
|
411
|
+
res = requests.request(
|
|
412
|
+
"PUT",
|
|
413
|
+
f"{self._sia_domain}/gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}",
|
|
414
|
+
headers={
|
|
415
|
+
"Content-Type": "application/octet-stream",
|
|
416
|
+
**common.get_headers(access_token),
|
|
417
|
+
"Content-MD5": md5,
|
|
418
|
+
"Content-Length": str(len(conteudo)),
|
|
419
|
+
"Content-Range": f"bytes {byte_inicial}-{fim}/{total}",
|
|
420
|
+
},
|
|
421
|
+
data=conteudo,
|
|
422
|
+
allow_redirects=False,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if res.status_code >= 400:
|
|
426
|
+
raise Exception("Não foi possível enviar o arquivo para o BB Sia.")
|
|
427
|
+
|
|
428
|
+
return res
|
|
429
|
+
|
|
430
|
+
def listar_protocolos(
|
|
431
|
+
self,
|
|
432
|
+
pagina: int = 1,
|
|
433
|
+
por_pagina: int = 20,
|
|
434
|
+
protocolo: Sequence[int] | None = None,
|
|
435
|
+
cod_fta: Sequence[int] | None = None,
|
|
436
|
+
cod_estado_protocolo: Sequence[int] | None = None,
|
|
437
|
+
dt_criacao_min: common.DateLike | None = None,
|
|
438
|
+
dt_criacao_max: common.DateLike | None = None,
|
|
439
|
+
) -> pd.DataFrame:
|
|
440
|
+
access_token = self._get_access_token()
|
|
441
|
+
|
|
442
|
+
body: dict[str, object] = {
|
|
443
|
+
"metadata": {
|
|
444
|
+
"pagina": pagina,
|
|
445
|
+
"porPagina": por_pagina,
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
if protocolo is not None:
|
|
449
|
+
body["protocolo"] = list(protocolo)
|
|
450
|
+
if cod_fta is not None:
|
|
451
|
+
body["codFta"] = list(cod_fta)
|
|
452
|
+
if cod_estado_protocolo is not None:
|
|
453
|
+
body["codEstadoProtocolo"] = list(cod_estado_protocolo)
|
|
454
|
+
if dt_criacao_min is not None:
|
|
455
|
+
body["dtCriacaoMin"] = _to_iso_z(dt_criacao_min)
|
|
456
|
+
if dt_criacao_max is not None:
|
|
457
|
+
body["dtCriacaoMax"] = _to_iso_z(dt_criacao_max, fim_do_dia=True)
|
|
458
|
+
|
|
459
|
+
res = requests.request(
|
|
460
|
+
"POST",
|
|
461
|
+
f"{self._sia_domain}/gmt-protocolo-api/listaProtocolos",
|
|
462
|
+
headers=common.get_headers(access_token),
|
|
463
|
+
json=body,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if res.status_code != 200:
|
|
467
|
+
raise Exception("Não foi possível listar os protocolos do BB Sia.")
|
|
468
|
+
|
|
469
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
@@ -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
|