mirror of
				https://github.com/bunny-lab-io/Borealis.git
				synced 2025-10-26 15:41:58 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			190 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """SQLite persistence for site management."""
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import logging
 | |
| import sqlite3
 | |
| import time
 | |
| from contextlib import closing
 | |
| from typing import Dict, Iterable, List, Optional, Sequence
 | |
| 
 | |
| from Data.Engine.domain.sites import SiteDeviceMapping, SiteSummary
 | |
| from Data.Engine.repositories.sqlite.connection import SQLiteConnectionFactory
 | |
| 
 | |
| __all__ = ["SQLiteSiteRepository"]
 | |
| 
 | |
| 
 | |
| class SQLiteSiteRepository:
 | |
|     """Repository exposing site CRUD and device assignment helpers."""
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         connection_factory: SQLiteConnectionFactory,
 | |
|         *,
 | |
|         logger: Optional[logging.Logger] = None,
 | |
|     ) -> None:
 | |
|         self._connections = connection_factory
 | |
|         self._log = logger or logging.getLogger("borealis.engine.repositories.sites")
 | |
| 
 | |
|     def list_sites(self) -> List[SiteSummary]:
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             cur.execute(
 | |
|                 """
 | |
|                 SELECT s.id, s.name, s.description, s.created_at,
 | |
|                        COALESCE(ds.cnt, 0) AS device_count
 | |
|                   FROM sites s
 | |
|              LEFT JOIN (
 | |
|                         SELECT site_id, COUNT(*) AS cnt
 | |
|                           FROM device_sites
 | |
|                       GROUP BY site_id
 | |
|                     ) ds
 | |
|                     ON ds.site_id = s.id
 | |
|               ORDER BY LOWER(s.name) ASC
 | |
|                 """
 | |
|             )
 | |
|             rows = cur.fetchall()
 | |
|         return [self._row_to_site(row) for row in rows]
 | |
| 
 | |
|     def create_site(self, name: str, description: str) -> SiteSummary:
 | |
|         now = int(time.time())
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             try:
 | |
|                 cur.execute(
 | |
|                     "INSERT INTO sites(name, description, created_at) VALUES (?, ?, ?)",
 | |
|                     (name, description, now),
 | |
|                 )
 | |
|             except sqlite3.IntegrityError as exc:
 | |
|                 raise ValueError("duplicate") from exc
 | |
|             site_id = cur.lastrowid
 | |
|             conn.commit()
 | |
| 
 | |
|             cur.execute(
 | |
|                 "SELECT id, name, description, created_at, 0 FROM sites WHERE id = ?",
 | |
|                 (site_id,),
 | |
|             )
 | |
|             row = cur.fetchone()
 | |
|         if not row:
 | |
|             raise RuntimeError("site not found after insert")
 | |
|         return self._row_to_site(row)
 | |
| 
 | |
|     def delete_sites(self, ids: Sequence[int]) -> int:
 | |
|         if not ids:
 | |
|             return 0
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             placeholders = ",".join("?" for _ in ids)
 | |
|             try:
 | |
|                 cur.execute(
 | |
|                     f"DELETE FROM device_sites WHERE site_id IN ({placeholders})",
 | |
|                     tuple(ids),
 | |
|                 )
 | |
|                 cur.execute(
 | |
|                     f"DELETE FROM sites WHERE id IN ({placeholders})",
 | |
|                     tuple(ids),
 | |
|                 )
 | |
|             except sqlite3.DatabaseError as exc:
 | |
|                 conn.rollback()
 | |
|                 raise
 | |
|             deleted = cur.rowcount
 | |
|             conn.commit()
 | |
|         return deleted
 | |
| 
 | |
|     def rename_site(self, site_id: int, new_name: str) -> SiteSummary:
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             try:
 | |
|                 cur.execute("UPDATE sites SET name = ? WHERE id = ?", (new_name, site_id))
 | |
|             except sqlite3.IntegrityError as exc:
 | |
|                 raise ValueError("duplicate") from exc
 | |
|             if cur.rowcount == 0:
 | |
