lbxtoolkit 2.4.3__tar.gz → 2.4.5__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: lbxtoolkit
3
- Version: 2.4.3
3
+ Version: 2.4.5
4
4
  Summary: Caixa ferramentas da LBX para automações de processos (RPA), integrações, utilitários Windows e componentes reutilizáveis.
5
5
  Author: Cristiano P. Ferrari
6
6
  Author-email: cristiano.ferrari@lbx.com.br
@@ -14,8 +14,8 @@ Requires-Dist: numpy (>=2.0.1)
14
14
  Requires-Dist: pandas (>=2.2.2)
15
15
  Requires-Dist: psutil (>=6.0.0)
16
16
  Requires-Dist: psycopg2 (>=2.9.9)
17
- Requires-Dist: pygetwindow (>=0.0.9)
18
- Requires-Dist: pywin32 (>=306)
17
+ Requires-Dist: pygetwindow (>=0.0.9) ; sys_platform == "win32"
18
+ Requires-Dist: pywin32 (>=306) ; sys_platform == "win32"
19
19
  Requires-Dist: requests (>=2.32.3)
20
20
  Requires-Dist: selenium (>=4.23.1)
21
21
  Requires-Dist: validators (>=0.33.0)
@@ -265,8 +265,9 @@ Para uso em APIs com autenticação JWT (JSON Web Token), OAuth, Bearer Token Au
265
265
  MsgErro = Retorno
266
266
  except:
267
267
  MsgErro = Retorno.replace(r'\n\t',' ')
268
- finally:
269
- return MsgErro
268
+ # antes: "finally: return MsgErro" — mesmo efeito, sem o
269
+ # SyntaxWarning do Python 3.14 (PEP 765: return em finally)
270
+ return MsgErro
270
271
  else:
271
272
  return Retorno
272
273
  #
@@ -1,13 +1,24 @@
1
1
  import ctypes
2
2
  import os
3
3
  import re
4
- import tkinter as tk
5
4
  from pathlib import Path
6
- from tkinter import filedialog
7
5
  from typing import Any, Optional
8
6
  from unicodedata import normalize
9
7
 
10
- import pygetwindow as gw
8
+ # Dependências gráficas/Windows são opcionais: em Linux headless (VPS) o
9
+ # tkinter pode não existir e o pygetwindow lança NotImplementedError no import.
10
+ # Os métodos que dependem delas verificam a disponibilidade em tempo de execução.
11
+ try:
12
+ import tkinter as tk
13
+ from tkinter import filedialog
14
+ except ImportError: # pragma: no cover - depende da plataforma
15
+ tk = None # type: ignore[assignment]
16
+ filedialog = None # type: ignore[assignment]
17
+
18
+ try:
19
+ import pygetwindow as gw
20
+ except (ImportError, NotImplementedError): # pragma: no cover - depende da plataforma
21
+ gw = None # type: ignore[assignment]
11
22
 
12
23
 
13
24
  class misc:
@@ -55,6 +66,9 @@ class misc:
55
66
  if TiposArquivo is None:
56
67
  TiposArquivo = [("Todos os arquivos", "*.*")]
57
68
 
69
+ if tk is None or filedialog is None:
70
+ raise RuntimeError("seleciona_arquivo indisponível: tkinter não está instalado neste ambiente")
71
+
58
72
  root = tk.Tk()
59
73
  root.withdraw()
60
74
 
@@ -83,6 +97,9 @@ class misc:
83
97
  Caminho do diretório selecionado como `Path`.
84
98
  Se o usuário cancelar, retorna `Path('.')`.
85
99
  """
100
+ if tk is None or filedialog is None:
101
+ raise RuntimeError("seleciona_dir indisponível: tkinter não está instalado neste ambiente")
102
+
86
103
  root = tk.Tk()
87
104
  root.withdraw()
88
105
 
@@ -167,8 +184,12 @@ class misc:
167
184
 
168
185
  Returns:
169
186
  Objeto de janela retornado por `pygetwindow`, ou `None`
170
- se nenhuma janela adequada for encontrada.
187
+ se nenhuma janela adequada for encontrada ou se a plataforma
188
+ não suportar `pygetwindow` (ex.: Linux).
171
189
  """
190
+ if gw is None:
191
+ return None
192
+
172
193
  windows = gw.getWindowsWithTitle("")
173
194
  for window in windows:
174
195
  if window.title and window.visible and window.topleft:
@@ -181,8 +202,12 @@ class misc:
181
202
  """
182
203
  Maximiza a janela atual do console no Windows.
183
204
 
184
- Não faz nada se o handle da janela não puder ser obtido.
205
+ Não faz nada se o handle da janela não puder ser obtido
206
+ ou se a plataforma não for Windows.
185
207
  """
