# app/tools/dzengi_probe/rest_probe.py from __future__ import annotations import json from dataclasses import dataclass from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen from app.tools.dzengi_probe.config import DzengiProbeConfig from app.tools.dzengi_probe.response_store import save_json @dataclass(frozen=True) class RestProbeResult: endpoint: str path: str output_file: str ok: bool error: str | None = None class DzengiRestProbe: def __init__(self, config: DzengiProbeConfig) -> None: self.config = config def run_all(self) -> list[RestProbeResult]: return [ self.probe_time(), self.probe_exchange_info(), self.probe_ticker24hr(), self.probe_depth(), self.probe_klines(), self.probe_agg_trades(), ] def probe_time(self) -> RestProbeResult: return self._get_and_save( endpoint="time", path=f"/api/{self.config.api_version}/time", params=None, output_file="rest/time/response.json", ) def probe_exchange_info(self) -> RestProbeResult: return self._get_and_save( endpoint="exchangeInfo", path=f"/api/{self.config.api_version}/exchangeInfo", params=None, output_file="rest/exchangeInfo/all.json", ) def probe_ticker24hr(self) -> RestProbeResult: return self._get_and_save( endpoint="ticker24hr", path=f"/api/{self.config.api_version}/ticker/24hr", params={"symbol": self.config.symbol}, output_file=f"rest/ticker24hr/{self.config.symbol_file_name}.json", ) def probe_depth(self) -> RestProbeResult: return self._get_and_save( endpoint="depth", path=f"/api/{self.config.api_version}/depth", params={ "symbol": self.config.symbol, "limit": self.config.depth_limit, }, output_file=f"rest/depth/{self.config.symbol_file_name}.json", ) def probe_klines(self) -> RestProbeResult: return self._get_and_save( endpoint="klines", path=f"/api/{self.config.api_version}/klines", params={ "symbol": self.config.symbol, "interval": self.config.interval, "limit": "10", }, output_file=( f"rest/klines/" f"{self.config.symbol_file_name}_{self.config.interval}.json" ), ) def probe_agg_trades(self) -> RestProbeResult: return self._get_and_save( endpoint="aggTrades", path=f"/api/{self.config.api_version}/aggTrades", params={ "symbol": self.config.symbol, "limit": self.config.agg_trades_limit, }, output_file=f"rest/aggTrades/{self.config.symbol_file_name}.json", ) def _get_and_save( self, *, endpoint: str, path: str, params: dict[str, str] | None, output_file: str, ) -> RestProbeResult: url = self._build_url(path, params) request = Request( url=url, method="GET", headers={ "Accept": "application/json", "User-Agent": "dzentra-dzengi-probe/1.0", }, ) try: with urlopen(request, timeout=self.config.timeout_seconds) as response: status = getattr(response, "status", 200) body = response.read().decode("utf-8") if status != 200: return RestProbeResult( endpoint=endpoint, path=path, output_file=output_file, ok=False, error=f"Unexpected HTTP status: {status}", ) payload = json.loads(body) output_path = self.config.output_dir / output_file save_json(output_path, payload) return RestProbeResult( endpoint=endpoint, path=path, output_file=str(output_path), ok=True, ) except HTTPError as exc: error_body = "" try: error_body = exc.read().decode("utf-8") except Exception: pass message = f"HTTP {exc.code}: {exc.reason}" if error_body: message += f" | body: {error_body}" return RestProbeResult( endpoint=endpoint, path=path, output_file=output_file, ok=False, error=message, ) except URLError as exc: return RestProbeResult( endpoint=endpoint, path=path, output_file=output_file, ok=False, error=f"Network error: {exc.reason}", ) except TimeoutError: return RestProbeResult( endpoint=endpoint, path=path, output_file=output_file, ok=False, error="Timeout while calling Dzengi API.", ) except json.JSONDecodeError as exc: return RestProbeResult( endpoint=endpoint, path=path, output_file=output_file, ok=False, error=f"Non-JSON response: {exc}", ) def _build_url( self, path: str, params: dict[str, str] | None, ) -> str: query = f"?{urlencode(params)}" if params else "" return f"{self.config.base_url}{path}{query}"