"""Client for the AER's public "Get Generic Plans" / "Get Generic Plan Detail" Consumer Data Right APIs. These are unauthenticated, government-operated endpoints (no accreditation required), but they are hosted per-retailer-brand rather than as one aggregated feed: each retailer publishes its own plans under `https://cdr.energymadeeasy.gov.au//cds-au/v1/energy/plans`. There is no single "give me every QLD plan" call -- this client queries a maintained list of known retailer brand slugs and merges the results. Confirmed against the live API while building this: - The plans-list endpoint only supports API version 1 (`x-v: 1`); the plan-detail endpoint supports version 3 (`x-v: 3`). Using the wrong version returns an HTTP 406. - Distributor names in `geography.distributors` are plain strings like "Energex" (QLD's south-east distributor); Ergon's exact string should be confirmed for a given retailer/plan once one is found, hence the case-insensitive substring match used when filtering by distributor. """ from __future__ import annotations import time import httpx BASE_HOST = "https://cdr.energymadeeasy.gov.au" # Seed list of retailer CDR "brand" slugs confirmed to respond while # building this. Not exhaustive, and retailers can add/rename/retire brands. # To add one: find the retailer's own published CDR base URI (usually # linked from their "Consumer Data Right" policy page) or check the AER's # register at # https://www.aer.gov.au/documents/consumer-data-right-energy-retailer-base-uris-and-cdr-brands KNOWN_RETAILER_BRANDS: list[str] = [ "agl", "origin", "energyaustralia", "alintaenergy", "redenergy", "powershop", "tango", "globird", "sumo", "dodo", "click", "ovoenergy", "simplyenergy", "qenergy", "nectr", "amberelectric", ] class AERClient: def __init__(self, brands: list[str] | None = None) -> None: self.brands = brands or KNOWN_RETAILER_BRANDS self._client = httpx.Client(timeout=30) def fetch_all_plans(self, *, fuel_type: str = "ELECTRICITY") -> list[dict]: """Fetch every current, generally-available plan summary across all configured retailer brands (paginating each retailer's own list).""" plans: list[dict] = [] for brand in self.brands: plans.extend(self._fetch_brand_plans(brand, fuel_type)) return plans def _fetch_brand_plans(self, brand: str, fuel_type: str) -> list[dict]: url = f"{BASE_HOST}/{brand}/cds-au/v1/energy/plans" params = {"type": "ALL", "fuelType": fuel_type, "effective": "CURRENT", "page-size": 100, "page": 1} plans: list[dict] = [] while True: resp = self._client.get(url, params=params, headers={"x-v": "1"}) if resp.status_code == 404: # Brand slug no longer valid, or this retailer hasn't published a CDR endpoint. return plans resp.raise_for_status() body = resp.json() for plan in body["data"]["plans"]: plan.setdefault("brand", brand) plans.append(plan) total_pages = body.get("meta", {}).get("totalPages", 1) if params["page"] >= total_pages: break params["page"] += 1 time.sleep(0.2) # be a polite citizen of a public government API return plans def fetch_plan_detail(self, brand: str, plan_id: str) -> dict: url = f"{BASE_HOST}/{brand}/cds-au/v1/energy/plans/{plan_id}" resp = self._client.get(url, headers={"x-v": "3"}) resp.raise_for_status() return resp.json()["data"]