Incluye: - app.ipynb con las 10 pestanas (Conexion, Descargas, Analisis, NLP, KGS, CTM, Saldos Vencidos, DataStage, Estructuras SCAII, Valores). - launcher.py + run_dev.bat para lanzar Voila localmente. - docker-compose.yml para levantar Postgres 16 con healthcheck. - schema_registro.sql con las 27 tablas (Registro501..Registro701, RegistroInci/Resumen/Sel, base_numpartes). - requirements.txt con pandas, pyodbc, psycopg2-binary, sqlalchemy, scikit-learn, openpyxl, voila, ipywidgets, python-docx. - 6 manuales de usuario (CTM, SaldosVencidos, DataStage, EstructurasSCAII, Valores, GENPACT_V2 general) en .docx. - 6 generadores de manuales para regenerar los .docx tras editar. - .gitignore que excluye .env, .venv, outputs generados y caches. - README.md con instrucciones de setup en server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
3.6 KiB
Python
122 lines
3.6 KiB
Python
"""
|
|
Launcher de la app GENPACT V2.
|
|
|
|
Arranca un servidor Voila local en background y abre el browser apuntando al app.ipynb.
|
|
Cuando se cierra la consola, el servidor de Voila se mata.
|
|
|
|
Funciona tanto desde Python directo como empaquetado con PyInstaller.
|
|
"""
|
|
import os
|
|
import sys
|
|
import time
|
|
import socket
|
|
import threading
|
|
import webbrowser
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
# ------------------------------------------------------------------
|
|
# Localizar app.ipynb y .env
|
|
# ------------------------------------------------------------------
|
|
def base_dir() -> Path:
|
|
"""Carpeta donde estan los recursos de la app.
|
|
- En modo PyInstaller (--onedir), sys._MEIPASS apunta a la carpeta temporal.
|
|
- En modo dev, es la carpeta del script.
|
|
"""
|
|
if getattr(sys, 'frozen', False):
|
|
# PyInstaller --onedir: los datos quedan al lado del .exe
|
|
return Path(sys.executable).parent
|
|
return Path(__file__).resolve().parent
|
|
|
|
|
|
BASE = base_dir()
|
|
APP_NB = BASE / 'app.ipynb'
|
|
|
|
# Si el .env esta un nivel arriba (proyecto), copialo o setea el cwd
|
|
if not (BASE / '.env').exists() and (BASE.parent / '.env').exists():
|
|
os.chdir(BASE.parent)
|
|
else:
|
|
os.chdir(BASE)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Buscar puerto libre
|
|
# ------------------------------------------------------------------
|
|
def find_free_port(preferido: int = 8866) -> int:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
try:
|
|
sock.bind(('127.0.0.1', preferido))
|
|
sock.close()
|
|
return preferido
|
|
except OSError:
|
|
sock.close()
|
|
# Si el preferido esta ocupado, deja que el SO asigne uno
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.bind(('127.0.0.1', 0))
|
|
p = s.getsockname()[1]
|
|
s.close()
|
|
return p
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Arrancar Voila
|
|
# ------------------------------------------------------------------
|
|
def main():
|
|
if not APP_NB.exists():
|
|
print(f'ERROR: no se encuentra {APP_NB}')
|
|
sys.exit(1)
|
|
|
|
port = find_free_port(8866)
|
|
# Cuando Voila apunta a un notebook especifico, lo sirve en la raiz "/"
|
|
url = f'http://127.0.0.1:{port}/'
|
|
|
|
print('=' * 60)
|
|
print(' GENPACT V2 - Sistema de Descargas SCAII')
|
|
print('=' * 60)
|
|
print(f' Iniciando servidor Voila en puerto {port}...')
|
|
print(f' URL: {url}')
|
|
print(' Cierra esta ventana para detener la app.')
|
|
print('=' * 60)
|
|
|
|
# Construir comando de Voila
|
|
if getattr(sys, 'frozen', False):
|
|
# En modo empaquetado, llamamos al binario de python integrado
|
|
cmd = [sys.executable, '-m', 'voila',
|
|
str(APP_NB),
|
|
f'--port={port}',
|
|
'--no-browser',
|
|
'--Voila.ip=127.0.0.1',
|
|
'--strip_sources=True']
|
|
else:
|
|
cmd = [sys.executable, '-m', 'voila',
|
|
str(APP_NB),
|
|
f'--port={port}',
|
|
'--no-browser',
|
|
'--Voila.ip=127.0.0.1',
|
|
'--strip_sources=True']
|
|
|
|
# Lanzar proceso
|
|
proc = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
|
|
|
|
# Abrir browser cuando el server este listo
|
|
def _open():
|
|
for _ in range(60):
|
|
try:
|
|
with socket.create_connection(('127.0.0.1', port), timeout=0.5):
|
|
break
|
|
except OSError:
|
|
time.sleep(0.5)
|
|
webbrowser.open(url)
|
|
|
|
threading.Thread(target=_open, daemon=True).start()
|
|
|
|
try:
|
|
proc.wait()
|
|
except KeyboardInterrupt:
|
|
print('\nDeteniendo Voila...')
|
|
proc.terminate()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|