"""Project context management — persistent via SQLAlchemy + StorageService.""" import uuid import io from datetime import datetime, timezone from typing import Dict, Any, List, Optional from enum import Enum from ..db import get_session from ..models.db_models import ProjectModel, ProjectFileModel class ProjectStatus(str, Enum): CREATED = "created" ONTOLOGY_GENERATED = "ontology_generated" GRAPH_BUILDING = "graph_building" GRAPH_COMPLETED = "graph_completed" FAILED = "failed" class ProjectManager: """Gestiona projectes: metadades a BD, fitxers a StorageService.""" @classmethod def create_project(cls, name: str = "Unnamed Project", storage=None) -> Dict[str, Any]: project_id = str(uuid.uuid4()) with get_session() as db: proj = ProjectModel(id=project_id, name=name, status="created") db.add(proj) db.commit() db.refresh(proj) return cls._to_dict(proj) @classmethod def get_project(cls, project_id: str) -> Optional[Dict[str, Any]]: with get_session() as db: proj = db.get(ProjectModel, project_id) if proj is None: return None return cls._to_dict(proj) @classmethod def save_project(cls, project_data: Dict[str, Any]) -> None: """Actualitza els camps d'un projecte existent.""" project_id = project_data.get("id") or project_data.get("project_id") with get_session() as db: proj = db.get(ProjectModel, project_id) if proj is None: return updatable = [ "name", "status", "analysis_summary", "simulation_requirement", "chunk_size", "chunk_overlap", "active_task_id", ] for field in updatable: if field in project_data: setattr(proj, field, project_data[field]) proj.updated_at = datetime.now(timezone.utc) db.commit() @classmethod def list_projects(cls, limit: int = 50) -> List[Dict[str, Any]]: from sqlalchemy import select, desc with get_session() as db: stmt = select(ProjectModel).order_by(desc(ProjectModel.created_at)).limit(limit) projects = db.execute(stmt).scalars().all() return [cls._to_dict(p) for p in projects] @classmethod def delete_project(cls, project_id: str, storage=None) -> bool: with get_session() as db: proj = db.get(ProjectModel, project_id) if proj is None: return False if storage is not None: storage.delete_prefix(f"projects/{project_id}") db.delete(proj) db.commit() return True @classmethod def save_file_to_project( cls, project_id: str, file_storage, # Flask FileStorage original_filename: str, storage, ) -> Dict[str, Any]: import os ext = os.path.splitext(original_filename)[1].lower() safe_filename = f"{uuid.uuid4().hex[:8]}{ext}" storage_path = f"projects/{project_id}/files/{safe_filename}" data = file_storage.read() storage.upload(storage_path, data) mime_type = getattr(file_storage, "content_type", "application/octet-stream") or "application/octet-stream" with get_session() as db: file_rec = ProjectFileModel( id=str(uuid.uuid4()), project_id=project_id, original_name=original_filename, storage_path=storage_path, size=len(data), mime_type=mime_type, file_type="upload", ) db.add(file_rec) db.commit() return { "original_filename": original_filename, "saved_filename": safe_filename, "storage_path": storage_path, "size": len(data), } @classmethod def save_extracted_text(cls, project_id: str, text: str, storage) -> None: storage_path = f"projects/{project_id}/extracted_text.txt" storage.upload(storage_path, text.encode("utf-8"), "text/plain") with get_session() as db: from sqlalchemy import select stmt = select(ProjectFileModel).where( ProjectFileModel.project_id == project_id, ProjectFileModel.file_type == "extracted_text", ) existing = db.execute(stmt).scalar_one_or_none() if existing: existing.storage_path = storage_path existing.size = len(text.encode("utf-8")) else: rec = ProjectFileModel( id=str(uuid.uuid4()), project_id=project_id, original_name="extracted_text.txt", storage_path=storage_path, size=len(text.encode("utf-8")), mime_type="text/plain", file_type="extracted_text", ) db.add(rec) db.commit() @classmethod def get_extracted_text(cls, project_id: str, storage) -> Optional[str]: storage_path = f"projects/{project_id}/extracted_text.txt" if not storage.exists(storage_path): return None return storage.download(storage_path).decode("utf-8") @staticmethod def _to_dict(proj: ProjectModel) -> Dict[str, Any]: return { "id": proj.id, "project_id": proj.id, # compatibilitat amb codi existent "name": proj.name, "status": proj.status, "analysis_summary": proj.analysis_summary, "simulation_requirement": proj.simulation_requirement, "chunk_size": proj.chunk_size, "chunk_overlap": proj.chunk_overlap, "active_task_id": proj.active_task_id, "created_at": proj.created_at.isoformat(), "updated_at": proj.updated_at.isoformat(), # Camps llegits del model antic — ara buits per compatibilitat "files": [], "total_text_length": 0, "ontology": None, "graph_id": None, "graph_build_task_id": None, "error": None, }