build 039: complete Quotes Feed migration foundation
This commit is contained in:
195
app/tools/dzengi_probe/rest_probe.py
Normal file
195
app/tools/dzengi_probe/rest_probe.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# 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}"
|
||||
Reference in New Issue
Block a user