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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Wrapper da API do Banco do Brasil.
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -4,11 +4,12 @@ from importlib.metadata import version
4
4
  try:
5
5
  __version__ = version("bb_api")
6
6
  except:
7
- __version__ = "0.3.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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bb_api
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Wrapper da API do Banco do Brasil.
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -5,6 +5,7 @@ bb_api/__init__.py
5
5
  bb_api/accountability.py
6
6
  bb_api/common.py
7
7
  bb_api/gestao_agil.py
8
+ bb_api/sia.py
8
9
  bb_api.egg-info/PKG-INFO
9
10
  bb_api.egg-info/SOURCES.txt
10
11
  bb_api.egg-info/dependency_links.txt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bb_api"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Wrapper da API do Banco do Brasil."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes
File without changes
File without changes
File without changes
File without changes