lbxtoolkit 2.2.8__tar.gz → 2.2.10__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.1
2
2
  Name: lbxtoolkit
3
- Version: 2.2.8
3
+ Version: 2.2.10
4
4
  Summary:
5
5
  Author: Cristiano P. Ferrari
6
6
  Author-email: cristiano.ferrari@lbx.com.br
@@ -60,7 +60,7 @@ Interage com o banco de dados PostgreSQL
60
60
  Interage com APIs RESTfull, especialmente providas para a plataforma Sienge
61
61
  - `auth_base` Autentica (HTTPBasicAuth) sessão na API
62
62
  - `auth_bearer` Autentica sessão na API pelos métodos: OAuth, JWT, Bearer
63
- - `endpoint_json` Realizad chama ao endpoint. Payload em formato `json` opcional.
63
+ - `endpoint_json` Realizad chama ao endpoint. Aceita opcionalmente Payload em formato `json` ou arquivo binário.
64
64
  - `trata_erro_sienge` Retorna a mensagem de erro do Sienge caso código de retorno seja diferente de 200.
65
65
  - `close` Encerra a sessão autenticada
66
66
 
@@ -145,7 +145,7 @@ São nessários 2 parâmetros posicionais obrigatórios, e 5 parametros nominais
145
145
  - `cadencia` Número máximo de chamadas *por segudo* à API
146
146
  - `timeout` Tempo máximo (segundos) para aguardar retorno à chamada. Padrão 6s, se omisso.
147
147
  - `logger` O objeto _log handler_ para lidar com as informações de saída. Se não informado, todas as saídas serão direcionadas para a stdout.
148
- - `headers` Cabeçalhos _http_ para a requisição à API.
148
+ - `headers` Cabeçalhos _http_ para a requisição à API. Se não fornecido, será incluida implicitamente a chave `Content-Type` com valor `application/json`.
149
149
  - `verify` Verifica a validade do certificado SSL do servidor de destino da requisição.
150
150
 
151
151
  Quanto às credenciais de autenticação, assim como a classe de interação com o PostgreSQL, elas precisam ser fornecidas na forma de um *dicionário*.
@@ -162,12 +162,14 @@ Caso a autenticação seja pelo método `api_rest.aut_bearer()`, o dicionário d
162
162
  A classe possui 3 métodos:
163
163
  - `api_rest.auth_basic()`: instanciamento da sessão autenticando pelo método HTTPBasicAuth
164
164
  - `api_rest.auth_bearer()`: instanciamento da sessão autenticando pelos métodos OAuth, JWT, Bearer
165
- - `api_rest.endpoint_json([endereço], [método], payload=None)`: para a chamada ao endpoint
165
+ - `api_rest.endpoint_json([endereço], [método], payload=None, files=None)`: para a chamada ao endpoint
166
166
  - `close()` para encerra a instância/sessão
167
167
 
168
- O consumo é feito pelo método `api_rest.endpoint_json` que suporta apenas APIs cujo payload (opcional) seja aceito no formato JSON.
168
+ O consumo é feito pelo método `api_rest.endpoint_json` que suporta APIs cujo payload (opcional) seja aceito no formato JSON ou binario (files).
169
169
 
170
- Esse método espera 2 parametros posicionais obrigatórios: o endereço do endpoint e o verbo (get, post, patch ou put), tendo parametro opcional o objeto de 'payload' (json).
170
+ Esse método espera 2 parametros posicionais obrigatórios: o endereço do endpoint e o verbo (get, post, patch ou put), tendo parametro opcional o objeto de 'payload' (json) ou 'files' (binário).
171
+ O método define no `headers` a chavee `Content-Type` implicita e automaticamente com o valor `application/json`, independente de ser ou não fornecido payload.
172
+ Se a carga for do tipo binário, é necessário informar o Content-Type correto (`multipart/form-data`) ou repassar um dicionario vazio (`{}`) para o parametro `headers` no instanciamento da classe `api_rest`
171
173
  Note que o endereço do endpoint deve ser informado completo. A URL informada no instanciamento da classe corresponde apenas ao endereço de autenticação.
172
174
 
173
175
  O tempo, em segundos, transcorrido entre a chamada a atual e a chamada anterior ao endpoint pode ser consultado pelo argumento `.Intervalo` no objeto recebido do retorno à chamada ao método `.endpoint_json`.
