""" Project Model Modelo de datos para manejo de proyectos y su estado """ import json from datetime import datetime, timezone from enum import Enum from pathlib import Path from typing import Dict, List, Any, Optional class ProjectStatus(Enum): """Estados posibles de un proyecto""" READY = "ready" BACKING_UP = "backing_up" ERROR = "error" FILES_IN_USE = "files_in_use" RETRY_PENDING = "retry_pending" DISABLED = "disabled" class Project: """Clase para representar un proyecto individual""" def __init__(self, project_data: Dict[str, Any]): self.id = project_data.get("id", "") self.name = project_data.get("name", "") self.path = project_data.get("path", "") self.type = project_data.get("type", "") self.s7p_file = project_data.get("s7p_file", "") self.observation_directory = project_data.get("observation_directory", "") self.relative_path = project_data.get("relative_path", "") self.backup_path = project_data.get("backup_path", "") # Configuración de schedule schedule_config = project_data.get("schedule_config", {}) self.schedule = schedule_config.get("schedule", "daily") self.schedule_time = schedule_config.get("schedule_time", "02:00") self.enabled = schedule_config.get("enabled", True) self.next_scheduled_backup = schedule_config.get("next_scheduled_backup", "") # Historia de backups backup_history = project_data.get("backup_history", {}) self.last_backup_date = backup_history.get("last_backup_date", "") self.last_backup_file = backup_history.get("last_backup_file", "") self.backup_count = backup_history.get("backup_count", 0) self.last_successful_backup = backup_history.get("last_successful_backup", "") # Información de hash hash_info = project_data.get("hash_info", {}) self.last_s7p_hash = hash_info.get("last_s7p_hash", "") self.last_full_hash = hash_info.get("last_full_hash", "") self.last_s7p_timestamp = hash_info.get("last_s7p_timestamp", "") self.last_s7p_size = hash_info.get("last_s7p_size", 0) self.last_scan_timestamp = hash_info.get("last_scan_timestamp", "") self.file_count = hash_info.get("file_count", 0) self.total_size_bytes = hash_info.get("total_size_bytes", 0) # Estado actual status = project_data.get("status", {}) self.current_status = ProjectStatus(status.get("current_status", "ready")) self.last_error = status.get("last_error", None) self.retry_count = status.get("retry_count", 0) self.next_retry = status.get("next_retry", None) self.files_in_use = status.get("files_in_use", False) self.exclusivity_check_passed = status.get("exclusivity_check_passed", True) self.last_status_update = status.get("last_status_update", "") # Información de descubrimiento discovery_info = project_data.get("discovery_info", {}) self.discovered_date = discovery_info.get("discovered_date", "") self.discovery_method = discovery_info.get("discovery_method", "") self.auto_discovered = discovery_info.get("auto_discovered", True) def to_dict(self) -> Dict[str, Any]: """Convertir el proyecto a diccionario para serialización JSON""" return { "id": self.id, "name": self.name, "path": self.path, "type": self.type, "s7p_file": self.s7p_file, "observation_directory": self.observation_directory, "relative_path": self.relative_path, "backup_path": self.backup_path, "schedule_config": { "schedule": self.schedule, "schedule_time": self.schedule_time, "enabled": self.enabled, "next_scheduled_backup": self.next_scheduled_backup }, "backup_history": { "last_backup_date": self.last_backup_date, "last_backup_file": self.last_backup_file, "backup_count": self.backup_count, "last_successful_backup": self.last_successful_backup }, "hash_info": { "last_s7p_hash": self.last_s7p_hash, "last_full_hash": self.last_full_hash, "last_s7p_timestamp": self.last_s7p_timestamp, "last_s7p_size": self.last_s7p_size, "last_scan_timestamp": self.last_scan_timestamp, "file_count": self.file_count, "total_size_bytes": self.total_size_bytes }, "status": { "current_status": self.current_status.value, "last_error": self.last_error, "retry_count": self.retry_count, "next_retry": self.next_retry, "files_in_use": self.files_in_use, "exclusivity_check_passed": self.exclusivity_check_passed, "last_status_update": self.last_status_update }, "discovery_info": { "discovered_date": self.discovered_date, "discovery_method": self.discovery_method, "auto_discovered": self.auto_discovered } } def update_status(self, status: ProjectStatus, error_message: str = None) -> None: """Actualizar el estado del proyecto""" self.current_status = status self.last_error = error_message self.last_status_update = datetime.now(timezone.utc).isoformat() if status == ProjectStatus.ERROR: self.retry_count += 1 elif status == ProjectStatus.READY: self.retry_count = 0 self.last_error = None def update_hash_info(self, s7p_hash: str = None, full_hash: str = None, s7p_timestamp: str = None, s7p_size: int = None, file_count: int = None, total_size: int = None) -> None: """Actualizar información de hash""" if s7p_hash is not None: self.last_s7p_hash = s7p_hash if full_hash is not None: self.last_full_hash = full_hash if s7p_timestamp is not None: self.last_s7p_timestamp = s7p_timestamp if s7p_size is not None: self.last_s7p_size = s7p_size if file_count is not None: self.file_count = file_count if total_size is not None: self.total_size_bytes = total_size self.last_scan_timestamp = datetime.now(timezone.utc).isoformat() def update_backup_info(self, backup_file_path: str) -> None: """Actualizar información después de un backup exitoso""" now = datetime.now(timezone.utc).isoformat() self.last_backup_date = now self.last_successful_backup = now self.last_backup_file = backup_file_path self.backup_count += 1 self.update_status(ProjectStatus.READY) class ProjectManager: """Clase para manejar la colección de proyectos""" def __init__(self, projects_file_path: str = None): if projects_file_path is None: # Buscar projects.json en el directorio del proyecto current_dir = Path(__file__).parent.parent.parent projects_file_path = current_dir / "projects.json" self.projects_file_path = Path(projects_file_path) self.projects: Dict[str, Project] = {} self.metadata = {} self.statistics = {} self.load_projects() def load_projects(self) -> None: """Cargar proyectos desde archivo JSON""" try: if self.projects_file_path.exists(): with open(self.projects_file_path, 'r', encoding='utf-8') as f: data = json.load(f) self.metadata = data.get("metadata", {}) self.statistics = data.get("statistics", {}) # Cargar proyectos for project_data in data.get("projects", []): project = Project(project_data) self.projects[project.id] = project else: # Crear archivo por defecto si no existe self._create_default_projects_file() self.save_projects() except Exception as e: raise Exception(f"Error cargando proyectos: {e}") def save_projects(self) -> None: """Guardar proyectos al archivo JSON""" try: data = { "metadata": self.metadata, "projects": [project.to_dict() for project in self.projects.values()], "statistics": self.statistics } with open(self.projects_file_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception as e: raise Exception(f"Error guardando proyectos: {e}") def _create_default_projects_file(self) -> None: """Crear archivo de proyectos por defecto""" self.metadata = { "version": "1.0", "last_updated": datetime.now(timezone.utc).isoformat(), "total_projects": 0 } self.statistics = { "total_backups_created": 0, "total_backup_size_mb": 0.0, "average_backup_time_seconds": 0.0, "last_global_scan": "", "projects_with_errors": 0, "projects_pending_retry": 0 } def add_project(self, project_data: Dict[str, Any]) -> Project: """Agregar un nuevo proyecto""" project = Project(project_data) self.projects[project.id] = project self.metadata["total_projects"] = len(self.projects) self.metadata["last_updated"] = datetime.now(timezone.utc).isoformat() self.save_projects() return project def get_project(self, project_id: str) -> Optional[Project]: """Obtener un proyecto por ID""" return self.projects.get(project_id) def get_all_projects(self) -> List[Project]: """Obtener todos los proyectos""" return list(self.projects.values()) def get_projects_by_status(self, status: ProjectStatus) -> List[Project]: """Obtener proyectos por estado""" return [p for p in self.projects.values() if p.current_status == status] def get_enabled_projects(self) -> List[Project]: """Obtener proyectos habilitados""" return [p for p in self.projects.values() if p.enabled] def update_statistics(self) -> None: """Actualizar estadísticas generales""" self.statistics["projects_with_errors"] = len( self.get_projects_by_status(ProjectStatus.ERROR) ) self.statistics["projects_pending_retry"] = len( self.get_projects_by_status(ProjectStatus.RETRY_PENDING) ) self.save_projects()