Sandbox environment: prices, stock and coupons are synthetic test data. What this means

How do I call Cartroute from Python?

Use requests (or httpx) with the key in an Authorization header, a timeout, an Idempotency-Key per logical request, and a retry loop that retries only rate limits, server errors and network failures. The client below does all of that in about 50 lines.

What does a small production client look like?

import os
import time
import uuid

import requests

class CartrouteError(Exception):
    def __init__(self, status, problem):
        super().__init__(problem.get("detail", f"HTTP {status}"))
        self.status = status
        self.code = problem.get("code")
        self.problem = problem


class Cartroute:
    def __init__(self, api_key=None, base_url="https://cart-route.com/api/v1", timeout=15, max_retries=3):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key or os.environ['CARTROUTE_API_KEY']}"

    def _get(self, path, params=None):
        key = str(uuid.uuid4())  # same key on every retry of this call
        for attempt in range(self.max_retries + 1):
            try:
                resp = self.session.get(f"{self.base_url}{path}", params=params,
                                        headers={"Idempotency-Key": key}, timeout=self.timeout)
            except requests.RequestException:
                if attempt == self.max_retries:
                    raise
                time.sleep(2 ** attempt)
                continue
            if resp.status_code == 429 and attempt < self.max_retries:
                time.sleep(int(resp.headers.get("Retry-After", "1")))
                continue
            if resp.status_code >= 500 and attempt < self.max_retries:
                time.sleep(2 ** attempt)
                continue
            body = resp.json()
            if not resp.ok:
                raise CartrouteError(resp.status_code, body)
            return body

    def search(self, q, **filters):
        return self._get("/search", {"q": q, **filters})

    def product(self, product_id):
        return self._get(f"/products/{product_id}")["product"]

    def price_history(self, product_id, days=90):
        return self._get(f"/products/{product_id}/price-history", {"days": days})["history"]

    def coupons(self, **filters):
        return self._get("/coupons", filters)["coupons"]

    def account(self):
        return self._get("/account")

How do I use it?

cr = Cartroute()

try:
    result = cr.search("robot vacuum", max_price=600, in_stock="true", include_membership="false")
except CartrouteError as e:
    if e.code == "insufficient_credits":
        print("Out of credits:", e.problem.get("top_up_url"))
    else:
        raise
else:
    for p in result["results"]:
        best = p["price_summary"]
        print(f'{p["title"]}: {best["best_retailer"]} ${best["best_effective_price_usd"]:.2f}',
              "-", p["price_insight"]["verdict"])
    print("credits left:", result["credits"]["remaining"])

Why these choices?