Skip to content

Commit 2a64dd5

Browse files
chore: pytest
1 parent 4da8f3f commit 2a64dd5

File tree

8 files changed

+126
-113
lines changed

8 files changed

+126
-113
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ Start using TLS Requests with just a few lines of code:
4444
200
4545
```
4646

47+
Basic automatically rotates:
48+
49+
```pycon
50+
>>> import tls_requests
51+
>>> proxy_list = [
52+
"http://user1:pass1@proxy.example.com:8080",
53+
"http://user2:pass2@proxy.example.com:8081",
54+
"socks5://proxy.example.com:8082",
55+
"proxy.example.com:8083", # (defaults to http)
56+
"http://user:pass@proxy.example.com:8084|1.0|US", # http://user:pass@host:port|weight|region
57+
]
58+
>>> r = tls_requests.get(
59+
"https://httpbin.org/get",
60+
proxy=proxy,
61+
headers=tls_requests.HeaderRotator(),
62+
tls_identifier=tls_requests.TLSIdentifierRotator()
63+
)
64+
>>> r
65+
<Response [200 OK]>
66+
>>> r.status_code
67+
200
68+
>>> tls_requests.HeaderRotator(strategy = "round_robin") # strategy: Literal["round_robin", "random", "weighted"]
69+
>>> tls_requests.Proxy("http://user1:pass1@proxy.example.com:8080", weight=0.1) # default weight: 1.0
70+
```
71+
4772
**Introduction**
4873
----------------
4974

tests/test_headers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ def test_request_headers(httpserver: HTTPServer):
2323
httpserver.expect_request("/headers").with_post_hook(hook_request_headers).respond_with_data(b"OK")
2424
response = tls_requests.get(httpserver.url_for("/headers"), headers={"foo": "bar"})
2525
assert response.status_code == 200
26-
assert response.headers.get("foo") == "bar"
26+
assert response.request.headers["foo"] == "bar"
2727

2828

2929
def test_response_headers(httpserver: HTTPServer):
3030
httpserver.expect_request("/headers").with_post_hook(hook_response_headers).respond_with_data(b"OK")
3131
response = tls_requests.get(httpserver.url_for("/headers"))
3232
assert response.status_code, 200
33-
assert response.headers.get("foo") == "bar"
33+
assert response.headers["foo"] == "bar"
3434

3535

3636
def test_response_case_insensitive_headers(httpserver: HTTPServer):
3737
httpserver.expect_request("/headers").with_post_hook(hook_response_case_insensitive_headers).respond_with_data(b"OK")
3838
response = tls_requests.get(httpserver.url_for("/headers"))
3939
assert response.status_code, 200
40-
assert response.headers.get("foo") == "bar"
40+
assert response.headers["foo"] == "bar"

tests/test_rotators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async def test_async_strategies(self, strategy, proxy_list_fixture):
183183

184184
class TestProxyRotator:
185185
def test_mark_result_weighted(self):
186-
proxy = Proxy("p1:8080", weight=2.0)
186+
proxy = Proxy("proxy.example.com:8080", weight=2.0)
187187
rotator = ProxyRotator([proxy], strategy="weighted")
188188

189189
initial_weight = proxy.weight
@@ -196,7 +196,7 @@ def test_mark_result_weighted(self):
196196

197197
@pytest.mark.asyncio
198198
async def test_async_mark_result_weighted(self):
199-
proxy = Proxy("p1:8080", weight=2.0)
199+
proxy = Proxy("proxy.example.com:8080", weight=2.0)
200200
rotator = ProxyRotator([proxy], strategy="weighted")
201201

202202
initial_weight = proxy.weight

tls_requests/client.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,11 @@ def prepare_auth(
193193

194194
def prepare_headers(self, headers: HeaderTypes = None, user_agent: Optional[str] = None) -> Headers:
195195
"""Prepare Headers. Gets base headers from rotator if available."""
196+
if headers is None:
197+
return self.headers.copy()
196198
if isinstance(headers, HeaderRotator):
197-
headers_copy = headers.next(user_agent=user_agent)
198-
else:
199-
headers_copy = self.headers.copy()
200-
201-
return headers_copy
199+
return headers.next(user_agent=user_agent)
200+
return Headers(headers)
202201

203202
def prepare_cookies(self, cookies: CookieTypes = None) -> Cookies:
204203
"""Prepare Cookies"""
@@ -371,7 +370,8 @@ def _send(
371370
self, request: Request, *, history: list = None, start: float = None
372371
) -> Response:
373372
start = start or time.perf_counter()
374-
config = self.prepare_config(request, tls_identifier=self.prepare_tls_identifier(self.client_identifier))
373+
tls_identifier = self.prepare_tls_identifier(self.client_identifier)
374+
config = self.prepare_config(request, tls_identifier=tls_identifier)
375375
response = Response.from_tls_response(
376376
self.session.request(config.to_dict()),
377377
is_byte_response=config.isByteResponse,
@@ -515,6 +515,12 @@ def send(
515515
self.follow_redirects = follow_redirects
516516
response = self._send(request, start=time.perf_counter(), history=[])
517517

518+
if isinstance(self.proxy, ProxyRotator) and response.request.proxy:
519+
proxy_success = 200 <= response.status_code < 500 and response.status_code not in [407]
520+
self.proxy.mark_result(
521+
proxy=response.request.proxy, success=proxy_success, latency=response.elapsed
522+
)
523+
518524
if self.hooks.get("response"):
519525
response_ = self.build_hook_response(response)
520526
if isinstance(response_, Response):
@@ -757,12 +763,11 @@ class AsyncClient(BaseClient):
757763

758764
async def aprepare_headers(self, headers: HeaderTypes = None, user_agent: Optional[str] = None) -> Headers:
759765
"""Prepare Headers. Gets base headers from rotator if available."""
766+
if headers is None:
767+
return self.headers.copy()
760768
if isinstance(headers, HeaderRotator):
761-
headers_copy = await headers.anext(user_agent=user_agent)
762-
else:
763-
headers_copy = self.headers.copy()
764-
765-
return headers_copy
769+
return await headers.anext(user_agent=user_agent)
770+
return Headers(headers)
766771

767772
async def aprepare_proxy(self, proxy: ProxyTypes | None) -> Optional[Proxy]:
768773
if proxy is None:
@@ -1070,6 +1075,12 @@ async def send(
10701075
self.follow_redirects = follow_redirects
10711076
response = await self._send(request, start=time.perf_counter(), history=[])
10721077

1078+
if isinstance(self.proxy, ProxyRotator) and response.request.proxy:
1079+
proxy_success = 200 <= response.status_code < 500 and response.status_code not in [407]
1080+
await self.proxy.amark_result(
1081+
proxy=response.request.proxy, success=proxy_success, latency=response.elapsed
1082+
)
1083+
10731084
if self.hooks.get("response"):
10741085
response_ = self.build_hook_response(response)
10751086
if isinstance(response_, Response):
@@ -1084,7 +1095,8 @@ async def _send(
10841095
self, request: Request, *, history: list = None, start: float = None
10851096
) -> Response:
10861097
start = start or time.perf_counter()
1087-
config = self.prepare_config(request, tls_identifier=await self.aprepare_tls_identifier(self.client_identifier))
1098+
tls_identifier = await self.aprepare_tls_identifier(self.client_identifier)
1099+
config = self.prepare_config(request, tls_identifier=tls_identifier)
10881100
response = Response.from_tls_response(
10891101
await self.session.arequest(config.to_dict()),
10901102
is_byte_response=config.isByteResponse,

tls_requests/models/rotators.py

Lines changed: 23 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,60 +16,21 @@
1616

1717
T = TypeVar("T")
1818

19-
TLS_IDENTIFIER_TEMPLATES = [
20-
"chrome_103",
21-
"chrome_104",
22-
"chrome_105",
23-
"chrome_106",
24-
"chrome_107",
25-
"chrome_108",
26-
"chrome_109",
27-
"chrome_110",
28-
"chrome_111",
29-
"chrome_112",
30-
"chrome_116_PSK",
31-
"chrome_116_PSK_PQ",
32-
"chrome_117",
19+
TLS_IDENTIFIER_TEMPLATES: list[str] = sorted([
3320
"chrome_120",
3421
"chrome_124",
35-
"safari_15_6_1",
22+
"chrome_131",
23+
"chrome_133",
24+
"firefox_120",
25+
"firefox_123",
26+
"firefox_132",
27+
"firefox_133",
3628
"safari_16_0",
37-
"safari_ios_15_5",
38-
"safari_ios_15_6",
3929
"safari_ios_16_0",
40-
"firefox_102",
41-
"firefox_104",
42-
"firefox_105",
43-
"firefox_106",
44-
"firefox_108",
45-
"firefox_110",
46-
"firefox_117",
47-
"firefox_120",
48-
"opera_89",
49-
"opera_90",
50-
"opera_91",
51-
"okhttp4_android_7",
52-
"okhttp4_android_8",
53-
"okhttp4_android_9",
54-
"okhttp4_android_10",
55-
"okhttp4_android_11",
56-
"okhttp4_android_12",
57-
"okhttp4_android_13",
58-
"zalando_ios_mobile",
59-
"zalando_android_mobile",
60-
"nike_ios_mobile",
61-
"nike_android_mobile",
62-
"mms_ios",
63-
"mms_ios_2",
64-
"mms_ios_3",
65-
"mesh_ios",
66-
"mesh_ios_2",
67-
"mesh_android",
68-
"mesh_android_2",
69-
"confirmed_ios",
70-
"confirmed_android",
71-
"confirmed_android_2",
72-
]
30+
"safari_ios_17_0",
31+
"safari_ios_18_0",
32+
"safari_ios_18_5",
33+
])
7334

7435
USER_AGENTS = [
7536
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
@@ -92,10 +53,9 @@
9253
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
9354
"Mozilla/5.0 (Linux; Android 14; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
9455
"Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
95-
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
9656
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1",
9757
"Mozilla/5.0 (Linux; Android 15; SM-S931B Build/AP3A.240905.015.A2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36",
98-
"Mozila/5.0 (Linux; Android 14; SM-S928B/DS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36",
58+
"Mozilla/5.0 (Linux; Android 14; SM-S928B/DS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36",
9959
"Mozilla/5.0 (Linux; Android 14; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36",
10060
"Mozilla/5.0 (Linux; Android 13; SM-S911U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Mobile Safari/537.36",
10161
"Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36",
@@ -318,11 +278,9 @@ class ProxyRotator(BaseRotator[Proxy]):
318278
def rebuild_item(cls, item: Any) -> Optional[Proxy]:
319279
"""Constructs a `Proxy` object from various input types."""
320280
try:
321-
if isinstance(item, Proxy):
322-
return item
323281
if isinstance(item, dict):
324282
return Proxy.from_dict(item)
325-
if isinstance(item, str):
283+
if isinstance(item, (str, Proxy)):
326284
return Proxy.from_string(item)
327285
except Exception:
328286
return None
@@ -414,7 +372,7 @@ class HeaderRotator(BaseRotator[Headers]):
414372
def __init__(
415373
self,
416374
items: Optional[Iterable[T]] = None,
417-
strategy: Literal["round_robin", "random", "weighted"] = "round_robin",
375+
strategy: Literal["round_robin", "random", "weighted"] = "random",
418376
) -> None:
419377
super().__init__(items or HEADER_TEMPLATES, strategy)
420378

@@ -434,11 +392,9 @@ def rebuild_item(cls, item: HeaderTypes) -> Optional[Headers]:
434392
try:
435393
if isinstance(item, Headers):
436394
return item
437-
if isinstance(item, (dict, list)):
438-
return Headers(item)
395+
return Headers(item)
439396
except Exception:
440397
return None
441-
return None
442398

443399
def next(self, user_agent: Optional[str] = None) -> Headers:
444400
"""
@@ -452,12 +408,12 @@ def next(self, user_agent: Optional[str] = None) -> Headers:
452408
Returns:
453409
A copy of the next `Headers` object, potentially with a modified User-Agent.
454410
"""
455-
base_headers = super().next()
456-
headers_copy = base_headers.copy()
457-
411+
headers = super().next()
412+
headers_copy = headers.copy()
413+
if not isinstance(headers_copy, Headers):
414+
headers_copy = Headers(headers_copy)
458415
if user_agent:
459416
headers_copy["User-Agent"] = user_agent
460-
461417
return headers_copy
462418

463419
async def anext(self, user_agent: Optional[str] = None) -> Headers:
@@ -472,8 +428,10 @@ async def anext(self, user_agent: Optional[str] = None) -> Headers:
472428
Returns:
473429
A copy of the next `Headers` object, potentially with a modified User-Agent.
474430
"""
475-
base_headers = await super().anext()
476-
headers_copy = base_headers.copy()
431+
headers = await super().anext()
432+
headers_copy = headers.copy()
433+
if not isinstance(headers_copy, Headers):
434+
headers_copy = Headers(headers_copy)
477435
if user_agent:
478436
headers_copy["User-Agent"] = user_agent
479437
return headers_copy

tls_requests/models/urls.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -455,18 +455,18 @@ def __init__(
455455
Raises:
456456
ProxyError: If the URL is invalid or the scheme is not supported.
457457
"""
458-
super().__init__(url, **kwargs)
459-
self.weight = weight
458+
self.weight = weight or 1.0
460459
self.region = region
461460
self.latency = latency
462461
self.success_rate = success_rate
463462
self.meta = meta or {}
464463
self.failures: int = 0
465464
self.last_used: Optional[float] = None
465+
super().__init__(url, **kwargs)
466466

467467
def __repr__(self):
468468
"""Returns a secure representation of the proxy with its weight."""
469-
return "<%s: %s, weight=%s>" % (self.__class__.__name__, unquote(self._build(True)), self.weight)
469+
return "<%s: %s, weight=%s>" % (self.__class__.__name__, unquote(self._build(True)), getattr(self, "weight", "unset"))
470470

471471
def _prepare(self, url: ProxyTypes) -> ParseResult:
472472
"""
@@ -490,6 +490,9 @@ def _prepare(self, url: ProxyTypes) -> ParseResult:
490490
if isinstance(url, str):
491491
url = url.strip()
492492

493+
if "://" not in str(url):
494+
url = f"http://{url}"
495+
493496
parsed = super(Proxy, self)._prepare(url)
494497
if str(parsed.scheme).lower() not in self.ALLOWED_SCHEMES:
495498
raise ProxyError(
@@ -632,13 +635,13 @@ def from_string(cls, raw: str, separator: str = "|") -> "Proxy":
632635

633636
parts = [p.strip() for p in raw.split(separator)]
634637
url = parts[0]
635-
weight = None
638+
weight = 1.0
636639
region = None
637640
if len(parts) >= 2 and parts[1]:
638641
try:
639642
weight = float(parts[1])
640643
except Exception:
641-
weight = None
644+
pass
642645
if len(parts) >= 3 and parts[2]:
643646
region = parts[2]
644647

tls_requests/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
DEFAULT_TLS_DEBUG = False
1010
DEFAULT_TLS_INSECURE_SKIP_VERIFY = False
1111
DEFAULT_TLS_HTTP2 = "auto"
12-
DEFAULT_TLS_IDENTIFIER = "chrome_120"
12+
DEFAULT_TLS_IDENTIFIER = "chrome_133"
1313
DEFAULT_HEADERS = {
1414
"accept": "*/*",
1515
"connection": "keep-alive",

0 commit comments

Comments
 (0)