bb_api 0.1.0__py3-none-any.whl

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/common.py ADDED
@@ -0,0 +1,83 @@
1
+ import re
2
+ import requests
3
+ import pandas as pd
4
+ from enum import Enum
5
+ from collections.abc import Hashable, Mapping, Sequence
6
+ from datetime import date, datetime, timedelta
7
+ from typing import cast
8
+
9
+ dese_oauth_domain = "https://oauth.desenv.bb.com.br"
10
+ homo_oauth_domain = "https://oauth.hm.bb.com.br"
11
+ homo_alt_oauth_domain = "https://oauth.sandbox.bb.com.br"
12
+ prod_oauth_domain = "https://oauth.bb.com.br"
13
+
14
+ dese_api_domain = "https://api.desenv.bb.com.br"
15
+ homo_api_domain = "https://api.hm.bb.com.br"
16
+ homo_alt_api_domain = "https://api.sandbox.bb.com.br"
17
+ prod_api_domain = "https://api.bb.com.br"
18
+
19
+ time_between_access_token_requests = timedelta(minutes=10)
20
+
21
+
22
+ type DateLike = str | date | datetime
23
+ type Scalar = str | int | float | bool | None
24
+
25
+
26
+ class Ambiente(Enum):
27
+ DESENVOLVIMENTO = 0
28
+ HOMOLOGACAO = 1
29
+ HOMOLOGACAO_ALTERNATIVO = 2
30
+ PRODUCAO = 3
31
+
32
+
33
+ def get_headers(access_token: str) -> dict[str, str]:
34
+ return {
35
+ "Authorization": f"Bearer {access_token}",
36
+ }
37
+
38
+
39
+ def handle_numeric_string_with_symbols(v: str) -> str:
40
+ return re.sub(r"\D", "", v)
41
+
42
+
43
+ def handle_dates(v: DateLike) -> str:
44
+ if isinstance(v, str):
45
+ dt = datetime.strptime(v, "%Y-%m-%d")
46
+ elif isinstance(v, datetime):
47
+ dt = v
48
+ else:
49
+ dt = datetime.combine(v, datetime.min.time())
50
+
51
+ return dt.strftime("%Y-%m-%d")
52
+
53
+
54
+ def parse_json_object(res: requests.Response) -> dict[str, object]:
55
+ return cast("dict[str, object]", res.json())
56
+
57
+
58
+ def handle_results(
59
+ data: object,
60
+ main_list: str | None = None,
61
+ insertables: Sequence[str] | None = None,
62
+ explodeables: Sequence[str] | None = None,
63
+ rename_dict: Mapping[str, str] | None = None,
64
+ ) -> pd.DataFrame:
65
+ record = cast("Mapping[str, object]", data)
66
+
67
+ if main_list is not None:
68
+ df = pd.DataFrame(cast("list[dict[Hashable, object]]", record[main_list]))
69
+ else:
70
+ df = pd.DataFrame([dict(record)])
71
+
72
+ if insertables is not None:
73
+ for insertable in insertables:
74
+ df[insertable] = cast("Scalar", record[insertable])
75
+
76
+ if explodeables is not None:
77
+ for explodeable in explodeables:
78
+ df = df.explode(explodeable, ignore_index=True)
79
+
80
+ if rename_dict is not None:
81
+ df = df.rename(rename_dict, axis=1)
82
+
83
+ return df
bb_api/gestao_agil.py ADDED
@@ -0,0 +1,251 @@
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
+ import os
16
+ from collections.abc import Mapping, Sequence
17
+
18
+ import pandas as pd
19
+
20
+ import bb_api.common as common
21
+
22
+
23
+ _HEADER_ID = "00000"
24
+ _TRAILER_ID = "99999"
25
+ _LINE_LENGTH = 150
26
+
27
+ # Cada campo: (nome, posição inicial, posição final, formato).
28
+ # Posições 1-based e inclusivas, como na planilha de leiaute.
29
+ _HEADER_LAYOUT = [
30
+ ("preenchimento", 1, 5, "N"),
31
+ ("data_remessa", 6, 13, "N"), # DDMMAAAA
32
+ ("nome_arquivo", 14, 21, "A"),
33
+ ("numero_processo", 22, 26, "N"),
34
+ ("sequencial_remessa", 27, 31, "N"),
35
+ ("versao_leiaute", 32, 33, "N"),
36
+ ("espacos_em_branco", 34, 150, "A"),
37
+ ]
38
+
39
+ _DETALHE_LAYOUT = [
40
+ ("sequencial", 1, 5, "N"),
41
+ ("cpf_cnpj", 6, 19, "N"),
42
+ ("data_nascimento", 20, 27, "N"), # DDMMAAAA
43
+ ("nome_cliente", 28, 87, "A"),
44
+ ("uso_cliente", 88, 95, "A"),
45
+ ("numero_programa_gestao_agil", 96, 104, "A"),
46
+ ("agencia_cliente", 105, 108, "N"),
47
+ ("dv_agencia_cliente", 109, 109, "N"),
48
+ ("grupo_setex", 110, 111, "N"),
49
+ ("dv_grupo_setex", 112, 112, "N"),
50
+ ("conta", 113, 123, "N"),
51
+ ("dv_conta", 124, 124, "N"),
52
+ ("ocorrencia_cliente", 125, 127, "N"),
53
+ ("ocorrencia_conta", 128, 130, "N"),
54
+ ("ocorrencia_limite_credito", 131, 133, "N"),
55
+ ("codigo_mci", 134, 142, "N"),
56
+ ("espacos_em_branco", 143, 150, "A"),
57
+ ]
58
+
59
+ _TRAILER_LAYOUT = [
60
+ ("preenchimento", 1, 5, "N"),
61
+ ("quantidade_registros", 6, 14, "N"),
62
+ ("espacos_em_branco", 15, 150, "A"),
63
+ ]
64
+
65
+ # Tabela 1 - Ocorrências do Cliente
66
+ _TABELA_OCORRENCIA_CLIENTE = {
67
+ "001": "tipo pessoa inválido",
68
+ "002": "tipo CPF/CNPJ inválido",
69
+ "003": "CPF/CNPJ inválido",
70
+ "004": "data nascimento invalida",
71
+ "005": "nome cliente inválido",
72
+ "006": "agência/dv inválido",
73
+ "007": "mais de 5 clientes cadastrados para CPF informado",
74
+ "008": "cliente BB-Campus fora da faixa etária (16 a 28 anos)",
75
+ "009": "cliente BBCampus não é pessoa física",
76
+ "010": "dados pessoa física divergentes",
77
+ "011": "dados pessoa jurídica divergentes",
78
+ "013": "cliente BBCampus não titular CPF",
79
+ "014": "perfil agência incompatível com tipo pessoa",
80
+ "015": "tipo de pessoa incompátivel com natureza jurídica",
81
+ "016": "tipo de repasse inválido",
82
+ "017": "tipo de pessoa não permitido para esse processo/tipo de repasse",
83
+ }
84
+
85
+ # Tabela 2 - Ocorrências da Conta
86
+ _TABELA_OCORRENCIA_CONTA = {
87
+ "001": "ind cheque especial inválido",
88
+ "002": "setex/dv inválido",
89
+ "003": "não atende ao credit scoring",
90
+ "004": "dados pessoa física divergente",
91
+ }
92
+
93
+ # Tabela 3 - Ocorrências do Limite de Crédito
94
+ _TABELA_OCORRENCIA_LIMITE = {
95
+ "001": "cod estado civil inválido",
96
+ "002": "cod natureza ocupação inválido",
97
+ "003": "cod ocupação inválido",
98
+ "004": "valor rendimento inválido",
99
+ "005": "data rendimento invalida",
100
+ "006": "tipo contrato trabalho inválido",
101
+ "007": "data início emprego inválida",
102
+ "008": "nao atende ao credit scoring",
103
+ }
104
+
105
+ # Colunas de preenchimento/controle que não interessam ao DataFrame final.
106
+ _COLUNAS_DESCARTADAS = ["tipo", "espacos_em_branco"]
107
+
108
+ _RENAME_DETALHE = {
109
+ "linha": "Linha",
110
+ "sequencial": "Sequencial",
111
+ "cpf_cnpj": "CPF/CNPJ",
112
+ "data_nascimento": "Data Nascimento",
113
+ "nome_cliente": "Nome Cliente",
114
+ "uso_cliente": "Uso Cliente",
115
+ "numero_programa_gestao_agil": "Número Programa Gestão Ágil",
116
+ "agencia_cliente": "Agência Cliente",
117
+ "dv_agencia_cliente": "DV Agência Cliente",
118
+ "grupo_setex": "Grupo Setex",
119
+ "dv_grupo_setex": "DV Grupo Setex",
120
+ "conta": "Conta",
121
+ "dv_conta": "DV Conta",
122
+ "ocorrencia_cliente": "Código Ocorrência Cliente",
123
+ "ocorrencia_conta": "Código Ocorrência Conta",
124
+ "ocorrencia_limite_credito": "Código Ocorrência Limite Crédito",
125
+ "codigo_mci": "Código MCI",
126
+ "ocorrencia_cliente_desc": "Ocorrência Cliente",
127
+ "ocorrencia_conta_desc": "Ocorrência Conta",
128
+ "ocorrencia_limite_credito_desc": "Ocorrência Limite Crédito",
129
+ "numero_processo": "Número Processo",
130
+ "data_remessa": "Data Remessa",
131
+ "sequencial_remessa": "Sequencial Remessa",
132
+ }
133
+
134
+
135
+ def _parse_fields(
136
+ linha: str,
137
+ layout: Sequence[tuple[str, int, int, str]],
138
+ ) -> dict[str, str | int]:
139
+ """Fatia a linha conforme o layout. Campos "N" viram int quando possível."""
140
+ rec: dict[str, str | int] = {}
141
+ for nome, ini, fim, fmt in layout:
142
+ valor = linha[ini - 1:fim].strip()
143
+ if fmt == "N" and valor.isdigit():
144
+ rec[nome] = int(valor)
145
+ else:
146
+ rec[nome] = valor
147
+ return rec
148
+
149
+
150
+ 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
+ cod = str(codigo).zfill(3) if str(codigo).strip() else ""
153
+ if cod in ("", "000"):
154
+ return None
155
+ return tabela.get(cod, "código desconhecido")
156
+
157
+
158
+ 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
+ linha = linha.rstrip("\r\n")
161
+ if not linha.strip():
162
+ return None
163
+
164
+ if len(linha) < _LINE_LENGTH:
165
+ linha = linha.ljust(_LINE_LENGTH)
166
+
167
+ ident = linha[:5]
168
+ if ident == _HEADER_ID:
169
+ return {"tipo": "HEADER", "linha": numero, **_parse_fields(linha, _HEADER_LAYOUT)}
170
+ if ident == _TRAILER_ID:
171
+ return {"tipo": "TRAILER", "linha": numero, **_parse_fields(linha, _TRAILER_LAYOUT)}
172
+
173
+ rec: dict[str, object] = {
174
+ "tipo": "DETALHE",
175
+ "linha": numero,
176
+ **_parse_fields(linha, _DETALHE_LAYOUT),
177
+ }
178
+ rec["ocorrencia_cliente_desc"] = _decode_ocorrencia(
179
+ rec["ocorrencia_cliente"], _TABELA_OCORRENCIA_CLIENTE)
180
+ rec["ocorrencia_conta_desc"] = _decode_ocorrencia(
181
+ rec["ocorrencia_conta"], _TABELA_OCORRENCIA_CONTA)
182
+ rec["ocorrencia_limite_credito_desc"] = _decode_ocorrencia(
183
+ rec["ocorrencia_limite_credito"], _TABELA_OCORRENCIA_LIMITE)
184
+ return rec
185
+
186
+
187
+ 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
+ registros: list[dict[str, object]] = []
201
+ for numero, linha in enumerate(conteudo.splitlines(), start=1):
202
+ rec = _parse_linha(linha, numero)
203
+ if rec is not None:
204
+ registros.append(rec)
205
+
206
+ detalhes = [r for r in registros if r["tipo"] == "DETALHE"]
207
+
208
+ header: dict[str, object] = {}
209
+ for registro in registros:
210
+ if registro["tipo"] == "HEADER":
211
+ header = registro
212
+ break
213
+
214
+ data = {
215
+ "detalhes": detalhes,
216
+ "numero_processo": header.get("numero_processo"),
217
+ "data_remessa": header.get("data_remessa"),
218
+ "sequencial_remessa": header.get("sequencial_remessa"),
219
+ }
220
+ df = common.handle_results(
221
+ data,
222
+ main_list="detalhes",
223
+ insertables=[
224
+ "numero_processo",
225
+ "data_remessa",
226
+ "sequencial_remessa",
227
+ ],
228
+ rename_dict=_RENAME_DETALHE,
229
+ )
230
+ return df.drop(columns=_COLUNAS_DESCARTADAS, errors="ignore")
231
+
232
+
233
+ def ler_retorno_abertura_massificada(
234
+ caminho: str | os.PathLike[str],
235
+ encoding: str = "latin-1",
236
+ ) -> 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
+ with open(caminho, "r", encoding=encoding) as arquivo:
250
+ conteudo = arquivo.read()
251
+ return parse_retorno_abertura_massificada(conteudo)
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: bb_api
3
+ Version: 0.1.0
4
+ Summary: Wrapper da API do Banco do Brasil.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: pandas>=3.0.1
9
+ Requires-Dist: requests>=2.32.5
10
+ Dynamic: license-file
11
+
12
+ # Biblioteca API BB
13
+
14
+ *Wrapper* da API do Banco do Brasil.
15
+
16
+ ## Como instalar?
17
+
18
+ Se você usar o `uv`:
19
+
20
+ ```sh
21
+ uv add bb_api
22
+ ```
23
+
24
+ Se você usar o `pip`:
25
+
26
+ ```sh
27
+ pip install bb_api
28
+ ```
29
+
30
+ ## Funcionalidades
31
+
32
+ - Lê os parâmetros `app_key`, `client_id` e `client_secret` das variáveis de
33
+ ambiente `BB_API_APP_KEY`, `BB_API_CLIENT_ID` e `BB_API_CLIENT_SECRET`,
34
+ respectivamente, caso não sejam passadas na instanciação das classes
35
+ - Gera o token de acesso automaticamente, gerando um novo a cada 10 minutos,
36
+ tempo de expiração do token definido pelo Banco do Brasil
37
+ - Separa operaçãos disponíveis aos órgãos de repasse e de controle em classes
38
+ separadas, mantendo as operações comuns aos dois
39
+ - Retorna os resultados das chamadas às APIS em formato de `DataFrame` do
40
+ [`pandas`][pandas]
41
+ - Aceita parâmetros em múltiplos formatos, como:
42
+ - CNPJ e CEP podem estar pontuados ou não
43
+ - Datas podem estar em formato `str`, `date` ou `datetime`
44
+
45
+ ## Referências
46
+
47
+ Os documentos utilizados de referência para criação dessa API foram:
48
+
49
+ - [Portal Developers BB]
50
+ - [Documentação Swagger API BB]
51
+
52
+ [pandas]: https://pandas.pydata.org/
53
+ [Portal Developers BB]: https://apoio.developers.bb.com.br/referency/post/641877548600960012b32cd6
54
+ [Documentação Swagger API BB]: https://api.bb.com.br/accountability/v3/swagger
@@ -0,0 +1,9 @@
1
+ bb_api/__init__.py,sha256=K-QN7K2yzWDIJMSXf5RvQmigYv2TyvYJrXQdEyGcYpY,520
2
+ bb_api/accountability.py,sha256=8acRN30vmWydcPf5L7lctnQsq0bAA8QMTrKNap6CM2o,74988
3
+ bb_api/common.py,sha256=A1kOGKHJuF79ZZyDDAdFcctEfzr11Bh0l9KyuFXgeP0,2265
4
+ bb_api/gestao_agil.py,sha256=sEMlVH8CB8A1kkGbAGfy9wF374Jn5FT2jny8Vg9-bAE,8955
5
+ bb_api-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
6
+ bb_api-0.1.0.dist-info/METADATA,sha256=rgAjG_QBO4gy6gAwWTXZ1-glNyKC0X1xM0AjlWTjts8,1588
7
+ bb_api-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ bb_api-0.1.0.dist-info/top_level.txt,sha256=ZMdA0Lx6osrxQHw-iizi8tXTIofFtsg_qPApwC2xPOM,7
9
+ bb_api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+