""" Rutas de importación CSV para Trailers y Cajas. Flujo: upload -> scan -> status (polling) -> commit. """ import base64 import json import logging import os import threading from uuid import uuid4 from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends from sqlalchemy.orm import Session from typing import Dict, Any from core.celery_app import celery_app from core.database import get_core_db from core.paths import layout_path from core.security import get_current_user, validate_access_to_resource from .schemas import ImportJobResponse from .tasks import ( scan_file, run_scan_sync, run_commit_sync, TRL_IMPORT_FILE_PREFIX, TRL_IMPORT_META_PREFIX, TRL_IMPORT_STATUS_PREFIX, TRL_IMPORT_REDIS_TTL, ) router = APIRouter() logger = logging.getLogger(__name__) def _get_redis(): import redis url = os.getenv("VALKEY_URL", os.getenv("REDIS_URL", "redis://valkey:6379/0")) return redis.Redis.from_url(url, decode_responses=False) @router.post("/upload", response_model=ImportJobResponse) async def upload_import_file( file: UploadFile = File(...), company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: Dict[str, Any] = Depends(get_current_user), ): try: tenant_id = validate_access_to_resource(db, company_id, current_user) except Exception as e: logger.error(f"Trailers import: access validation failed: {e}") raise HTTPException(status_code=403, detail="Invalid company access") if not file.filename or not file.filename.lower().endswith(".csv"): raise HTTPException(status_code=400, detail="Solo se permiten archivos .csv") job_id = str(uuid4()) contents = await file.read() meta_data = { "tenant_id": tenant_id, "company_id": company_id, "user_id": current_user.get("id"), "template_id": "trailers", } try: r = _get_redis() r.set( f"{TRL_IMPORT_FILE_PREFIX}{job_id}", base64.b64encode(contents), ex=TRL_IMPORT_REDIS_TTL, ) r.set( f"{TRL_IMPORT_META_PREFIX}{job_id}", json.dumps(meta_data).encode("utf-8"), ex=TRL_IMPORT_REDIS_TTL, ) except Exception as e: logger.error(f"Trailers import: Redis store error: {e}") raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.") try: upload_dir = layout_path("imports", "temp") os.makedirs(upload_dir, exist_ok=True) with open(os.path.join(upload_dir, f"trl_{job_id}.csv"), "wb") as f: f.write(contents) with open(os.path.join(upload_dir, f"trl_{job_id}.meta.json"), "w") as f: json.dump(meta_data, f) except Exception as e: logger.warning(f"Trailers import: local file save failed: {e}") scan_file.apply_async(args=[job_id], task_id=job_id) def run_scan_background(): try: run_scan_sync(job_id) except Exception as e: logger.exception(f"Trailers import: background scan failed: {e}") threading.Thread(target=run_scan_background, daemon=True).start() return ImportJobResponse( job_id=job_id, status="queued", message="Archivo subido. Escaneo iniciado.", ) @router.get("/{job_id}/status") async def get_import_status(job_id: str): try: r = _get_redis() raw = r.get(f"{TRL_IMPORT_STATUS_PREFIX}{job_id}") if raw: data = json.loads(raw.decode("utf-8")) return data except Exception as e: logger.debug(f"Trailers import: could not read status from Redis: {e}") task_result = celery_app.AsyncResult(job_id) if task_result.state == "PENDING": return {"status": "processing", "progress": 0} if task_result.state == "PROGRESS": info = (task_result.info or {}) return { "status": "processing", "progress": info.get("current", 0), "total": info.get("total", 0), } if task_result.state == "SUCCESS": result = task_result.result if isinstance(result, dict) and "status" in result: return result return {"status": "finished", "result": result} result = getattr(task_result, "result", None) if isinstance(result, dict) and result.get("status") in ("finished", "warning"): return result logger.warning("Trailers import task %s failed: state=%s", job_id, task_result.state) err_msg = None tb = getattr(task_result, "traceback", None) if tb and isinstance(tb, str): lines = [l.strip() for l in tb.strip().split("\n") if l.strip()] if lines: err_msg = lines[-1] if not err_msg: try: exc = task_result.get(propagate=False) if exc is not None: err_msg = str(exc) except Exception: pass if not err_msg and result is not None: if not isinstance(result, dict): err_msg = str(result) elif result.get("error") or result.get("message"): err_msg = result.get("error") or result.get("message") return {"status": "failed", "error": err_msg or "Task failed"} @router.post("/{job_id}/commit") async def commit_import_job(job_id: str): try: r = _get_redis() r.set( f"{TRL_IMPORT_STATUS_PREFIX}{job_id}", json.dumps({"status": "processing", "message": "Insertando..."}).encode("utf-8"), ex=TRL_IMPORT_REDIS_TTL, ) except Exception as e: logger.debug(f"Trailers import: could not write processing status: {e}") def run_commit_background(): try: run_commit_sync(job_id) except Exception as e: logger.exception(f"Trailers import: background commit failed: {e}") threading.Thread(target=run_commit_background, daemon=True).start() return { "status": "committing", "message": "Inserción iniciada.", "commit_job_id": job_id, }