nonecap 0.6.2__tar.gz → 0.8.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nonecap
3
- Version: 0.6.2
3
+ Version: 0.8.0
4
4
  Summary: Official Python client for the NoneCap hCaptcha solving API.
5
5
  Project-URL: Homepage, https://nonecap.com
6
6
  Project-URL: Documentation, https://nonecap.com/api-reference
@@ -67,6 +67,7 @@ solve = nc.solve(
67
67
 
68
68
  print(solve.token) # the hCaptcha token, ready to submit
69
69
  print(solve.resp_key) # hcaptcha.getRespKey() equivalent, for sites that verify the pair
70
+ print(solve.user_agent) # submit the token with this User-Agent: some sites check the browser version
70
71
  ```
71
72
 
72
73
  `solve()` submits the captcha and waits until it's done, using the API's long-poll so you aren't hammering it with requests. It returns the solved solve, or raises if the solve fails or your timeout runs out.
@@ -170,7 +171,7 @@ except RateLimitError as err:
170
171
  time.sleep(err.retry_after or 1)
171
172
  ```
172
173
 
173
- 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` and `SitekeyRateLimitedError` telling the two 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` 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
175
 
175
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.
176
177
 
@@ -36,6 +36,7 @@ solve = nc.solve(
36
36
 
37
37
  print(solve.token) # the hCaptcha token, ready to submit
38
38
  print(solve.resp_key) # hcaptcha.getRespKey() equivalent, for sites that verify the pair
39
+ print(solve.user_agent) # submit the token with this User-Agent: some sites check the browser version
39
40
  ```
40
41
 
41
42
  `solve()` submits the captcha and waits until it's done, using the API's long-poll so you aren't hammering it with requests. It returns the solved solve, or raises if the solve fails or your timeout runs out.
@@ -139,7 +140,7 @@ except RateLimitError as err:
139
140
  time.sleep(err.retry_after or 1)
140
141
  ```
141
142
 
142
- 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` and `SitekeyRateLimitedError` telling the two 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` 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
144
 
144
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.
145
146
 
@@ -27,6 +27,7 @@ from ._errors import (
27
27
  NotFoundError,
28
28
  PayloadTooLargeError,
29
29
  PermissionDeniedError,
30
+ ProxyUnavailableError,
30
31
  RateLimitError,
31
32
  ServiceUnavailableError,
32
33
  SitekeyRateLimitedError,
@@ -97,6 +98,7 @@ __all__ = [
97
98
  "RateLimitError",
98
99
  "ConcurrencyLimitError",
99
100
  "SitekeyRateLimitedError",
101
+ "ProxyUnavailableError",
100
102
  "APIError",
101
103
  "ServiceUnavailableError",
102
104
  "APIConnectionError",
@@ -85,6 +85,12 @@ class SitekeyRateLimitedError(RateLimitError):
85
85
  Wait ``retry_after`` seconds; the message says whether a different proxy pool can help."""
86
86
 
87
87
 
88
+ class ProxyUnavailableError(RateLimitError):
89
+ """429 — nearly every recent solve through the proxy you supplied failed because the
90
+ proxy refused the connection, so submits through it are shed. Fix the proxy or switch
91
+ exit; ``retry_after`` says when the next submit is let through to re-check it."""
92
+
93
+
88
94
  class APIError(NoneCapError):
89
95
  """5xx, or a response that was not the expected shape. ``request_id`` is the id to quote."""
90
96
 
@@ -184,6 +190,8 @@ def error_from_response(
184
190
  cls = ConcurrencyLimitError
185
191
  elif code == "sitekey_rate_limited":
186
192
  cls = SitekeyRateLimitedError
193
+ elif code == "proxy_unavailable":
194
+ cls = ProxyUnavailableError
187
195
  elif code in ("rate_limited", "ext_daily_limit"):
188
196
  cls = RateLimitError
189
197
  elif code == "maintenance":
@@ -110,6 +110,11 @@ class Solve:
110
110
  """Milliseconds the solve waited in the queue before a worker picked it up."""
111
111
  resolve_ms: Optional[int]
112
112
  """Milliseconds of actual solving."""
113
+ user_agent: Optional[str] = None
114
+ """The browser user agent the solve presented while it earned ``token``. Some sites
115
+ reject a token submitted by a different browser version, so send the token with this
116
+ as your ``User-Agent`` header (and from the same IP as your proxy, if you gave one).
117
+ Set alongside ``token``, otherwise None."""
113
118
 
114
119
  @property
115
120
  def is_terminal(self) -> bool:
@@ -144,6 +149,7 @@ class Solve:
144
149
  finished_at=data.get("finished_at"),
145
150
  queue_ms=data.get("queue_ms"),
146
151
  resolve_ms=data.get("resolve_ms"),
152
+ user_agent=data.get("user_agent"),
147
153
  )
148
154
 
149
155
 
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -45,6 +45,7 @@ def solve_payload(**overrides: Any) -> dict[str, Any]:
45
45
  "url": "https://example.com",
46
46
  "token": None,
47
47
  "resp_key": None,
48
+ "user_agent": None,
48
49
  "error": None,
49
50
  "credits_charged": None,
50
51
  "proxy_bytes": None,
@@ -16,6 +16,7 @@ from nonecap import (
16
16
  NotFoundError,
17
17
  PayloadTooLargeError,
18
18
  PermissionDeniedError,
19
+ ProxyUnavailableError,
19
20
  RateLimitError,
20
21
  ServiceUnavailableError,
21
22
  SitekeyRateLimitedError,
@@ -66,6 +67,23 @@ class TestRetrieve:
66
67
  nc = client_for(script)
67
68
  assert nc.solves.retrieve("solve_1").resp_key is None
68
69
 
70
+ def test_returns_the_user_agent_the_solve_presented(self) -> None:
71
+ ua = (
72
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
73
+ " (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
74
+ )
75
+ script = Script((200, solve_payload(status="solved", token="P1_tok", user_agent=ua)))
76
+ nc = client_for(script)
77
+ assert nc.solves.retrieve("solve_1").user_agent == ua
78
+
79
+ def test_user_agent_is_none_until_solved_or_when_absent(self) -> None:
80
+ nc = client_for(Script((200, solve_payload())))
81
+ assert nc.solves.retrieve("solve_1").user_agent is None
82
+ payload = solve_payload(status="solved", token="P1_tok")
83
+ del payload["user_agent"]
84
+ nc = client_for(Script((200, payload)))
85
+ assert nc.solves.retrieve("solve_1").user_agent is None
86
+
69
87
 
70
88
  class TestCreate:
71
89
  def test_posts_with_bearer_auth_and_json_body(self) -> None:
@@ -186,6 +204,8 @@ class TestErrorMapping:
186
204
  (429, "concurrency_limit_exceeded", ConcurrencyLimitError),
187
205
  (429, "concurrency_limit_exceeded", RateLimitError),
188
206
  (429, "sitekey_rate_limited", SitekeyRateLimitedError),
207
+ (429, "proxy_unavailable", ProxyUnavailableError),
208
+ (429, "proxy_unavailable", RateLimitError),
189
209
  (429, "rate_limited", RateLimitError),
190
210
  (409, "conflict", ConflictError),
191
211
  (404, "not_found", NotFoundError),
@@ -1 +0,0 @@
1
- __version__ = "0.6.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes