http_client.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. """Shared HTTP client wrapper for zhuce6."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass
  4. import time
  5. from typing import Any
  6. from curl_cffi import requests as cffi_requests
  7. from curl_cffi.requests import Response, Session
  8. @dataclass
  9. class RequestConfig:
  10. timeout: int = 30
  11. max_retries: int = 3
  12. retry_delay: float = 1.0
  13. impersonate: str = "chrome120"
  14. verify_ssl: bool = True
  15. follow_redirects: bool = True
  16. class HTTPClientError(Exception):
  17. """Raised when the wrapped HTTP client cannot complete a request."""
  18. class HTTPClient:
  19. def __init__(
  20. self,
  21. proxy_url: str | None = None,
  22. config: RequestConfig | None = None,
  23. session: Session | None = None,
  24. ) -> None:
  25. self.proxy_url = proxy_url
  26. self.config = config or RequestConfig()
  27. self._session = session
  28. @property
  29. def proxies(self) -> dict[str, str] | None:
  30. if not self.proxy_url:
  31. return None
  32. return {"http": self.proxy_url, "https": self.proxy_url}
  33. @property
  34. def session(self) -> Session:
  35. if self._session is None:
  36. self._session = Session(
  37. proxies=self.proxies,
  38. impersonate=self.config.impersonate,
  39. verify=self.config.verify_ssl,
  40. timeout=self.config.timeout,
  41. )
  42. return self._session
  43. def request(self, method: str, url: str, **kwargs: Any) -> Response:
  44. kwargs.setdefault("timeout", self.config.timeout)
  45. kwargs.setdefault("allow_redirects", self.config.follow_redirects)
  46. if self.proxies and "proxies" not in kwargs:
  47. kwargs["proxies"] = self.proxies
  48. last_error: Exception | None = None
  49. for attempt in range(self.config.max_retries):
  50. try:
  51. return self.session.request(method, url, **kwargs)
  52. except Exception as exc:
  53. last_error = exc
  54. if attempt < self.config.max_retries - 1:
  55. time.sleep(self.config.retry_delay * (attempt + 1))
  56. continue
  57. break
  58. raise HTTPClientError(f"Request failed: {method} {url} - {last_error}")
  59. def get(self, url: str, **kwargs: Any) -> Response:
  60. return self.request("GET", url, **kwargs)
  61. def post(self, url: str, data: Any = None, json: Any = None, **kwargs: Any) -> Response:
  62. return self.request("POST", url, data=data, json=json, **kwargs)
  63. def close(self) -> None:
  64. if self._session is not None:
  65. self._session.close()
  66. self._session = None