csc-cia-stne 0.0.13__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.
csc_cia_stne/bc_sta.py ADDED
@@ -0,0 +1,301 @@
1
+ from requests.auth import HTTPBasicAuth
2
+ import sys
3
+ from pydantic import BaseModel
4
+ import requests
5
+ import xml.etree.ElementTree as ET
6
+ import hashlib
7
+ from pydantic import BaseModel, StrictStr, StrictInt, ValidationError, field_validator, FieldValidationInfo
8
+ from typing import Literal, Dict, Union, Optional
9
+
10
+ # Validações dos inputs
11
+ class InitParamsValidator(BaseModel):
12
+ usuario: str
13
+ senha: str
14
+ ambiente: Literal["prd", "hml"] # Aceita apenas "prd" ou "sdx"
15
+
16
+ # Validação para garantir que cada parâmetro é uma string não vazia
17
+ @field_validator('usuario', 'senha')
18
+ def check_non_empty_string(cls, value, info):
19
+ if not isinstance(value, str) or not value.strip():
20
+
21
+ raise ValueError(f"O parâmetro '{info.field_name}' deve ser uma string não vazia.")
22
+
23
+ return value
24
+
25
+ class EnviarArquivoValidator(BaseModel):
26
+ tipo_arquivo:str
27
+ file_content:bytes
28
+ nome_arquivo:str
29
+ observacao:str
30
+ destinatarios:Optional[Union[Dict[str, str], str]] = None # Aceita um dicionário
31
+
32
+ @field_validator("tipo_arquivo")
33
+ def check_tipo_arquivo(cls, value):
34
+ if not isinstance(value, str) or not value.strip():
35
+
36
+ raise ValueError("O parâmetro 'tipo_arquivo' deve ser uma string não vazia, com o código do tipo de envio a ser utilizado para o STA, verificar codigos disponíveis na documentação STA")
37
+
38
+ return value
39
+
40
+ # Validador para file_content: deve ter conteúdo em bytes
41
+ @field_validator("file_content")
42
+ def check_file_content(cls, value):
43
+ if not isinstance(value, bytes) or len(value) == 0:
44
+ raise ValueError("O parâmetro 'file_content' deve ser um byte array não vazio.")
45
+ return value
46
+
47
+ # Validador para nome_arquivo: deve ser uma string não vazia e terminar com uma extensão de arquivo comum
48
+ @field_validator("nome_arquivo")
49
+ def check_nome_arquivo(cls, value):
50
+ if not isinstance(value, str) or not value.strip():
51
+ raise ValueError("O nome do arquivo deve ser uma string não vazia.")
52
+ if not value.lower().endswith(('.zip', '.xpto')):
53
+ raise ValueError("O nome do arquivo deve ter uma extensão válida.")
54
+ return value
55
+
56
+ @field_validator("observacao")
57
+ def check_observacao(cls, value):
58
+ if not isinstance(value, str) or not value.strip():
59
+
60
+ raise ValueError("O parâmetro 'observacao' deve ser uma string não vazia")
61
+
62
+ return value
63
+
64
+ # Validador para destinatarios: aceita um dicionário ou uma string XML opcional
65
+ @field_validator("destinatarios")
66
+ def check_destinatarios(cls, value):
67
+ if value is not None:
68
+
69
+ if not isinstance(value, list):
70
+
71
+ raise ValueError("O parâmetro 'destinatarios' deve ser uma lista de dicionários.")
72
+
73
+ for item in value:
74
+
75
+ if not isinstance(item, dict):
76
+
77
+ raise ValueError("Cada destinatário deve ser um dicionário.")
78
+
79
+ required_keys = {"unidade", "dependencia", "operador"}
80
+
81
+ if not required_keys.issubset(item.keys()):
82
+
83
+ raise ValueError("Cada destinatário deve conter as chaves 'unidade', 'dependencia' e 'operador'. Verifique a documentação da API BC STA para entender o que colocar cada campo")
84
+
85
+ return value
86
+
87
+ class BC_STA:
88
+
89
+ def __init__(self, usuario:str, senha:str, ambiente:str):
90
+
91
+ try:
92
+
93
+ InitParamsValidator(usuario=usuario, senha=senha, ambiente=ambiente)
94
+
95
+ except ValidationError as e:
96
+
97
+ raise ValueError("Erro na validação dos dados de input da inicialização da instância 'BC_STA':", e.errors())
98
+
99
+ if ambiente == 'prd':
100
+
101
+ self.base_url = "https://sta.bcb.gov.br/staws"
102
+
103
+ else:
104
+
105
+ self.base_url = "https://sta-h.bcb.gov.br/staws"
106
+
107
+ try:
108
+
109
+ self.auth = HTTPBasicAuth(usuario,senha)
110
+ self.error = None
111
+ self.headers = {'Content-Type': 'application/xml'}
112
+ self.is_connected = self.verifica_conexao()
113
+
114
+ except Exception as e:
115
+
116
+ self.is_connected = False
117
+ self.error = e
118
+
119
+ def verifica_conexao(self):
120
+ try:
121
+
122
+ response = requests.get("https://sta.bcb.gov.br/staws/arquivos?tipoConsulta=AVANC&nivelDetalhe=RES", auth=self.auth)
123
+
124
+ # Verificando o status e retornando a resposta
125
+ if response.status_code == 200:
126
+
127
+ return True
128
+
129
+ else:
130
+
131
+ return False
132
+
133
+
134
+ except Exception as e:
135
+
136
+ raise e
137
+
138
+ def enviar_arquivo(self, tipo_arquivo:str, file_content:bytes, nome_arquivo:str, observacao:str, destinatarios:dict=None):
139
+
140
+ try:
141
+
142
+ EnviarArquivoValidator(tipo_arquivo=tipo_arquivo, file_content=file_content, nome_arquivo=nome_arquivo, observacao=observacao, destinatarios=destinatarios)
143
+
144
+ except ValidationError as e:
145
+
146
+ raise ValueError("Erro na validação dos dados de input do método 'enviar_arquivo':", e.errors())
147
+
148
+ def generate_sha256_hash(file_content):
149
+
150
+ # Gera o hash SHA-256 do arquivo
151
+ sha256_hash = hashlib.sha256()
152
+ sha256_hash.update(file_content)
153
+ return sha256_hash.hexdigest()
154
+
155
+ def process_response(xml_content):
156
+
157
+ try:
158
+
159
+ root = ET.fromstring(xml_content)
160
+
161
+ # Verifica se há um elemento <Erro> no XML
162
+ erro = None
163
+ erro_elem = root.find('Erro')
164
+
165
+ if erro_elem is not None:
166
+
167
+ codigo_erro = erro_elem.find('Codigo').text
168
+ descricao_erro = erro_elem.find('Descricao').text
169
+ erro = f"Erro {codigo_erro}: {descricao_erro}"
170
+ resposta = {
171
+ 'enviado':False,
172
+ 'protocolo': None,
173
+ 'link': None,
174
+ 'erro': erro
175
+ }
176
+
177
+ else:
178
+
179
+ protocolo = root.find('Protocolo').text
180
+ link = root.find('.//atom:link', namespaces={'atom': 'http://www.w3.org/2005/Atom'}).attrib['href']
181
+ resposta = {
182
+ 'enviado':True,
183
+ 'protocolo': protocolo,
184
+ 'link': link,
185
+ 'erro': None
186
+ }
187
+
188
+ return resposta
189
+
190
+ except ET.ParseError as e:
191
+
192
+ resposta = {
193
+ 'enviado': False,
194
+ 'protocolo': None,
195
+ 'link': None,
196
+ 'erro': f"Error processing XML: {str(e)}"
197
+ }
198
+ return resposta
199
+
200
+ url = self.base_url + '/arquivos'
201
+
202
+ # Calcula o hash SHA-256 do conteúdo do arquivo
203
+ hash_sha256 = generate_sha256_hash(file_content)
204
+ tamanho_arquivo = len(file_content) # Tamanho do arquivo em bytes
205
+
206
+ # Constrói o XML de requisição
207
+ parametros = ET.Element('Parametros')
208
+ ET.SubElement(parametros, 'IdentificadorDocumento').text = tipo_arquivo
209
+ ET.SubElement(parametros, 'Hash').text = hash_sha256
210
+ ET.SubElement(parametros, 'Tamanho').text = str(tamanho_arquivo)
211
+ ET.SubElement(parametros, 'NomeArquivo').text = nome_arquivo
212
+
213
+ # Campo observação é opcional
214
+ if observacao:
215
+ ET.SubElement(parametros, 'Observacao').text = observacao
216
+
217
+ # Campo destinatários é opcional
218
+ if destinatarios:
219
+
220
+ destinatarios_elem = ET.SubElement(parametros, 'Destinatarios')
221
+
222
+ for dest in destinatarios:
223
+
224
+ destinatario_elem = ET.SubElement(destinatarios_elem, 'Destinatario')
225
+ ET.SubElement(destinatario_elem, 'Unidade').text = dest['unidade']
226
+
227
+ if 'dependencia' in dest:
228
+
229
+ ET.SubElement(destinatario_elem, 'Dependencia').text = dest['dependencia']
230
+
231
+ if 'operador' in dest:
232
+
233
+ ET.SubElement(destinatario_elem, 'Operador').text = dest['operador']
234
+
235
+ # Converte o XML para string
236
+ xml_data = ET.tostring(parametros, encoding='utf-8', method='xml')
237
+
238
+ # Envia a requisição POST
239
+ response = requests.post(url, headers=self.headers, data=xml_data, auth=self.auth, timeout=60)
240
+
241
+ if response.status_code == 201: # Verifica se o protocolo foi criado com sucesso
242
+
243
+ resultado_protocolo = process_response(response.text)
244
+
245
+ # Protocolo gerado, prosseguir com envio do arquivo
246
+ if resultado_protocolo["enviado"]:
247
+
248
+ try:
249
+
250
+ # Solicita o envio
251
+ protocolo = resultado_protocolo["protocolo"]
252
+ # URL do endpoint, incluindo o protocolo
253
+ url = url + f"/{protocolo}/conteudo"
254
+
255
+ # Envia a requisição PUT com o conteúdo binário do arquivo
256
+ response = requests.put(url, data=file_content, auth=self.auth, timeout=60)
257
+
258
+ if response.status_code == 200:
259
+
260
+ return resultado_protocolo
261
+
262
+ else:
263
+
264
+ resposta = {
265
+ 'enviado':False,
266
+ 'protocolo': None,
267
+ 'link': None,
268
+ 'erro': f"Falha ao enviar arquivo. Status code: {response.status_code}, Text: {response.text}, Reason: {response.reason}"
269
+ }
270
+ return resposta
271
+
272
+ except Exception as e:
273
+
274
+ erro = str(e)
275
+ resposta = {
276
+ 'enviado':False,
277
+ 'protocolo': None,
278
+ 'link': None,
279
+ 'erro': erro
280
+ }
281
+ return resposta
282
+
283
+
284
+
285
+ # Protocolo não foi gerado, retornar erro
286
+ else:
287
+
288
+ return resultado_protocolo
289
+
290
+ else:
291
+
292
+ print(response.text)
293
+ resposta = {
294
+ 'enviado': False,
295
+ 'protocolo': None,
296
+ 'link': None,
297
+ 'erro': f"Failed to create protocol. Status code: {response.status_code}, Reason: {response.reason}"
298
+ }
299
+ #return f"Failed to create protocol. Status code: {response.status_code}, Reason: {response.reason}"
300
+ return resposta
301
+
@@ -0,0 +1,149 @@
1
+ import logging
2
+ from rich.logging import RichHandler
3
+ from rich.theme import Theme
4
+ from rich.console import Console
5
+ import re
6
+ from pythonjsonlogger import jsonlogger
7
+ from rich.traceback import install
8
+
9
+ def logger(env:str = "local", log_level:str = "DEBUG"):
10
+
11
+ def get_log_level(log_level):
12
+ if log_level.upper().strip() == "DEBUG":
13
+
14
+ log_config_level = logging.DEBUG
15
+
16
+ elif log_level.upper().strip() == "INFO":
17
+
18
+ log_config_level = logging.INFO
19
+
20
+ elif log_level.upper().strip() == "WARNING":
21
+
22
+ log_config_level = logging.WARNING
23
+
24
+ elif log_level.upper().strip() == "ERROR":
25
+
26
+ log_config_level = logging.ERROR
27
+
28
+ elif log_level.upper().strip() == "CRITICAL":
29
+
30
+ log_config_level = logging.CRITICAL
31
+
32
+ else:
33
+
34
+ log_config_level = logging.INFO # ou outro nível padrão
35
+
36
+ return log_config_level
37
+
38
+ def add_log_level(level_name, level_num, method_name=None):
39
+ """
40
+ Adiciona um log level
41
+
42
+ Parâmetros:
43
+ level_name (str): Nome do level
44
+ level_num (int): Número do level
45
+ """
46
+ if not method_name:
47
+
48
+ method_name = level_name.lower()
49
+
50
+ if hasattr(logging, level_name):
51
+
52
+ raise AttributeError('{} already defined in logging module'.format(level_name))
53
+
54
+ if hasattr(logging, method_name):
55
+
56
+ raise AttributeError('{} already defined in logging module'.format(method_name))
57
+
58
+ if hasattr(logging.getLoggerClass(), method_name):
59
+
60
+ raise AttributeError('{} already defined in logger class'.format(method_name))
61
+
62
+ def log_for_level(self, message, *args, **kwargs):
63
+
64
+ if self.isEnabledFor(level_num):
65
+
66
+ self._log(level_num, message, args, **kwargs)
67
+
68
+ def log_to_root(message, *args, **kwargs):
69
+
70
+ logging.log(level_num, message, *args, **kwargs)
71
+
72
+ logging.addLevelName(level_num, level_name)
73
+ setattr(logging, level_name, level_num)
74
+ setattr(logging.getLoggerClass(), method_name, log_for_level)
75
+ setattr(logging, method_name, log_to_root)
76
+
77
+ add_log_level("SUCCESS",21)
78
+
79
+ if env.upper().strip() != "KARAVELA":
80
+
81
+ install()
82
+
83
+ # Definindo o tema customizado
84
+ custom_theme = Theme({
85
+ # python -m rich.color - cores
86
+ # python -m rich.default_styles - item + cor padrão
87
+ "logging.level.debug": "bold bright_cyan",
88
+ "logging.level.info": "bold bright_white",
89
+ "logging.level.warning": "bold orange1",
90
+ "logging.level.error": "bold red blink",
91
+ "logging.level.critical": "bold white on red blink",
92
+ "logging.level.success": "bold bright_green",
93
+ "log.time":"bold white",
94
+ "log.message":"bold gray70",
95
+ "repr.str":"dark_olive_green3",
96
+ "inspect.value.border":"blue",
97
+ })
98
+
99
+ console = Console(theme=custom_theme)
100
+
101
+ class CustomRichHandler(RichHandler):
102
+ def __init__(self, *args, rich_tracebacks=True, show_time=True, show_level=True, show_path=True, console=console, **kwargs):
103
+ super().__init__(rich_tracebacks=rich_tracebacks, show_time=show_time, show_level=show_level, show_path=show_path, console=console, *args, **kwargs)
104
+ self.show_time = show_time
105
+
106
+ def format(self, record: logging.LogRecord) -> str:
107
+
108
+ def remover_prefixo_log(mensagem):
109
+ # Expressão regular que encontra o padrão 'log_level:nivel: '
110
+ padrao = r'^(ERROR|WARNING|INFO|DEBUG|SUCCESS|CRITICAL):[^:]*:'
111
+
112
+ # Substitui o padrão encontrado por uma string vazia
113
+ return re.sub(padrao, '', mensagem).strip()
114
+
115
+ msg = f"| {record.getMessage()}"
116
+
117
+ return(str(msg))
118
+
119
+ # Configurando o logging com o CustomRichHandler
120
+ logging.basicConfig(
121
+ level=get_log_level(log_level),
122
+ handlers=[CustomRichHandler()],
123
+ datefmt="%d/%m/%Y %H:%M:%S |",
124
+ )
125
+
126
+ return logging.getLogger()
127
+
128
+ else:
129
+
130
+ def setup_json_logger():
131
+ logger = logging.getLogger()
132
+ logger.setLevel(get_log_level(log_level))
133
+
134
+ # Remove handlers anteriores, se houver
135
+ if logger.hasHandlers():
136
+ logger.handlers.clear()
137
+
138
+ log_handler = logging.StreamHandler()
139
+ formatter = jsonlogger.JsonFormatter(
140
+ fmt='%(asctime)s %(levelname)s %(name)s %(message)s %(pathname)s %(lineno)d %(exc_info)s %(stack_info)s %(funcName)s %(module)s',
141
+ json_ensure_ascii=False
142
+ )
143
+ log_handler.setFormatter(formatter)
144
+ logger.addHandler(log_handler)
145
+
146
+ return logger
147
+
148
+ # Chama a função para configurar o logger
149
+ return setup_json_logger()
@@ -0,0 +1,182 @@
1
+ from google.cloud import bigquery
2
+ import json
3
+ import os
4
+ from google.oauth2 import service_account
5
+
6
+ class BigQuery():
7
+
8
+ def __init__(self, client: bigquery.Client, limit: int = 3):
9
+ """
10
+ Inicializa a classe BigQuery.
11
+ Parâmetros:
12
+ limit (int): O limite de resultados a serem retornados. O valor padrão é 3.
13
+ client (bigquery.Client): O cliente BigQuery a ser utilizado.
14
+ """
15
+
16
+ self.limit = limit
17
+ self.client = client
18
+
19
+ def create_client(id_project: str, creds_dict: dict = None, creds_file: str = "") -> bigquery.Client:
20
+ """
21
+ Cria um cliente BigQuery com base nas credenciais fornecidas.
22
+ Parâmetros:
23
+ - creds_dict (dict): Dicionário contendo as informações das credenciais. Opcional se creds_file for fornecido.
24
+ - creds_file (str): Caminho do arquivo de credenciais. Opcional se creds_dict for fornecido.
25
+ - id_project (str): ID do projeto BigQuery.
26
+ Retorna:
27
+ - client (bigquery.Client): Cliente BigQuery criado com base nas credenciais fornecidas.
28
+ Exceções:
29
+ - Retorna um dicionário com as seguintes chaves em caso de erro:
30
+ - 'status' (bool): False
31
+ - 'error' (str): Mensagem de erro
32
+ - 'details' (str): Detalhes do erro
33
+ """
34
+
35
+ try:
36
+
37
+ if(creds_dict not None):
38
+
39
+ credentials = service_account.Credentials.from_service_account_info(
40
+ creds_dict,
41
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
42
+ )
43
+
44
+ client = bigquery.Client(credentials=credentials, project=id_project)
45
+
46
+ elif(str(creds_file) > 0):
47
+
48
+ credentials = service_account.Credentials.from_service_account_file(
49
+ creds_file,
50
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
51
+ )
52
+
53
+ else:
54
+
55
+ return {
56
+ 'status':False,
57
+ 'error':"Credenciais não fornecidas"
58
+ }
59
+
60
+ return client
61
+
62
+ except Exception as e:
63
+
64
+ return {
65
+ 'status':False,
66
+ 'error':'Problema ao tentar gerar o client do big query',
67
+ 'details':str(e)
68
+ }
69
+
70
+ def try_query(self, query_to_execute:str,organize:bool=True) -> dict:
71
+ """
72
+ Executa uma consulta no BigQuery e retorna o resultado.
73
+ Args:
74
+ query_to_execute (str): A consulta a ser executada.
75
+ organize (bool, optional): Indica se o resultado deve ser organizado em um formato específico.
76
+ O padrão é True.
77
+ Returns:
78
+ dict: Um dicionário contendo o status da consulta e o resultado, se houver.
79
+ Exemplo:
80
+ {
81
+ 'status': True,
82
+ 'resultado': result_query
83
+ Se ocorrer um erro durante a execução da consulta, o dicionário de retorno terá o seguinte formato:
84
+ {
85
+ 'status': False,
86
+ """
87
+
88
+ error = ""
89
+
90
+ for try_out in range(self.limit):
91
+
92
+ try:
93
+
94
+ result_query = self.client.query(query_to_execute).result()
95
+
96
+ error = False
97
+
98
+ if organize:
99
+
100
+ result_rows = [dict(row) for row in result_query['resultado']]
101
+
102
+ result_query = result_rows
103
+
104
+ break
105
+
106
+ except Exception as e:
107
+
108
+ error = e
109
+
110
+ if not error:
111
+
112
+ return {
113
+ 'status':True,
114
+ 'resultado':result_query
115
+ }
116
+
117
+ else:
118
+
119
+ return {
120
+ 'status':False,
121
+ 'error': str(error)
122
+ }
123
+
124
+ def insert_list(self,table:str,dict_to_insert:dict=[]) -> dict:
125
+
126
+ """
127
+ Insere uma lista de dicionários em uma tabela do BigQuery.
128
+ Args:
129
+ client (bigquery.Client): Cliente do BigQuery.
130
+ table (str): Nome da tabela onde os dados serão inseridos.
131
+ dict_to_insert (dict, optional): Lista de dicionários a serem inseridos. O padrão é [].
132
+ limit_trys (int, optional): Número máximo de tentativas de inserção. O padrão é 3.
133
+ Returns:
134
+ dict: Dicionário contendo o status da inserção e informações adicionais.
135
+ - Se a inserção for bem-sucedida:
136
+ {'status': True, 'inserted': inserted}
137
+ - Se ocorrer um erro durante a inserção:
138
+ {'status': False, 'error': error, 'last_try': list_to_insert, 'inserted': inserted}
139
+ """
140
+
141
+ table_ref = self.client.get_table(table)
142
+
143
+ error = ""
144
+
145
+ inserted = []
146
+
147
+ for data in range(0, len(dict_to_insert), 10000):
148
+
149
+ # listagem que será inserida no big query
150
+ list_to_insert = dict_to_insert[data:data+10000]
151
+
152
+ for try_out in range(self.limit):
153
+
154
+ try:
155
+
156
+ self.client.insert_rows(table_ref, list_to_insert)
157
+
158
+ error = False
159
+
160
+ except Exception as e:
161
+
162
+ error = e
163
+
164
+ if not error:
165
+
166
+ inserted.extend(list_to_insert)
167
+
168
+ continue
169
+
170
+ else:
171
+
172
+ return{
173
+ 'status':False,
174
+ 'error':str(error),
175
+ 'last_try':list_to_insert,
176
+ 'inserted':inserted
177
+ }
178
+
179
+ return {
180
+ 'status':True,
181
+ 'inserted':inserted
182
+ }