@@ -38,7 +38,7 @@ Interage com o banco de dados PostgreSQL
38
38
  Interage com APIs RESTfull, especialmente providas para a plataforma Sienge
39
39
  - `auth_base` Autentica (HTTPBasicAuth) sessão na API
40
40
  - `auth_bearer` Autentica sessão na API pelos métodos: OAuth, JWT, Bearer
41
- - `endpoint_json` Realizad chama ao endpoint. Payload em formato `json` opcional.
41
+ - `endpoint_json` Realizad chama ao endpoint. Aceita opcionalmente Payload em formato `json` ou arquivo binário.
42
42
  - `trata_erro_sienge` Retorna a mensagem de erro do Sienge caso código de retorno seja diferente de 200.
43
43
  - `close` Encerra a sessão autenticada
44
44
 
@@ -123,7 +123,7 @@ São nessários 2 parâmetros posicionais obrigatórios, e 5 parametros nominais
123
123
  - `cadencia` Número máximo de chamadas *por segudo* à API
124
124
  - `timeout` Tempo máximo (segundos) para aguardar retorno à chamada. Padrão 6s, se omisso.
125
125
  - `logger` O objeto _log handler_ para lidar com as informações de saída. Se não informado, todas as saídas serão direcionadas para a stdout.
126
- - `headers` Cabeçalhos _http_ para a requisição à API.
126
+ - `headers` Cabeçalhos _http_ para a requisição à API. Se não fornecido, será incluida implicitamente a chave `Content-Type` com valor `application/json`.
127
127
  - `verify` Verifica a validade do certificado SSL do servidor de destino da requisição.
128
128
 
129
129
  Quanto às credenciais de autenticação, assim como a classe de interação com o PostgreSQL, elas precisam ser fornecidas na forma de um *dicionário*.
@@ -140,12 +140,14 @@ Caso a autenticação seja pelo método `api_rest.aut_bearer()`, o dicionário d
140
140
  A classe possui 3 métodos:
141
141
  - `api_rest.auth_basic()`: instanciamento da sessão autenticando pelo método HTTPBasicAuth
142
142
  - `api_rest.auth_bearer()`: instanciamento da sessão autenticando pelos métodos OAuth, JWT, Bearer
143
- - `api_rest.endpoint_json([endereço], [método], payload=None)`: para a chamada ao endpoint
143
+ - `api_rest.endpoint_json([endereço], [método], payload=None, files=None)`: para a chamada ao endpoint
144
144
  - `close()` para encerra a instância/sessão
145
145
 
146
- O consumo é feito pelo método `api_rest.endpoint_json` que suporta apenas APIs cujo payload (opcional) seja aceito no formato JSON.
146
+ O consumo é feito pelo método `api_rest.endpoint_json` que suporta APIs cujo payload (opcional) seja aceito no formato JSON ou binario (files).
147
147
 
148
- Esse método espera 2 parametros posicionais obrigatórios: o endereço do endpoint e o verbo (get, post, patch ou put), tendo parametro opcional o objeto de 'payload' (json).
148
+ Esse método espera 2 parametros posicionais obrigatórios: o endereço do endpoint e o verbo (get, post, patch ou put), tendo parametro opcional o objeto de 'payload' (json) ou 'files' (binário).
149
+ O método define no `headers` a chavee `Content-Type` implicita e automaticamente com o valor `application/json`, independente de ser ou não fornecido payload.
150
+ Se a carga for do tipo binário, é necessário informar o Content-Type correto (`multipart/form-data`) ou repassar um dicionario vazio (`{}`) para o parametro `headers` no instanciamento da classe `api_rest`
149
151
  Note que o endereço do endpoint deve ser informado completo. A URL informada no instanciamento da classe corresponde apenas ao endereço de autenticação.
150
152
 
151
153
  O tempo, em segundos, transcorrido entre a chamada a atual e a chamada anterior ao endpoint pode ser consultado pelo argumento `.Intervalo` no objeto recebido do retorno à chamada ao método `.endpoint_json`.
@@ -216,17 +216,23 @@ Para uso em APIs com autenticação JWT (JSON Web Token), OAuth, Bearer Token Au
216
216
  return self.RetEndPoint
217
217
  #
218
218
  #
219
- def endpoint_json(self, endpoint, metodo, payload=None): # Interage com End Point
219
+ def endpoint_json(self, endpoint, metodo, payload=None, file=None): # Interage com End Point
220
220
  self.ult_tempo_req = time()
