Coverage for human_requests / abstraction / http.py: 94%
127 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-07 17:38 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-07 17:38 +0000
1from __future__ import annotations
3import os
4from dataclasses import dataclass, field
5from enum import Enum
6from typing import Any, Dict, Optional
7from urllib.parse import parse_qs, urlparse, urlunparse
10class HttpMethod(Enum):
11 """Represents an HTTP method."""
13 GET = "GET"
14 """Retrieves data from a server.
15 It only reads data and does not modify it."""
16 POST = "POST"
17 """Submits data to a server to create a new resource.
18 It can also be used to update existing resources."""
19 PUT = "PUT"
20 """Updates a existing resource on a server.
21 It can also be used to create a new resource."""
22 PATCH = "PATCH"
23 """Updates a existing resource on a server.
24 It only updates the fields that are provided in the request body."""
25 DELETE = "DELETE"
26 """Deletes a resource from a server."""
27 HEAD = "HEAD"
28 """Retrieves metadata from a server.
29 It only reads the headers and does not return the response body."""
30 OPTIONS = "OPTIONS"
31 """Provides information about the HTTP methods supported by a server.
32 It can be used for Cross-Origin Resource Sharing (CORS) request."""
35@dataclass(frozen=True)
36class URL:
37 """A dataclass containing the parsed URL components."""
39 full_url: str
40 """The full URL."""
41 base_url: str = ""
42 """The base URL, without query parameters."""
43 secure: bool = False
44 """Whether the URL is secure (https/wss)."""
45 protocol: str = ""
46 """The protocol of the URL."""
47 path: str = ""
48 """The path of the URL."""
49 domain_with_port: str = ""
50 """The domain of the URL with port."""
51 domain: str = ""
52 """The domain of the URL."""
53 port: Optional[int] = None
54 """The port of the URL."""
55 params: dict[str, list[str]] = field(default_factory=dict)
56 """A dictionary of query parameters."""
58 def __post_init__(self) -> None:
59 parsed_url = urlparse(self.full_url)
61 object.__setattr__(self, "base_url", parsed_url._replace(query="").geturl())
62 object.__setattr__(self, "secure", parsed_url.scheme in ["https", "wss"])
63 object.__setattr__(self, "protocol", parsed_url.scheme)
65 object.__setattr__(self, "path", parsed_url.path)
67 full_domen = parsed_url.netloc.split(":")
68 object.__setattr__(self, "domain_with_port", parsed_url.netloc)
69 object.__setattr__(self, "domain", full_domen[0])
70 if len(full_domen) > 1:
71 object.__setattr__(self, "port", int(full_domen[1]))
73 object.__setattr__(self, "params", parse_qs(parsed_url.query))
76class Proxy:
77 """
78 Универсальный класс для работы с прокси в двух форматах:
79 1. Строковый: 'http://user:pass@host:port' или 'socks5://host:port'
80 2. Playwright dict: {
81 'server': 'http://host:port',
82 'username': 'user',
83 'password': 'pass'
84 }
85 """
87 @staticmethod
88 def from_env() -> Proxy:
89 """
90 Создаёт Proxy из переменных окружения.
92 Приоритет:
93 1) https_proxy / HTTPS_PROXY
94 2) http_proxy / HTTP_PROXY
95 3) all_proxy / ALL_PROXY
96 """
97 env_keys = (
98 "https_proxy",
99 "HTTPS_PROXY",
100 "http_proxy",
101 "HTTP_PROXY",
102 "all_proxy",
103 "ALL_PROXY",
104 )
106 for key in env_keys:
107 value = os.getenv(key)
108 if value and value.strip():
109 return Proxy(value.strip())
110 return Proxy()
112 def __init__(
113 self,
114 proxy: Optional[str | Dict[str, Any]] = None,
115 *,
116 server: Optional[str] = None,
117 username: Optional[str] = None,
118 password: Optional[str] = None,
119 ):
120 """
121 Инициализация через строку или dict (Playwright-формат).
122 Можно также передать параметры напрямую.
123 """
124 self._server: str | None = None
125 self._username: str | None = None
126 self._password: str | None = None
128 if proxy is not None:
129 if isinstance(proxy, str):
130 self._from_str(proxy)
131 elif isinstance(proxy, dict):
132 self._from_dict(proxy)
133 else:
134 raise ValueError("proxy должен быть str или dict")
135 elif server is not None:
136 self._server = server
137 self._username = username
138 self._password = password
139 else:
140 # Прокси не указан
141 pass
143 def _from_str(self, proxy_str: str) -> None:
144 """Парсит строку вида protocol://user:pass@host:port"""
145 if not proxy_str.strip():
146 raise ValueError("Прокси-строка не может быть пустой")
148 original = proxy_str
149 # Поддержка без схемы: host:port или user:pass@host:port → всегда добавляем http://
150 if "://" not in proxy_str:
151 proxy_str = "http://" + proxy_str
153 parsed = urlparse(proxy_str)
155 if not parsed.hostname:
156 raise ValueError(f"Некорректный хост в прокси: {original}")
158 self._server = urlunparse(
159 (parsed.scheme, f"{parsed.hostname}:{parsed.port or ''}".rstrip(":"), "", "", "", "")
160 )
162 # parsed.username может быть '', но мы ставим None если пусто
163 self._username = parsed.username if parsed.username else None
164 self._password = parsed.password if parsed.password else None
166 def _from_dict(self, proxy_dict: Dict[str, Any]) -> None:
167 """Принимает dict в формате Playwright"""
168 server = proxy_dict.get("server")
169 if not server:
170 raise ValueError("В dict должен быть ключ 'server'")
172 parsed = urlparse(server)
173 if not parsed.hostname:
174 raise ValueError(f"Некорректный server в dict: {server}")
176 self._server = server
177 self._username = proxy_dict.get("username")
178 self._password = proxy_dict.get("password")
180 def as_dict(self) -> Optional[Dict[str, Any]]:
181 """
182 Возвращает прокси в формате Playwright
183 """
184 if not self._server:
185 return None
187 result: Dict[str, Any] = {"server": self._server}
188 if self._username:
189 result["username"] = self._username
190 if self._password:
191 result["password"] = self._password
192 return result
194 def as_str(self, include_auth: bool = True) -> Optional[str]:
195 """
196 Возвращает прокси в строковом формате.
197 Если include_auth=False — без логина и пароля.
198 """
199 if not self._server:
200 return None
202 parsed = urlparse(self._server)
203 if not parsed.scheme or not parsed.hostname:
204 raise ValueError(f"Некорректный server: {self._server}")
206 netloc = parsed.hostname
207 if parsed.port:
208 netloc += f":{parsed.port}"
210 if include_auth and (self._username or self._password):
211 auth = f"{self._username or ''}"
212 if self._password:
213 auth += f":{self._password}"
214 netloc = f"{auth}@{netloc}"
216 return urlunparse((parsed.scheme, netloc, "", "", "", ""))
218 def __repr__(self) -> str:
219 return f"Proxy(server={self._server}, username={'***' if self._username else None})"
221 def __bool__(self) -> bool:
222 return bool(self._server)