chore: initial commit del deploy de utilerias-recon-2

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>
This commit is contained in:
Ernesto Herrera
2026-05-21 08:18:13 -06:00
commit a0081f54d2
21 changed files with 8509 additions and 0 deletions

121
app/launcher.py Normal file
View File

@@ -0,0 +1,121 @@
"""
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()