lbxtoolkit 2.2.13__tar.gz → 2.3.0__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,23 +1,24 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: lbxtoolkit
3
- Version: 2.2.13
3
+ Version: 2.3.0
4
4
  Summary:
5
5
  Author: Cristiano P. Ferrari
6
6
  Author-email: cristiano.ferrari@lbx.com.br
7
7
  Requires-Python: >=3.12,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
- Requires-Dist: msal (>=1.30.0,<2.0.0)
11
- Requires-Dist: numpy (>=2.0.1,<3.0.0)
12
- Requires-Dist: pandas (>=2.2.2,<3.0.0)
13
- Requires-Dist: psutil (>=6.0.0,<7.0.0)
14
- Requires-Dist: psycopg2 (>=2.9.9,<3.0.0)
15
- Requires-Dist: pygetwindow (>=0.0.9,<0.0.10)
16
- Requires-Dist: pywin32 (>=306,<307)
17
- Requires-Dist: requests (>=2.32.3,<3.0.0)
18
- Requires-Dist: selenium (>=4.23.1,<5.0.0)
19
- Requires-Dist: validators (>=0.33.0,<0.34.0)
20
- Requires-Dist: webdriver-manager (>=4.0.2,<5.0.0)
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: msal (>=1.30.0)
12
+ Requires-Dist: numpy (>=2.0.1)
13
+ Requires-Dist: pandas (>=2.2.2)
14
+ Requires-Dist: psutil (>=6.0.0)
15
+ Requires-Dist: psycopg2 (>=2.9.9)
16
+ Requires-Dist: pygetwindow (>=0.0.9)
17
+ Requires-Dist: pywin32 (>=306)
18
+ Requires-Dist: requests (>=2.32.3)
19
+ Requires-Dist: selenium (>=4.23.1)
20
+ Requires-Dist: validators (>=0.33.0)
21
+ Requires-Dist: webdriver-manager (>=4.0.2)
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  <a id="lbxtoolkit"></a>
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  import re
3
4
  from pathlib import Path
4
5
  from unicodedata import normalize
@@ -83,6 +84,49 @@ Sintaxe e exemplos de uso. Parametros omissos assume-se os valores padrão indic
83
84
  user32.ShowWindow(hWnd, 3) # 3 = SW_MAXIMIZE
84
85
  #
85
86
  #
87
+ def encontra_projeto_raiz(caminho_partida):
88
+ """Sobe a árvore procurando por .venv OU pyproject.toml como raiz do projeto"""
89
+ dir_atual = os.path.abspath(caminho_partida)
90
+ while True:
91
+ if (
92
+ os.path.isdir(os.path.join(dir_atual, '.venv')) or
93
+ os.path.isfile(os.path.join(dir_atual, 'pyproject.toml'))
94
+ ):
95
+ return dir_atual
96
+ novo_dir = os.path.dirname(dir_atual)
97
+ if novo_dir == dir_atual:
98
+ return None # Chegou na raiz do sistema
99
+ dir_atual = novo_dir
100
+ #
101
+ #
102
+ def encontra_python_venv(raiz_projeto):
103
+ """Retorna o caminho do python da .venv se existir, senão None"""
104
+ if os.name == 'nt':
105
+ # Windows
106
+ python_venv = os.path.join(raiz_projeto, '.venv', 'Scripts', 'python.exe')
107
+ else:
108
+ # Linux/Mac
109
+ python_venv = os.path.join(raiz_projeto, '.venv', 'bin', 'python')
110
+ return python_venv if os.path.isfile(python_venv) else None
111
+ #
112
+ #
113
+ def nome_modulo(script_path, raiz_projeto):
114
+ """
115
+ Dado o caminho absoluto do script (ex: C:/.../src/cnab/main.py)
116
+ e da raiz do projeto (ex: C:/...),
117
+ retorna o caminho do módulo python no formato pacote.subpacote.modulo
118
+ Exemplo: src.cnab.main
119
+ """
120
+ relpath = os.path.relpath(script_path, raiz_projeto)
121
+ if relpath.endswith('.py'):
122
+ relpath = relpath[:-3] # remove .py
123
+ partes = []
124
+ for parte in relpath.replace("\\", "/").split("/"):
125
+ if parte not in ("", "__init__"):
126
+ partes.append(parte)
127
+ return ".".join(partes)
128
+ #
129
+ #
86
130
  @classmethod
