from __future__ import annotations from dataclasses import dataclass from typing import Any import httpx from .config import HockeySettingsStore class Stat2TVNotFoundError(RuntimeError): def __init__(self, url: str, status_code: int = 404) -> None: self.url = url self.status_code = status_code super().__init__(f"Stat2TV resource not found: HTTP {status_code}") @dataclass(slots=True) class Stat2TVResponse: content: bytes status_code: int content_type: str auth_mode: str url: str class Stat2TVClient: def __init__(self, settings: HockeySettingsStore) -> None: self.settings_store = settings self._working_auth_mode: str | None = None async def fetch_xml(self, endpoint: str) -> Stat2TVResponse: settings = self.settings_store.public() username, password = self.settings_store.credentials() if not username or not password: raise RuntimeError( "Логин и пароль Stat2TV не настроены. " "Откройте настройки хоккейного модуля." ) url = settings["base_url"].rstrip("/") + "/" + endpoint.lstrip("/") requested_mode = settings["auth_mode"] if requested_mode in {"basic", "digest"}: modes = [requested_mode] elif self._working_auth_mode in {"basic", "digest"}: other = "digest" if self._working_auth_mode == "basic" else "basic" modes = [self._working_auth_mode, other] else: modes = ["basic", "digest"] last_error: Exception | None = None for mode in modes: auth: Any = ( httpx.BasicAuth(username, password) if mode == "basic" else httpx.DigestAuth(username, password) ) try: async with httpx.AsyncClient( timeout=float(settings["request_timeout_seconds"]), verify=bool(settings["verify_ssl"]), follow_redirects=True, headers={ "Accept": "application/xml,text/xml,*/*", "User-Agent": "HockeyControlPanel/20.2", }, ) as client: response = await client.get(url, auth=auth) if response.status_code in {401, 403} and requested_mode == "auto": last_error = RuntimeError( f"Авторизация {mode} отклонена: HTTP {response.status_code}" ) continue if response.status_code in {404, 410}: raise Stat2TVNotFoundError(str(response.url), response.status_code) response.raise_for_status() self._working_auth_mode = mode return Stat2TVResponse( content=response.content, status_code=response.status_code, content_type=response.headers.get("content-type", ""), auth_mode=mode, url=str(response.url), ) except Stat2TVNotFoundError: raise except Exception as error: last_error = error if requested_mode != "auto": break raise RuntimeError(f"Stat2TV недоступен: {last_error}")