from datetime import datetime from typing import Any from uuid import UUID from sqlalchemy import func, select from sqlalchemy.orm import Session from app.domain.models import AttributeDefinition, AttributeStatement, Offer, PriceObservation, Product, ProductVariant from app.domain.validation import parse_profile, parse_scoring_model from app.persistence import models as orm from app.persistence.mapping import to_attribute, to_definition, to_observation, to_offer, to_product, to_variant class CatalogRepository: def __init__(self, session: Session) -> None: self.session = session def get_definition(self, key: str) -> AttributeDefinition | None: row = self.session.get(orm.AttributeDefinitionRecord, key) return None if row is None else to_definition(row) def list_definitions(self, limit: int, offset: int) -> tuple[list[AttributeDefinition], int]: total = self.session.scalar(select(func.count()).select_from(orm.AttributeDefinitionRecord)) or 0 rows = self.session.scalars( select(orm.AttributeDefinitionRecord).order_by(orm.AttributeDefinitionRecord.key).limit(limit).offset(offset) ).all() return [to_definition(row) for row in rows], total def add_definition(self, row: orm.AttributeDefinitionRecord) -> AttributeDefinition: self.session.add(row) self.session.flush() return to_definition(row) def list_sources(self) -> list[orm.SourceRecord]: return list(self.session.scalars(select(orm.SourceRecord).order_by(orm.SourceRecord.id)).all()) def get_source(self, source_id: str) -> orm.SourceRecord | None: return self.session.get(orm.SourceRecord, source_id) def get_scoring_model(self, version: str): row = self.session.get(orm.ScoringModelRecord, version) return None if row is None else parse_scoring_model(row.definition) def scoring_versions(self) -> set[str]: return set(self.session.scalars(select(orm.ScoringModelRecord.version)).all()) def all_definitions(self) -> dict[str, AttributeDefinition]: rows = self.session.scalars(select(orm.AttributeDefinitionRecord)).all() return {row.key: to_definition(row) for row in rows} def get_profile(self, profile_id: str, version: int): row = self.session.get(orm.ProductProfileRecord, (profile_id, version)) if row is None: return None model = self.get_scoring_model(row.scoring_version) if model is None: return None return parse_profile( row.definition, attribute_definitions=self.all_definitions(), scoring_versions=self.scoring_versions(), supported_strategies=set(model.supported_preference_strategies), ) def add_profile(self, row: orm.ProductProfileRecord): self.session.add(row) self.session.flush() return row def list_profiles(self, limit: int, offset: int) -> tuple[list[orm.ProductProfileRecord], int]: total = self.session.scalar(select(func.count()).select_from(orm.ProductProfileRecord)) or 0 rows = self.session.scalars( select(orm.ProductProfileRecord) .order_by(orm.ProductProfileRecord.id, orm.ProductProfileRecord.version) .limit(limit) .offset(offset) ).all() return list(rows), total def get_product(self, product_id: UUID) -> Product | None: row = self.session.get(orm.ProductRecord, product_id) return None if row is None else to_product(row) def add_product(self, row: orm.ProductRecord) -> Product: self.session.add(row) self.session.flush() return to_product(row) def get_variant(self, variant_id: UUID) -> ProductVariant | None: row = self.session.get(orm.ProductVariantRecord, variant_id) return None if row is None else to_variant(row) def add_variant(self, row: orm.ProductVariantRecord) -> ProductVariant: self.session.add(row) self.session.flush() return to_variant(row) def list_attributes(self, variant_id: UUID) -> list[AttributeStatement]: rows = self.session.scalars( select(orm.VariantAttributeRecord) .where(orm.VariantAttributeRecord.variant_id == variant_id) .order_by( orm.VariantAttributeRecord.observed_at, orm.VariantAttributeRecord.ingested_at, orm.VariantAttributeRecord.id, ) ).all() return [to_attribute(row) for row in rows] def get_attribute(self, attribute_id: UUID) -> AttributeStatement | None: row = self.session.get(orm.VariantAttributeRecord, attribute_id) return None if row is None else to_attribute(row) def add_attribute(self, row: orm.VariantAttributeRecord) -> AttributeStatement: self.session.add(row) self.session.flush() return to_attribute(row) def get_offer(self, offer_id: UUID) -> Offer | None: row = self.session.get(orm.OfferRecord, offer_id) return None if row is None else to_offer(row) def get_offer_by_external(self, source_id: str, external_id: str) -> Offer | None: row = self.session.scalar( select(orm.OfferRecord).where( orm.OfferRecord.source_id == source_id, orm.OfferRecord.external_id == external_id, ) ) return None if row is None else to_offer(row) def add_offer(self, row: orm.OfferRecord) -> Offer: self.session.add(row) self.session.flush() return to_offer(row) def list_observations(self, offer_id: UUID, limit: int | None = None, offset: int = 0) -> tuple[list[PriceObservation], int]: total = ( self.session.scalar( select(func.count()) .select_from(orm.PriceObservationRecord) .where(orm.PriceObservationRecord.offer_id == offer_id) ) or 0 ) query = ( select(orm.PriceObservationRecord) .where(orm.PriceObservationRecord.offer_id == offer_id) .order_by( orm.PriceObservationRecord.observed_at, orm.PriceObservationRecord.ingested_at, orm.PriceObservationRecord.id, ) .offset(offset) ) if limit is not None: query = query.limit(limit) rows = self.session.scalars(query).all() return [to_observation(row) for row in rows], total def get_observation_by_external(self, offer_id: UUID, external_id: str) -> orm.PriceObservationRecord | None: return self.session.scalar( select(orm.PriceObservationRecord).where( orm.PriceObservationRecord.offer_id == offer_id, orm.PriceObservationRecord.external_observation_id == external_id, ) ) def add_observation(self, row: orm.PriceObservationRecord) -> PriceObservation: self.session.add(row) self.session.flush() return to_observation(row) def get_evaluation(self, evaluation_id: UUID) -> orm.EvaluationRecord | None: return self.session.get(orm.EvaluationRecord, evaluation_id) def add_evaluation(self, row: orm.EvaluationRecord) -> orm.EvaluationRecord: self.session.add(row) self.session.flush() return row def add_component(self, row: orm.EvaluationComponentRecord) -> None: self.session.add(row)