|                 raise LookupError("not_found")
 | |
|             conn.commit()
 | |
|             cur.execute(
 | |
|                 """
 | |
|                 SELECT s.id, s.name, s.description, s.created_at,
 | |
|                        COALESCE(ds.cnt, 0) AS device_count
 | |
|                   FROM sites s
 | |
|              LEFT JOIN (
 | |
|                         SELECT site_id, COUNT(*) AS cnt
 | |
|                           FROM device_sites
 | |
|                       GROUP BY site_id
 | |
|                     ) ds
 | |
|                     ON ds.site_id = s.id
 | |
|                  WHERE s.id = ?
 | |
|                 """,
 | |
|                 (site_id,),
 | |
|             )
 | |
|             row = cur.fetchone()
 | |
|         if not row:
 | |
|             raise LookupError("not_found")
 | |
|         return self._row_to_site(row)
 | |
| 
 | |
|     def map_devices(self, hostnames: Optional[Iterable[str]] = None) -> Dict[str, SiteDeviceMapping]:
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             if hostnames:
 | |
|                 normalized = [hn.strip() for hn in hostnames if hn and hn.strip()]
 | |
|                 if not normalized:
 | |
|                     return {}
 | |
|                 placeholders = ",".join("?" for _ in normalized)
 | |
|                 cur.execute(
 | |
|                     f"""
 | |
|                     SELECT ds.device_hostname, s.id, s.name
 | |
|                       FROM device_sites ds
 | |
|                 INNER JOIN sites s ON s.id = ds.site_id
 | |
|                      WHERE ds.device_hostname IN ({placeholders})
 | |
|                     """,
 | |
|                     tuple(normalized),
 | |
|                 )
 | |
|             else:
 | |
|                 cur.execute(
 | |
|                     """
 | |
|                     SELECT ds.device_hostname, s.id, s.name
 | |
|                       FROM device_sites ds
 | |
|                 INNER JOIN sites s ON s.id = ds.site_id
 | |
|                     """
 | |
|                 )
 | |
|             rows = cur.fetchall()
 | |
|         mapping: Dict[str, SiteDeviceMapping] = {}
 | |
|         for hostname, site_id, site_name in rows:
 | |
|             mapping[str(hostname)] = SiteDeviceMapping(
 | |
|                 hostname=str(hostname),
 | |
|                 site_id=int(site_id) if site_id is not None else None,
 | |
|                 site_name=str(site_name or ""),
 | |
|             )
 | |
|         return mapping
 | |
| 
 | |
|     def assign_devices(self, site_id: int, hostnames: Sequence[str]) -> None:
 | |
|         now = int(time.time())
 | |
|         normalized = [hn.strip() for hn in hostnames if isinstance(hn, str) and hn.strip()]
 | |
|         if not normalized:
 | |
|             return
 | |
|         with closing(self._connections()) as conn:
 | |
|             cur = conn.cursor()
 | |
|             cur.execute("SELECT 1 FROM sites WHERE id = ?", (site_id,))
 | |
|             if not cur.fetchone():
 | |
|                 raise LookupError("not_found")
 | |
|             for hostname in normalized:
 | |
|                 cur.execute(
 | |
|                     """
 | |
|                     INSERT INTO device_sites(device_hostname, site_id, assigned_at)
 | |
|                     VALUES (?, ?, ?)
 | |
|                     ON CONFLICT(device_hostname)
 | |
|                     DO UPDATE SET site_id = excluded.site_id,
 | |
|                                   assigned_at = excluded.assigned_at
 | |
|                     """,
 | |
|                     (hostname, site_id, now),
 | |
|                 )
 | |
|             conn.commit()
 | |
| 
 | |
|     def _row_to_site(self, row: Sequence[object]) -> SiteSummary:
 | |
|         return SiteSummary(
 | |
|             id=int(row[0]),
 | |
|             name=str(row[1] or ""),
 | |
|             description=str(row[2] or ""),
 | |
|             created_at=int(row[3] or 0),
 | |
|             device_count=int(row[4] or 0),
 | |
|         )
 |