74 lines
2.9 KiB
Python
74 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
|
|
class SCMDBError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class SCMDBClient:
|
|
def __init__(self, base_url: str = "https://scmdb.net") -> None:
|
|
self.base_url = base_url.rstrip("/")
|
|
self._versions: list[dict[str, Any]] | None = None
|
|
self._data_cache: dict[str, dict[str, Any]] = {}
|
|
|
|
async def list_versions(self) -> list[dict[str, Any]]:
|
|
if self._versions is not None:
|
|
return self._versions
|
|
body = await self._get_json("data/versions.json")
|
|
if not isinstance(body, list):
|
|
raise SCMDBError("SCMDB versions response was not a list.")
|
|
self._versions = [
|
|
item
|
|
for item in body
|
|
if isinstance(item, dict) and item.get("version") and item.get("file")
|
|
]
|
|
return self._versions
|
|
|
|
async def get_data(self, version: str | None = None, channel: str = "live") -> dict[str, Any]:
|
|
selected = await self.resolve_version(version=version, channel=channel)
|
|
cache_key = str(selected["version"])
|
|
if cache_key not in self._data_cache:
|
|
body = await self._get_json(f"data/{selected['file']}")
|
|
if not isinstance(body, dict):
|
|
raise SCMDBError(f"SCMDB data for {cache_key} was not an object.")
|
|
self._data_cache[cache_key] = body
|
|
return self._data_cache[cache_key]
|
|
|
|
async def resolve_version(self, version: str | None = None, channel: str = "live") -> dict[str, Any]:
|
|
versions = await self.list_versions()
|
|
if not versions:
|
|
raise SCMDBError("SCMDB did not return any data versions.")
|
|
|
|
if version:
|
|
needle = version.casefold().strip()
|
|
for item in versions:
|
|
item_version = str(item["version"])
|
|
if item_version.casefold() == needle or needle in item_version.casefold():
|
|
return item
|
|
raise SCMDBError(f"SCMDB version not found: {version}")
|
|
|
|
channel = (channel or "live").casefold().strip()
|
|
if channel in {"latest", "any", "all"}:
|
|
return versions[0]
|
|
if channel not in {"live", "ptu"}:
|
|
raise SCMDBError("SCMDB channel must be live, ptu, or latest.")
|
|
for item in versions:
|
|
if f"-{channel}." in str(item["version"]).casefold():
|
|
return item
|
|
return versions[0]
|
|
|
|
async def _get_json(self, path: str) -> Any:
|
|
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
|
|
response = await client.get(f"{self.base_url}/{path.lstrip('/')}", headers={"Accept": "application/json"})
|
|
try:
|
|
body = response.json()
|
|
except ValueError as exc:
|
|
raise SCMDBError(f"SCMDB returned non-JSON response: HTTP {response.status_code}") from exc
|
|
if response.status_code >= 400:
|
|
raise SCMDBError(f"SCMDB HTTP {response.status_code}: {body}")
|
|
return body
|