87
131
  def cor(cls, text, color=None, style=None):
88
132
  """
@@ -42,7 +42,9 @@ O objetos mandatórios a serem inicilizados pelo **ConfigManager** são:
42
42
  - **servicepath** = Objeto do Path com o caminho deste script
43
43
  - **argparse_cfg** = Dicionário contendo os parametros de configuração (descrition, usage, add_help e formatter_classe) do módulo argparse (manipulado dos parametros de linha de comando)
44
44
  - **argparse_opt** = Lista de dicionários com a configuração do argumentos de entrada aceitos (short, long, action, help)
45
- - **ambiente** = Definie o ambiente de excução: Linux, Windows [console] ou Serviço [Windows]
45
+ - **ambiente** = Definie o ambiente de excução: Linux, Windows [console] ou Serviço [Windows]
46
+ - **raiz_projeto** = A raiz do projeto para fins de execução por módulo (python -m) e não diretamente pelo script. Utilize `misc.encrontra_projeto_raiz` no módulo principal para obter essa informação
47
+ - **modulo** = O nome do módulo quando o projeto for extrutura para execução por módulo. Utilize `misc.nome_modulo` no módulo principal para obter essa informação
46
48
 
47
49
  > [!NOTE]
48
50
  > Para ser funcional, é necessária que a classe local herde essa classe base e redefina (por sobrecarga) ao menos os métodos `on_run` e `args_parser()`.
@@ -148,11 +150,21 @@ class ServicerConfig(Servicer):
148
150
  ```
149
151
  """
150
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
153
+ """
154
+ variáveis abaixo são mandatórias e precisa inicializadas no ConfigManager.inicialize() do módulo principal/pai:
155
+ - log
156
+ - argparse_cfg
157
+ - argparse_opt
158
+ - ambiente
159
+ - raiz_projeto
160
+ - modulo
161
+ """
151
162
  # PRE-REQUISITOS/DEPENDÊNCIAS:
152
163
  self.log = ConfigManager.get('log')
153
164
  self.kwargs = ConfigManager.get('argparse_cfg')
154
165
  self.kwopts = ConfigManager.get('argparse_opt')
155
- self.ambiente = ConfigManager.get('ambiente')
166
+ self.ambiente = ConfigManager.get('ambiente')
167
+
156
168
  if self.log is None or not isinstance(self.log, lbx_logger):
157
169
  raise ValueError(f'Argumento "log" é mandatório e deve ser uma instância de "lbxtoolkit.lbx_logger"')
158
170
  if self.kwargs is None:
@@ -161,7 +173,7 @@ class ServicerConfig(Servicer):
161
173
  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')
162
174
  if self.ambiente is None or self.ambiente not in ['Linux', 'Windows', 'Serviço']:
163
175
  raise ValueError(f'Argumento "ambiente" é mandatório e deve ser uma string com um dos seguintes valores: [Linux, Windows, Serviço]')
164
-
176
+
165
177
  self.on_init_pre() ## método opcional a ser definito por sobrecarga na função local
166
178
 
167
179
  # Argumentos padrão obrigatórios
@@ -208,7 +220,7 @@ class ServicerConfig(Servicer):
208
220
  servicemanager.PrepareToHostSingle(ServicoWindows)
209
221
  servicemanager.StartServiceCtrlDispatcher()
210
222
  elif len(sys.argv) > 1 and sys.argv[1] == 'install':
