creación de scheduler.json para ejecución de servicio

This commit is contained in:
George committed 2026-08-04 09:34:44 -05:00
1 parent 839aad698d
commit 1c39f6f45f
11 files changed
+104 -392

No files matched your search

+8 -7
View File
@@ -12,6 +12,7 @@ from datetime import datetime
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ============================================================================
class TokenExpiredError(Exception):
pass
@@ -29,7 +30,7 @@ TIPO_COMPROBANTE = {
TIPO_DESCARGA = {"01": "PDF", "02": "XML", "03": "CDR"}
# ============================================================================
def load_token():
if not TOKEN_FILE.exists():
print(f"❌ No se encontró {TOKEN_FILE}")
@@ -49,11 +50,11 @@ def load_token():
print(f"🔑 Token válido por {int(remaining // 60)} min {int(remaining % 60)} seg")
return token
# ============================================================================
def cpe_id(ruc, tipo, serie, numero):
return f"{ruc}-{tipo}-{serie}-{numero}-2"
# ============================================================================
def get_headers(token):
return {
"Authorization": f"Bearer {token}",
@@ -63,7 +64,7 @@ def get_headers(token):
"Referer": "https://e-factura.sunat.gob.pe/",
}
# ============================================================================
def consultar_cpe(token, ruc_emisor, tipo, serie, numero, descargar=True, verbose=True):
cid = cpe_id(ruc_emisor, tipo, serie, numero)
url = f"{API_HOST}{BASE_PATH}/{cid}"
@@ -125,7 +126,7 @@ def consultar_cpe(token, ruc_emisor, tipo, serie, numero, descargar=True, verbos
return None, r.status_code
# ============================================================================
def descargar_archivo(token, cid, tipo, ts, verbose=True):
label = TIPO_DESCARGA.get(tipo, tipo)
url = f"{API_HOST}{BASE_PATH}/{cid}/{tipo}"
@@ -219,7 +220,7 @@ def descargar_archivo(token, cid, tipo, ts, verbose=True):
return False
# ============================================================================
def main():
print("=" * 60)
print("🔍 CONSULTA CPE - SUNAT API")
@@ -239,6 +240,6 @@ def main():
consultar_cpe(token, args.ruc, args.tipo, args.serie, args.numero, descargar=not args.no_download)
# ============================================================================
if __name__ == "__main__":
main()
+9 -8
View File
@@ -3,10 +3,10 @@ from pathlib import Path
from datetime import datetime, timedelta
DB_PATH = Path(__file__).parent / "fallas_descarga.db"
# ============================================================================
def _conn():
return sqlite3.connect(str(DB_PATH))
# ============================================================================
def init_db():
with _conn() as conn:
conn.execute("""
@@ -22,7 +22,7 @@ def init_db():
fecha_ultimo TEXT NOT NULL
)
""")
# ============================================================================
def _calcular_proximo_intento(intentos):
if intentos >= 20:
return None
@@ -43,7 +43,7 @@ def _calcular_proximo_intento(intentos):
else:
delta = timedelta(hours=12)
return (datetime.now() + delta).isoformat()
# ============================================================================
def registrar_falla(docentry, ruc, tipo_cpe, serie, numero, error, intentos=None):
ahora = datetime.now().isoformat()
if intentos is None:
@@ -59,11 +59,11 @@ def registrar_falla(docentry, ruc, tipo_cpe, serie, numero, error, intentos=None
(docentry, ruc, tipo, serie, numero, error, intentos, proximo_intento, fecha_ultimo)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (docentry, ruc, tipo_cpe, serie, numero, str(error)[:200], intentos, proximo, ahora))
# ============================================================================
def eliminar_falla(docentry):
with _conn() as conn:
conn.execute("DELETE FROM fallas WHERE docentry = ?", (docentry,))
# ============================================================================
def obtener_fallas_pendientes() -> list[dict]:
ahora = datetime.now().isoformat()
with _conn() as conn:
@@ -75,12 +75,13 @@ def obtener_fallas_pendientes() -> list[dict]:
ORDER BY intentos ASC, fecha_ultimo ASC
""", (ahora,))
return [dict(row) for row in cursor.fetchall()]
# ============================================================================
def obtener_docentries_falla() -> set[int]:
with _conn() as conn:
cursor = conn.execute("SELECT docentry FROM fallas")
return {row[0] for row in cursor.fetchall()}
# ============================================================================
def total_fallas() -> int:
with _conn() as conn:
return conn.execute("SELECT COUNT(*) FROM fallas").fetchone()[0]
# ============================================================================
+13 -11
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
# ============================================================================
load_dotenv()
SOL_URL = "https://www.sunat.gob.pe/sol.html"
@@ -17,13 +18,13 @@ RUC = os.getenv("SUNAT_RUC", "").strip()
USER = os.getenv("SUNAT_USER", "").strip()
PASSWORD = os.getenv("SUNAT_PASSWORD", "").strip()
# ============================================================================
def save_screenshot(page, name):
path = SCREENSHOT_DIR / f"{name}.png"
page.screenshot(path=str(path))
print(f" 📸 Screenshot guardado: {path}")
# ============================================================================
def get_token(headless=False, timeout=60000):
print("🚀 Iniciando navegador...")
with sync_playwright() as p:
@@ -109,7 +110,7 @@ def get_token(headless=False, timeout=60000):
browser.close()
return None
# ============================================================================
def fill_login_form(page):
page.wait_for_timeout(1000)
@@ -143,7 +144,7 @@ def fill_login_form(page):
else:
print(" ✅ Captcha no detectado")
# ============================================================================
def get_token_from_direct_access(context, login_page):
print(" Buscando enlace con token en el menú SOL...")
token = extract_token_from_menu(login_page)
@@ -154,7 +155,7 @@ def get_token_from_direct_access(context, login_page):
print(" Navegando árbol de menú SOL...")
return navigate_sol_menu(login_page)
# ============================================================================
def navigate_sol_menu(page):
page.wait_for_timeout(2000)
@@ -227,7 +228,7 @@ def navigate_sol_menu(page):
return None
# ============================================================================
def click_menu_item_by_id(page, element_id, expected_text):
print(f" Buscando elemento: #{element_id} ('{expected_text}')...")
@@ -271,7 +272,7 @@ def click_menu_item_by_id(page, element_id, expected_text):
print(f" ❌ No se pudo encontrar: {expected_text}")
return None
# ============================================================================
def check_for_token(page):
urls = [page.url] + [f.url for f in page.frames]
for url in urls:
@@ -348,7 +349,7 @@ def check_for_token(page):
return None
# ============================================================================
def extract_token_from_menu(page):
print(" Analizando estructura del menú SOL...")
@@ -399,7 +400,7 @@ def extract_token_from_menu(page):
return None
# ============================================================================
def extract_token_from_url(url):
if "token=" in url:
token = url.split("token=")[-1]
@@ -409,7 +410,7 @@ def extract_token_from_url(url):
return token
return None
# ============================================================================
def save_token(token):
try:
payload_b64 = token.split(".")[1]
@@ -434,7 +435,7 @@ def save_token(token):
print(f" 💾 Token guardado en: {TOKEN_FILE}")
print(f" ⏰ Expira: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(data['expires_at']))}")
# ============================================================================
if __name__ == "__main__":
print("=" * 60)
print("🔐 OBTENCIÓN DE TOKEN SUNAT")
@@ -447,3 +448,4 @@ if __name__ == "__main__":
else:
print(f"\n❌ No se pudo obtener el token.")
print(" Revisa las capturas en la carpeta screenshots/")
# ============================================================================
+5 -4
View File
@@ -10,14 +10,14 @@ from get_token import (
fill_login_form, save_token, save_screenshot,
check_for_token, navigate_sol_menu, extract_token_from_menu,
)
# ============================================================================
USER_AGENT = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
# ============================================================================
def chrome_instalado():
sistema = platform.system().lower()
if sistema == "windows":
@@ -33,7 +33,7 @@ def chrome_instalado():
return bool(shutil.which("google-chrome") or shutil.which("google-chrome-stable"))
return False
# ============================================================================
def get_token_headless(timeout=90000):
with sync_playwright() as p:
browser = p.chromium.launch(
@@ -94,6 +94,7 @@ def get_token_headless(timeout=90000):
print(" ❌ No se encontró token")
return None
# ============================================================================
if __name__ == "__main__":
get_token_headless()
# ============================================================================
+8 -6
View File
@@ -3,6 +3,7 @@ import json
from pathlib import Path
from dotenv import load_dotenv
# ============================================================================
load_dotenv()
HANA_HOST = os.getenv("HANA_HOST", "")
@@ -13,7 +14,7 @@ HANA_SCHEMA = os.getenv("HANA_SCHEMA", "")
_conn = None
# ============================================================================
def get_connection():
global _conn
if _conn is None:
@@ -30,7 +31,7 @@ def get_connection():
return None
return _conn
# ============================================================================
def obtener_pendientes() -> list[dict]:
conn = get_connection()
@@ -70,7 +71,7 @@ def obtener_pendientes() -> list[dict]:
print(f" ❌ Error en query HANA: {e}")
return []
# ============================================================================
def marcar_pdf_descargado(docentry: int, url: str, ruta: str):
conn = get_connection()
if not conn:
@@ -89,7 +90,7 @@ def marcar_pdf_descargado(docentry: int, url: str, ruta: str):
print(f" ❌ Error actualizando PDF HANA: {e}")
return False
# ============================================================================
def marcar_xml_descargado(docentry: int, url: str, ruta: str):
conn = get_connection()
if not conn:
@@ -108,7 +109,7 @@ def marcar_xml_descargado(docentry: int, url: str, ruta: str):
print(f" ❌ Error actualizando XML HANA: {e}")
return False
# ============================================================================
def cerrar_conexion():
global _conn
if _conn:
@@ -118,7 +119,7 @@ def cerrar_conexion():
pass
_conn = None
# ============================================================================
if __name__ == "__main__":
print("=" * 60)
print("📋 HANA MANAGER - Prueba")
@@ -132,3 +133,4 @@ if __name__ == "__main__":
f"PDF={d['tienePDF']} XML={d['tieneXML']}")
if len(docs) > 5:
print(f" ... y {len(docs) - 5} más")
# ============================================================================
+9 -7
View File
@@ -8,6 +8,7 @@ from dotenv import load_dotenv
import requests
import urllib3
# ============================================================================
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
load_dotenv()
@@ -21,11 +22,11 @@ USER = os.getenv("SUNAT_USER", "").strip()
PASSWORD = os.getenv("SUNAT_PASSWORD", "").strip()
J_USERNAME = USER
# ============================================================================
def log(msg):
print(f" {msg}")
# ============================================================================
def decode_jwt(token):
try:
payload_b64 = token.split(".")[1]
@@ -36,7 +37,7 @@ def decode_jwt(token):
except:
return None
# ============================================================================
def save_token(token):
payload = decode_jwt(token) or {}
expires_at = payload.get("exp", 0)
@@ -56,7 +57,7 @@ def save_token(token):
print(f" Usuario SOL: {data['usuario_sol']}")
return True
# ============================================================================
def try_password_grant():
print("\n🔑 Intentando grant_type=password...")
data = {
@@ -88,7 +89,7 @@ def try_password_grant():
print(f" ❌ Error: {e}")
return False
# ============================================================================
def try_auth_code_flow():
print("\n🔑 Intentando flujo authorization_code...")
log("1. Accediendo a MenuInternet.htm para obtener redirect...")
@@ -275,7 +276,7 @@ def try_auth_code_flow():
return False
# ============================================================================
def main():
print("=" * 60)
print("🔐 LOGIN SUNAT API (sin Playwright)")
@@ -302,6 +303,7 @@ def main():
print(" - Cambios en el flujo OAuth2 de SUNAT")
print("\n Usa get_token.py como respaldo: python get_token.py")
# ============================================================================
if __name__ == "__main__":
main()
# ============================================================================
+26 -11
View File
@@ -9,6 +9,7 @@ from pathlib import Path
from dotenv import load_dotenv
import urllib3
# ============================================================================
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
load_dotenv()
@@ -24,8 +25,22 @@ from consulta_cpe import (
)
# ============================================================================
# CONFIGURACIÓN DE HORARIOS
# CONFIGURACIÓN DE HORARIOS (desde scheduler.json)
# ============================================================================
SCHEDULER_FILE = Path(__file__).parent / "scheduler.json"
def _cargar_scheduler():
with open(SCHEDULER_FILE, encoding="utf-8") as f:
data = json.load(f)
horarios = [tuple(h) for h in data["HORARIOS"]]
return data["DIAS_ACTIVOS"], horarios
try:
DIAS_ACTIVOS, HORARIOS = _cargar_scheduler()
except Exception as e:
print(f"⚠️ Error cargando {SCHEDULER_FILE}: {e}. Usando defaults.")
# (hora_inicio, min_inicio, hora_fin, min_fin, intervalo_segundos, modo)
HORARIOS = [
(6, 30, 13, 15, 300, "rapido"), # 06:30 - 13:15 -> cada 5 minutos
@@ -33,7 +48,6 @@ HORARIOS = [
(14, 0, 17, 30, 300, "rapido"), # 14:00 - 17:30 -> cada 5 minutos
(17, 30, 21, 30, 3600, "full"), # 17:30 - 21:30 -> cada 1 hora
]
# 0=lun, 1=mar, 2=mié, 3=jue, 4=vie, 5=sáb, 6=dom
DIAS_ACTIVOS = [0, 1, 2, 3, 4]
@@ -45,14 +59,14 @@ color_amarillo = "\033[93m"
color_rojo = "\033[91m"
color_fin = "\033[0m"
# ============================================================================
def ic(ok, texto_ok, texto_fail):
"""Icono + texto según estado."""
icono = "✅" if ok else "⚠️"
color = color_verde if ok else color_amarillo
return f"{icono} {color}{texto_ok if ok else texto_fail}{color_fin}"
# ============================================================================
def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
minutos = dt.hour * 60 + dt.minute
for hi, mi, hf, mf, intervalo, modo in HORARIOS:
@@ -62,14 +76,14 @@ def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
return (intervalo, modo)
return None
# ============================================================================
def segundos_hasta_proximo_inicio(dt: datetime) -> int:
target = dt.replace(hour=6, minute=30, second=0, microsecond=0)
if dt >= target:
target += timedelta(days=1)
return int((target - dt).total_seconds())
# ============================================================================
def segundos_hasta_proximo_dia_activo(dt: datetime) -> int:
for dias in range(1, 8):
prox = dt + timedelta(days=dias)
@@ -78,7 +92,7 @@ def segundos_hasta_proximo_dia_activo(dt: datetime) -> int:
return int((target - dt).total_seconds())
return 86400
# ============================================================================
def obtener_token():
"""Obtiene o renueva el token si es necesario."""
token = load_token()
@@ -111,7 +125,7 @@ def obtener_token():
print(" ⚠️ No se detectó token tras 30s. Continuando...")
return load_token()
# ============================================================================
def procesar_lote(token):
"""Procesa documentos pendientes desde HANA."""
print(f"\n📋 Buscando documentos pendientes en HANA...")
@@ -265,13 +279,13 @@ def procesar_lote(token):
for idx, doc in enumerate(docs_retry):
_procesar(doc, idx, len(docs_retry), es_retry=True)
# ============================================================================
def signal_handler(signum, frame):
global _running
print("\n\n⏹️ Señal de parada recibida. Deteniendo...")
_running = False
# ============================================================================
def main():
global _running
signal.signal(signal.SIGINT, signal_handler)
@@ -360,6 +374,7 @@ def main():
cerrar_conexion()
print("\n👋 Daemon detenido.")
# ============================================================================
if __name__ == "__main__":
main()
# ============================================================================
+10 -8
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from datetime import datetime
from collections import Counter
# ============================================================================
try:
import matplotlib
matplotlib.use("Agg")
@@ -24,14 +25,14 @@ COLORS = {
}
CMAP = list(COLORS.values())
# ============================================================================
def _query(sql, params=()):
with sqlite3.connect(str(DB_PATH)) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
# ============================================================================
def reporte_texto():
print("\n" + "=" * 60)
print("📊 REPORTE DE FALLAS - SUNAT")
@@ -61,11 +62,11 @@ def reporte_texto():
for r in _query("SELECT DATE(fecha_ultimo) AS dia, COUNT(*) AS c FROM fallas GROUP BY dia ORDER BY dia"):
print(f" {r['dia']} → {r['c']} falla(s)")
# ============================================================================
def _color_error(error):
return COLORS.get(error, "#95a5a6")
# ============================================================================
def grafico_barras_errores():
data = _query("SELECT COUNT(*) AS c, error FROM fallas GROUP BY error ORDER BY c DESC")
if not data:
@@ -90,7 +91,7 @@ def grafico_barras_errores():
plt.close(fig)
return path
# ============================================================================
def grafico_barras_ruc():
data = _query("SELECT COUNT(*) AS c, ruc FROM fallas GROUP BY ruc ORDER BY c DESC LIMIT 10")
if not data:
@@ -115,7 +116,7 @@ def grafico_barras_ruc():
plt.close(fig)
return path
# ============================================================================
def grafico_torta():
data = _query("SELECT COUNT(*) AS c, error FROM fallas GROUP BY error ORDER BY c DESC")
if not data:
@@ -139,7 +140,7 @@ def grafico_torta():
plt.close(fig)
return path
# ============================================================================
def main():
reporte_texto()
@@ -161,6 +162,7 @@ def main():
print(f" {p.name}")
print(f"\n💡 Abre con: open {REPORTES_DIR}")
# ============================================================================
if __name__ == "__main__":
main()
# ============================================================================
+9
View File
@@ -0,0 +1,9 @@
{
"HORARIOS": [
[6, 30, 13, 15, 10, "rapido"],
[13, 15, 14, 0, 1800, "full"],
[14, 0, 17, 30, 10, "rapido"],
[17, 30, 21, 30, 1800, "full"]
],
"DIAS_ACTIVOS": [0, 1, 2, 3, 4]
}
+7 -5
View File
@@ -5,6 +5,7 @@ import subprocess
from pathlib import Path
from dotenv import load_dotenv
# ============================================================================
load_dotenv()
SMB_SERVER = os.getenv("SMB_SERVER", "").strip()
@@ -16,7 +17,7 @@ SMB_MOUNT_MAC = os.getenv("SMB_MOUNT_MAC", "").strip()
SMB_MOUNT_LINUX = os.getenv("SMB_MOUNT_LINUX", "").strip()
SMB_MOUNT_WINDOWS = os.getenv("SMB_MOUNT_WINDOWS", "").strip()
# ============================================================================
def get_current_os():
system = platform.system().lower()
if system == "darwin":
@@ -25,7 +26,7 @@ def get_current_os():
return "windows"
return "linux"
# ============================================================================
def get_mount_point():
current_os = get_current_os()
mapping = {
@@ -35,7 +36,7 @@ def get_mount_point():
}
return mapping.get(current_os, "")
# ============================================================================
def esta_montado(mount_point):
if not mount_point:
return False
@@ -45,7 +46,7 @@ def esta_montado(mount_point):
os.path.join(mount_point, ".mounted_check")
)
# ============================================================================
def montar_smb():
mount_point = get_mount_point()
if not mount_point:
@@ -87,7 +88,7 @@ def montar_smb():
print(f" ⚠️ Error montando SMB: {e}")
return False
# ============================================================================
def sincronizar_a_smb(ruta_local):
if not os.path.isdir(ruta_local):
print(f" ⚠️ La carpeta local no existe: {ruta_local}")
@@ -152,3 +153,4 @@ def sincronizar_a_smb(ruta_local):
except Exception as e:
print(f" ❌ Error en sincronización: {e}")
return False
# ============================================================================
-325
View File
@@ -1,325 +0,0 @@
import os
import re
import json
import time
import base64
from pathlib import Path
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
load_dotenv()
from get_token import (
SOL_URL, MENU_URL, TOKEN_FILE, SCREENSHOT_DIR,
RUC, USER, PASSWORD,
fill_login_form, save_screenshot, save_token,
check_for_token, navigate_sol_menu,
click_menu_item_by_id, extract_token_from_menu,
)
TEST_DIR = SCREENSHOT_DIR
TEST_DIR.mkdir(exist_ok=True)
USER_AGENT = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
BROWSER_ARGS = [
"--disable-blink-features=AutomationControlled",
"--disable-popup-blocking",
"--no-sandbox",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
"--window-size=1280,800",
"--start-maximized",
]
def captura(page, tag, nombre):
safe = f"{tag}_{nombre}"
page.screenshot(path=str(TEST_DIR / f"{safe}.png"))
def _salto_linea(texto, ancho=100):
for i in range(0, len(texto), ancho):
print(f" {texto[i:i+ancho]}")
def _login_si_necesario(page, tag):
page.wait_for_load_state("networkidle")
has_inputs = len(page.locator("input[type='password']").all()) > 0
necesita_login = has_inputs or any(
k in page.url.lower() for k in ["login", "autentica", "auth"]
)
if not necesita_login:
print(" No se detectó login")
return True
print(" Login detectado. Rellenando credenciales...")
fill_login_form(page)
captura(page, tag, "form_rellenado")
page.keyboard.press("Enter")
try:
page.wait_for_load_state("networkidle", timeout=30000)
except Exception as e:
print(f" ⚠️ Timeout post-login: {e}")
page.wait_for_timeout(3000)
captura(page, tag, "post_login")
print(f" URL post-login: {page.url}")
return True
def _buscar_token_en_pagina(page, tag):
time.sleep(2)
token = check_for_token(page)
if token:
return token
token = navigate_sol_menu(page)
if token:
return token
token = extract_token_from_menu(page)
return token
def _navegar_con_cookies(page, url, tag):
page.goto(url, wait_until="domcontentloaded")
captura(page, tag, "navegacion")
print(f" URL tras navegar: {page.url}")
# ─── ESTRATEGIAS ─────────────────────────────────────────────
def estrategia_1_fixed_popup(context, page):
tag = "e1"
print("\n" + "=" * 60)
print("ESTRATEGIA 1: Popup con --disable-popup-blocking + event listener")
print("=" * 60)
page.goto(SOL_URL, wait_until="domcontentloaded")
captura(page, tag, "sol_home")
popup_found = [None]
got_popup = [False]
def on_page(new_page):
if not got_popup[0]:
print(f" 📌 Página detectada: {new_page.url[:100]}")
popup_found[0] = new_page
got_popup[0] = True
page.context().on("page", on_page)
with page.expect_popup() as popup_info:
page.evaluate("tramiteConsulta()")
try:
pup = popup_info.value
pup.wait_for_load_state("domcontentloaded", timeout=20000)
print(f" Popup URL: {pup.url}")
captura(pup, tag, "popup")
if "chrome-error" in pup.url:
print(" ⚠️ Popup en chrome-error. Reintentando recarga...")
try:
pup.wait_for_load_state("domcontentloaded", timeout=20000)
except:
pass
captura(pup, tag, "popup_retry")
if "chrome-error" in pup.url:
print(" ❌ Sigue en chrome-error tras recarga")
print(f" URL final: {pup.url}")
return None
_login_si_necesario(pup, tag)
token = _buscar_token_en_pagina(pup, tag)
return token
except Exception as e:
print(f" ❌ Error con popup: {e}")
return None
def estrategia_2_direct_menu(context, page):
tag = "e2"
print("\n" + "=" * 60)
print("ESTRATEGIA 2: Menú directo con sesión desde SOL")
print("=" * 60)
page.goto(SOL_URL, wait_until="domcontentloaded")
captura(page, tag, "sol_home")
_navegar_con_cookies(page, MENU_URL, tag)
if not _login_si_necesario(page, tag):
return None
token = _buscar_token_en_pagina(page, tag)
return token
def estrategia_3_direct_login(context, page):
tag = "e3"
print("\n" + "=" * 60)
print("ESTRATEGIA 3: Login directo al menú (sin SOL)")
print("=" * 60)
_navegar_con_cookies(page, MENU_URL, tag)
if not _login_si_necesario(page, tag):
return None
token = _buscar_token_en_pagina(page, tag)
return token
def estrategia_4_popup_url_directo(context, page):
tag = "e4"
print("\n" + "=" * 60)
print("ESTRATEGIA 4: Extraer URL del popup y navegar directo")
print("=" * 60)
page.goto(SOL_URL, wait_until="domcontentloaded")
captura(page, tag, "sol_home")
try:
popup_url = page.evaluate("""() => {
const fn = tramiteConsulta.toString();
const match = fn.match(/window\\.open\\(['"]([^'"]+)['"]/);
return match ? match[1] : null;
}""")
if popup_url:
if popup_url.startswith("/"):
from urllib.parse import urljoin
popup_url = urljoin(SOL_URL, popup_url)
print(f" URL del popup: {popup_url}")
else:
print(" No se pudo extraer URL")
return None
except Exception as e:
print(f" Error extrayendo URL: {e}")
return None
_navegar_con_cookies(page, popup_url, tag)
if not _login_si_necesario(page, tag):
return None
token = _buscar_token_en_pagina(page, tag)
return token
# ─── CONFIGURACIONES DE LANZAMIENTO ──────────────────────────
def lanzar_chrome(p, channel=None, extra_args=None):
"""Lanza navegador con la configuracion dada."""
args = list(BROWSER_ARGS)
if extra_args:
args.extend(extra_args)
kwargs = {
"headless": True,
"args": args,
}
if channel:
kwargs["channel"] = channel
print(f" Lanzando: {channel} (headless)")
else:
print(f" Lanzando: chromium empaquetado (headless)")
browser = p.chromium.launch(**kwargs)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="es-PE",
timezone_id="America/Lima",
user_agent=USER_AGENT,
no_viewport=False,
ignore_https_errors=True,
)
page = context.new_page()
page.set_default_timeout(90000)
return browser, context, page
def ejecutar_escenario(p, nombre_browser, channel, extra_args):
"""Ejecuta todas las estrategias con un navegador/configuracion."""
print(f"\n{'#'*60}")
print(f"# ESCENARIO: {nombre_browser}")
print(f"{'#'*60}")
browser, context, page = lanzar_chrome(p, channel, extra_args)
token = None
estrategias = [
("1_fixed_popup", estrategia_1_fixed_popup),
("2_direct_menu", estrategia_2_direct_menu),
("3_direct_login", estrategia_3_direct_login),
("4_popup_url_directo", estrategia_4_popup_url_directo),
]
for nombre, fn in estrategias:
print()
try:
token = fn(context, page)
except Exception as e:
print(f" ⚠️ Excepción en {nombre}: {e}")
import traceback
traceback.print_exc()
token = None
if token:
print(f"\n✅ Token obtenido con '{nombre}' en escenario '{nombre_browser}'!")
return token
else:
print(f"\n ❌ '{nombre}' falló")
context.close()
browser.close()
return None
def main():
print("=" * 60)
print("🔐 TEST HEADLESS TOKEN - SUNAT")
print("=" * 60)
print(f" RUC: {RUC}")
print(f" USER: {USER}")
if not RUC or not USER or not PASSWORD:
print("\n❌ Faltan credenciales en .env (SUNAT_RUC, SUNAT_USER, SUNAT_PASSWORD)")
return
escenarios = [
("Chrome real (channel=chrome)", "chrome", None),
("Chromium empaquetado", None, None),
("Chromium + extra stealth", None, ["--disable-gpu", "--disable-accelerated-2d-canvas"]),
]
with sync_playwright() as p:
for nombre_browser, channel, extra_args in escenarios:
token = ejecutar_escenario(p, nombre_browser, channel, extra_args)
if token:
save_token(token)
print(f"\n✅ Token guardado en {TOKEN_FILE}")
print(f" Primeros 50 chars: {token[:50]}...")
# Quitar token de prueba, no guardarlo permanentemente
if TOKEN_FILE.exists():
TOKEN_FILE.unlink()
return
print(f"\n❌ Todas las estrategias y escenarios fallaron.")
print(" Revisa screenshots en:", TEST_DIR)
if __name__ == "__main__":
main()