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.
- {nonecap-0.6.2 → nonecap-0.8.0}/PKG-INFO +3 -2
- {nonecap-0.6.2 → nonecap-0.8.0}/README.md +2 -1
- {nonecap-0.6.2 → nonecap-0.8.0}/src/nonecap/__init__.py +2 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/src/nonecap/_errors.py +8 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/src/nonecap/_types.py +6 -0
- nonecap-0.8.0/src/nonecap/_version.py +1 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/conftest.py +1 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/test_client.py +20 -0
- nonecap-0.6.2/src/nonecap/_version.py +0 -1
- {nonecap-0.6.2 → nonecap-0.8.0}/.github/workflows/ci.yml +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/.gitignore +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/LICENSE +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/pyproject.toml +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/src/nonecap/_client.py +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/src/nonecap/py.typed +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/__init__.py +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/test_async_client.py +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/test_feedback.py +0 -0
- {nonecap-0.6.2 → nonecap-0.8.0}/tests/typing_assertions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nonecap
|
|
3
|
-
Version: 0.
|
|
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 `
|
|
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 `
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|