139 lines
5.5 KiB
Python
139 lines
5.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import urllib.error
|
||
|
|
import urllib.request
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
PROFILE = json.loads((ROOT / "profiles" / "mobile-development-notebook.json").read_text(encoding="utf-8"))
|
||
|
|
FIXTURE = json.loads((ROOT / "fixtures" / "manual-demo.json").read_text(encoding="utf-8"))
|
||
|
|
BASE = "http://127.0.0.1:8000"
|
||
|
|
AS_OF = "2026-10-03T12:00:00Z"
|
||
|
|
|
||
|
|
DEFINITIONS = [
|
||
|
|
{"key": "ram_gb", "value_type": "number", "canonical_unit": "GB", "minimum": 0, "description": "RAM in GB"},
|
||
|
|
{"key": "storage_gb", "value_type": "number", "canonical_unit": "GB", "minimum": 0, "description": "Storage in GB"},
|
||
|
|
{"key": "weight_kg", "value_type": "number", "canonical_unit": "kg", "minimum": 0, "description": "Weight in kg"},
|
||
|
|
{"key": "battery_score", "value_type": "number", "minimum": 0, "maximum": 100, "description": "Manual battery score"},
|
||
|
|
{"key": "display_score", "value_type": "number", "minimum": 0, "maximum": 100, "description": "Manual display score"},
|
||
|
|
{"key": "cpu_score", "value_type": "number", "minimum": 0, "maximum": 100, "description": "Manual CPU score"},
|
||
|
|
{"key": "ports", "value_type": "string_list", "description": "Ports"},
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def request(method: str, path: str, body=None, expected=(200, 201, 409)):
|
||
|
|
data = None if body is None else json.dumps(body, default=str).encode()
|
||
|
|
req = urllib.request.Request(
|
||
|
|
f"{BASE}{path}",
|
||
|
|
data=data,
|
||
|
|
method=method,
|
||
|
|
headers={"Content-Type": "application/json"} if body is not None else {},
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(req) as response:
|
||
|
|
payload = json.loads(response.read().decode())
|
||
|
|
if response.status not in expected:
|
||
|
|
raise SystemExit(f"{method} {path} -> {response.status}: {payload}")
|
||
|
|
return response.status, payload
|
||
|
|
except urllib.error.HTTPError as exc:
|
||
|
|
payload = json.loads(exc.read().decode())
|
||
|
|
if exc.code not in expected:
|
||
|
|
raise SystemExit(f"{method} {path} -> {exc.code}: {payload}") from exc
|
||
|
|
return exc.code, payload
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
for definition in DEFINITIONS:
|
||
|
|
request("POST", "/api/v1/attribute-definitions", definition)
|
||
|
|
request("POST", "/api/v1/profiles", PROFILE)
|
||
|
|
|
||
|
|
product = FIXTURE["products"][0]
|
||
|
|
request(
|
||
|
|
"POST",
|
||
|
|
"/api/v1/products",
|
||
|
|
{
|
||
|
|
"id": product["id"],
|
||
|
|
"category": product["category"],
|
||
|
|
"brand": product["brand"],
|
||
|
|
"name": product["name"],
|
||
|
|
"model": product.get("model"),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
for variant in product["variants"]:
|
||
|
|
request(
|
||
|
|
"POST",
|
||
|
|
f"/api/v1/products/{product['id']}/variants",
|
||
|
|
{
|
||
|
|
"id": variant["id"],
|
||
|
|
"name": variant["name"],
|
||
|
|
"manufacturer_sku": variant.get("manufacturer_sku"),
|
||
|
|
"identifiers": variant.get("identifiers") or {},
|
||
|
|
},
|
||
|
|
)
|
||
|
|
for attribute in variant["attributes"]:
|
||
|
|
request("POST", f"/api/v1/variants/{variant['id']}/attributes", {**attribute, "source_id": "manual"})
|
||
|
|
|
||
|
|
for offer in FIXTURE["offers"]:
|
||
|
|
request(
|
||
|
|
"POST",
|
||
|
|
f"/api/v1/variants/{offer['variant_id']}/offers",
|
||
|
|
{
|
||
|
|
"id": offer["id"],
|
||
|
|
"source_id": "manual",
|
||
|
|
"external_id": offer["external_id"],
|
||
|
|
"seller": offer["seller"],
|
||
|
|
"url": offer.get("url"),
|
||
|
|
"market": offer["market"],
|
||
|
|
"currency": offer["currency"],
|
||
|
|
"condition": offer["condition"],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
for price in FIXTURE["prices"]:
|
||
|
|
offer = next(item for item in FIXTURE["offers"] if item["external_id"] == price["external_offer_id"])
|
||
|
|
request(
|
||
|
|
"POST",
|
||
|
|
f"/api/v1/offers/{offer['id']}/prices",
|
||
|
|
{
|
||
|
|
"id": price["id"],
|
||
|
|
"source_id": "manual",
|
||
|
|
"external_observation_id": price["external_observation_id"],
|
||
|
|
"observed_at": price["observed_at"],
|
||
|
|
"item_price": price["item_price"],
|
||
|
|
"shipping_price": price["shipping_price"],
|
||
|
|
"currency": price["currency"],
|
||
|
|
"availability": price["availability"],
|
||
|
|
"raw_payload": price["raw_payload"],
|
||
|
|
"confidence": price["confidence"],
|
||
|
|
},
|
||
|
|
expected=(200, 201),
|
||
|
|
)
|
||
|
|
|
||
|
|
cases = {
|
||
|
|
"A": ("22222222-2222-4222-8222-222222222222", "33333333-3333-4333-8333-333333333333"),
|
||
|
|
"B": ("22222222-2222-4222-8222-22222222222b", "33333333-3333-4333-8333-33333333333b"),
|
||
|
|
"C": ("22222222-2222-4222-8222-22222222222c", "33333333-3333-4333-8333-33333333333c"),
|
||
|
|
"D": ("22222222-2222-4222-8222-222222222222", "33333333-3333-4333-8333-33333333333d"),
|
||
|
|
}
|
||
|
|
for label, (variant_id, offer_id) in cases.items():
|
||
|
|
status, result = request(
|
||
|
|
"POST",
|
||
|
|
f"/api/v1/evaluate/{variant_id}",
|
||
|
|
{
|
||
|
|
"profile_id": "mobile-development-notebook",
|
||
|
|
"profile_version": 1,
|
||
|
|
"offer_id": offer_id,
|
||
|
|
"as_of": AS_OF,
|
||
|
|
},
|
||
|
|
expected=(201,),
|
||
|
|
)
|
||
|
|
print(
|
||
|
|
f"case {label}: {status} overall={result['overall_eligibility']} "
|
||
|
|
f"product={result['product_score']} price={result['price_score']} "
|
||
|
|
f"deal={result['deal_score']} class={result['classification']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|