Persist endpoint observations in SQLite

This commit is contained in:
Lyncoln
2026-09-19 17:40:57 -04:00
parent 8380380d46
commit 3fdcbd1a54
2 changed files with 615 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
import json
import sqlite3
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from datetime import datetime, timezone
from os import PathLike
from typing import Any
from .adapters import NormalizedObject, Observation
_FORBIDDEN_PERSISTED_KEYS = frozenset(
{
"private_key",
"certificate_key",
"certificate",
"dns_provider_credentials",
"secret",
}
)
@dataclass(frozen=True)
class ObservationStatus:
endpoint_id: str
object_type: str
last_attempt_at: datetime
last_success_at: datetime | None
last_error: str | None
@property
def is_live(self) -> bool:
return self.last_error is None
def _utc(value: datetime, label: str) -> datetime:
if value.tzinfo is None:
raise RuntimeError(f"{label} must be timezone-aware")
return value.astimezone(timezone.utc)
def _timestamp(value: datetime, label: str) -> str:
return _utc(value, label).isoformat()
def _parse_timestamp(value: str | None) -> datetime | None:
if value is None:
return None
parsed = datetime.fromisoformat(value)
if parsed.tzinfo is None:
raise RuntimeError(
f"Persisted timestamp is not timezone-aware: {value!r}"
)
return parsed.astimezone(timezone.utc)
def _validate_safe_payload(value: Any, path: str = "payload") -> None:
if isinstance(value, Mapping):
for raw_key, child in value.items():
key = str(raw_key).strip().lower().replace("-", "_")
if key in _FORBIDDEN_PERSISTED_KEYS:
raise RuntimeError(
f"Refusing to persist sensitive field {path}.{key}"
)
_validate_safe_payload(child, f"{path}.{key}")
elif isinstance(value, (list, tuple)):
for index, child in enumerate(value):
_validate_safe_payload(child, f"{path}[{index}]")
def _serialize_payload(payload: Mapping[str, Any]) -> str:
_validate_safe_payload(payload)
try:
return json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
)
except (TypeError, ValueError) as exc:
raise RuntimeError(
f"Object payload is not JSON serializable: {exc}"
) from exc
def _deserialize_payload(value: str) -> dict[str, Any]:
try:
payload = json.loads(value)
except json.JSONDecodeError as exc:
raise RuntimeError("Persisted object payload is invalid JSON") from exc
if not isinstance(payload, dict):
raise RuntimeError("Persisted object payload is not a mapping")
return payload
class SQLiteStateStore:
def __init__(self, path: str | PathLike[str]):
self.path = str(path)
self.connection = sqlite3.connect(self.path)
self.connection.row_factory = sqlite3.Row
self.connection.execute("PRAGMA foreign_keys = ON")
self.connection.execute("PRAGMA busy_timeout = 5000")
if self.path != ":memory:":
self.connection.execute("PRAGMA journal_mode = WAL")
self._initialize()
def _initialize(self) -> None:
with self.connection:
self.connection.executescript(
"""
CREATE TABLE IF NOT EXISTS schema_meta (
version INTEGER NOT NULL
);
INSERT INTO schema_meta(version)
SELECT 1
WHERE NOT EXISTS (
SELECT 1 FROM schema_meta
);
CREATE TABLE IF NOT EXISTS observations (
endpoint_id TEXT NOT NULL,
object_type TEXT NOT NULL,
logical_key TEXT NOT NULL,
local_key TEXT NOT NULL,
payload_json TEXT NOT NULL,
observed_at TEXT NOT NULL,
PRIMARY KEY (
endpoint_id,
object_type,
local_key
)
);
CREATE INDEX IF NOT EXISTS
observations_by_logical_identity
ON observations (
object_type,
logical_key
);
CREATE TABLE IF NOT EXISTS observation_status (
endpoint_id TEXT NOT NULL,
object_type TEXT NOT NULL,
last_attempt_at TEXT NOT NULL,
last_success_at TEXT,
last_error TEXT,
PRIMARY KEY (
endpoint_id,
object_type
)
);
"""
)
def close(self) -> None:
self.connection.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
self.close()
def replace_observations(
self,
endpoint_id: str,
object_type: str,
observations: Iterable[Observation],
snapshot_at: datetime,
) -> None:
rows = list(observations)
snapshot_timestamp = _timestamp(
snapshot_at,
"Observation snapshot timestamp",
)
prepared: list[tuple[str, str, str, str, str, str]] = []
local_keys: set[str] = set()
for observation in rows:
normalized = observation.object
if observation.endpoint_id != endpoint_id:
raise RuntimeError(
"Observation endpoint does not match snapshot endpoint"
)
if normalized.object_type != object_type:
raise RuntimeError(
"Observation object type does not match snapshot type"
)
if normalized.local_key is None:
raise RuntimeError(
"Observed objects require an endpoint-local key"
)
if normalized.local_key in local_keys:
raise RuntimeError(
f"Duplicate endpoint-local key: {normalized.local_key}"
)
local_keys.add(normalized.local_key)
prepared.append(
(
endpoint_id,
object_type,
normalized.logical_key,
normalized.local_key,
_serialize_payload(normalized.payload),
_timestamp(
observation.observed_at,
"Observation timestamp",
),
)
)
with self.connection:
self.connection.execute(
"""
DELETE FROM observations
WHERE endpoint_id = ? AND object_type = ?
""",
(endpoint_id, object_type),
)
self.connection.executemany(
"""
INSERT INTO observations (
endpoint_id,
object_type,
logical_key,
local_key,
payload_json,
observed_at
) VALUES (?, ?, ?, ?, ?, ?)
""",
prepared,
)
self.connection.execute(
"""
INSERT INTO observation_status (
endpoint_id,
object_type,
last_attempt_at,
last_success_at,
last_error
) VALUES (?, ?, ?, ?, NULL)
ON CONFLICT(endpoint_id, object_type) DO UPDATE SET
last_attempt_at = excluded.last_attempt_at,
last_success_at = excluded.last_success_at,
last_error = NULL
""",
(
endpoint_id,
object_type,
snapshot_timestamp,
snapshot_timestamp,
),
)
def record_discovery_failure(
self,
endpoint_id: str,
object_type: str,
error: str,
attempted_at: datetime,
) -> None:
attempted_timestamp = _timestamp(
attempted_at,
"Discovery attempt timestamp",
)
with self.connection:
self.connection.execute(
"""
INSERT INTO observation_status (
endpoint_id,
object_type,
last_attempt_at,
last_success_at,
last_error
) VALUES (?, ?, ?, NULL, ?)
ON CONFLICT(endpoint_id, object_type) DO UPDATE SET
last_attempt_at = excluded.last_attempt_at,
last_error = excluded.last_error
""",
(
endpoint_id,
object_type,
attempted_timestamp,
str(error),
),
)
def load_observations(
self,
object_type: str | None = None,
) -> list[Observation]:
sql = """
SELECT
endpoint_id,
object_type,
logical_key,
local_key,
payload_json,
observed_at
FROM observations
"""
parameters: tuple[str, ...] = ()
if object_type is not None:
sql += " WHERE object_type = ?"
parameters = (object_type,)
sql += """
ORDER BY
endpoint_id,
object_type,
logical_key,
local_key
"""
rows = self.connection.execute(sql, parameters).fetchall()
return [
Observation(
endpoint_id=row["endpoint_id"],
object=NormalizedObject(
object_type=row["object_type"],
logical_key=row["logical_key"],
local_key=row["local_key"],
payload=_deserialize_payload(row["payload_json"]),
),
observed_at=_parse_timestamp(row["observed_at"]),
)
for row in rows
]
def observation_status(
self,
endpoint_id: str,
object_type: str,
) -> ObservationStatus | None:
row = self.connection.execute(
"""
SELECT
endpoint_id,
object_type,
last_attempt_at,
last_success_at,
last_error
FROM observation_status
WHERE endpoint_id = ? AND object_type = ?
""",
(endpoint_id, object_type),
).fetchone()
if row is None:
return None
return ObservationStatus(
endpoint_id=row["endpoint_id"],
object_type=row["object_type"],
last_attempt_at=_parse_timestamp(row["last_attempt_at"]),
last_success_at=_parse_timestamp(row["last_success_at"]),
last_error=row["last_error"],
)