From 35e8314036ce0d7cd9a6cfa86cae3237845e2219 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:23:46 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20script=20desatendido=20para=20despl?= =?UTF-8?q?iegue=20de=20promociones=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se agregó un nuevo script `publicador_desatendido.py` capaz de monitorear un directorio de forma desatendida y publicar su contenido JSON en producción a través de la API. Se incluye en la actualización una guía `publicador_desatendido.md` con su documentación en español y se actualiza el `.gitignore` para ignorar los directorios y logs locales correspondientes. Co-authored-by: LVT-ENG <214667862+LVT-ENG@users.noreply.github.com> --- .gitignore | 4 + publicador_desatendido.md | 57 ++++++++++++++ scripts/publicador_desatendido.py | 120 ++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 publicador_desatendido.md create mode 100755 scripts/publicador_desatendido.py diff --git a/.gitignore b/.gitignore index 7b2cfc3fa2..e3740a0108 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ Brewfile.lock.json examples/*.mp3 promotions.log +monitoreo.log +promotions/ +promotions_processed/ +promotions_failed/ diff --git a/publicador_desatendido.md b/publicador_desatendido.md new file mode 100644 index 0000000000..a274fbb7ad --- /dev/null +++ b/publicador_desatendido.md @@ -0,0 +1,57 @@ +# Publicador Desatendido de Promociones + +Este documento explica cómo utilizar el script `scripts/publicador_desatendido.py`, diseñado para automatizar la publicación de promociones para `tryonyou.pro` de manera desatendida y continua. + +## Funcionamiento + +El script implementa un ciclo de monitoreo infinito que vigila el directorio `promotions/`. Cuando detecta un nuevo archivo JSON en este directorio, realiza las siguientes acciones: + +1. **Lectura y Parseo:** Lee el archivo y verifica que contenga un diccionario JSON válido. +2. **Validación:** Verifica que la estructura de la promoción contenga todos los campos requeridos (`title`, `description`, `discount_code`, `valid_until`). +3. **Despliegue:** Ejecuta una petición POST a la API de producción para publicar la promoción. +4. **Enrutamiento:** Si el proceso es exitoso, mueve el archivo a la carpeta `promotions_processed/`. Si ocurre algún error durante el parseo, validación o despliegue, el archivo es movido a `promotions_failed/`. + +## Requisitos + +El sistema de despliegue requiere de una clave API válida configurada como variable de entorno para realizar las peticiones al entorno de producción: + +```bash +export TRYONYOU_API_KEY="tu_clave_api_aqui" +``` + +## Uso + +### Ejecución estándar + +Para ejecutar el script y que empiece a monitorear el directorio (por defecto sondea cada 5 segundos): + +```bash +./scripts/publicador_desatendido.py +``` + +### Ejecución en modo simulación (Dry-Run) + +Si deseas probar el enrutamiento y la validación de archivos sin realizar peticiones HTTP reales a la API, puedes usar el modo `--dry-run`: + +```bash +./scripts/publicador_desatendido.py --dry-run +``` +*Nota: En este modo, el sistema simulará que el despliegue es exitoso sin requerir la variable `TRYONYOU_API_KEY`.* + +### Opciones avanzadas + +Puedes personalizar la URL de la API y el intervalo de monitoreo en segundos: + +```bash +./scripts/publicador_desatendido.py --api-url "https://api.tryonyou.pro/v1/promotions" --intervalo 10 +``` + +### Ejecución en Background + +Para mantener el script funcionando en segundo plano de manera continua e independiente de la sesión de la terminal, puedes usar: + +```bash +nohup python3 -u scripts/publicador_desatendido.py > monitoreo.log 2>&1 & +``` + +Esto enviará toda la salida al archivo `monitoreo.log`, el cual puedes inspeccionar con `tail -f monitoreo.log`. \ No newline at end of file diff --git a/scripts/publicador_desatendido.py b/scripts/publicador_desatendido.py new file mode 100755 index 0000000000..0bab0d0f83 --- /dev/null +++ b/scripts/publicador_desatendido.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +import json +import os +from typing import Any, Dict + +def validar_promocion(data: Dict[str, Any]) -> bool: + """Valida que los datos de la promoción contengan los campos requeridos.""" + campos_requeridos = ["title", "description", "discount_code", "valid_until"] + for campo in campos_requeridos: + if campo not in data: + raise ValueError(f"Falta el campo requerido: {campo}") + return True + +def parsear_archivo(ruta_archivo: str) -> Dict[str, Any]: + """Lee y parsea un archivo JSON de promoción.""" + with open(ruta_archivo, 'r', encoding='utf-8') as f: + datos = json.load(f) + if not isinstance(datos, dict): + raise ValueError("El archivo JSON debe contener un diccionario") + return datos + +def desplegar_promocion(data: Dict[str, Any], api_url: str, api_key: str, dry_run: bool = False) -> bool: + """Ejecuta el despliegue automático en producción o simula en modo dry-run.""" + if dry_run: + print(f"Simulando el despliegue de '{data.get('title')}' (modo dry-run)...") + return True + + if not api_key: + print("Error: TRYONYOU_API_KEY no configurada. Usa --dry-run para simular.") + return False + + import urllib.request + import urllib.error + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + req = urllib.request.Request( + api_url, + data=json.dumps(data).encode('utf-8'), + headers=headers, + method='POST' + ) + + try: + titulo = data.get("title", "Sin título") + print(f"Desplegando promoción '{titulo}' en {api_url}...") + respuesta = urllib.request.urlopen(req) + print(f"Despliegue exitoso: {respuesta.read().decode('utf-8')}") + return True + except urllib.error.URLError as e: + print(f"Error en el despliegue: {e}") + return False + +def ciclo_monitoreo(directorio_base: str, api_url: str, api_key: str, dry_run: bool = False, intervalo_segundos: int = 5) -> None: + """Monitorea infinitamente un directorio en busca de promociones para desplegar.""" + import time + import shutil + + dir_promociones = os.path.join(directorio_base, "promotions") + dir_procesadas = os.path.join(directorio_base, "promotions_processed") + dir_fallidas = os.path.join(directorio_base, "promotions_failed") + + for directorio in [dir_promociones, dir_procesadas, dir_fallidas]: + os.makedirs(directorio, exist_ok=True) + + print(f"Iniciando monitoreo de {dir_promociones} cada {intervalo_segundos} segundos...") + if dry_run: + print("Modo DRY-RUN activado. No se realizarán peticiones reales.") + + while True: + try: + archivos = [f for f in os.listdir(dir_promociones) if f.endswith(".json")] + for archivo in archivos: + ruta_archivo = os.path.join(dir_promociones, archivo) + print(f"Encontrado nuevo archivo: {archivo}") + exito = False + try: + datos = parsear_archivo(ruta_archivo) + validar_promocion(datos) + exito = desplegar_promocion(datos, api_url, api_key, dry_run) + except Exception as e: + print(f"Error procesando {archivo}: {e}") + + if exito: + shutil.move(ruta_archivo, os.path.join(dir_procesadas, archivo)) + print(f"Movido {archivo} a procesadas.") + else: + shutil.move(ruta_archivo, os.path.join(dir_fallidas, archivo)) + print(f"Movido {archivo} a fallidas.") + + time.sleep(intervalo_segundos) + except KeyboardInterrupt: + print("Deteniendo el monitoreo por interrupción del usuario.") + break + except Exception as e: + print(f"Error en el ciclo de monitoreo: {e}") + time.sleep(intervalo_segundos) + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Automatiza la publicación desatendida de promociones.") + parser.add_argument("--api-url", default="https://api.tryonyou.pro/v1/promotions", help="URL de la API de promociones.") + parser.add_argument("--dry-run", action="store_true", help="Simula el despliegue sin realizar llamadas de red.") + parser.add_argument("--intervalo", type=int, default=5, help="Intervalo de sondeo en segundos (por defecto 5).") + + args = parser.parse_args() + + api_key = os.environ.get("TRYONYOU_API_KEY", "") + + ciclo_monitoreo( + directorio_base=os.getcwd(), + api_url=args.api_url, + api_key=api_key, + dry_run=args.dry_run, + intervalo_segundos=args.intervalo + ) From 95238979bcbbd43c01953c007c22cbf29209134a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:29:06 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20script=20desatendido=20para=20despl?= =?UTF-8?q?iegue=20de=20promociones=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se agregó un nuevo script `publicador_desatendido.py` capaz de monitorear un directorio de forma desatendida y publicar su contenido JSON en producción a través de la API. Se corrigió un error de linting en los imports según Ruff detectado en CI. Se incluye en la actualización una guía `publicador_desatendido.md` con su documentación en español y se actualiza el `.gitignore` para ignorar los directorios y logs locales correspondientes. Co-authored-by: LVT-ENG <214667862+LVT-ENG@users.noreply.github.com> --- scripts/publicador_desatendido.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/publicador_desatendido.py b/scripts/publicador_desatendido.py index 0bab0d0f83..d5dedbc9df 100755 --- a/scripts/publicador_desatendido.py +++ b/scripts/publicador_desatendido.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import json import os +import json from typing import Any, Dict + def validar_promocion(data: Dict[str, Any]) -> bool: """Valida que los datos de la promoción contengan los campos requeridos.""" campos_requeridos = ["title", "description", "discount_code", "valid_until"] @@ -29,8 +30,8 @@ def desplegar_promocion(data: Dict[str, Any], api_url: str, api_key: str, dry_ru print("Error: TRYONYOU_API_KEY no configurada. Usa --dry-run para simular.") return False - import urllib.request import urllib.error + import urllib.request headers = { "Content-Type": "application/json", From 472ee95d08b52a30304308868e1030c8c17b4187 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:33:37 +0000 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20script=20desatendido=20para=20despl?= =?UTF-8?q?iegue=20de=20promociones=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se agregó un nuevo script `publicador_desatendido.py` capaz de monitorear un directorio de forma desatendida y publicar su contenido JSON en producción a través de la API. Se corrigió un error de linting en los imports según Ruff detectado en CI. Se incluye en la actualización una guía `publicador_desatendido.md` con su documentación en español y se actualiza el `.gitignore` para ignorar los directorios y logs locales correspondientes. Co-authored-by: LVT-ENG <214667862+LVT-ENG@users.noreply.github.com> From 73f52c7fc58cdca8a2b1f75c034d704834672862 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:39:24 +0000 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20script=20desatendido=20para=20despl?= =?UTF-8?q?iegue=20de=20promociones=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se agregó un nuevo script `publicador_desatendido.py` capaz de monitorear un directorio de forma desatendida y publicar su contenido JSON en producción a través de la API. Se corrigió un error de linting en los imports según Ruff detectado en CI. Se incluyen casts explícitos de tipado en los parsers JSON en ambos scripts (`despliegue_desatendido.py` y `publicador_desatendido.py`) para evitar problemas con Pyright. Se incluye en la actualización una guía `publicador_desatendido.md` con su documentación en español y se actualiza el `.gitignore` para ignorar los directorios y logs locales correspondientes. Co-authored-by: LVT-ENG <214667862+LVT-ENG@users.noreply.github.com> --- scripts/despliegue_desatendido.py | 6 ++++-- scripts/publicador_desatendido.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/despliegue_desatendido.py b/scripts/despliegue_desatendido.py index 736f7ecfb8..681990a648 100755 --- a/scripts/despliegue_desatendido.py +++ b/scripts/despliegue_desatendido.py @@ -41,10 +41,12 @@ def procesar_archivos(directorio_base: str, dry_run: bool) -> None: print(f"Procesando archivo: {archivo}") try: + from typing import cast with open(ruta_archivo, 'r', encoding='utf-8') as f: - datos = json.load(f) - if not isinstance(datos, dict): + datos_cargados = json.load(f) + if not isinstance(datos_cargados, dict): raise ValueError("El archivo JSON debe contener un diccionario") + datos = cast(Dict[str, Any], datos_cargados) validar_promocion(datos) print("Validación completada.") diff --git a/scripts/publicador_desatendido.py b/scripts/publicador_desatendido.py index d5dedbc9df..f329bc2d81 100755 --- a/scripts/publicador_desatendido.py +++ b/scripts/publicador_desatendido.py @@ -14,11 +14,12 @@ def validar_promocion(data: Dict[str, Any]) -> bool: def parsear_archivo(ruta_archivo: str) -> Dict[str, Any]: """Lee y parsea un archivo JSON de promoción.""" + from typing import cast with open(ruta_archivo, 'r', encoding='utf-8') as f: datos = json.load(f) if not isinstance(datos, dict): raise ValueError("El archivo JSON debe contener un diccionario") - return datos + return cast(Dict[str, Any], datos) def desplegar_promocion(data: Dict[str, Any], api_url: str, api_key: str, dry_run: bool = False) -> bool: """Ejecuta el despliegue automático en producción o simula en modo dry-run."""