"""PostgreSQL persistence for the minimal component registry.""" from collections.abc import Callable, Iterator from contextlib import contextmanager from uuid import UUID import psycopg from psycopg.errors import UniqueViolation from psycopg.rows import dict_row from app.components.exceptions import DuplicateComponentAssetID from app.config import settings ConnectionFactory = Callable[[], psycopg.Connection] COMPONENT_ASSET_ID_CONSTRAINT = "uq_components_normalized_asset_id" def default_connection_factory() -> psycopg.Connection: return psycopg.connect( host=settings.postgres_host, port=settings.postgres_port, dbname=settings.postgres_db, user=settings.postgres_user, password=settings.postgres_password, connect_timeout=5, ) class ComponentRepository: """Persistence boundary for minimal component identity.""" def __init__( self, connection_factory: ConnectionFactory | None = None, *, connection: psycopg.Connection | None = None, ): self._connection_factory = connection_factory or default_connection_factory self._connection = connection @contextmanager def transaction(self) -> Iterator["ComponentRepository"]: if self._connection is not None: yield self return connection = self._connection_factory() repository = ComponentRepository( self._connection_factory, connection=connection, ) try: yield repository connection.commit() except UniqueViolation as exc: connection.rollback() if exc.diag.constraint_name == COMPONENT_ASSET_ID_CONSTRAINT: raise DuplicateComponentAssetID("normalized Asset ID") from exc raise except Exception: connection.rollback() raise finally: connection.close() @contextmanager def _connection_scope(self) -> Iterator[psycopg.Connection]: if self._connection is not None: yield self._connection return connection = self._connection_factory() try: yield connection finally: connection.close() def create_component( self, *, component_type: str, asset_id: str | None, manufacturer: str | None, model: str | None, manufacturer_serial: str | None, created_by: UUID, ) -> dict: with self._connection_scope() as connection: with connection.cursor(row_factory=dict_row) as cur: cur.execute( """ INSERT INTO components ( component_type, asset_id, manufacturer, model, manufacturer_serial, created_by ) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id AS component_id, component_type, asset_id, manufacturer, model, manufacturer_serial, created_at, created_by """, ( component_type, asset_id, manufacturer, model, manufacturer_serial, created_by, ), ) return dict(cur.fetchone()) def get_component(self, component_id: UUID) -> dict | None: with self._connection_scope() as connection: with connection.cursor(row_factory=dict_row) as cur: cur.execute( """ SELECT id AS component_id, component_type, asset_id, manufacturer, model, manufacturer_serial, created_at, created_by FROM components WHERE id = %s """, (component_id,), ) row = cur.fetchone() return dict(row) if row else None