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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nonecap
3
- Version: 0.8.2
4
- Summary: Official Python client for the NoneCap hCaptcha solving API.
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">NoneCap</a> hCaptcha solving API.</p>
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` and `ProxyUnavailableError` (your own proxy is refusing connections) 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.
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">NoneCap</a> hCaptcha solving API.</p>
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` and `ProxyUnavailableError` (your own proxy is refusing connections) 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.
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 NoneCap hCaptcha solving API."
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":
@@ -62,6 +62,7 @@ SolveErrorReason = Literal[
62
62
  "browser_lane_capped",
63
63
  "recaptcha_not_loaded",
64
64
  "refused_wording_unescaped",
65
+ "session_capped",
65
66
  ]
66
67
  """A typed sub-reason within :data:`SolveErrorCode`, set when the solver knows more
67
68
  than the code says."""
@@ -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