144 lines
4.4 KiB
Python
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
|