208
+ if os.name != "nt":
209
+ return
210
+
186
211
  kernel32 = ctypes.WinDLL("kernel32")
187
212
  user32 = ctypes.WinDLL("user32")
188
213
  hWnd = kernel32.GetConsoleWindow()
@@ -487,4 +512,4 @@ class misc:
487
512
  try:
488
513
  return int(str(valor).strip())
489
514
  except ValueError as exc:
490
- raise RuntimeError(f"Variável de ambiente {nome} deve ser numérica.") from exc
515
+ raise RuntimeError(f"Variável de ambiente {nome} deve ser numérica.") from exc
@@ -8,8 +8,12 @@ from pathlib import Path
8
8
  import platform
9
9
  import socket
10
10
  import argparse
11
- import servicemanager #type: ignore
12
- import win32serviceutil #type: ignore
11
+ try:
12
+ import servicemanager #type: ignore
13
+ import win32serviceutil #type: ignore
14
+ except ImportError: # pragma: no cover - fora do Windows
15
+ servicemanager = None # type: ignore[assignment]
16
+ win32serviceutil = None # type: ignore[assignment]
13
17
 
14
18
  class Servicer(): # Cria um daemon para rodar como serviço
15
19
  """
@@ -62,8 +66,8 @@ from pathlib import Path
62
66
  from lbxtoolkit import Servicer, ServicoWindows, lbx_logger
63
67
 
64
68
  class ServicerConfig(Servicer):
65
- def __init__(self, Log=None, piddir=None): ## Log e piddir são parametros mandatórios. outros parametros posicionais ou nominais (*args e **kwargs) pode ser adicionados aqui para serem utilizados pelos métodos locais
66
- super().__init__(Log=Log, piddir=piddir) ## provoca a inicialização da classe pai
69
+ def __init__(self, Log=None, piddir=None, NewModuleMode=False): ## Log e piddir são parametros mandatórios. outros parametros posicionais ou nominais (*args e **kwargs) pode ser adicionados aqui para serem utilizados pelos métodos locais
70
+ super().__init__(Log=Log, piddir=piddir, NewModuleMode=NewModuleMode) ## provoca a inicialização da classe pai
67
71
  ## complete o código de instanciamento da classe aqui, se necessário
68
72
  ## os métodos on_init(), on_init_pre() e on_init_pos()
69
73
  ## SOBRECARGA DOS MÉTODOS DA CLASSE PAI
@@ -141,7 +145,8 @@ class ServicerConfig(Servicer):
141
145
  argparse_opt=ArgparseOpt,
142
146
  ambiente=Ambiente
143
147
  )
144
- Daemon = ServicerConfig(DirRetorno, Log=LogDaemon, piddir=DirPIDDaemons)
148
+ NewModuleMode = misc.to_bool(os.getenv('NEW_MODULE_MODE'), False)
149
+ Daemon = ServicerConfig(DirRetorno, Log=LogDaemon, piddir=DirPIDDaemons, NewModuleMode=NewModuleMode)
145
150
  ConfigManager.set('daemon',Daemon)
146
151
 
147
152
  if __name__ == "__main__":
@@ -149,7 +154,7 @@ class ServicerConfig(Servicer):
149
154
  Daemon.main()
150
155
  ```
151
156
  """
