import time from typing import Any, Dict, List, Optional import httpx from pydantic import BaseModel from tenacity import retry, stop_after_attempt, wait_exponential from .config import settings class AuthResponse(BaseModel): token: str exp: Optional[int] = None # epoch seconds class EipClient: def __init__(self, base_url: str, username: str, password: str) -> None: self.base_url = base_url.rstrip("/") self.username = username self.password = password self._token: Optional[str] = None self._token_exp: Optional[int] = None self._client = httpx.Client(timeout=15.0) def _is_token_valid(self) -> bool: if not self._token or not self._token_exp: return False # refresh 60s earlier return time.time() < (self._token_exp - 60) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def authenticate(self) -> AuthResponse: url = f"{self.base_url}/client/auth" resp = self._client.post(url, json={"username": self.username, "password": self.password}) resp.raise_for_status() data = resp.json() # 尝试从响应中提取 exp;如果没有,设置 1 小时后过期 exp = data.get("exp") or int(time.time()) + 3600 token = data.get("token") or data.get("X-Token") or data.get("access_token") if not token: # 兼容后端直接返回字符串 token 的情况 if isinstance(data, str) and data: token = data else: raise ValueError("Auth response missing token") self._token = token self._token_exp = exp return AuthResponse(token=token, exp=exp) def _get_headers(self) -> Dict[str, str]: if not self._is_token_valid(): self.authenticate() assert self._token return {"X-Token": self._token, "Accept": "application/json"} @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def list_cities(self) -> List[Dict[str, Any]]: url = f"{self.base_url}/edge/city" resp = self._client.get(url, headers=self._get_headers()) resp.raise_for_status() data = resp.json() return data if isinstance(data, list) else data.get("data", []) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def list_devices(self, geo: str, offset: int, num: int) -> List[Dict[str, Any]]: url = f"{self.base_url}/edge/device" payload = {"geo": geo, "offset": offset, "num": num} resp = self._client.post(url, headers=self._get_headers(), json=payload) resp.raise_for_status() data = resp.json() return data if isinstance(data, list) else data.get("data", []) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def gateway_config_get(self, macaddr: str) -> Dict[str, Any]: url = f"{self.base_url}/gateway/config/get" resp = self._client.post(url, headers=self._get_headers(), json={"macaddr": macaddr}) resp.raise_for_status() return resp.json() @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def gateway_config_set(self, macaddr: str, config: Dict[str, Any]) -> Dict[str, Any]: url = f"{self.base_url}/gateway/config/set" payload = {"macaddr": macaddr, "config": config} resp = self._client.post(url, headers=self._get_headers(), json=payload) resp.raise_for_status() return resp.json() @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, min=0.5, max=2)) def gateway_status(self, macaddr: str) -> Dict[str, Any]: url = f"{self.base_url}/gateway/status" resp = self._client.post(url, headers=self._get_headers(), json={"macaddr": macaddr}) resp.raise_for_status() return resp.json() client_singleton = EipClient(settings.eip_base_url, settings.eip_username, settings.eip_password)