import json import urllib.request import urllib.error from datetime import datetime from django.db import connection from django.utils import timezone from common.utils import clean_sql_string, clean_sql_int # ========================================================= # Helper HTTP (stdlib) — evita dependencia extra en requirements # ========================================================= def _http_get_json(url, timeout=8): """GET a un servicio externo que responde JSON. Usa stdlib para no añadir deps.""" req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'}) with urllib.request.urlopen(req, timeout=timeout) as response: raw = response.read().decode('utf-8') try: return json.loads(raw) except json.JSONDecodeError: return {'raw': raw} def _http_get_text(url, timeout=8): """GET a un servicio externo que responde texto plano.""" req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'}) with urllib.request.urlopen(req, timeout=timeout) as response: return response.read().decode('utf-8').strip() # ========================================================= # ACCIÓN 1 — Base de datos (INSERT histórico de ejecución) # set_parameterized # ========================================================= def setEjecucion(params): """ Inserta una fila en automatizacion_ejecuciones registrando una ejecución. params esperados: nombre, descripcion, estado, origen, resultado, error """ query = """ INSERT INTO automatizacion_ejecuciones (nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """ ahora = timezone.now() resultado_json = json.dumps(params.get('resultado')) if params.get('resultado') is not None else None parameter_dict = [ clean_sql_string(params.get('nombre')), clean_sql_string(params.get('descripcion')), clean_sql_string(params.get('estado') or 'ok'), clean_sql_string(params.get('origen') or 'manual'), resultado_json, clean_sql_string(params.get('error')) if params.get('error') else None, ahora, ahora, bool(params.get('activo', True)), ] with connection.cursor() as cursor: cursor.execute(query, parameter_dict) row = cursor.fetchone() nuevo_id = row[0] if row else None return {'id': nuevo_id, 'fecha': ahora.isoformat()} # ========================================================= # ACCIÓN 2 — Servicio externo: Cat Fact API # https://catfact.ninja/fact (gratis, sin auth) # ========================================================= def getCatFact(): url = 'https://catfact.ninja/fact' data = _http_get_json(url) return { 'servicio': 'catfact.ninja', 'url': url, 'fact': data.get('fact'), 'length': data.get('length'), } # ========================================================= # ACCIÓN 3 — Servicio externo: GitHub Zen # https://api.github.com/zen (gratis, sin auth) # ========================================================= def getGithubZen(): url = 'https://api.github.com/zen' texto = _http_get_text(url) return { 'servicio': 'api.github.com/zen', 'url': url, 'zen': texto, } # ========================================================= # Orquestador — ejecuta las 3 acciones en secuencia # ========================================================= def ejecutarAutomatizaciones(params): """ Ejecuta las 3 acciones automatizadas: 1) Llamada a servicio externo: catfact.ninja 2) Llamada a servicio externo: api.github.com/zen 3) Persistencia en BD: inserta la ejecución en automatizacion_ejecuciones """ resultados = {'acciones': []} errores = [] # Acción 2 — catfact try: resultados['acciones'].append({'step': 1, 'ok': True, 'data': getCatFact()}) except Exception as err: errores.append(f'catfact: {err}') resultados['acciones'].append({'step': 1, 'ok': False, 'error': str(err)}) # Acción 3 — github zen try: resultados['acciones'].append({'step': 2, 'ok': True, 'data': getGithubZen()}) except Exception as err: errores.append(f'github_zen: {err}') resultados['acciones'].append({'step': 2, 'ok': False, 'error': str(err)}) # Acción 1 — persistir en BD estado_final = 'ok' if not errores else 'error' nombre = clean_sql_string(params.get('nombre')) or f'Ejecucion {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' origen = clean_sql_string(params.get('origen')) or 'manual' registro = setEjecucion({ 'nombre': nombre, 'descripcion': params.get('descripcion') or 'Ejecución orquestada por /api/automatizados/ejecutar/', 'estado': estado_final, 'origen': origen, 'resultado': resultados, 'error': '; '.join(errores) if errores else None, 'activo': True, }) resultados['acciones'].append({'step': 3, 'ok': True, 'data': registro}) return { 'ejecucion_id': registro.get('id'), 'estado': estado_final, 'total_acciones': len(resultados['acciones']), 'resultados': resultados, 'errores': errores, } # ========================================================= # Lectura — historial (get_parameterized) # ========================================================= def getHistorial(params): """ Devuelve el histórico de ejecuciones. Permite filtrar por estado y limit. """ query = """ SELECT id, nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo FROM automatizacion_ejecuciones WHERE (%s = '' OR estado = %s) ORDER BY fecha_inicio DESC LIMIT %s """ estado = clean_sql_string(params.get('estado')) if params.get('estado') else '' limite = clean_sql_int(params.get('limit')) or 20 parameter_dict = [estado, estado, limite] with connection.cursor() as cursor: cursor.execute(query, parameter_dict) columns = [col[0] for col in cursor.description] rows = [dict(zip(columns, row)) for row in cursor.fetchall()] # Normalizamos el campo resultado (JSON serializado en SQLite) for row in rows: if isinstance(row.get('resultado'), str): try: row['resultado'] = json.loads(row['resultado']) except (ValueError, TypeError): pass return {'total': len(rows), 'data': rows} # ========================================================= # Estado del módulo # ========================================================= def getEstado(): """ Devuelve el estado general del módulo de automatizaciones: cuántas ejecuciones hay, última ejecución, endpoints disponibles. """ query = """ SELECT COUNT(*) AS total, SUM(CASE WHEN estado = 'ok' THEN 1 ELSE 0 END) AS ok, SUM(CASE WHEN estado = 'error' THEN 1 ELSE 0 END) AS errores, MAX(fecha_inicio) AS ultima_ejecucion FROM automatizacion_ejecuciones """ with connection.cursor() as cursor: cursor.execute(query) columns = [col[0] for col in cursor.description] row = cursor.fetchone() resumen = dict(zip(columns, row)) if row else {} return { 'status': 'ok', 'modulo': 'automatizados', 'endpoints': [ 'POST /api/automatizados/ejecutar/', 'POST /api/automatizados/historial/', 'POST /api/automatizados/estado/', ], 'resumen': resumen, }