feat: add smdb intergration
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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
|
||||
Reference in New Issue
Block a user