import json from datetime import datetime from decimal import Decimal from pathlib import Path from uuid import UUID from app.errors import AppError from app.sources.base import ( SourceAttribute, SourceOffer, SourcePriceObservation, SourceProduct, SourceVariant, ) def _parse_dt(value: str) -> datetime: return datetime.fromisoformat(value.replace("Z", "+00:00")) class ManualSource: source_key = "manual" def __init__(self, path: Path) -> None: self.path = path self._data = json.loads(path.read_text(encoding="utf-8")) self._products = {item["external_id"]: item for item in self._data["products"]} self._offers = {item["external_id"]: item for item in self._data.get("offers", [])} self._prices = self._data.get("prices", []) def list_external_product_ids(self) -> list[str]: return list(self._products) def get_product(self, external_id: str) -> SourceProduct: raw = self._products.get(external_id) if raw is None: raise AppError(404, "NOT_FOUND", "manual product not found", {"external_id": external_id}) variants = [] for variant in raw["variants"]: attributes = tuple( SourceAttribute( id=UUID(item["id"]), attribute=item["attribute"], raw_value=item["raw_value"], normalized_value=item.get("normalized_value"), unit=item.get("unit"), normalization_status=item["normalization_status"], normalizer_version=item.get("normalizer_version", "manual-v1"), confidence=Decimal(str(item["confidence"])), source_reference=item["source_reference"], observed_at=_parse_dt(item["observed_at"]), normalization_error=item.get("normalization_error"), ) for item in variant["attributes"] ) variants.append( SourceVariant( id=UUID(variant["id"]), name=variant["name"], manufacturer_sku=variant.get("manufacturer_sku"), identifiers=variant.get("identifiers") or {}, attributes=attributes, ) ) return SourceProduct( source_key=self.source_key, external_id=external_id, id=UUID(raw["id"]), category=raw["category"], brand=raw["brand"], name=raw["name"], model=raw.get("model"), variants=tuple(variants), ) def get_offers(self, external_product_id: str) -> list[SourceOffer]: product = self.get_product(external_product_id) variant_ids = {variant.id for variant in product.variants} return [ SourceOffer( id=UUID(item["id"]), variant_id=UUID(item["variant_id"]), external_id=item["external_id"], seller=item["seller"], url=item.get("url"), market=item["market"], currency=item["currency"], condition=item["condition"], ) for item in self._offers.values() if UUID(item["variant_id"]) in variant_ids ] def get_price_history(self, external_offer_id: str) -> list[SourcePriceObservation]: return [ SourcePriceObservation( id=UUID(item["id"]) if item.get("id") else None, external_offer_id=external_offer_id, external_observation_id=item["external_observation_id"], observed_at=_parse_dt(item["observed_at"]), item_price=Decimal(str(item["item_price"])), shipping_price=None if item.get("shipping_price") is None else Decimal(str(item["shipping_price"])), currency=item["currency"], availability=item["availability"], raw_payload=item["raw_payload"], confidence=Decimal(str(item["confidence"])), ) for item in self._prices if item["external_offer_id"] == external_offer_id ]