Initial commit: CloudRestoreAS v1.0.0 - Aplicación completa de restauración automática SQL Server
This commit is contained in:
1
app/db/__init__.py
Normal file
1
app/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Capa de acceso a datos."""
|
||||
69
app/db/config_repository.py
Normal file
69
app/db/config_repository.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Repositorio para gestionar configuración."""
|
||||
|
||||
from typing import Optional, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
from .database import db
|
||||
|
||||
|
||||
class ConfigRepository:
|
||||
"""Repositorio para operaciones con configuración."""
|
||||
|
||||
@staticmethod
|
||||
def get(key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Obtiene un valor de configuración.
|
||||
|
||||
Args:
|
||||
key: Clave de configuración
|
||||
default: Valor por defecto si no existe
|
||||
|
||||
Returns:
|
||||
Valor de configuración (deserializado de JSON)
|
||||
"""
|
||||
row = db.fetchone("SELECT value FROM config WHERE key = ?", (key,))
|
||||
if not row:
|
||||
return default
|
||||
try:
|
||||
return json.loads(row["value"])
|
||||
except:
|
||||
return row["value"]
|
||||
|
||||
@staticmethod
|
||||
def set(key: str, value: Any):
|
||||
"""
|
||||
Establece un valor de configuración.
|
||||
|
||||
Args:
|
||||
key: Clave de configuración
|
||||
value: Valor a guardar (se serializa a JSON)
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
value_str = json.dumps(value) if not isinstance(value, str) else value
|
||||
|
||||
db.execute("""
|
||||
INSERT OR REPLACE INTO config (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
""", (key, value_str, now))
|
||||
|
||||
@staticmethod
|
||||
def get_all() -> dict:
|
||||
"""
|
||||
Obtiene toda la configuración.
|
||||
|
||||
Returns:
|
||||
Diccionario con todas las claves y valores
|
||||
"""
|
||||
rows = db.fetchall("SELECT key, value FROM config")
|
||||
config = {}
|
||||
for row in rows:
|
||||
try:
|
||||
config[row["key"]] = json.loads(row["value"])
|
||||
except:
|
||||
config[row["key"]] = row["value"]
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def delete(key: str):
|
||||
"""Elimina una clave de configuración."""
|
||||
db.execute("DELETE FROM config WHERE key = ?", (key,))
|
||||
140
app/db/database.py
Normal file
140
app/db/database.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Esquema y gestión de la base de datos SQLite."""
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from ..constants import DB_PATH
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
"""Gestor de la base de datos SQLite."""
|
||||
|
||||
def __init__(self, db_path: Optional[Path] = None):
|
||||
"""
|
||||
Inicializa el gestor de base de datos.
|
||||
|
||||
Args:
|
||||
db_path: Ruta a la base de datos (usa DB_PATH por defecto)
|
||||
"""
|
||||
self.db_path = db_path or DB_PATH
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._initialize_schema()
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
"""Obtiene una conexión a la base de datos."""
|
||||
conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _initialize_schema(self):
|
||||
"""Crea las tablas si no existen."""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Tabla de jobs
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
job_id TEXT PRIMARY KEY,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
source_name TEXT NOT NULL,
|
||||
source_hash TEXT,
|
||||
node_name TEXT,
|
||||
db_name TEXT,
|
||||
status TEXT NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
last_error TEXT,
|
||||
started_at TEXT,
|
||||
finished_at TEXT,
|
||||
total_ms INTEGER,
|
||||
extract_ms INTEGER,
|
||||
restore_ms INTEGER,
|
||||
filelist_ms INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# Tabla de pasos de jobs
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS job_steps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id TEXT NOT NULL,
|
||||
step TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT,
|
||||
duration_ms INTEGER,
|
||||
exit_code INTEGER,
|
||||
stdout TEXT,
|
||||
stderr TEXT,
|
||||
error TEXT,
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(job_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Tabla de nodos (mapeo node_name -> db_name)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
node_name TEXT PRIMARY KEY,
|
||||
db_name TEXT NOT NULL,
|
||||
active INTEGER DEFAULT 1,
|
||||
notes TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Tabla de eventos
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
job_id TEXT,
|
||||
message TEXT NOT NULL,
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(job_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Tabla de configuración
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Índices
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_created ON jobs(created_at)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_events_job ON events(job_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_job_steps_job ON job_steps(job_id)")
|
||||
|
||||
conn.commit()
|
||||
|
||||
def execute(self, query: str, params: tuple = ()) -> sqlite3.Cursor:
|
||||
"""Ejecuta una consulta SQL."""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor
|
||||
|
||||
def fetchone(self, query: str, params: tuple = ()) -> Optional[sqlite3.Row]:
|
||||
"""Ejecuta una consulta y devuelve una fila."""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchone()
|
||||
|
||||
def fetchall(self, query: str, params: tuple = ()) -> list[sqlite3.Row]:
|
||||
"""Ejecuta una consulta y devuelve todas las filas."""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
# Instancia global
|
||||
db = DatabaseManager()
|
||||
77
app/db/event_repository.py
Normal file
77
app/db/event_repository.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Repositorio para gestionar eventos."""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from .database import db
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
"""Modelo de Event."""
|
||||
id: int
|
||||
created_at: str
|
||||
level: str
|
||||
job_id: Optional[str]
|
||||
message: str
|
||||
|
||||
|
||||
class EventRepository:
|
||||
"""Repositorio para operaciones con eventos."""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
level: str,
|
||||
message: str,
|
||||
job_id: Optional[str] = None
|
||||
) -> int:
|
||||
"""
|
||||
Crea un nuevo evento.
|
||||
|
||||
Args:
|
||||
level: Nivel del evento (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
message: Mensaje del evento
|
||||
job_id: ID del job asociado (opcional)
|
||||
|
||||
Returns:
|
||||
ID del evento creado
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
cursor = db.execute("""
|
||||
INSERT INTO events (created_at, level, job_id, message)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (now, level, job_id, message))
|
||||
|
||||
return cursor.lastrowid
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
level: Optional[str] = None,
|
||||
job_id: Optional[str] = None,
|
||||
limit: int = 100
|
||||
) -> List[Event]:
|
||||
"""
|
||||
Obtiene eventos con filtros opcionales.
|
||||
|
||||
Args:
|
||||
level: Filtrar por nivel
|
||||
job_id: Filtrar por job
|
||||
limit: Límite de resultados
|
||||
|
||||
Returns:
|
||||
Lista de eventos
|
||||
"""
|
||||
query = "SELECT * FROM events WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if level:
|
||||
query += " AND level = ?"
|
||||
params.append(level)
|
||||
if job_id:
|
||||
query += " AND job_id = ?"
|
||||
params.append(job_id)
|
||||
|
||||
query += f" ORDER BY created_at DESC LIMIT {limit}"
|
||||
|
||||
rows = db.fetchall(query, tuple(params))
|
||||
return [Event(**dict(row)) for row in rows]
|
||||
232
app/db/job_repository.py
Normal file
232
app/db/job_repository.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""Repositorio para gestionar jobs."""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
import uuid
|
||||
from .database import db
|
||||
from ..constants import JobStatus
|
||||
|
||||
|
||||
@dataclass
|
||||
class Job:
|
||||
"""Modelo de Job."""
|
||||
job_id: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
source_path: str
|
||||
source_name: str
|
||||
source_hash: Optional[str]
|
||||
node_name: Optional[str]
|
||||
db_name: Optional[str]
|
||||
status: str
|
||||
attempts: int
|
||||
last_error: Optional[str]
|
||||
started_at: Optional[str]
|
||||
finished_at: Optional[str]
|
||||
total_ms: Optional[int]
|
||||
extract_ms: Optional[int]
|
||||
restore_ms: Optional[int]
|
||||
filelist_ms: Optional[int]
|
||||
|
||||
|
||||
class JobRepository:
|
||||
"""Repositorio para operaciones con jobs."""
|
||||
|
||||
@staticmethod
|
||||
def create(source_path: str, source_name: str, source_hash: str) -> str:
|
||||
"""
|
||||
Crea un nuevo job.
|
||||
|
||||
Args:
|
||||
source_path: Ruta completa del archivo fuente
|
||||
source_name: Nombre del archivo
|
||||
source_hash: Hash del archivo
|
||||
|
||||
Returns:
|
||||
job_id del job creado
|
||||
"""
|
||||
job_id = str(uuid.uuid4())
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
db.execute("""
|
||||
INSERT INTO jobs (
|
||||
job_id, created_at, updated_at, source_path, source_name,
|
||||
source_hash, status, attempts
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (job_id, now, now, source_path, source_name, source_hash, JobStatus.QUEUED, 0))
|
||||
|
||||
return job_id
|
||||
|
||||
@staticmethod
|
||||
def get(job_id: str) -> Optional[Job]:
|
||||
"""Obtiene un job por ID."""
|
||||
row = db.fetchone("SELECT * FROM jobs WHERE job_id = ?", (job_id,))
|
||||
if not row:
|
||||
return None
|
||||
return Job(**dict(row))
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
status: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: int = 0
|
||||
) -> List[Job]:
|
||||
"""
|
||||
Obtiene todos los jobs con filtros opcionales.
|
||||
|
||||
Args:
|
||||
status: Filtrar por estado
|
||||
limit: Límite de resultados
|
||||
offset: Desplazamiento
|
||||
|
||||
Returns:
|
||||
Lista de jobs
|
||||
"""
|
||||
query = "SELECT * FROM jobs"
|
||||
params = []
|
||||
|
||||
if status:
|
||||
query += " WHERE status = ?"
|
||||
params.append(status)
|
||||
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
if limit:
|
||||
query += f" LIMIT {limit} OFFSET {offset}"
|
||||
|
||||
rows = db.fetchall(query, tuple(params))
|
||||
return [Job(**dict(row)) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
def update_status(
|
||||
job_id: str,
|
||||
status: str,
|
||||
error: Optional[str] = None,
|
||||
increment_attempts: bool = False
|
||||
):
|
||||
"""Actualiza el estado de un job."""
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
updates = ["status = ?", "updated_at = ?"]
|
||||
params = [status, now]
|
||||
|
||||
if error is not None:
|
||||
updates.append("last_error = ?")
|
||||
params.append(error)
|
||||
|
||||
if increment_attempts:
|
||||
updates.append("attempts = attempts + 1")
|
||||
|
||||
if status == JobStatus.EXTRACTING and not db.fetchone(
|
||||
"SELECT started_at FROM jobs WHERE job_id = ?", (job_id,)
|
||||
)["started_at"]:
|
||||
updates.append("started_at = ?")
|
||||
params.append(now)
|
||||
|
||||
if status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED]:
|
||||
updates.append("finished_at = ?")
|
||||
params.append(now)
|
||||
|
||||
params.append(job_id)
|
||||
|
||||
db.execute(
|
||||
f"UPDATE jobs SET {', '.join(updates)} WHERE job_id = ?",
|
||||
tuple(params)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_node_and_db(job_id: str, node_name: str, db_name: str):
|
||||
"""Actualiza el nodo y base de datos de un job."""
|
||||
now = datetime.utcnow().isoformat()
|
||||
db.execute(
|
||||
"UPDATE jobs SET node_name = ?, db_name = ?, updated_at = ? WHERE job_id = ?",
|
||||
(node_name, db_name, now, job_id)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_timing(
|
||||
job_id: str,
|
||||
total_ms: Optional[int] = None,
|
||||
extract_ms: Optional[int] = None,
|
||||
restore_ms: Optional[int] = None,
|
||||
filelist_ms: Optional[int] = None
|
||||
):
|
||||
"""Actualiza los tiempos de ejecución."""
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if total_ms is not None:
|
||||
updates.append("total_ms = ?")
|
||||
params.append(total_ms)
|
||||
if extract_ms is not None:
|
||||
updates.append("extract_ms = ?")
|
||||
params.append(extract_ms)
|
||||
if restore_ms is not None:
|
||||
updates.append("restore_ms = ?")
|
||||
params.append(restore_ms)
|
||||
if filelist_ms is not None:
|
||||
updates.append("filelist_ms = ?")
|
||||
params.append(filelist_ms)
|
||||
|
||||
if updates:
|
||||
params.append(job_id)
|
||||
db.execute(
|
||||
f"UPDATE jobs SET {', '.join(updates)} WHERE job_id = ?",
|
||||
tuple(params)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def exists_by_hash(source_hash: str) -> bool:
|
||||
"""Verifica si existe un job con el hash dado."""
|
||||
row = db.fetchone(
|
||||
"SELECT COUNT(*) as count FROM jobs WHERE source_hash = ?",
|
||||
(source_hash,)
|
||||
)
|
||||
return row["count"] > 0
|
||||
|
||||
@staticmethod
|
||||
def get_stats() -> dict:
|
||||
"""Obtiene estadísticas de jobs."""
|
||||
stats = {
|
||||
"total": 0,
|
||||
"queued": 0,
|
||||
"running": 0,
|
||||
"completed": 0,
|
||||
"failed": 0
|
||||
}
|
||||
|
||||
rows = db.fetchall("SELECT status, COUNT(*) as count FROM jobs GROUP BY status")
|
||||
for row in rows:
|
||||
status = row["status"]
|
||||
count = row["count"]
|
||||
stats["total"] += count
|
||||
|
||||
if status == JobStatus.QUEUED:
|
||||
stats["queued"] = count
|
||||
elif status in [JobStatus.EXTRACTING, JobStatus.RESTORING, JobStatus.CLEANING]:
|
||||
stats["running"] += count
|
||||
elif status == JobStatus.COMPLETED:
|
||||
stats["completed"] = count
|
||||
elif status in [JobStatus.FAILED, JobStatus.FAILED_RESTART, JobStatus.CANCELLED]:
|
||||
stats["failed"] += count
|
||||
|
||||
return stats
|
||||
|
||||
@staticmethod
|
||||
def get_average_times() -> dict:
|
||||
"""Obtiene tiempos promedio de ejecución."""
|
||||
row = db.fetchone("""
|
||||
SELECT
|
||||
AVG(total_ms) as avg_total,
|
||||
AVG(extract_ms) as avg_extract,
|
||||
AVG(restore_ms) as avg_restore
|
||||
FROM jobs
|
||||
WHERE status = ? AND total_ms IS NOT NULL
|
||||
""", (JobStatus.COMPLETED,))
|
||||
|
||||
return {
|
||||
"total_ms": int(row["avg_total"] or 0),
|
||||
"extract_ms": int(row["avg_extract"] or 0),
|
||||
"restore_ms": int(row["avg_restore"] or 0)
|
||||
}
|
||||
81
app/db/job_step_repository.py
Normal file
81
app/db/job_step_repository.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Repositorio para gestionar job steps."""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from .database import db
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobStep:
|
||||
"""Modelo de JobStep."""
|
||||
id: int
|
||||
job_id: str
|
||||
step: str
|
||||
started_at: str
|
||||
finished_at: Optional[str]
|
||||
duration_ms: Optional[int]
|
||||
exit_code: Optional[int]
|
||||
stdout: Optional[str]
|
||||
stderr: Optional[str]
|
||||
error: Optional[str]
|
||||
|
||||
|
||||
class JobStepRepository:
|
||||
"""Repositorio para operaciones con job steps."""
|
||||
|
||||
@staticmethod
|
||||
def create(job_id: str, step: str) -> int:
|
||||
"""
|
||||
Crea un nuevo step para un job.
|
||||
|
||||
Args:
|
||||
job_id: ID del job
|
||||
step: Nombre del paso
|
||||
|
||||
Returns:
|
||||
ID del step creado
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
cursor = db.execute("""
|
||||
INSERT INTO job_steps (job_id, step, started_at)
|
||||
VALUES (?, ?, ?)
|
||||
""", (job_id, step, now))
|
||||
|
||||
return cursor.lastrowid
|
||||
|
||||
@staticmethod
|
||||
def complete(
|
||||
step_id: int,
|
||||
exit_code: Optional[int] = None,
|
||||
stdout: Optional[str] = None,
|
||||
stderr: Optional[str] = None,
|
||||
error: Optional[str] = None
|
||||
):
|
||||
"""Marca un step como completado."""
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
# Obtener tiempo de inicio para calcular duración
|
||||
row = db.fetchone("SELECT started_at FROM job_steps WHERE id = ?", (step_id,))
|
||||
if row:
|
||||
started = datetime.fromisoformat(row["started_at"])
|
||||
finished = datetime.fromisoformat(now)
|
||||
duration_ms = int((finished - started).total_seconds() * 1000)
|
||||
else:
|
||||
duration_ms = None
|
||||
|
||||
db.execute("""
|
||||
UPDATE job_steps
|
||||
SET finished_at = ?, duration_ms = ?, exit_code = ?,
|
||||
stdout = ?, stderr = ?, error = ?
|
||||
WHERE id = ?
|
||||
""", (now, duration_ms, exit_code, stdout, stderr, error, step_id))
|
||||
|
||||
@staticmethod
|
||||
def get_by_job(job_id: str) -> List[JobStep]:
|
||||
"""Obtiene todos los steps de un job."""
|
||||
rows = db.fetchall(
|
||||
"SELECT * FROM job_steps WHERE job_id = ? ORDER BY id ASC",
|
||||
(job_id,)
|
||||
)
|
||||
return [JobStep(**dict(row)) for row in rows]
|
||||
133
app/db/node_repository.py
Normal file
133
app/db/node_repository.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Repositorio para gestionar nodos."""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from .database import db
|
||||
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
"""Modelo de Node."""
|
||||
node_name: str
|
||||
db_name: str
|
||||
active: bool
|
||||
notes: Optional[str]
|
||||
updated_at: str
|
||||
|
||||
|
||||
class NodeRepository:
|
||||
"""Repositorio para operaciones con nodos."""
|
||||
|
||||
@staticmethod
|
||||
def create(node_name: str, db_name: str, notes: Optional[str] = None):
|
||||
"""
|
||||
Crea o actualiza un nodo.
|
||||
|
||||
Args:
|
||||
node_name: Nombre del nodo (case-insensitive)
|
||||
db_name: Nombre de la base de datos
|
||||
notes: Notas opcionales
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
node_name_upper = node_name.upper()
|
||||
|
||||
db.execute("""
|
||||
INSERT OR REPLACE INTO nodes (node_name, db_name, active, notes, updated_at)
|
||||
VALUES (?, ?, 1, ?, ?)
|
||||
""", (node_name_upper, db_name, notes, now))
|
||||
|
||||
@staticmethod
|
||||
def get(node_name: str) -> Optional[Node]:
|
||||
"""Obtiene un nodo por nombre (case-insensitive)."""
|
||||
node_name_upper = node_name.upper()
|
||||
row = db.fetchone("SELECT * FROM nodes WHERE node_name = ?", (node_name_upper,))
|
||||
if not row:
|
||||
return None
|
||||
return Node(
|
||||
node_name=row["node_name"],
|
||||
db_name=row["db_name"],
|
||||
active=bool(row["active"]),
|
||||
notes=row["notes"],
|
||||
updated_at=row["updated_at"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all(active_only: bool = False) -> List[Node]:
|
||||
"""
|
||||
Obtiene todos los nodos.
|
||||
|
||||
Args:
|
||||
active_only: Si True, solo devuelve nodos activos
|
||||
|
||||
Returns:
|
||||
Lista de nodos
|
||||
"""
|
||||
query = "SELECT * FROM nodes"
|
||||
if active_only:
|
||||
query += " WHERE active = 1"
|
||||
query += " ORDER BY node_name"
|
||||
|
||||
rows = db.fetchall(query)
|
||||
return [Node(
|
||||
node_name=row["node_name"],
|
||||
db_name=row["db_name"],
|
||||
active=bool(row["active"]),
|
||||
notes=row["notes"],
|
||||
updated_at=row["updated_at"]
|
||||
) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
node_name: str,
|
||||
db_name: Optional[str] = None,
|
||||
active: Optional[bool] = None,
|
||||
notes: Optional[str] = None
|
||||
):
|
||||
"""Actualiza un nodo."""
|
||||
node_name_upper = node_name.upper()
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if db_name is not None:
|
||||
updates.append("db_name = ?")
|
||||
params.append(db_name)
|
||||
if active is not None:
|
||||
updates.append("active = ?")
|
||||
params.append(1 if active else 0)
|
||||
if notes is not None:
|
||||
updates.append("notes = ?")
|
||||
params.append(notes)
|
||||
|
||||
if updates:
|
||||
now = datetime.utcnow().isoformat()
|
||||
updates.append("updated_at = ?")
|
||||
params.append(now)
|
||||
params.append(node_name_upper)
|
||||
|
||||
db.execute(
|
||||
f"UPDATE nodes SET {', '.join(updates)} WHERE node_name = ?",
|
||||
tuple(params)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(node_name: str):
|
||||
"""Elimina un nodo."""
|
||||
node_name_upper = node_name.upper()
|
||||
db.execute("DELETE FROM nodes WHERE node_name = ?", (node_name_upper,))
|
||||
|
||||
@staticmethod
|
||||
def get_db_for_node(node_name: str) -> Optional[str]:
|
||||
"""
|
||||
Obtiene el nombre de DB para un nodo (si está activo).
|
||||
|
||||
Args:
|
||||
node_name: Nombre del nodo
|
||||
|
||||
Returns:
|
||||
Nombre de la DB o None si no existe o está inactivo
|
||||
"""
|
||||
node = NodeRepository.get(node_name)
|
||||
if node and node.active:
|
||||
return node.db_name
|
||||
return None
|
||||
Reference in New Issue
Block a user