nonecap 0.6.0__tar.gz → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nonecap
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -55,6 +55,11 @@ SolveErrorReason = Literal[
55
55
  "proxy_rejected",
56
56
  "proxy_tls",
57
57
  "proxy_stalled",
58
+ "proxy_egress_blocked",
59
+ "target_egress_blocked",
60
+ "profile_engine_unavailable",
61
+ "type_not_served",
62
+ "browser_lane_capped",
58
63
  ]
59
64
  """A typed sub-reason within :data:`SolveErrorCode`, set when the solver knows more
60
65
  than the code says."""
@@ -0,0 +1 @@
1
+ __version__ = "0.6.2"
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import get_args
4
+
3
5
  import httpx
4
6
  import pytest
5
7
 
@@ -17,6 +19,7 @@ from nonecap import (
17
19
  RateLimitError,
18
20
  ServiceUnavailableError,
19
21
  SitekeyRateLimitedError,
22
+ SolveErrorReason,
20
23
  SolveFailedError,
21
24
  SolveTimeoutError,
22
25
  UnsupportedMediaTypeError,
@@ -254,6 +257,63 @@ class TestErrorMapping:
254
257
  assert info.value.reason == "proxy_rejected"
255
258
  assert info.value.retryable is False
256
259
 
260
+ @pytest.mark.parametrize(
261
+ ("code", "reason"),
262
+ [
263
+ ("proxy_error", "proxy_egress_blocked"),
264
+ ("target_unreachable", "target_egress_blocked"),
265
+ ],
266
+ )
267
+ def test_egress_blocked_solves_are_typed_and_not_retryable(
268
+ self, code: str, reason: str
269
+ ) -> None:
270
+ assert reason in get_args(SolveErrorReason)
271
+ failed = solve_payload(
272
+ status="failed",
273
+ error={
274
+ "code": code,
275
+ "message": "points at a private or internal network address",
276
+ "reason": reason,
277
+ "retryable": False,
278
+ "docs_url": "https://nonecap.com/api-reference#errors",
279
+ },
280
+ )
281
+ nc = client_for(Script((200, failed)))
282
+ with pytest.raises(SolveFailedError) as info:
283
+ nc.solve(type="hcaptcha", sitekey="sk", url="https://example.com")
284
+ assert info.value.solve_code == code
285
+ assert info.value.reason == reason
286
+ assert info.value.retryable is False
287
+
288
+ @pytest.mark.parametrize(
289
+ ("code", "reason", "retryable"),
290
+ [
291
+ ("capacity_exhausted", "profile_engine_unavailable", True),
292
+ ("capacity_exhausted", "browser_lane_capped", True),
293
+ ("internal_error", "type_not_served", False),
294
+ ],
295
+ )
296
+ def test_uncharged_capacity_and_type_reasons_are_typed(
297
+ self, code: str, reason: str, retryable: bool
298
+ ) -> None:
299
+ assert reason in get_args(SolveErrorReason)
300
+ failed = solve_payload(
301
+ status="failed",
302
+ error={
303
+ "code": code,
304
+ "message": "not charged",
305
+ "reason": reason,
306
+ "retryable": retryable,
307
+ "docs_url": "https://nonecap.com/api-reference#errors",
308
+ },
309
+ )
310
+ nc = client_for(Script((200, failed)))
311
+ with pytest.raises(SolveFailedError) as info:
312
+ nc.solve(type="hcaptcha", sitekey="sk", url="https://example.com")
313
+ assert info.value.solve_code == code
314
+ assert info.value.reason == reason
315
+ assert info.value.retryable is retryable
316
+
257
317
  def test_older_server_error_object_still_parses(self) -> None:
258
318
  failed = solve_payload(status="failed", error={"code": "vision_error", "message": "x"})
259
319
  nc = client_for(Script((200, failed)))
@@ -20,6 +20,7 @@ from nonecap import (
20
20
  FeedbackReport,
21
21
  NoneCap,
22
22
  Solve,
23
+ SolveErrorReason,
23
24
  SolveHandle,
24
25
  SolveTimeoutError,
25
26
  )
@@ -84,6 +85,17 @@ def negative_cases() -> None:
84
85
  nc.solve(type="recaptcha", sitekey="s", url="u") # type: ignore[call-overload]
85
86
 
86
87
 
88
+ def egress_blocked_reasons() -> None:
89
+ # The egress guard's reasons are part of the typed vocabulary.
90
+ _r1: SolveErrorReason = "proxy_egress_blocked"
91
+ _r2: SolveErrorReason = "target_egress_blocked"
92
+ _r3: SolveErrorReason = "profile_engine_unavailable"
93
+ _r4: SolveErrorReason = "type_not_served"
94
+ _r5: SolveErrorReason = "browser_lane_capped"
95
+ # A reason the API never defined must not type-check.
96
+ _bad: SolveErrorReason = "proxy_egress_blockd" # type: ignore[assignment]
97
+
98
+
87
99
  def timeout_error_fields(err: SolveTimeoutError) -> None:
88
100
  # SolveTimeoutError exposes the solve it timed out on.
89
101
  _sid: Optional[str] = err.solve_id
@@ -1 +0,0 @@
1
- __version__ = "0.6.0"
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
File without changes