221
221
  self.Metodo = metodo.lower()
222
222
  #self.EndPoint = self.Url + endpoint
223
223
  self.EndPoint = endpoint
224
224
  self.Payload = payload
225
+ self.File = file
226
+
225
227
  MetodosAceitos = ['post', 'get', 'patch', 'put']
226
228
  if not any(element in self.Metodo for element in MetodosAceitos):
227
229
  self.logger.critico(f'Método {self.Metodo} não previsto. Abortando chamada!', exit=1)
228
230
  else:
229
- ChamadaApi = f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify)' if self.Payload is None else f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify, json=self.Payload)'
231
+ ChamadaApi = f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify)' if self.Payload is None and self.File is None else \
232
+ f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify, json=self.Payload)' if self.Payload and self.File is None else \
233
+ f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify, files=self.File)' if self.Payload is None and self.File else \
234
+ f'self.Sessao.{self.Metodo}(self.EndPoint, timeout=self.Timeout, headers=self.Headers, verify=self.Verify, json=self.Payload, files=self.File)'
235
+
230
236
  self.controla_cadencia()
231
237
  self.TempoUltReq = time()
232
238
  try:
@@ -3,6 +3,7 @@ import sys
3
3
  import re
4
4
  import datetime
5
5
  import logging
6
+ import traceback
6
7
  from pathlib import Path
7
8
 
8
9
  class lbx_logger: # Classe para gerenciar a saída para log
@@ -75,6 +76,9 @@ Considerando que os parametros são posicionais, caso queira omitir apenas um do
75
76
 
76
77
  A saída dessa função retorna um objeto, que pode ser salvo em disco ou impresso na tela.
77
78
 
79
+ 5. O método `.traceback_log`, quando acionado, grava, de maneira incremental, o tracback do último erro no arquivo `traceback.log` no mesmo diretório de log onde o arquivo principal de log está salvo. Se o log não está sendo salvo, a mensagem será exibida na tela.
80
+
81
+ Os métodos `.erro()` e `.critico()` fazem isso automaticamente por padrão.
78
82
 
79
83
  5. Exemplos de uso:
80
84
 
@@ -168,7 +172,7 @@ for line in filtered_lines:
168
172
 
169
173
  if log_file_path:
170
174
  # Criando um handler para escrever em um arquivo de log
171
- file_handler = logging.FileHandler(self.log_file_path)
175
+ file_handler = logging.FileHandler(self.log_file_path, encoding="utf-8")
172
176
  file_handler.setLevel(log_level) # Sempre registrar tudo no arquivo
173
177
 
174
178
  # Criando um handler para exibir no console
@@ -187,6 +191,9 @@ for line in filtered_lines:
187
191
  # Adicionando os handlers ao logger
188
192
  self.logger.addHandler(file_handler)
189
193
  self.logger.addHandler(console_handler)
194
+
195
+ # Saida para o traceback
196
+ self.traceback_file = Path(self.log_file_path.parent, 'traceback.log')
190
197
  else:
191
198
  # Tudo direcionado para o console
192
199
  console_handler = logging.StreamHandler()
@@ -245,6 +252,7 @@ for line in filtered_lines:
245
252
  #
246
253
  #
247
254
  def write(self, message):
255
+ message=message
248
256
  if message.strip(): # Ignorar mensagens vazias
249
257
  self.logger.info(message.strip())
250
258
  #
@@ -285,6 +293,7 @@ for line in filtered_lines:
285
293
  msg = self.msg[:corte] if corte else self.msg
286
294
  self.logger.error(msg)
287
295
  self.msg = ''
296
+ self.log_traceback()
288
297
  if exit:
289
298
  os._exit(exit)
290
299
  #
@@ -294,6 +303,7 @@ for line in filtered_lines:
294
303
  msg = self.msg[:corte] if corte else self.msg
295
304
  self.logger.critical(msg)
296
305
  self.msg = ''
306
+ self.log_traceback()
297
307
  if exit:
298
308
  os._exit(exit)
299
309
  #
@@ -387,7 +397,16 @@ for line in filtered_lines:
387
397
  # Retornar o objeto contendo as linhas filtradas
388
398
  return filtered_lines
389
399
  #
