164 lines
5.5 KiB
Python
164 lines
5.5 KiB
Python
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from uuid import UUID, uuid4
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("GEIZHALS_USE_FIXTURES", "1")
|
|
|
|
from app.domain.enums import Availability, NormalizationStatus, OfferCondition, ValueType
|
|
from app.domain.models import (
|
|
AttributeDefinition,
|
|
AttributeStatement,
|
|
Offer,
|
|
PriceObservation,
|
|
Product,
|
|
ProductVariant,
|
|
)
|
|
from app.domain.validation import parse_profile, parse_scoring_model
|
|
|
|
from app.settings import get_settings
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _geizhals_fixtures(monkeypatch):
|
|
monkeypatch.setenv("GEIZHALS_USE_FIXTURES", "1")
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
AS_OF = datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc)
|
|
CUTOFF = datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
@pytest.fixture
|
|
def scoring_model():
|
|
definition = json.loads((ROOT / "rules" / "generic-v1.json").read_text(encoding="utf-8"))
|
|
return parse_scoring_model(definition)
|
|
|
|
|
|
@pytest.fixture
|
|
def definitions() -> dict[str, AttributeDefinition]:
|
|
return {
|
|
"ram_gb": AttributeDefinition("ram_gb", ValueType.NUMBER, "GB", Decimal("0"), None, None),
|
|
"storage_gb": AttributeDefinition("storage_gb", ValueType.NUMBER, "GB", Decimal("0"), None, None),
|
|
"weight_kg": AttributeDefinition("weight_kg", ValueType.NUMBER, "kg", Decimal("0"), None, None),
|
|
"battery_score": AttributeDefinition("battery_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None),
|
|
"display_score": AttributeDefinition("display_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None),
|
|
"cpu_score": AttributeDefinition("cpu_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None),
|
|
"ports": AttributeDefinition("ports", ValueType.STRING_LIST, None, None, None, None),
|
|
"color": AttributeDefinition("color", ValueType.STRING, None, None, None, None),
|
|
"has_lte": AttributeDefinition("has_lte", ValueType.BOOLEAN, None, None, None, None),
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def profile(scoring_model, definitions):
|
|
raw = json.loads((ROOT / "profiles" / "mobile-development-notebook.json").read_text(encoding="utf-8"))
|
|
return parse_profile(
|
|
raw,
|
|
attribute_definitions=definitions,
|
|
scoring_versions={scoring_model.version},
|
|
supported_strategies=set(scoring_model.supported_preference_strategies),
|
|
)
|
|
|
|
|
|
def utc(year, month, day, hour=10, minute=0) -> datetime:
|
|
return datetime(year, month, day, hour, minute, tzinfo=timezone.utc)
|
|
|
|
|
|
def statement(attribute: str, value, *, confidence="1.0", status="normalized", observed=None, ingested=None, row_id=None):
|
|
return AttributeStatement(
|
|
id=row_id or uuid4(),
|
|
variant_id=UUID("22222222-2222-4222-8222-222222222222"),
|
|
attribute=attribute,
|
|
raw_value=value,
|
|
normalized_value=None if status != "normalized" else value,
|
|
unit="GB" if attribute.endswith("_gb") else ("kg" if attribute.endswith("_kg") else None),
|
|
normalization_status=NormalizationStatus(status),
|
|
normalizer_version="manual-v1",
|
|
confidence=Decimal(str(confidence)),
|
|
source_id="manual",
|
|
source_reference=f"test/{attribute}",
|
|
observed_at=observed or utc(2026, 10, 3, 9),
|
|
ingested_at=ingested or utc(2026, 10, 3, 9, 1),
|
|
)
|
|
|
|
|
|
def product_a() -> Product:
|
|
return Product(
|
|
id=UUID("11111111-1111-4111-8111-111111111111"),
|
|
category="notebook",
|
|
brand="Example",
|
|
name="Example Mobile 13",
|
|
model="EM13",
|
|
created_at=utc(2026, 10, 1, 8),
|
|
)
|
|
|
|
|
|
def variant_a() -> ProductVariant:
|
|
return ProductVariant(
|
|
id=UUID("22222222-2222-4222-8222-222222222222"),
|
|
product_id=UUID("11111111-1111-4111-8111-111111111111"),
|
|
name="32/1024",
|
|
manufacturer_sku="EM13-32-1T",
|
|
identifiers={},
|
|
created_at=utc(2026, 10, 1, 8),
|
|
)
|
|
|
|
|
|
def offer_a() -> Offer:
|
|
return Offer(
|
|
id=UUID("33333333-3333-4333-8333-333333333333"),
|
|
variant_id=UUID("22222222-2222-4222-8222-222222222222"),
|
|
source_id="manual",
|
|
external_id="demo-offer-a",
|
|
seller="DemoShop",
|
|
url=None,
|
|
market="DE",
|
|
currency="EUR",
|
|
condition=OfferCondition.NEW,
|
|
created_at=utc(2026, 10, 1, 8),
|
|
)
|
|
|
|
|
|
def observation(day, total, *, offer_id=None, obs_id=None, availability="in_stock", confidence="1.0", hour=10):
|
|
return PriceObservation(
|
|
id=obs_id or uuid4(),
|
|
offer_id=offer_id or UUID("33333333-3333-4333-8333-333333333333"),
|
|
source_id="manual",
|
|
external_observation_id=str(obs_id or uuid4()),
|
|
observed_at=utc(2026, day[0], day[1], hour) if isinstance(day, tuple) else day,
|
|
ingested_at=utc(2026, 10, 3, 11),
|
|
item_price=Decimal(str(total)),
|
|
shipping_price=Decimal("0.00"),
|
|
currency="EUR",
|
|
availability=Availability(availability),
|
|
raw_payload={"total": str(total)},
|
|
confidence=Decimal(str(confidence)),
|
|
)
|
|
|
|
|
|
def case_a_attributes():
|
|
return (
|
|
statement("ram_gb", 32),
|
|
statement("storage_gb", 1024),
|
|
statement("weight_kg", Decimal("1.2")),
|
|
statement("battery_score", 90),
|
|
statement("display_score", 90),
|
|
statement("cpu_score", 90),
|
|
)
|
|
|
|
|
|
def case_a_prices():
|
|
return (
|
|
observation((9, 1), "1599.00"),
|
|
observation((9, 15), "1599.00"),
|
|
observation((10, 1), "1599.00"),
|
|
observation((10, 3), "1298.00"),
|
|
)
|