152
- def __init__(self, Log=None, piddir=None):#TODO: ao criar uma classe padrão usar args/kwargs para lidar como parametros variáveis no instanciamento
157
+ def __init__(self, Log=None, piddir=None, NewModuleMode: bool = False):#TODO: ao criar uma classe padrão usar args/kwargs para lidar como parametros variáveis no instanciamento
153
158
  """
154
159
  variáveis abaixo são mandatórias e precisa inicializadas no ConfigManager.inicialize() do módulo principal/pai:
155
160
  - log
@@ -173,6 +178,10 @@ class ServicerConfig(Servicer):
173
178
  raise ValueError(f'Argumento "argparse_opt" é mandatório e deve ser uma lista de dicionários ao mínimo as chaves: [short, long, action, help] para tratamento dos argumentos recebidos da linha de comando')
174
179
  if self.ambiente is None or self.ambiente not in ['Linux', 'Windows', 'Serviço']:
175
180
  raise ValueError(f'Argumento "ambiente" é mandatório e deve ser uma string com um dos seguintes valores: [Linux, Windows, Serviço]')
181
+ if not isinstance(NewModuleMode, bool):
182
+ raise ValueError(f'Argumento "NewModuleMode" deve ser booleano [True ou False]')
183
+
184
+ self.NewModuleMode = NewModuleMode
176
185
 
177
186
  self.on_init_pre() ## método opcional a ser definito por sobrecarga na função local
178
187
 
@@ -211,6 +220,10 @@ class ServicerConfig(Servicer):
211
220
  elif [chave for chave in config if config[chave] is None]:
212
221
  raise ValueError(f'Argumento "config" ({config}) possui as seguintes chaves com valores inválidos [None]: {[chave for chave in config if config[chave] is None]:}')
213
222
 
223
+ _cmd_servico = len(sys.argv) > 1 and sys.argv[1] in ['install', 'remove', 'start', 'stop', 'restart', 'debug']
224
+ if (servicemanager is None or win32serviceutil is None) and (_cmd_servico or self.ambiente == 'Serviço'):
225
+ raise RuntimeError('Gerenciamento de serviço Windows indisponível nesta plataforma (pywin32 ausente); no Linux use systemd')
226
+
214
227
  ServicoWindows._svc_name_ = config['name']
215
228
  ServicoWindows._svc_display_name_ = config['display_name']
216
229
  ServicoWindows._svc_description_ = config['description']
@@ -7,12 +7,38 @@ from pathlib import Path
7
7
  import subprocess
8
8
  import platform
9
9
  import socket
10
- import servicemanager
11
- import win32serviceutil
12
- import win32service
13
- import win32event
10
+ # pywin32 só existe no Windows. Em outras plataformas o módulo continua
11
+ # importável (o __init__ do pacote o importa sempre), mas a classe não pode
12
+ # ser instanciada — no Linux o equivalente é uma unit do systemd.
13
+ try:
14
+ import servicemanager
15
+ import win32serviceutil
16
+ import win32service
17
+ import win32event
18
+ _BaseServico = win32serviceutil.ServiceFramework
19
+ PYWIN32_DISPONIVEL = True
20
+ except ImportError: # pragma: no cover - depende da plataforma
21
+ servicemanager = None # type: ignore[assignment]
22
+ win32serviceutil = None # type: ignore[assignment]
23
+ win32service = None # type: ignore[assignment]
24
+ win32event = None # type: ignore[assignment]
25
+ PYWIN32_DISPONIVEL = False
14
26
 
15
- class ServicoWindows(win32serviceutil.ServiceFramework): # Gerencia a execução/instalação como serviço do windows
27
+ class _BaseServico: # type: ignore[no-redef]
28
+ """
29
+ Base substituta usada quando o pywin32 não está disponível.
30
+
31
+ Permite importar o pacote fora do Windows; qualquer tentativa de
32
+ instanciar o serviço falha com mensagem explícita.
33
+
34
+ Example:
35
+ ServicoWindows(args) # RuntimeError fora do Windows
36
+ """
37
+
38
+ def __init__(self, *args, **kwargs) -> None:
39
+ raise RuntimeError("ServicoWindows requer pywin32 (somente Windows); no Linux use systemd")
40
+
41
+ class ServicoWindows(_BaseServico): # type: ignore[misc, valid-type] # Gerencia a execução/instalação como serviço do windows
16
42
  """
17
43
  #### Classe **ServicoWindows**
18
44
 
@@ -41,6 +67,8 @@ O objetos mandatórios a serem inicilizados pelo **ConfigManager** são:
41
67
  _svc_display_name_ = None
42
68
  _svc_description_ = None
43
69
  def __init__(self, args):
70
+ if not PYWIN32_DISPONIVEL:
71
+ raise RuntimeError("ServicoWindows requer pywin32 (somente Windows); no Linux use systemd")
44
72
  self.log = ConfigManager.get('log')
45
73
  self.daemon = ConfigManager.get('daemon')
46
74
  self.config = ConfigManager.get('config')
@@ -98,13 +126,18 @@ O objetos mandatórios a serem inicilizados pelo **ConfigManager** são:
98
126
  _svc_display_name_ = config['display_name']
99
127
  _svc_description_ = config['description']
100
128
 
101
- ## para legado. suportar tanto a criação de execução direta do .py (originalmente) como por módulo
102
- ## a lógica é que raiz_projeto e modulo só sejam definidos se o projeto foi estrutura para execução por módulo/entrypoint (pythom -m ou poetry run).
103
- # neste caso, é esperado que service_path e python_path sejam nulos
104
- ## por outro lado, se a execução for diretamente pelo script (python script.py), é esperado que raiz_projeto e modulo sejam nulos. se service_path e python_path forem nulos também, serão definidos arbitrariamente
105
- if raiz_projeto and modulo: ## execução por módulo (novo)
106
- service_path = None
107
- python_path = None
129
+ NewModuleMode = getattr(daemon, 'NewModuleMode', False)
130
+
131
+ if NewModuleMode:
132
+ ## Novo padrão de execução por módulo.
133
+ ## Usa exatamente o interpretador que executou o comando "install",
134
+ ## independentemente de onde o ambiente virtual esteja armazenado.
135
+ python_path = Path(sys.executable).resolve()
136
+ if not python_path.is_file():
137
+ raise FileNotFoundError(
138
+ f'Interpretador Python não encontrado: {python_path}'
139
+ )
140
+
108
141
  wrapper = Path(raiz_projeto, f'wrapper_servico_{modulo}.bat') ## tem que ser igual a SvcRemove. Mudou lá, muda aqui.
