solari-browser 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solari_browser-0.2.1 → solari_browser-0.2.2}/PKG-INFO +1 -1
- {solari_browser-0.2.1 → solari_browser-0.2.2}/pyproject.toml +1 -1
- {solari_browser-0.2.1 → solari_browser-0.2.2}/solari_browser/__init__.py +1 -1
- {solari_browser-0.2.1 → solari_browser-0.2.2}/solari_browser/client.py +16 -1
- {solari_browser-0.2.1 → solari_browser-0.2.2}/solari_browser/errors.py +3 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/tests/test_retry_hint.py +32 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/.gitignore +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/README.md +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/solari_browser/browser_session.py +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/solari_browser/types.py +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/tests/conftest.py +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/tests/test_client.py +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/tests/test_launch_seed.py +0 -0
- {solari_browser-0.2.1 → solari_browser-0.2.2}/tests/test_resolved_proxy.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solari-browser"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Python SDK for Solari Browser — managed, stealthy remote Chromium over the Playwright wire protocol / raw CDP."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -81,6 +81,21 @@ def _says_retryable(res: httpx.Response) -> bool:
|
|
|
81
81
|
return False
|
|
82
82
|
return isinstance(body, dict) and body.get("retryable") is True
|
|
83
83
|
|
|
84
|
+
# A few 503s are answers, not outages: retrying cannot change them, and swallowing the body would hide the
|
|
85
|
+
# code from the caller. `ProxyTierUnavailable` (explicitly requested proxy tier not servable) is one.
|
|
86
|
+
_TERMINAL_503_CODES = frozenset({"ProxyTierUnavailable"})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_terminal_503(res: httpx.Response) -> bool:
|
|
90
|
+
if res.status_code != 503:
|
|
91
|
+
return False
|
|
92
|
+
try:
|
|
93
|
+
body = res.json()
|
|
94
|
+
except Exception: # noqa: BLE001 - non-JSON body is an ordinary outage
|
|
95
|
+
return False
|
|
96
|
+
return isinstance(body, dict) and body.get("code") in _TERMINAL_503_CODES
|
|
97
|
+
|
|
98
|
+
|
|
84
99
|
# Mirrors `isLikelyTransientConnect`: which connect failures are worth relaunching.
|
|
85
100
|
_TRANSIENT_CONNECT = re.compile(
|
|
86
101
|
r"ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|socket hang up|WebSocket|connect|"
|
|
@@ -471,7 +486,7 @@ class Solari:
|
|
|
471
486
|
res = await self._http.request(
|
|
472
487
|
method, url, headers=headers, content=payload
|
|
473
488
|
)
|
|
474
|
-
retry_this = res.status_code in _RETRYABLE_STATUS or (
|
|
489
|
+
retry_this = (res.status_code in _RETRYABLE_STATUS and not _is_terminal_503(res)) or (
|
|
475
490
|
_is_safe_to_replay(method, idempotency_key)
|
|
476
491
|
and _says_retryable(res)
|
|
477
492
|
)
|
|
@@ -17,6 +17,9 @@ BROWSER_UNHEALTHY = "BrowserUnhealthy"
|
|
|
17
17
|
# `_release_rejection` in client.py for why a 404 is not blanket-success on
|
|
18
18
|
# DELETE /sessions/:id.
|
|
19
19
|
INVALID_SESSION_ID = "InvalidSessionId"
|
|
20
|
+
# PROXY_TIER_UNAVAILABLE — POST /sessions 503: a proxy tier you explicitly requested cannot be served right now.
|
|
21
|
+
# No session was created. Not retried by the client (the answer cannot change within a retry).
|
|
22
|
+
PROXY_TIER_UNAVAILABLE = "ProxyTierUnavailable"
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class SolariError(Exception):
|
|
@@ -83,3 +83,35 @@ def test_507_is_retryable_even_though_this_gateway_never_emits_one():
|
|
|
83
83
|
# Negative control: permanent statuses must stay out.
|
|
84
84
|
assert 500 not in _RETRYABLE_STATUS
|
|
85
85
|
assert 501 not in _RETRYABLE_STATUS
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_proxy_tier_unavailable_503_is_not_retried_and_keeps_its_code(gw): # noqa: F811
|
|
89
|
+
gw.route("POST", "/sessions", 503,
|
|
90
|
+
{"error": "Requested proxy tier 'residential' is unavailable", "code": "ProxyTierUnavailable", "tier": "residential"})
|
|
91
|
+
gw.route("POST", "/sessions", 201, {"sessionId": "s-1", "wsEndpoint": "ws://127.0.0.1:1/ws"})
|
|
92
|
+
|
|
93
|
+
c = client(gw, backoff_ms=1)
|
|
94
|
+
|
|
95
|
+
@sync
|
|
96
|
+
async def create():
|
|
97
|
+
return await c.sessions.create(proxy={"tier": "residential", "country": "us"})
|
|
98
|
+
|
|
99
|
+
with pytest.raises(SolariError) as info:
|
|
100
|
+
create()
|
|
101
|
+
assert info.value.code == "ProxyTierUnavailable"
|
|
102
|
+
assert info.value.status == 503
|
|
103
|
+
assert len(gw.seen) == 1, "a terminal 503 must not be retried"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_ordinary_503_is_still_retried(gw): # noqa: F811
|
|
107
|
+
gw.route("POST", "/sessions", 503, {"error": "No stealth pool available", "kind": "stealth"})
|
|
108
|
+
gw.route("POST", "/sessions", 201, {"sessionId": "s-1", "wsEndpoint": "ws://127.0.0.1:1/ws"})
|
|
109
|
+
|
|
110
|
+
c = client(gw, backoff_ms=1)
|
|
111
|
+
|
|
112
|
+
@sync
|
|
113
|
+
async def create():
|
|
114
|
+
return await c.sessions.create()
|
|
115
|
+
|
|
116
|
+
create()
|
|
117
|
+
assert len(gw.seen) == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|