lbxtoolkit 2.2.9__tar.gz → 2.2.11__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.9
3
+ Version: 2.2.11
4
4
  Summary:
5
5
  Author: Cristiano P. Ferrari
6
6
  Author-email: cristiano.ferrari@lbx.com.br
@@ -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
  #
@@ -308,7 +318,7 @@ for line in filtered_lines:
308
318
  self.logger.removeHandler(handler)
309
319
  #
310
320
  #
311
- def filtra(self, log_file, dh_ini, dh_fim):
321
+ def filtra(self, log_file, dh_ini, dh_fim, logEncode='ISO-8859-1'):
312
322
  # Validar parametros de entrada
313
323
  if dh_ini:
314
324
  if not isinstance(dh_ini, datetime.datetime):
@@ -364,7 +374,7 @@ for line in filtered_lines:
364
374
  return dh_inicio <= timestamp <= dh_fim
365
375
 
366
376
  # Ler e filtrar o arquivo de log com a codificação ISO-8859-1
367
- with open(log_file_path, 'r', encoding='ISO-8859-1') as log_file:
377
+ with open(log_file_path, 'r', encoding=logEncode) as log_file:
368
378
  log_lines = log_file.readlines()
369
379
 
370
380
  # Variável para armazenar o último timestamp válido
@@ -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='keep', 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.lower() == '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,25 +170,75 @@ 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, Returning=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
- 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]
186
+ Colunas = Colunas or DataFrame.columns.tolist()
172
187
  Schema = Schema or 'public'
173
- Query = f'insert into {Schema}.{Tabela} ({', '.join(Colunas)}) values %s '
174
- if not OnConflict is None:
175
- Query = Query + OnConflict
188
+ TypeCast = TypeCast or {}
189
+
190
+ Valores = []
191
+
192
+ for _, Linha in DataFrame.iterrows():
193
+ Linha_convertida = []
194
+ for coluna in Colunas:
195
+ valor = Linha[coluna]
196
+
197
+ # 🔹 Converte Timestamp para string ISO 8601
198
+ if isinstance(valor, pd.Timestamp):
199
+ valor = valor.strftime('%Y-%m-%d %H:%M:%S')
200
+
201
+ # 🔹 Converte JSON para string válida
202
+ if coluna in TypeCast and TypeCast[coluna] == "jsonb":
203
+ if isinstance(valor, str):
204
+ try:
205
+ json.loads(valor) # Valida se já é um JSON válido
206
+ except json.JSONDecodeError:
207
+ valor = json.dumps(valor) # Serializa para JSON
208
+ else:
209
+ valor = json.dumps(valor) # Converte para JSON corretamente
210
+
211
+ Linha_convertida.append(valor)
212
+
213
+ Valores.append(tuple(Linha_convertida)) # Garante que cada linha seja uma tupla
214
+
215
+ Retorno = None
216
+
217
+ Retornar = f"RETURNING {', '.join(Returning)}" if Returning else ''
218
+ Conflito = OnConflict if OnConflict else ''
219
+
220
+ Parametros = ", ".join(["%s"] * len(Colunas))
221
+ Query = f'INSERT INTO {Schema}.{Tabela} ({", ".join(Colunas)}) VALUES ({Parametros}) {Conflito} {Retornar}'
176
222
 
177
223
  try:
178
- self.Cursor = self.Conexao.cursor()
179
- execute_values(self.Cursor, Query, Valores)
180
- self.Conexao.commit()
224
+ self.Cursor = self.Conexao.cursor()
225
+
226
+ if Returning:
227
+ Retorno = []
228
+ for valor in Valores:
229
+ self.Cursor.execute(Query, valor)
230
+ Retorno.append(self.Cursor.fetchone())
231
+ else:
232
+ self.Cursor.executemany(Query, Valores)
233
+
234
+ self.Conexao.commit()
181
235
  except Exception as Err:
236
+ self.logger.erro(f'{__name__}: Erro [{Err}] ao inserir dados na tabela {Schema}.{Tabela}!')
182
237
  self.Conexao.rollback()
183
- raise
184
238
  finally:
185
239
  self.Cursor.close()
186
240
  #Conexao.close() ## conexão precisa ser fechada explicitamente fora da classe
241
+ return Retorno if Retorno else None
187
242
  #
188
243
  #
189
244
  def db_select(self, Query): # Retorna um cursor à partir de um select
@@ -195,6 +195,46 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
195
195
  self.RunAsDaemon()
196
196
  servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
197
197
  servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
198
+
199
+
200
+ @staticmethod
201
+ def SvcStatus(ServiceName=None): # Verifica o status de um serviço específico ou do próprio serviço
202
+ """
203
+ Retorna o status do serviço do Windows.
204
+ Se um nome for passado como parâmetro, verifica o status do serviço informado.
205
+ Caso contrário, retorna o status do próprio serviço.
206
+ """
207
+ try:
208
+ # Recuperar nome do serviço, se não for passado
209
+ if not ServiceName:
210
+ config = ConfigManager.get('config')
211
+ if not config or 'name' not in config:
212
+ raise ValueError("Configuração do serviço inválida. O nome do serviço não pode ser determinado.")
213
+ ServiceName = config['name']
214
+
215
+ # Executa o comando SC QUERY para verificar o status
216
+ resultado = subprocess.run(["sc", "query", ServiceName], capture_output=True, text=True)
217
+ saida = resultado.stdout
218
+
219
+ # Buscar o status na saída do comando
220
+ if "RUNNING" in saida:
221
+ return "RUNNING"
222
+ elif "STOPPED" in saida:
223
+ return "STOPPED"
224
+ elif "PAUSED" in saida:
225
+ return "PAUSED"
226
+ elif "START_PENDING" in saida:
227
+ return "START_PENDING"
228
+ elif "STOP_PENDING" in saida:
229
+ return "STOP_PENDING"
230
+ else:
231
+ return f"UNKNOWN STATUS: {saida.strip()}"
232
+
233
+ except Exception as Err:
234
+ log = ConfigManager.get('log')
235
+ if log:
236
+ log.erro(f"Erro ao obter status do serviço [{ServiceName}]: {Err}")
237
+ return f"ERROR: {Err}"
198
238
  #
199
239
  #
200
240
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lbxtoolkit"
3
- version = "2.2.9"
3
+ version = "2.2.11"
4
4
  description = ""
5
5
  authors = ["Cristiano P. Ferrari <cristiano.ferrari@lbx.com.br>"]
6
6
  readme = "README.md"
File without changes