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