390
- #
400
+ #
401
+ def log_traceback(self):
402
+ if self.traceback_file:
403
+ with open(self.traceback_file, "a", encoding="utf-8") as f:
404
+ f.write("\n" + "-"*50 + "\n") # Separador entre erros
405
+ f.write(f"\n{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]}")
406
+ f.write(traceback.format_exc())
407
+ else:
408
+ traceback.format_exc()
409
+
391
410
  class SafeLogger: ## intercepta as entradas de log para parar o daemon/watcher quando parametro EXIT existir
392
411
  """
393
412
  Essa classe tem o propoósito de interceptar as ordens de saída (exit) disparadas pelos métodos do lbx_logger e interromper um daemon/serviço que esteja em execução antes.
@@ -44,14 +44,23 @@ Sintaxe e exemplos de uso. Parametros omissos assume-se os valores padrão indic
44
44
  return Diretorio
45
45
  #
46
46
  #
47
- def normaliza(Original): # Limpa e padroniza nomes
48
- Lixo = r'/\\?%§ªº°`´^~*:|"<>!@#$%¨&*()_+=-[]{}"\' '
49
- Normalizar = normalize('NFKD', Original).encode('ASCII', 'ignore').decode('ASCII')
47
+ def normaliza(Original, Case=None, KeepSpaces=False, KeepDelim=False, KeepDirTree=False): # Limpa e padroniza nomes
48
+
49
+ Lixo = r'?%§ªº°`´^~*|"<>!@#$%¨&*_+=-"\''
50
+ Lixo = Lixo + r' ' if not KeepSpaces else Lixo
51
+ Lixo = Lixo + r'(){}[]' if not KeepDelim else Lixo
52
+ Lixo = Lixo + r':/\\' if not KeepDirTree else Lixo
53
+
54
+ if Case and not Case.lower() in ['upper', 'lower', 'keep']:
55
+ raise ValueError('Parametro opcioanl "Case" deve ser ["upper", "lower", "keep"]. Se omisso (None), string será convertida para caixa baixa.')
56
+
57
+ Normalizar = (''.join(c for c in normalize('NFKD', Original) if c.encode('ascii', 'ignore').decode('ascii'))) if Case.upper() == 'keep' else normalize('NFKD', Original).encode('ASCII', 'ignore').decode('ASCII')
50
58
  RemoverLixo = [c if c not in Lixo else '_' for c in Normalizar]
51
59
  Limpo = "".join(RemoverLixo)
52
60
  Limpo = re.sub(r'\.(?=.*\.)', '_', Limpo) # troca todos os pontos por underline
53
61
  Limpo = re.sub(r'_+', '_', Limpo) # limpa as reptições do underline
54
- return Limpo.lower()
62
+ Limpo = Limpo.lower() if not Case or Case.lower() == 'lower' else Limpo.upper() if Case.lower() == 'upper' else Limpo
63
+ return Limpo
55
64
  #
56
65
  #
57
66
  def get_cmd_window(): # Captura a referencia da janela atual para retornar o foco à ela depois de chamar os pickers
@@ -4,6 +4,9 @@ import psycopg2
4
4
  from psycopg2.extras import execute_values
5
5
  import pandas as pd
6
6
  import numpy as np
7
+ from io import StringIO
8
+ import json
9
+ from pathlib import Path
7
10
 
8
11
  class postgreSQL: # Classe de acesso e interação com banco PostgreSQL
9
12
  """
@@ -153,11 +156,13 @@ conexao.close()
153
156
  raise
154
157
  #
155
158
  #
156
- def csv_df(self, CsvPath, CsvDelim=';', **kwargs): # Le arquivo CSV e gera Dataframe do Pandas
159
+ def csv_df(self, Csv, CsvDelim=';', **kwargs): # Le arquivo CSV e gera Dataframe do Pandas
157
160
  """
161
+ Parametro 'Csv' pode ser um caminho do pathlib [Path()] ou uma string com o conteúdo do CSV.
158
162
  Os parametros de read_csv() do Pandas podem ser passados opcionalmente para a função para ajustar a importação
159
163
  """
160
164
  try:
165
+ CsvPath = Csv if isinstance(Csv,Path) else StringIO(Csv)
161
166
  DataFrame = pd.read_csv(CsvPath, delimiter=CsvDelim, **kwargs) # Verifique se o delimitador é ';'
162
167
  DataFrame.replace({np.nan: None}, inplace=True) ## troca 'NaN' por None (null no postgresql)