109
142
  wrapper_content = f"""
110
143
  @echo off
@@ -113,16 +146,39 @@ REM !!!!NÃO TOQUE NESSE ARQUIVO. NÃO O APAQUE NEM O MODIFIQUE.!!!!
113
146
  REM O serviço do windows [{config['display_name']}] que roda nesta máquina depende do conteúdo deste arquivo de lotes.
114
147
  REM ==================================================================================================================
115
148
  set PYTHONPATH=%~dp0
116
- "%~dp0.venv\\Scripts\\python.exe" -m {modulo} %*
117
- """
149
+ "{python_path}" -m {modulo} %*
150
+ """
118
151
  with open(wrapper, "w", encoding="utf-8") as file:
119
152
  file.write(wrapper_content)
120
153
  service_cmd = f'{wrapper}'
121
154
 
122
- else: ## execução direta do script (legado)
123
- service_path = ConfigManager.get('service_path') if ConfigManager.get('service_path') else os.path.abspath(__file__)
124
- python_path = ConfigManager.get('python_path') if ConfigManager.get('python_path') else os.path.join(os.getenv('VIRTUAL_ENV'), 'Scripts', 'python.exe') if os.getenv('VIRTUAL_ENV') else sys.executable
125
- service_cmd = f'{python_path} {service_path}'
155
+ else:
156
+ ## Comportamento anterior preservado integralmente para compatibilidade com o legado.
157
+ ## Suporta tanto a criação de execução direta do .py como por módulo.
158
+ ## A lógica é que raiz_projeto e modulo só sejam definidos se o projeto foi estruturado
159
+ ## para execução por módulo/entrypoint (python -m ou poetry run).
160
+ if raiz_projeto and modulo: ## execução por módulo no padrão anterior
161
+ service_path = None
162
+ python_path = None
163
+ wrapper = Path(raiz_projeto, f'wrapper_servico_{modulo}.bat') ## tem que ser igual a SvcRemove. Mudou lá, muda aqui.
164
+ wrapper_content = f"""
165
+ @echo off
166
+ REM ==================================================================================================================
167
+ REM !!!!NÃO TOQUE NESSE ARQUIVO. NÃO O APAQUE NEM O MODIFIQUE.!!!!
168
+ REM O serviço do windows [{config['display_name']}] que roda nesta máquina depende do conteúdo deste arquivo de lotes.
169
+ REM ==================================================================================================================
170
+ set PYTHONPATH=%~dp0
171
+ "%~dp0.venv\\Scripts\\python.exe" -m {modulo} %*
172
+ """
173
+ with open(wrapper, "w", encoding="utf-8") as file:
174
+ file.write(wrapper_content)
175
+ service_cmd = f'{wrapper}'
176
+
177
+ else: ## execução direta do script (legado)
178
+ service_path = ConfigManager.get('service_path') if ConfigManager.get('service_path') else os.path.abspath(__file__)
179
+ python_path = ConfigManager.get('python_path') if ConfigManager.get('python_path') else os.path.join(os.getenv('VIRTUAL_ENV'), 'Scripts', 'python.exe') if os.getenv('VIRTUAL_ENV') else sys.executable
180
+ service_cmd = f'{python_path} {service_path}'
181
+
126
182
 
127
183
 
128
184
  IP = socket.gethostbyname(socket.gethostname())
@@ -329,4 +385,4 @@ set PYTHONPATH=%~dp0
329
385
  finally:
330
386
  log.info('daemon encerrado!')
331
387
  #
332
- #
388
+ #
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lbxtoolkit"
3
- version = "2.4.3"
3
+ version = "2.4.5"
4
4
  description = "Caixa ferramentas da LBX para automações de processos (RPA), integrações, utilitários Windows e componentes reutilizáveis."
5
5
  authors = ["Cristiano P. Ferrari <cristiano.ferrari@lbx.com.br>"]
6
6
  readme = "README.md"
@@ -16,8 +16,8 @@ requests = ">=2.32.3"
16
16
  selenium = ">=4.23.1"
17
17
  webdriver-manager = ">=4.0.2"
18
18
  validators = ">=0.33.0"
19
- pygetwindow = ">=0.0.9"
20
- pywin32 = ">=306"
19
+ pygetwindow = { version = ">=0.0.9", markers = "sys_platform == 'win32'" }
20
+ pywin32 = { version = ">=306", markers = "sys_platform == 'win32'" }
21
21
 
22
22
 
23
23
  [tool.poetry.group.docs.dependencies]
File without changes