211
- ServicoWindows.SvcInstall()
223
+ ServicoWindows.SvcInstall()
212
224
  elif len(sys.argv) > 1 and sys.argv[1] == 'remove':
213
225
  ServicoWindows.SvcRemove()
214
226
  else:
@@ -22,6 +22,20 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
22
22
  - `SvcRemove`: Remove o serviço do windows configurado à partir de `SvcInstall`
23
23
  - `SvcStop`: Para/Interrompe a execução do serviço do windows criado por este script (método herdado da classe pai _win32serviceutil.ServiceFramework_ invocado automaticamente pela chamada do service manager do windows)
24
24
  - `SvcDoRun`: Inicializa a execução do serviço do windows criado por este script (método herdado da classe pai _win32serviceutil.ServiceFramework_ invocado autoamticamente pela chamada do service manager do windwos)
25
+
26
+ A classe base possui ainda algumas dependÊncias que precisam ser inicializadas fora do contexto da classe configuradora/local.
27
+ Para isso, use classe ConfigManager.initialize(), disponível também no módulo `lbxtoolkit`.
28
+ O objetos mandatórios a serem inicilizados pelo **ConfigManager** são:
29
+ - **log** = Objeto instanciado da classe lbx_logger, para manipular as entradas de log
30
+ - **daemon** = Objeto resultante do instanciamento da própria própria classe configuradora (deve ser inicializado após o instanciamento do objeto daemon, usando `ConfigManager.set()`)
31
+ - **config** = Dicionário contento os parametros (name, display_name e description) para instalar o serviço do windows
32
+ - **servicepath** = Objeto do Path com o caminho deste script
33
+ - **argparse_cfg** = Dicionário contendo os parametros de configuração (descrition, usage, add_help e formatter_classe) do módulo argparse (manipulado dos parametros de linha de comando)
34
+ - **argparse_opt** = Lista de dicionários com a configuração do argumentos de entrada aceitos (short, long, action, help)
35
+ - **ambiente** = Definie o ambiente de excução: Linux, Windows [console] ou Serviço [Windows]
36
+ - **raiz_projeto** = A raiz do projeto para fins de execução por módulo (python -m) e não diretamente pelo script. Utilize `misc.encrontra_projeto_raiz` no módulo principal para obter essa informação
37
+ - **modulo** = O nome do módulo quando o projeto for extrutura para execução por módulo. Utilize `misc.nome_modulo` no módulo principal para obter essa informação
38
+
25
39
  """
26
40
  _svc_name_ = None
27
41
  _svc_display_name_ = None
@@ -29,7 +43,9 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
29
43
  def __init__(self, args):
30
44
  self.log = ConfigManager.get('log')
31
45
  self.daemon = ConfigManager.get('daemon')
32
- self.config = ConfigManager.get('config')
46
+ self.config = ConfigManager.get('config')
47
+ self.raiz_projeto = ConfigManager.get('raiz_projeto')
48
+ self.modulo = ConfigManager.get('modulo')
33
49
 
34
50
  if self.log is None or (not isinstance(self.log, lbx_logger) and not isinstance(self.log, SafeLogger)):
35
51
  raise ValueError(f'Argumento "log" é mandatório e deve ser uma instância de "lbxtoolkit.lbx_logger" ou "lbxtoolkit.SaffeLogger"')
@@ -39,6 +55,10 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
39
55
  raise ValueError(f'Argumento "config" é mandatório e deve ser um dicionário contendo as chaves/valores para "name", "display_name" e "description"')
40
56
  elif [chave for chave in self.config if self.config[chave] is None]:
41
57
  raise ValueError(f'Argumento "config" ({self.config}) possui as seguintes chaves com valores inválidos [None]: {[chave for chave in self.config if self.config[chave] is None]:}')
58
+ elif self.raiz_projeto is None:
59
+ raise ValueError(f'Argumento "raiz_projeto" é mandatório e deve ser obtido usando "misc.encrontra_projeto_raiz" no módulo principal')
60
+ elif self.modulo is None:
61
+ raise ValueError(f'Argumento "modulo" é mandatório e deve ser obtido usando "misc.nome_modulo" no módulo principal')
42
62
 
43
63
  self._svc_name_ = self.config['name']
44
64
  self._svc_display_name_ = self.config['display_name']
@@ -61,9 +81,10 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
61
81
 
62
82
  log = ConfigManager.get('log')
63
83
  daemon = ConfigManager.get('daemon')
64
- config = ConfigManager.get('config')
65
- service_path = ConfigManager.get('service_path') if ConfigManager.get('service_path') else os.path.abspath(sys.argv[0])
66
-
84
+ config = ConfigManager.get('config')
85
+ raiz_projeto = ConfigManager.get('raiz_projeto')
86
+ modulo = ConfigManager.get('modulo')
87
+
67
88
  if log is None or (not isinstance(log, lbx_logger) and not isinstance(log, SafeLogger)):
68
89
  raise ValueError(f'Argumento "log" é mandatório e deve ser uma instância de "lbxtoolkit.lbx_logger" ou "lbxtoolkit.SaffeLogger"')
69
90
  elif daemon is None or not hasattr(daemon, 'run') or not hasattr(daemon, 'stop'):
@@ -77,6 +98,33 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
77
98
  _svc_display_name_ = config['display_name']
78
99
  _svc_description_ = config['description']
79
100
 
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
108
+ wrapper = Path(raiz_projeto, f'wrapper_servico_{modulo}.bat') ## tem que ser igual a SvcRemove. Mudou lá, muda aqui.
109
+ wrapper_content = f"""
110
+ @echo off
111
+ REM ==================================================================================================================
112
+ REM !!!!NÃO TOQUE NESSE ARQUIVO. NÃO O APAQUE NEM O MODIFIQUE.!!!!
113
+ REM O serviço do windows [{config['display_name']}] que roda nesta máquina depende do conteúdo deste arquivo de lotes.
114
+ REM ==================================================================================================================
115
+ set PYTHONPATH=%~dp0
116
+ "%~dp0.venv\\Scripts\\python.exe" -m {modulo} %*
117
+ """
118
+ with open(wrapper, "w", encoding="utf-8") as file:
119
+ file.write(wrapper_content)
120
+ service_cmd = f'{wrapper}'
121
+
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}'
126
+
127
+
80
128
  IP = socket.gethostbyname(socket.gethostname())
81
129
  Host = socket.gethostname()
82
130
  OS = platform.system()
@@ -98,19 +146,17 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
98
146
  if delete:
99
147
  log.info(f'Excluindo serviço pre-existente [{_svc_display_name_} ({_svc_name_})] em {IP}/{Host}({Usuario}): {saida}')
100
148
  # Create the service
101
- python_path = Path(os.path.join(os.getenv('VIRTUAL_ENV'), 'Scripts', 'python.exe'))
102
- install = subprocess.run(
103
- [
104
- 'sc',
105
- 'create',
106
- _svc_name_,
107
- 'binPath=',
108
- f'{python_path} {service_path}',
109
- 'DisplayName=',
110
- _svc_display_name_,
111
- 'start=delayed-auto',
112
- ]
113
- , capture_output=True)
149
+ svc = [
150
+ 'sc',
151
+ 'create',
152
+ _svc_name_,
153
+ 'binPath=',
154
+ f'{service_cmd}',
155
+ 'DisplayName=',
156
+ _svc_display_name_,
157
+ 'start=delayed-auto',
158
+ ]
159
+ install = subprocess.run(svc, capture_output=True)
114
160
  saida = install.stdout.decode('cp850').strip()
115
161
  log.info(f'Criando novo serviço [{_svc_display_name_} ({_svc_name_})] em {IP}/{Host}({Usuario}): {saida}')
116
162
  # Set the service description
@@ -138,7 +184,9 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
138
184
 
139
185
  log = ConfigManager.get('log')
140
186
  daemon = ConfigManager.get('daemon')
141
- config = ConfigManager.get('config')
187
+ config = ConfigManager.get('config')
188
+ raiz_projeto = ConfigManager.get('raiz_projeto')
189
+ modulo = ConfigManager.get('modulo')
142
190
 
143
191
  if log is None or (not isinstance(log, lbx_logger) and not isinstance(log, SafeLogger)):
144
192
  raise ValueError(f'Argumento "log" é mandatório e deve ser uma instância de "lbxtoolkit.lbx_logger" ou "lbxtoolkit.SaffeLogger"')
@@ -172,6 +220,11 @@ Gerencia a instalação e execução de um daemon como um serviço do windows. T
172
220
  saida = delete.stdout.decode('cp850').strip()
173
221
  if delete:
174
222
  log.info(f'Excluindo serviço pre-existente [{_svc_display_name_} ({_svc_name_})] em {IP}/{Host}({Usuario}): {saida}')
223
+ if raiz_projeto and modulo: ## execução por módulo (novo)
224
+ wrapper = Path(raiz_projeto, f'wrapper_servico_{modulo}.bat') ## tem que ser igual a SvcInstall. Mudou lá, muda aqui.
225
+ if wrapper.exists():
226
+ wrapper.unlink()
227
+
175
228
  except Exception as Err:
176
229
  log.erro(f'Falha ao tentar excluir o serviço [{_svc_display_name_} ({_svc_name_})]')
177
230
  raise
@@ -0,0 +1,31 @@
1
+ [tool.poetry]
2
+ name = "lbxtoolkit"
3
+ version = "2.3.0"
4
+ description = ""
5
+ authors = ["Cristiano P. Ferrari <cristiano.ferrari@lbx.com.br>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = ">=3.12,<4.0"
10
+ psutil = ">=6.0.0"
11
+ psycopg2 = ">=2.9.9"
12
+ pandas = ">=2.2.2"
13
+ numpy = ">=2.0.1"
14
+ msal = ">=1.30.0"
15
+ requests = ">=2.32.3"
16
+ selenium = ">=4.23.1"
17
+ webdriver-manager = ">=4.0.2"
18
+ validators = ">=0.33.0"
19
+ pygetwindow = ">=0.0.9"
20
+ pywin32 = ">=306"
21
+
22
+
23
+ [tool.poetry.group.docs.dependencies]
24
+
25
+
26
+ [tool.poetry.group.dev.dependencies]
27
+ pydoc-markdown = ">=4.8.2"
28
+
29
+ [build-system]
30
+ requires = ["poetry-core"]
31
+ build-backend = "poetry.core.masonry.api"
@@ -1,31 +0,0 @@
1
- [tool.poetry]
2
- name = "lbxtoolkit"
3
- version = "2.2.13"
4
- description = ""
5
- authors = ["Cristiano P. Ferrari <cristiano.ferrari@lbx.com.br>"]
6
- readme = "README.md"
7
-
8
- [tool.poetry.dependencies]
9
- python = "^3.12"
10
- psutil = "^6.0.0"
11
- psycopg2 = "^2.9.9"
12
- pandas = "^2.2.2"
13
- numpy = "^2.0.1"
14
- msal = "^1.30.0"
15
- requests = "^2.32.3"
16
- selenium = "^4.23.1"
17
- webdriver-manager = "^4.0.2"
18
- validators = "^0.33.0"
19
- pygetwindow = "^0.0.9"
20
- pywin32 = "^306"
21
-
22
-
23
- [tool.poetry.group.docs.dependencies]
24
-
25
-
26
- [tool.poetry.group.dev.dependencies]
27
- pydoc-markdown = "^4.8.2"
28
-
29
- [build-system]
30
- requires = ["poetry-core"]
31
- build-backend = "poetry.core.masonry.api"
File without changes