"""wowdb.py : client officiel de l'API de WoWDB des Défias, version 1.0.0. Documentation : https://docs.wowdb.assemblee-defias.fr Un seul fichier, bibliothèque standard seulement (Python 3.8+). Release 58 du site (01/10/2026). Licence : MIT. Données du jeu © Blizzard Entertainment. from wowdb import WowDb wowdb = WowDb(user_agent="bot-de-ma-guilde/1.0 (https://ma-guilde.example)") lame = wowdb.items.retrieve("classic", 5191) for dague in wowdb.items.iterate("classic", type="dagger", rogue=True): print(dague["name"]) """ from __future__ import annotations import json import time import urllib.error import urllib.parse import urllib.request from typing import Any, Dict, Iterator, Optional __version__ = "1.0.0" BASE_URL = "https://api.wowdb.assemblee-defias.fr/v1" Json = Dict[str, Any] class WowDbError(Exception): """Erreur de l'API (corps { error: { type, code, message, param, doc_url } }) ou du réseau.""" def __init__(self, message: str, status: int = 0, type: str = "api_error", code: str = "network_error", param: Optional[str] = None, doc_url: Optional[str] = None, request_id: Optional[str] = None): super().__init__(message) self.message = message self.status = status self.type = type self.code = code self.param = param self.doc_url = doc_url self.request_id = request_id def __repr__(self) -> str: return f"WowDbError(status={self.status}, code={self.code!r}, message={self.message!r})" def _valeur(v: Any) -> str: if isinstance(v, bool): return "true" if v else "false" if isinstance(v, (list, tuple, set)): return ",".join(_valeur(x) for x in v) return str(v) class _Ressource: """Une ressource de l'API : list, retrieve, iterate (et ses sous-listes).""" def __init__(self, client: "WowDb", segment: str, sous_listes: tuple = ()): self._client = client self._segment = segment for nom in sous_listes: setattr(self, nom, self._sous_liste(nom)) def _sous_liste(self, nom: str): def lire(version: str, id: int, **params: Any) -> Json: return self._client.list(f"/{version}/{self._segment}/{id}/{nom}", **params) lire.__name__ = nom return lire def list(self, version: str, **params: Any) -> Json: return self._client.list(f"/{version}/{self._segment}", **params) def retrieve(self, version: str, id: int) -> Json: return self._client.request(f"/{version}/{self._segment}/{id}") def iterate(self, version: str, **params: Any) -> Iterator[Json]: return self._client.iterate(f"/{version}/{self._segment}", **params) class WowDb: """Client de l'API de WoWDB des Défias (lecture seule, sans clé).""" def __init__(self, user_agent: Optional[str] = None, base_url: str = BASE_URL, max_retries: int = 2, timeout: float = 30.0): self.base_url = base_url.rstrip("/") self.user_agent = user_agent or f"wowdb.py/{__version__}" self.max_retries = max_retries self.timeout = timeout self.items = _Ressource(self, "items") self.spells = _Ressource(self, "spells") self.quests = _Ressource(self, "quests") self.npcs = _Ressource(self, "npcs", ("loot", "sells", "quests", "locations")) self.game_objects = _Ressource(self, "game-objects", ("loot", "locations")) self.item_sets = _Ressource(self, "item-sets") self.factions = _Ressource(self, "factions") self.instances = _Ressource(self, "instances", ("loot",)) def request(self, path: str, **params: Any) -> Json: """Requête GET brute : chemin relatif à /v1 (« /classic/items/5191 »).""" propres = {k: _valeur(v) for k, v in params.items() if v is not None} url = self.base_url + (path if path.startswith("/") else "/" + path) if propres: url += "?" + urllib.parse.urlencode(propres) req = urllib.request.Request(url, headers={"User-Agent": self.user_agent, "Accept": "application/json"}) essai = 0 while True: try: with urllib.request.urlopen(req, timeout=self.timeout) as res: return json.load(res) except urllib.error.HTTPError as e: if e.code == 429 and essai < self.max_retries: essai += 1 time.sleep(max(1, int(e.headers.get("Retry-After") or 1))) continue try: erreur = json.load(e).get("error", {}) except (ValueError, AttributeError): erreur = {} raise WowDbError(erreur.get("message") or f"Erreur HTTP {e.code}", status=e.code, type=erreur.get("type", "api_error"), code=erreur.get("code", "http_error"), param=erreur.get("param"), doc_url=erreur.get("doc_url"), request_id=e.headers.get("X-Request-Id")) from None except (urllib.error.URLError, TimeoutError, OSError) as e: raise WowDbError(f"Requête impossible : {e}") from None def list(self, path: str, **params: Any) -> Json: """Une page d'une liste : { object: "list", data, has_more, next_cursor, url }.""" return self.request(path, **params) def iterate(self, path: str, **params: Any) -> Iterator[Json]: """Tous les éléments d'une liste, page après page (pages de 100).""" params.setdefault("limit", 100) while True: page = self.request(path, **params) yield from page["data"] if not page.get("next_cursor"): return params["cursor"] = page["next_cursor"] def retrieve(self, version: str, resource: str, id: int) -> Json: """Fiche d'une ressource : retrieve("classic", "items", 5191).""" return self.request(f"/{version}/{resource}/{id}") def search(self, q: str, **params: Any) -> Json: """Recherche plein texte dans toute la base.""" return self.request("/search", q=q, **params) def stats(self) -> Json: return self.request("/stats") def versions(self) -> Json: return self.request("/versions") __all__ = ["WowDb", "WowDbError", "BASE_URL", "__version__"]