bb_api 0.3.0__tar.gz → 0.4.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.4.0}/PKG-INFO +1 -1
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api/__init__.py +3 -1
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api/common.py +25 -0
- bb_api-0.4.0/bb_api/sia.py +575 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api.egg-info/PKG-INFO +1 -1
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api.egg-info/SOURCES.txt +1 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/pyproject.toml +1 -1
- {bb_api-0.3.0 → bb_api-0.4.0}/LICENSE +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/README.md +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api/accountability.py +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api/gestao_agil.py +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api.egg-info/dependency_links.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api.egg-info/requires.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/bb_api.egg-info/top_level.txt +0 -0
- {bb_api-0.3.0 → bb_api-0.4.0}/setup.cfg +0 -0
|
@@ -4,11 +4,12 @@ from importlib.metadata import version
|
|
|
4
4
|
try:
|
|
5
5
|
__version__ = version("bb_api")
|
|
6
6
|
except:
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.4.0"
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from .common import Ambiente
|
|
11
11
|
from .accountability import AccountabilityV3RepasseAPI, AccountabilityV3ControleAPI
|
|
12
|
+
from .sia import BBSiaAPI
|
|
12
13
|
from .gestao_agil import (
|
|
13
14
|
parse_retorno_abertura_massificada,
|
|
14
15
|
ler_retorno_abertura_massificada,
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"Ambiente",
|
|
19
20
|
"AccountabilityV3RepasseAPI",
|
|
20
21
|
"AccountabilityV3ControleAPI",
|
|
22
|
+
"BBSiaAPI",
|
|
21
23
|
"parse_retorno_abertura_massificada",
|
|
22
24
|
"ler_retorno_abertura_massificada",
|
|
23
25
|
]
|
|
@@ -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,27 @@ 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
|
+
"""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
|
+
domain = _sia_domains.get(ambiente)
|
|
51
|
+
if domain is None:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"O ambiente '{ambiente.name}' não é suportado pela API BB Sia."
|
|
54
|
+
)
|
|
55
|
+
return domain
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
def get_headers(access_token: str) -> dict[str, str]:
|
|
34
59
|
return {
|
|
35
60
|
"Authorization": f"Bearer {access_token}",
|
|
@@ -0,0 +1,575 @@
|
|
|
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
|
+
import os
|
|
27
|
+
import base64
|
|
28
|
+
import hashlib
|
|
29
|
+
import datetime
|
|
30
|
+
from collections.abc import Mapping, Sequence
|
|
31
|
+
from typing import cast
|
|
32
|
+
|
|
33
|
+
import pandas as pd
|
|
34
|
+
import requests
|
|
35
|
+
|
|
36
|
+
import bb_api.common as common
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _to_dataframe(
|
|
40
|
+
res: object,
|
|
41
|
+
main_list: str | None = None,
|
|
42
|
+
rename_dict: Mapping[str, str] | None = None,
|
|
43
|
+
) -> 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
|
+
record = cast("Mapping[str, object]", res)
|
|
52
|
+
|
|
53
|
+
if main_list is not None and isinstance(record.get(main_list), list):
|
|
54
|
+
return common.handle_results(res, main_list=main_list, rename_dict=rename_dict)
|
|
55
|
+
|
|
56
|
+
for key, value in record.items():
|
|
57
|
+
if isinstance(value, list):
|
|
58
|
+
return common.handle_results(res, main_list=key, rename_dict=rename_dict)
|
|
59
|
+
|
|
60
|
+
return common.handle_results(res, rename_dict=rename_dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
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
|
+
digest = hashlib.md5(conteudo, usedforsecurity=False).digest()
|
|
70
|
+
return base64.b64encode(digest).decode("utf-8")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
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
|
+
if isinstance(value, str):
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
if isinstance(value, datetime.datetime):
|
|
84
|
+
dt = value
|
|
85
|
+
elif fim_do_dia:
|
|
86
|
+
dt = datetime.datetime.combine(value, datetime.time(23, 59, 59))
|
|
87
|
+
else:
|
|
88
|
+
dt = datetime.datetime.combine(value, datetime.time())
|
|
89
|
+
|
|
90
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BBSiaAPI:
|
|
94
|
+
"""Encapsulador da API BB Sia (Gestão Ágil) do Banco do Brasil.
|
|
95
|
+
|
|
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
|
+
|
|
102
|
+
_scope: str
|
|
103
|
+
_sia_domain: str
|
|
104
|
+
_access_token: str
|
|
105
|
+
_username: str | None
|
|
106
|
+
_password: str | None
|
|
107
|
+
_refresh_token: str | None
|
|
108
|
+
_ambiente: common.Ambiente
|
|
109
|
+
_last_access_token_request_timestamp: datetime.datetime | None
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
ambiente: common.Ambiente = common.Ambiente.HOMOLOGACAO,
|
|
114
|
+
username: str | None = None,
|
|
115
|
+
password: str | None = None,
|
|
116
|
+
refresh_token: str | None = None,
|
|
117
|
+
scope: str = "sia:usuario",
|
|
118
|
+
):
|
|
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
|
+
self._ambiente = ambiente
|
|
141
|
+
self._sia_domain = common.sia_domain_for(ambiente)
|
|
142
|
+
self._scope = scope
|
|
143
|
+
|
|
144
|
+
self._username = username if username is not None else os.getenv("BBS_USERNAME")
|
|
145
|
+
self._password = password if password is not None else os.getenv("BBS_PASSWORD")
|
|
146
|
+
self._refresh_token = (
|
|
147
|
+
refresh_token if refresh_token is not None else os.getenv("BBS_REFRESH_TOKEN")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not self._refresh_token and not (self._username and self._password):
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"Credenciais inválidas para o BB Sia. Forneça um 'refresh_token'"
|
|
153
|
+
+ " (ou a variável de ambiente 'BBS_REFRESH_TOKEN') ou o par"
|
|
154
|
+
+ " usuário/senha (parâmetros 'username'/'password' ou as"
|
|
155
|
+
+ " variáveis de ambiente 'BBS_USERNAME'/'BBS_PASSWORD')."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self._access_token = ""
|
|
159
|
+
self._last_access_token_request_timestamp = None
|
|
160
|
+
|
|
161
|
+
def _store_access_token(self, data: dict[str, object]) -> dict[str, object]:
|
|
162
|
+
access_token = data.get("access_token")
|
|
163
|
+
if not access_token:
|
|
164
|
+
raise Exception("A resposta de autorização do BB Sia não trouxe um 'access_token'.")
|
|
165
|
+
|
|
166
|
+
self._access_token = cast("str", access_token)
|
|
167
|
+
self._last_access_token_request_timestamp = datetime.datetime.now()
|
|
168
|
+
return data
|
|
169
|
+
|
|
170
|
+
def _password_grant(self) -> dict[str, object]:
|
|
171
|
+
if not (self._username and self._password):
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"Usuário e senha são necessários para o grant 'password' do BB Sia."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
res = requests.request(
|
|
177
|
+
"POST",
|
|
178
|
+
f"{self._sia_domain}/gmt-autorizador-api/autoriza",
|
|
179
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
180
|
+
data={
|
|
181
|
+
"grant_type": "password",
|
|
182
|
+
"username": self._username,
|
|
183
|
+
"password": self._password,
|
|
184
|
+
"scope": self._scope,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if res.status_code != 200:
|
|
189
|
+
raise Exception("Não foi possível autorizar o acesso ao BB Sia com usuário e senha.")
|
|
190
|
+
|
|
191
|
+
data = common.parse_json_object(res)
|
|
192
|
+
refresh_token = data.get("refresh_token")
|
|
193
|
+
if refresh_token:
|
|
194
|
+
self._refresh_token = cast("str", refresh_token)
|
|
195
|
+
|
|
196
|
+
return self._store_access_token(data)
|
|
197
|
+
|
|
198
|
+
def _refresh_grant(
|
|
199
|
+
self,
|
|
200
|
+
validade_refresh_token: int | None = None,
|
|
201
|
+
) -> dict[str, object]:
|
|
202
|
+
if not self._refresh_token:
|
|
203
|
+
raise ValueError("Nenhum refresh token disponível para renovar o acesso ao BB Sia.")
|
|
204
|
+
|
|
205
|
+
data = {
|
|
206
|
+
"grant_type": "refresh_token",
|
|
207
|
+
"refresh_token": self._refresh_token,
|
|
208
|
+
}
|
|
209
|
+
if validade_refresh_token is not None:
|
|
210
|
+
data["validade_refresh_token"] = str(validade_refresh_token)
|
|
211
|
+
|
|
212
|
+
res = requests.request(
|
|
213
|
+
"POST",
|
|
214
|
+
f"{self._sia_domain}/gmt-autorizador-api/autoriza",
|
|
215
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
216
|
+
data=data,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if res.status_code != 200:
|
|
220
|
+
raise Exception("Não foi possível renovar o token de acesso ao BB Sia.")
|
|
221
|
+
|
|
222
|
+
return self._store_access_token(common.parse_json_object(res))
|
|
223
|
+
|
|
224
|
+
def _check_and_update_access_token(self) -> None:
|
|
225
|
+
now = datetime.datetime.now()
|
|
226
|
+
last_request = self._last_access_token_request_timestamp
|
|
227
|
+
|
|
228
|
+
is_token_valid = (
|
|
229
|
+
self._access_token != ""
|
|
230
|
+
and last_request is not None
|
|
231
|
+
and now - last_request <= common.time_between_access_token_requests
|
|
232
|
+
)
|
|
233
|
+
if is_token_valid:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if self._refresh_token:
|
|
237
|
+
try:
|
|
238
|
+
self._refresh_grant()
|
|
239
|
+
return
|
|
240
|
+
except Exception:
|
|
241
|
+
if not (self._username and self._password):
|
|
242
|
+
raise
|
|
243
|
+
|
|
244
|
+
self._password_grant()
|
|
245
|
+
|
|
246
|
+
def _get_access_token(self) -> str:
|
|
247
|
+
self._check_and_update_access_token()
|
|
248
|
+
return self._access_token
|
|
249
|
+
|
|
250
|
+
def autorizar_novo_token(self) -> dict[str, object]:
|
|
251
|
+
"""Solicita um novo token via *grant* ``password`` (usuário e senha).
|
|
252
|
+
|
|
253
|
+
Atualiza o token de acesso e o refresh token da instância e devolve a
|
|
254
|
+
resposta crua da API (com ``access_token``, ``refresh_token`` etc.).
|
|
255
|
+
"""
|
|
256
|
+
return self._password_grant()
|
|
257
|
+
|
|
258
|
+
def renovar_token(
|
|
259
|
+
self,
|
|
260
|
+
validade_refresh_token: int | None = None,
|
|
261
|
+
) -> 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
|
+
return self._refresh_grant(validade_refresh_token)
|
|
276
|
+
|
|
277
|
+
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
|
+
res = requests.request(
|
|
288
|
+
"POST",
|
|
289
|
+
f"{self._sia_domain}/gmt-autorizador-api/revogar",
|
|
290
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
291
|
+
data={
|
|
292
|
+
"token": token,
|
|
293
|
+
"token_type_hint": token_type_hint,
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if res.status_code not in (200, 204):
|
|
298
|
+
raise Exception("Não foi possível revogar o token do BB Sia.")
|
|
299
|
+
|
|
300
|
+
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
|
+
access_token = self._get_access_token()
|
|
306
|
+
|
|
307
|
+
res = requests.request(
|
|
308
|
+
"GET",
|
|
309
|
+
f"{self._sia_domain}/gmt-catalogo-api/listaUploads/",
|
|
310
|
+
headers=common.get_headers(access_token),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if res.status_code != 200:
|
|
314
|
+
raise Exception("Não foi possível listar os uploads possíveis no BB Sia.")
|
|
315
|
+
|
|
316
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
317
|
+
|
|
318
|
+
def listar_downloads(self) -> pd.DataFrame:
|
|
319
|
+
"""Lista os arquivos disponíveis para download.
|
|
320
|
+
|
|
321
|
+
Endpoint ``GET /gmt-sia-api/listaDownloads``.
|
|
322
|
+
"""
|
|
323
|
+
access_token = self._get_access_token()
|
|
324
|
+
|
|
325
|
+
res = requests.request(
|
|
326
|
+
"GET",
|
|
327
|
+
f"{self._sia_domain}/gmt-sia-api/listaDownloads",
|
|
328
|
+
headers=common.get_headers(access_token),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if res.status_code != 200:
|
|
332
|
+
raise Exception("Não foi possível listar os downloads do BB Sia.")
|
|
333
|
+
|
|
334
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
335
|
+
|
|
336
|
+
def consultar_metadados(
|
|
337
|
+
self,
|
|
338
|
+
id_arquivo: int | str,
|
|
339
|
+
nome_arquivo: str,
|
|
340
|
+
) -> 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
|
+
access_token = self._get_access_token()
|
|
353
|
+
|
|
354
|
+
res = requests.request(
|
|
355
|
+
"GET",
|
|
356
|
+
f"{self._sia_domain}/gmt-sia-api/listaDownloads/{id_arquivo}/{nome_arquivo}",
|
|
357
|
+
headers=common.get_headers(access_token),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if res.status_code != 200:
|
|
361
|
+
raise Exception("Não foi possível consultar os metadados do arquivo no BB Sia.")
|
|
362
|
+
|
|
363
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
364
|
+
|
|
365
|
+
def baixar_arquivo(
|
|
366
|
+
self,
|
|
367
|
+
id_arquivo: int | str,
|
|
368
|
+
nome_arquivo: str,
|
|
369
|
+
caminho: str | os.PathLike[str] | None = None,
|
|
370
|
+
) -> 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
|
+
access_token = self._get_access_token()
|
|
387
|
+
|
|
388
|
+
res = requests.request(
|
|
389
|
+
"GET",
|
|
390
|
+
f"{self._sia_domain}/gmt-sia-api/download/{id_arquivo}/{nome_arquivo}",
|
|
391
|
+
headers=common.get_headers(access_token),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if res.status_code != 200:
|
|
395
|
+
raise Exception("Não foi possível baixar o arquivo do BB Sia.")
|
|
396
|
+
|
|
397
|
+
if caminho is not None:
|
|
398
|
+
with open(caminho, "wb") as arquivo:
|
|
399
|
+
arquivo.write(res.content)
|
|
400
|
+
|
|
401
|
+
return res.content
|
|
402
|
+
|
|
403
|
+
def pre_upload(
|
|
404
|
+
self,
|
|
405
|
+
fta: int | str,
|
|
406
|
+
nome_arquivo: str,
|
|
407
|
+
conteudo: bytes,
|
|
408
|
+
evento: int | str = 1,
|
|
409
|
+
content_md5: str | None = None,
|
|
410
|
+
) -> 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
|
+
access_token = self._get_access_token()
|
|
434
|
+
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
435
|
+
|
|
436
|
+
return requests.request(
|
|
437
|
+
"HEAD",
|
|
438
|
+
f"{self._sia_domain}/gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}",
|
|
439
|
+
headers={
|
|
440
|
+
**common.get_headers(access_token),
|
|
441
|
+
"Content-MD5": md5,
|
|
442
|
+
"x-gmt-content-length": str(len(conteudo)),
|
|
443
|
+
},
|
|
444
|
+
allow_redirects=False,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def upload(
|
|
448
|
+
self,
|
|
449
|
+
fta: int | str,
|
|
450
|
+
nome_arquivo: str,
|
|
451
|
+
conteudo: bytes,
|
|
452
|
+
evento: int | str = 1,
|
|
453
|
+
byte_inicial: int = 0,
|
|
454
|
+
byte_final: int | None = None,
|
|
455
|
+
total_bytes: int | None = None,
|
|
456
|
+
content_md5: str | None = None,
|
|
457
|
+
) -> 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
|
+
access_token = self._get_access_token()
|
|
488
|
+
md5 = content_md5 if content_md5 is not None else _content_md5(conteudo)
|
|
489
|
+
|
|
490
|
+
total = total_bytes if total_bytes is not None else len(conteudo)
|
|
491
|
+
fim = byte_final if byte_final is not None else byte_inicial + len(conteudo) - 1
|
|
492
|
+
|
|
493
|
+
res = requests.request(
|
|
494
|
+
"PUT",
|
|
495
|
+
f"{self._sia_domain}/gmt-sia-api/upload/{fta}/{evento}/{nome_arquivo}",
|
|
496
|
+
headers={
|
|
497
|
+
"Content-Type": "application/octet-stream",
|
|
498
|
+
**common.get_headers(access_token),
|
|
499
|
+
"Content-MD5": md5,
|
|
500
|
+
"Content-Length": str(len(conteudo)),
|
|
501
|
+
"Content-Range": f"bytes {byte_inicial}-{fim}/{total}",
|
|
502
|
+
},
|
|
503
|
+
data=conteudo,
|
|
504
|
+
allow_redirects=False,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if res.status_code >= 400:
|
|
508
|
+
raise Exception("Não foi possível enviar o arquivo para o BB Sia.")
|
|
509
|
+
|
|
510
|
+
return res
|
|
511
|
+
|
|
512
|
+
def listar_protocolos(
|
|
513
|
+
self,
|
|
514
|
+
pagina: int = 1,
|
|
515
|
+
por_pagina: int = 20,
|
|
516
|
+
protocolo: Sequence[int] | None = None,
|
|
517
|
+
cod_fta: Sequence[int] | None = None,
|
|
518
|
+
cod_estado_protocolo: Sequence[int] | None = None,
|
|
519
|
+
dt_criacao_min: common.DateLike | None = None,
|
|
520
|
+
dt_criacao_max: common.DateLike | None = None,
|
|
521
|
+
) -> 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
|
+
access_token = self._get_access_token()
|
|
547
|
+
|
|
548
|
+
body: dict[str, object] = {
|
|
549
|
+
"metadata": {
|
|
550
|
+
"pagina": pagina,
|
|
551
|
+
"porPagina": por_pagina,
|
|
552
|
+
},
|
|
553
|
+
}
|
|
554
|
+
if protocolo is not None:
|
|
555
|
+
body["protocolo"] = list(protocolo)
|
|
556
|
+
if cod_fta is not None:
|
|
557
|
+
body["codFta"] = list(cod_fta)
|
|
558
|
+
if cod_estado_protocolo is not None:
|
|
559
|
+
body["codEstadoProtocolo"] = list(cod_estado_protocolo)
|
|
560
|
+
if dt_criacao_min is not None:
|
|
561
|
+
body["dtCriacaoMin"] = _to_iso_z(dt_criacao_min)
|
|
562
|
+
if dt_criacao_max is not None:
|
|
563
|
+
body["dtCriacaoMax"] = _to_iso_z(dt_criacao_max, fim_do_dia=True)
|
|
564
|
+
|
|
565
|
+
res = requests.request(
|
|
566
|
+
"POST",
|
|
567
|
+
f"{self._sia_domain}/gmt-protocolo-api/listaProtocolos",
|
|
568
|
+
headers=common.get_headers(access_token),
|
|
569
|
+
json=body,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
if res.status_code != 200:
|
|
573
|
+
raise Exception("Não foi possível listar os protocolos do BB Sia.")
|
|
574
|
+
|
|
575
|
+
return _to_dataframe(common.parse_json_object(res))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|