Files
minerops/backend/app/components/repository.py

144 lines
4.4 KiB
Python

"""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