Landing AI integrado

This commit is contained in:
Sebastian
2025-11-06 13:29:43 +00:00
parent 7c6e8c4858
commit c03d0e27c4
32 changed files with 3908 additions and 728 deletions

View File

@@ -0,0 +1,4 @@
"""
Repositories for data persistence.
Implementa patrón Repository para abstraer la capa de datos.
"""

View File

@@ -0,0 +1,243 @@
"""
Schema Repository - Patrón Repository
Abstrae la persistencia de schemas, actualmente usando archivos JSON.
Fácil migrar a base de datos después.
"""
import logging
import json
from pathlib import Path
from typing import List, Optional
from datetime import datetime
from ..models.schema_models import CustomSchema
logger = logging.getLogger(__name__)
class SchemaRepository:
"""
Repository para gestión de schemas.
Implementa patrón Repository para abstraer almacenamiento.
Actualmente usa archivos JSON en disco.
Para migrar a DB: solo cambiar esta clase, resto del código no cambia.
"""
def __init__(self, schemas_dir: Path):
"""
Inicializa el repositorio.
Args:
schemas_dir: Directorio donde se guardan los schemas
"""
self.schemas_dir = Path(schemas_dir)
self.schemas_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"SchemaRepository inicializado en: {self.schemas_dir}")
def save(self, schema: CustomSchema) -> CustomSchema:
"""
Guarda o actualiza un schema.
Args:
schema: Schema a guardar
Returns:
Schema guardado con timestamps actualizados
Raises:
IOError: Si hay error escribiendo el archivo
"""
try:
# Actualizar timestamps
now = datetime.utcnow().isoformat()
if not schema.created_at:
schema.created_at = now
schema.updated_at = now
# Guardar archivo
file_path = self._get_file_path(schema.schema_id)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(schema.model_dump(), f, indent=2, ensure_ascii=False)
logger.info(f"Schema guardado: {schema.schema_id} - {schema.schema_name}")
return schema
except Exception as e:
logger.error(f"Error guardando schema {schema.schema_id}: {e}")
raise IOError(f"No se pudo guardar el schema: {str(e)}")
def get_by_id(self, schema_id: str) -> Optional[CustomSchema]:
"""
Obtiene un schema por su ID.
Args:
schema_id: ID del schema
Returns:
Schema si existe, None si no
Raises:
ValueError: Si el archivo está corrupto
"""
try:
file_path = self._get_file_path(schema_id)
if not file_path.exists():
logger.debug(f"Schema no encontrado: {schema_id}")
return None
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
schema = CustomSchema(**data)
logger.debug(f"Schema cargado: {schema_id}")
return schema
except json.JSONDecodeError as e:
logger.error(f"Archivo JSON corrupto para schema {schema_id}: {e}")
raise ValueError(f"Schema corrupto: {schema_id}")
except Exception as e:
logger.error(f"Error cargando schema {schema_id}: {e}")
return None
def list_all(self) -> List[CustomSchema]:
"""
Lista todos los schemas disponibles.
Returns:
Lista de schemas ordenados por fecha de creación (más reciente primero)
"""
schemas = []
try:
for file_path in self.schemas_dir.glob("*.json"):
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
schema = CustomSchema(**data)
schemas.append(schema)
except Exception as e:
logger.warning(f"Error cargando schema desde {file_path}: {e}")
continue
# Ordenar por fecha de creación (más reciente primero)
schemas.sort(key=lambda s: s.created_at or "", reverse=True)
logger.info(f"Listados {len(schemas)} schemas")
return schemas
except Exception as e:
logger.error(f"Error listando schemas: {e}")
return []
def list_by_tema(self, tema: str) -> List[CustomSchema]:
"""
Lista schemas disponibles para un tema específico.
Incluye schemas del tema + schemas globales.
Args:
tema: Nombre del tema
Returns:
Lista de schemas aplicables al tema
"""
all_schemas = self.list_all()
filtered = [
schema for schema in all_schemas
if schema.tema == tema or schema.is_global
]
logger.info(f"Encontrados {len(filtered)} schemas para tema '{tema}'")
return filtered
def delete(self, schema_id: str) -> bool:
"""
Elimina un schema.
Args:
schema_id: ID del schema a eliminar
Returns:
True si se eliminó, False si no existía
"""
try:
file_path = self._get_file_path(schema_id)
if not file_path.exists():
logger.warning(f"Intento de eliminar schema inexistente: {schema_id}")
return False
file_path.unlink()
logger.info(f"Schema eliminado: {schema_id}")
return True
except Exception as e:
logger.error(f"Error eliminando schema {schema_id}: {e}")
raise IOError(f"No se pudo eliminar el schema: {str(e)}")
def exists(self, schema_id: str) -> bool:
"""
Verifica si un schema existe.
Args:
schema_id: ID del schema
Returns:
True si existe, False si no
"""
file_path = self._get_file_path(schema_id)
return file_path.exists()
def count(self) -> int:
"""
Cuenta el número total de schemas.
Returns:
Número de schemas
"""
return len(list(self.schemas_dir.glob("*.json")))
def _get_file_path(self, schema_id: str) -> Path:
"""
Obtiene la ruta del archivo para un schema.
Args:
schema_id: ID del schema
Returns:
Path del archivo
"""
# Sanitizar schema_id para evitar path traversal
safe_id = schema_id.replace("/", "_").replace("\\", "_")
return self.schemas_dir / f"{safe_id}.json"
# Singleton factory pattern
_schema_repository: Optional[SchemaRepository] = None
def get_schema_repository() -> SchemaRepository:
"""
Factory para obtener instancia singleton del repositorio.
Returns:
Instancia única de SchemaRepository
Raises:
RuntimeError: Si la configuración no está disponible
"""
global _schema_repository
if _schema_repository is None:
try:
from ..core.config import settings
schemas_dir = getattr(settings, 'SCHEMAS_DIR', None) or "./data/schemas"
_schema_repository = SchemaRepository(Path(schemas_dir))
logger.info("SchemaRepository singleton inicializado")
except Exception as e:
logger.error(f"Error inicializando SchemaRepository: {e}")
raise RuntimeError(f"No se pudo inicializar SchemaRepository: {str(e)}")
return _schema_repository