from __future__ import annotations from typing import Any from urllib.parse import quote import httpx class StarCitizenWikiError(RuntimeError): pass class StarCitizenWikiClient: def __init__( self, base_url: str = "https://starcitizen.tools", api_base_url: str = "https://api.star-citizen.wiki", ) -> None: self.base_url = base_url.rstrip("/") self.api_base_url = api_base_url.rstrip("/") async def search_pages(self, query: str, limit: int = 5) -> list[dict[str, Any]]: body = await self._get_json( f"{self.base_url}/api.php", params={ "action": "query", "generator": "prefixsearch", "gpssearch": query, "gpslimit": max(1, min(limit, 10)), "prop": "description|pageimages|extracts", "exintro": 1, "explaintext": 1, "exchars": 320, "piprop": "thumbnail", "pithumbsize": 240, "format": "json", }, ) pages = body.get("query", {}).get("pages", {}) ordered = sorted( (item for item in pages.values() if isinstance(item, dict)), key=lambda item: int(item.get("index") or 0), ) return [ { "pageid": item.get("pageid"), "title": item.get("title"), "description": item.get("description"), "extract": item.get("extract"), "thumbnail": (item.get("thumbnail") or {}).get("source"), "url": f"{self.base_url}/{quote(str(item.get('title') or '').replace(' ', '_'), safe=':/_')}", } for item in ordered if item.get("title") ] async def get_page_summary(self, title: str | None = None, pageid: int | None = None, chars: int = 700) -> dict[str, Any] | None: params: dict[str, Any] = { "action": "query", "prop": "extracts|description|pageimages", "exintro": 1, "explaintext": 1, "exchars": max(120, min(chars, 1200)), "piprop": "thumbnail", "pithumbsize": 320, "format": "json", } if pageid is not None: params["pageids"] = pageid elif title: params["titles"] = title else: raise StarCitizenWikiError("title or pageid is required") body = await self._get_json(f"{self.base_url}/api.php", params=params) pages = body.get("query", {}).get("pages", {}) for item in pages.values(): if isinstance(item, dict) and item.get("pageid") and item.get("title"): return { "pageid": item.get("pageid"), "title": item.get("title"), "description": item.get("description"), "extract": item.get("extract"), "thumbnail": (item.get("thumbnail") or {}).get("source"), "url": f"{self.base_url}/{quote(str(item.get('title') or '').replace(' ', '_'), safe=':/_')}", } return None async def search_verse(self, query: str) -> list[dict[str, Any]]: body = await self._get_json( f"{self.api_base_url}/api/search", params={"filter[query]": query}, ) data = body.get("data") return data if isinstance(data, list) else [] async def get_vehicle(self, slug: str) -> dict[str, Any]: body = await self._get_json(f"{self.api_base_url}/api/vehicles/{slug.strip('/')}") data = body.get("data") if not isinstance(data, dict): raise StarCitizenWikiError(f"Vehicle response for {slug} was not an object.") return data async def _get_json(self, url: str, params: dict[str, Any] | None = None) -> Any: async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client: response = await client.get(url, params=params, headers={"Accept": "application/json"}) try: body = response.json() except ValueError as exc: raise StarCitizenWikiError(f"Star Citizen Wiki returned non-JSON response: HTTP {response.status_code}") from exc if response.status_code >= 400: raise StarCitizenWikiError(f"Star Citizen Wiki HTTP {response.status_code}: {body}") return body