nonecap 0.8.2__tar.gz → 0.8.4__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.
- {nonecap-0.8.2 → nonecap-0.8.4}/PKG-INFO +5 -5
- {nonecap-0.8.2 → nonecap-0.8.4}/README.md +2 -2
- {nonecap-0.8.2 → nonecap-0.8.4}/pyproject.toml +2 -2
- {nonecap-0.8.2 → nonecap-0.8.4}/src/nonecap/__init__.py +2 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/src/nonecap/_errors.py +7 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/src/nonecap/_types.py +1 -0
- nonecap-0.8.4/src/nonecap/_version.py +1 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/test_client.py +27 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/typing_assertions.py +1 -0
- nonecap-0.8.2/src/nonecap/_version.py +0 -1
- {nonecap-0.8.2 → nonecap-0.8.4}/.github/workflows/ci.yml +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/.gitignore +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/LICENSE +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/src/nonecap/_client.py +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/src/nonecap/py.typed +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/__init__.py +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/conftest.py +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/test_async_client.py +0 -0
- {nonecap-0.8.2 → nonecap-0.8.4}/tests/test_feedback.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nonecap
|
|
3
|
-
Version: 0.8.
|
|
4
|
-
Summary: Official Python client for the
|
|
5
|
-
Project-URL: Homepage, https://nonecap.com
|
|
3
|
+
Version: 0.8.4
|
|
4
|
+
Summary: Official Python client for NoneCap, the hCaptcha solver API.
|
|
5
|
+
Project-URL: Homepage, https://nonecap.com/
|
|
6
6
|
Project-URL: Documentation, https://nonecap.com/api-reference
|
|
7
7
|
Project-URL: Repository, https://github.com/nonecap/nonecap-py
|
|
8
8
|
Project-URL: Issues, https://github.com/nonecap/nonecap-py/issues
|
|
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
39
39
|
</p>
|
|
40
40
|
|
|
41
|
-
<p align="center">Official Python client for the <a href="https://nonecap.com">
|
|
41
|
+
<p align="center">Official Python client for the NoneCap <a href="https://nonecap.com/">hCaptcha solver</a> API.</p>
|
|
42
42
|
|
|
43
43
|
Submit a captcha, get back a token. The client handles the polling, the timeouts, and the error cases so you don't write the request loop yourself. Sync and async, fully typed.
|
|
44
44
|
|
|
@@ -171,7 +171,7 @@ except RateLimitError as err:
|
|
|
171
171
|
time.sleep(err.retry_after or 1)
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
The subclasses are `AuthenticationError` (401), `PermissionDeniedError` (403), `InsufficientCreditsError` (402, with `KeyCreditLimitError` when one API key hit its own cap), `ValidationError` (422/400, with a `param` naming the bad field; `PayloadTooLargeError` for 413 and `UnsupportedMediaTypeError` for 415), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `ConcurrencyLimitError`, `SitekeyRateLimitedError
|
|
174
|
+
The subclasses are `AuthenticationError` (401), `PermissionDeniedError` (403), `InsufficientCreditsError` (402, with `KeyCreditLimitError` when one API key hit its own cap), `ValidationError` (422/400, with a `param` naming the bad field; `PayloadTooLargeError` for 413 and `UnsupportedMediaTypeError` for 415), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `ConcurrencyLimitError`, `SitekeyRateLimitedError`, `ProxyUnavailableError` (your own proxy is refusing connections) and `RateCappedError` (your account reached its submit rate cap on this sitekey) telling them apart; `retry_after` is the seconds the API asked you to wait), `APIError` (5xx, with `ServiceUnavailableError` for a maintenance pause), `APIConnectionError` and `APITimeoutError` (the request never landed), and `SolveTimeoutError` (your `solve()` budget ran out). Every error from a response carries `request_id`, the id to quote to support.
|
|
175
175
|
|
|
176
176
|
`SolveFailedError` carries the full `solve`. `solve.error.code` is a `SolveErrorCode`, `solve.error.reason` a typed sub-reason or `None` (`proxy_rejected`, `sitekey_rate_limited`, …), and `solve.error.retryable` says whether resubmitting the same request unchanged can succeed; `err.retryable`, `err.reason` and `err.solve_code` are shortcuts to those fields. Failed solves are never charged.
|
|
177
177
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
<p align="center">Official Python client for the <a href="https://nonecap.com">
|
|
10
|
+
<p align="center">Official Python client for the NoneCap <a href="https://nonecap.com/">hCaptcha solver</a> API.</p>
|
|
11
11
|
|
|
12
12
|
Submit a captcha, get back a token. The client handles the polling, the timeouts, and the error cases so you don't write the request loop yourself. Sync and async, fully typed.
|
|
13
13
|
|
|
@@ -140,7 +140,7 @@ except RateLimitError as err:
|
|
|
140
140
|
time.sleep(err.retry_after or 1)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
The subclasses are `AuthenticationError` (401), `PermissionDeniedError` (403), `InsufficientCreditsError` (402, with `KeyCreditLimitError` when one API key hit its own cap), `ValidationError` (422/400, with a `param` naming the bad field; `PayloadTooLargeError` for 413 and `UnsupportedMediaTypeError` for 415), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `ConcurrencyLimitError`, `SitekeyRateLimitedError
|
|
143
|
+
The subclasses are `AuthenticationError` (401), `PermissionDeniedError` (403), `InsufficientCreditsError` (402, with `KeyCreditLimitError` when one API key hit its own cap), `ValidationError` (422/400, with a `param` naming the bad field; `PayloadTooLargeError` for 413 and `UnsupportedMediaTypeError` for 415), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `ConcurrencyLimitError`, `SitekeyRateLimitedError`, `ProxyUnavailableError` (your own proxy is refusing connections) and `RateCappedError` (your account reached its submit rate cap on this sitekey) telling them apart; `retry_after` is the seconds the API asked you to wait), `APIError` (5xx, with `ServiceUnavailableError` for a maintenance pause), `APIConnectionError` and `APITimeoutError` (the request never landed), and `SolveTimeoutError` (your `solve()` budget ran out). Every error from a response carries `request_id`, the id to quote to support.
|
|
144
144
|
|
|
145
145
|
`SolveFailedError` carries the full `solve`. `solve.error.code` is a `SolveErrorCode`, `solve.error.reason` a typed sub-reason or `None` (`proxy_rejected`, `sitekey_rate_limited`, …), and `solve.error.retryable` says whether resubmitting the same request unchanged can succeed; `err.retryable`, `err.reason` and `err.solve_code` are shortcuts to those fields. Failed solves are never charged.
|
|
146
146
|
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nonecap"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "Official Python client for the
|
|
8
|
+
description = "Official Python client for NoneCap, the hCaptcha solver API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
@@ -35,7 +35,7 @@ classifiers = [
|
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.urls]
|
|
38
|
-
Homepage = "https://nonecap.com"
|
|
38
|
+
Homepage = "https://nonecap.com/"
|
|
39
39
|
Documentation = "https://nonecap.com/api-reference"
|
|
40
40
|
Repository = "https://github.com/nonecap/nonecap-py"
|
|
41
41
|
Issues = "https://github.com/nonecap/nonecap-py/issues"
|
|
@@ -28,6 +28,7 @@ from ._errors import (
|
|
|
28
28
|
PayloadTooLargeError,
|
|
29
29
|
PermissionDeniedError,
|
|
30
30
|
ProxyUnavailableError,
|
|
31
|
+
RateCappedError,
|
|
31
32
|
RateLimitError,
|
|
32
33
|
ServiceUnavailableError,
|
|
33
34
|
SitekeyRateLimitedError,
|
|
@@ -99,6 +100,7 @@ __all__ = [
|
|
|
99
100
|
"ConcurrencyLimitError",
|
|
100
101
|
"SitekeyRateLimitedError",
|
|
101
102
|
"ProxyUnavailableError",
|
|
103
|
+
"RateCappedError",
|
|
102
104
|
"APIError",
|
|
103
105
|
"ServiceUnavailableError",
|
|
104
106
|
"APIConnectionError",
|
|
@@ -91,6 +91,11 @@ class ProxyUnavailableError(RateLimitError):
|
|
|
91
91
|
exit; ``retry_after`` says when the next submit is let through to re-check it."""
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
class RateCappedError(RateLimitError):
|
|
95
|
+
"""429 — your account has a submit rate cap on this sitekey and reached it for the
|
|
96
|
+
trailing minute. Nothing was charged; wait ``retry_after`` seconds."""
|
|
97
|
+
|
|
98
|
+
|
|
94
99
|
class APIError(NoneCapError):
|
|
95
100
|
"""5xx, or a response that was not the expected shape. ``request_id`` is the id to quote."""
|
|
96
101
|
|
|
@@ -192,6 +197,8 @@ def error_from_response(
|
|
|
192
197
|
cls = SitekeyRateLimitedError
|
|
193
198
|
elif code == "proxy_unavailable":
|
|
194
199
|
cls = ProxyUnavailableError
|
|
200
|
+
elif code == "rate_capped":
|
|
201
|
+
cls = RateCappedError
|
|
195
202
|
elif code in ("rate_limited", "ext_daily_limit"):
|
|
196
203
|
cls = RateLimitError
|
|
197
204
|
elif code == "maintenance":
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.4"
|
|
@@ -17,6 +17,7 @@ from nonecap import (
|
|
|
17
17
|
PayloadTooLargeError,
|
|
18
18
|
PermissionDeniedError,
|
|
19
19
|
ProxyUnavailableError,
|
|
20
|
+
RateCappedError,
|
|
20
21
|
RateLimitError,
|
|
21
22
|
ServiceUnavailableError,
|
|
22
23
|
SitekeyRateLimitedError,
|
|
@@ -206,6 +207,8 @@ class TestErrorMapping:
|
|
|
206
207
|
(429, "sitekey_rate_limited", SitekeyRateLimitedError),
|
|
207
208
|
(429, "proxy_unavailable", ProxyUnavailableError),
|
|
208
209
|
(429, "proxy_unavailable", RateLimitError),
|
|
210
|
+
(429, "rate_capped", RateCappedError),
|
|
211
|
+
(429, "rate_capped", RateLimitError),
|
|
209
212
|
(429, "rate_limited", RateLimitError),
|
|
210
213
|
(409, "conflict", ConflictError),
|
|
211
214
|
(404, "not_found", NotFoundError),
|
|
@@ -245,6 +248,29 @@ class TestErrorMapping:
|
|
|
245
248
|
assert info.value.retry_after == 15
|
|
246
249
|
assert info.value.request_id == "req_1"
|
|
247
250
|
|
|
251
|
+
def test_rate_capped_carries_the_retry_after_delay(self) -> None:
|
|
252
|
+
def responder(request: httpx.Request) -> httpx.Response:
|
|
253
|
+
return httpx.Response(
|
|
254
|
+
429,
|
|
255
|
+
json={
|
|
256
|
+
"error": {
|
|
257
|
+
"code": "rate_capped",
|
|
258
|
+
"message": "capped",
|
|
259
|
+
"param": None,
|
|
260
|
+
"request_id": "req_3",
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
headers={"Retry-After": "7"},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
nc = client_for(Script(responder))
|
|
267
|
+
with pytest.raises(RateCappedError) as info:
|
|
268
|
+
nc.me()
|
|
269
|
+
assert isinstance(info.value, RateLimitError)
|
|
270
|
+
assert info.value.code == "rate_capped"
|
|
271
|
+
assert info.value.retry_after == 7
|
|
272
|
+
assert info.value.request_id == "req_3"
|
|
273
|
+
|
|
248
274
|
def test_request_id_falls_back_to_the_header(self) -> None:
|
|
249
275
|
def responder(request: httpx.Request) -> httpx.Response:
|
|
250
276
|
return httpx.Response(
|
|
@@ -313,6 +339,7 @@ class TestErrorMapping:
|
|
|
313
339
|
("internal_error", "type_not_served", False),
|
|
314
340
|
("challenge_not_loaded", "recaptcha_not_loaded", False),
|
|
315
341
|
("token_not_granted", "refused_wording_unescaped", True),
|
|
342
|
+
("capacity_exhausted", "session_capped", True),
|
|
316
343
|
],
|
|
317
344
|
)
|
|
318
345
|
def test_uncharged_capacity_and_type_reasons_are_typed(
|
|
@@ -94,6 +94,7 @@ def egress_blocked_reasons() -> None:
|
|
|
94
94
|
_r5: SolveErrorReason = "browser_lane_capped"
|
|
95
95
|
_r6: SolveErrorReason = "recaptcha_not_loaded"
|
|
96
96
|
_r7: SolveErrorReason = "refused_wording_unescaped"
|
|
97
|
+
_r8: SolveErrorReason = "session_capped"
|
|
97
98
|
# A reason the API never defined must not type-check.
|
|
98
99
|
_bad: SolveErrorReason = "proxy_egress_blockd" # type: ignore[assignment]
|
|
99
100
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.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
|
|
File without changes
|