163
168
  return DataFrame
@@ -165,22 +170,59 @@ conexao.close()
165
170
  raise
166
171
  #
167
172
  #
168
- def db_insert_df(self, DataFrame, Tabela, Schema=None, Colunas=None, OnConflict=None): # Insere os dados de um dataframe em uma tabela equivalente no banco (exige mesma estrutura de colunas)
173
+ def db_insert_df(self, DataFrame, Tabela, Schema=None, Colunas=None, OnConflict=None, TypeCast=None): # Insere os dados de um dataframe em uma tabela equivalente no banco (exige mesma estrutura de colunas)
174
+ """
175
+ Insere um DataFrame no PostgreSQL usando psycopg2 e execute_values().
176
+
177
+ Parâmetros:
178
+ - DataFrame: O DataFrame do pandas contendo os dados.
179
+ - Tabela: Nome da tabela no PostgreSQL.
180
+ - Schema: (Opcional) Schema da tabela (padrão: 'public').
181
+ - Colunas: (Opcional) Lista de colunas para inserir (padrão: todas as colunas do DataFrame).
182
+ - OnConflict: (Opcional) Cláusula ON CONFLICT para tratamento de duplicatas.
183
+ - TypeCast: (Opcional) Dicionário mapeando colunas para seus respectivos tipos de dado no PostgreSQL.
184
+ """
169
185
  # Essa função exige que os nomes dos cabeçalhos das colunas do CSV sejam os mesmos das colunas da tabela de destino
170
186
  Colunas = Colunas or DataFrame.columns.tolist() # Caso não seja fornecida a lista de colunas, assume as colunas do DataFrame
171
- Valores = [tuple(Linha) for Linha in DataFrame[Colunas].values]
172
- Schema = Schema or 'public'
187
+ Schema = Schema or 'public'
188
+ #Valores = [tuple(Linha) for Linha in DataFrame[Colunas].values] ## código anterior para definir os valores. alterado em 05/02/25 para tratar o typecast das colunas
189
+ #Definição dos valores com typecast das colunas
190
+ TypeCast = TypeCast or {}
191
+
192
+ # Converter JSON de string para objeto Python, se necessário
193
+ # for coluna, tipo in TypeCast.items():
194
+ # if tipo == "jsonb" and coluna in DataFrame.columns:
195
+ # DataFrame[coluna] = DataFrame[coluna].apply(json.loads) # Transforma string JSON em objeto Python
196
+
197
+ # Gerar valores tratados, aplicando casts se necessário
198
+ Valores = []
199
+ for _, Linha in DataFrame.iterrows():
200
+ Linha_convertida = []
201
+ for coluna in Colunas:
202
+ valor = Linha[coluna]
203
+
204
+ # Tratamento especial para JSONB
205
+ if coluna in TypeCast and TypeCast[coluna] == "jsonb":
206
+ valor = json.dumps(valor) # Converte de volta para string JSON válida
207
+ #valor += "::jsonb" # Adiciona typecast explícito
208
+
209
+ # Aplicar typecast para outros tipos de dados
210
+ elif coluna in TypeCast:
211
+ valor = f"{valor}::{TypeCast[coluna]}"
212
+
213
+ Linha_convertida.append(valor)
214
+ Valores.append(tuple(Linha_convertida))
215
+
173
216
  Query = f'insert into {Schema}.{Tabela} ({', '.join(Colunas)}) values %s '
174
- if not OnConflict is None:
175
- Query = Query + OnConflict
217
+ Query = Query + OnConflict if OnConflict else Query
176
218
 
177
219
  try:
178
220
  self.Cursor = self.Conexao.cursor()
179
221
  execute_values(self.Cursor, Query, Valores)
180
222
  self.Conexao.commit()
181
223
  except Exception as Err:
224
+ self.logger.erro(f'{__name__}: Erro [{Err}] ao inserir dados na tabela {Schema}.{Tabela}!')
182
225
  self.Conexao.rollback()
183
- raise
184
226
  finally:
185
227
  self.Cursor.close()
186
228
  #Conexao.close() ## conexão precisa ser fechada explicitamente fora da classe
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lbxtoolkit"
3
- version = "2.2.8"
3
+ version = "2.2.10"
4
4
  description = ""
5
5
  authors = ["Cristiano P. Ferrari <cristiano.ferrari@lbx.com.br>"]
6
6
  readme = "README.md"