captcha-solver-api 1.0.0__tar.gz → 1.0.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.
Files changed (19) hide show
  1. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/PKG-INFO +36 -28
  2. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/README.md +35 -27
  3. captcha_solver_api-1.0.2/captcha_solver_api/_version.py +1 -0
  4. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/async_client.py +12 -7
  5. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/client.py +12 -7
  6. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/tasks.py +13 -11
  7. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api.egg-info/PKG-INFO +36 -28
  8. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api.egg-info/SOURCES.txt +2 -1
  9. captcha_solver_api-1.0.2/tests/test_polling.py +89 -0
  10. captcha_solver_api-1.0.0/captcha_solver_api/_version.py +0 -1
  11. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/LICENSE.md +0 -0
  12. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/__init__.py +0 -0
  13. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/exceptions.py +0 -0
  14. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api/py.typed +0 -0
  15. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api.egg-info/dependency_links.txt +0 -0
  16. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api.egg-info/requires.txt +0 -0
  17. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/captcha_solver_api.egg-info/top_level.txt +0 -0
  18. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/pyproject.toml +0 -0
  19. {captcha_solver_api-1.0.0 → captcha_solver_api-1.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: captcha-solver-api
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Official Python SDK for solving reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3/v4, Yandex SmartCaptcha, Tencent, and image/click captchas via the Captcha Solver API
5
5
  Author: Captcha Solver Team
6
6
  License-Expression: MIT
@@ -110,7 +110,7 @@ client = CaptchaClient("your_api_key")
110
110
 
111
111
  ## Quick Start
112
112
 
113
- Solve a reCAPTCHA v2 in 4 lines.
113
+ Solve a reCAPTCHA v2 with a task object and one `solve()` call.
114
114
 
115
115
  ```python
116
116
  from captcha_solver_api import CaptchaClient
@@ -156,8 +156,8 @@ Constructor.
156
156
  |---|---|---|---|
157
157
  | `client_key` | `str` | required | Your Captcha Solver API key. Raises `ValidationError` if empty. |
158
158
  | `base_url` | `str` | `https://api.captcha-solver.com` | API base URL. Override only for self-hosted/staging deployments. |
159
- | `timeout` | `int` | `120` | Default max seconds `solve()` waits for a solution before raising `CaptchaTimeoutError`. Overridable per call. |
160
- | `polling_interval` | `int` | `3` | Seconds between `getTaskResult` polls inside `solve()`. |
159
+ | `timeout` | `int` | `120` | Polling window after task creation, in seconds. Overridable per call; each HTTP request has a separate 30-second timeout. |
160
+ | `polling_interval` | `int` | `10` | Seconds before the first `getTaskResult` poll and between subsequent polls. |
161
161
  | `language_pool` | `Optional[str]` | `None` | Default worker pool (`"en"` or `"ru"`) applied to every call that doesn't pass its own `language_pool`. |
162
162
 
163
163
  Both clients hold a reusable connection pool (`requests.Session` / `httpx.AsyncClient`)
@@ -191,9 +191,9 @@ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
191
191
  ### `create_task(task, language_pool=None)`
192
192
 
193
193
  Submits `task` and returns its numeric task ID without waiting for a solution.
194
- Same parameters as `solve()`. Use this instead of `solve()` only if you need to
194
+ Accepts `task` and `language_pool`; it does not accept `timeout`. Use this instead of `solve()` if you need to
195
195
  manage polling yourself (e.g. checking on many tasks from a different process).
196
- Raises `ApiError`, `NetworkError`.
196
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
197
197
 
198
198
  ### `get_task_result(task_id)`
199
199
 
@@ -201,12 +201,12 @@ Fetches the current status of a task created with `create_task()`. Always
201
201
  returns a dict with a `status` key (`"processing"` or `"ready"`); when
202
202
  `"ready"`, also has a `solution` dict. This is a single poll, not a wait --
203
203
  call it repeatedly (as `solve()` does) until `status` is `"ready"`.
204
- Raises `ApiError`, `NetworkError`.
204
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
205
205
 
206
206
  ### `get_balance()`
207
207
 
208
208
  Returns the account's current balance (`float`) in the account's currency.
209
- Raises `ApiError`, `NetworkError`.
209
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
210
210
 
211
211
  ## Captcha Types
212
212
 
@@ -276,8 +276,10 @@ task = RecaptchaV2Task(
276
276
  Use this method to solve the Enterprise version of reCAPTCHA v2 and obtain a
277
277
  token for a page that uses `grecaptcha.enterprise`.
278
278
 
279
- `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask`. Same fields
280
- as reCAPTCHA v2, plus:
279
+ `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask` accept
280
+ `websiteURL`, `websiteKey`, `isInvisible`, `apiDomain`, `userAgent`, and `cookies`,
281
+ plus `enterprisePayload`. Use `enterprisePayload={"s": "..."}` for the
282
+ Enterprise `s` value; these classes do not accept `recaptchaDataSValue`.
281
283
 
282
284
  | Parameter | Required | Description |
283
285
  |---|---|---|
@@ -307,8 +309,8 @@ With proxy, use `RecaptchaV2EnterpriseTask` (same proxy fields as reCAPTCHA v2).
307
309
  Use this method to obtain a score-based reCAPTCHA v3 token for a specific site,
308
310
  action, and minimum score. This variant does not use a proxy.
309
311
 
310
- `RecaptchaV3TaskProxyless`. No proxy variant exists -- v3 is score-based and
311
- invisible, so there's no widget/session to pin to a proxy IP.
312
+ `RecaptchaV3TaskProxyless`. This API supports v3 through the service's IPs;
313
+ there is no corresponding task type for your own proxy.
312
314
 
313
315
  | Parameter | Required | Description |
314
316
  |---|---|---|
@@ -353,7 +355,10 @@ that the target page expects in `cf-turnstile-response`.
353
355
  | `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. |
354
356
  | `userAgent` | no | User-Agent to solve with -- the returned token is tied to it, submit with the same one. |
355
357
 
356
- **Response:** `token` -- submit as `cf-turnstile-response`.
358
+ **Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare
359
+ Challenge pages, the solution also includes `userAgent`; use that User-Agent
360
+ when submitting the token. Pass `action`, `data`, and `pagedata` when the page
361
+ provides them. The API field is spelled `pagedata`, all lowercase.
357
362
 
358
363
  ```python
359
364
  from captcha_solver_api import CaptchaClient
@@ -425,7 +430,7 @@ pass the values collected from the target page before creating the task.
425
430
  | `version` | no | `3` (default) or `4`. |
426
431
  | `gt` | v3 only | Public key of the GeeTest widget. |
427
432
  | `challenge` | v3 only | Session-specific challenge value from the page -- must be freshly fetched for every request, it cannot be reused. |
428
- | `initParameters` | v4 only | Extra parameters from the page's `initGeetest` call; for v4 must contain `captcha_id`. |
433
+ | `initParameters` | required for v4; optional for v3 | Extra initialization parameters; for v4 must contain `captcha_id`. |
429
434
  | `geetestApiServerSubdomain` | no | Custom GeeTest API subdomain, if the site uses one. |
430
435
  | `userAgent` | no | User-Agent to solve with. |
431
436
  | `risk_type` | no | Value of the `risk_type` parameter from the captcha-loading request, if present. Dynamic, single-use, and time-limited. |
@@ -471,7 +476,7 @@ for the widget on the target page. Use the coordinates method for image challeng
471
476
 
472
477
  `YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based
473
478
  challenge. For the image challenge instead, use `CoordinatesTask` with
474
- `imgType="smart_captcha"` (see [Coordinates](#coordinates-click-captcha) below).
479
+ [Coordinates](#coordinates-click-captcha) is available for generic click captchas.
475
480
 
476
481
  | Parameter | Required | Description |
477
482
  |---|---|---|
@@ -514,10 +519,9 @@ SmartCaptcha's image challenge. No proxy variant -- the image is submitted direc
514
519
  |---|---|---|
515
520
  | `body` | yes | The captcha image, base64-encoded. |
516
521
  | `comment` | no (recommended) | Hint for the worker, e.g. `"click on the green apple"`. |
517
- | `imgInstructions` | required for `imgType="smart_captcha"` | Instruction image, base64-encoded, showing what to click and in what order. |
522
+ | `imgInstructions` | no | Optional instruction image, base64-encoded. |
518
523
  | `minClicks` | no | Minimum number of clicks expected (default `1`). |
519
524
  | `maxClicks` | no | Maximum number of clicks allowed. |
520
- | `imgType` | no | `"smart_captcha"` to solve a Yandex SmartCaptcha image challenge instead of a generic click captcha. |
521
525
 
522
526
  **Response:** `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click, in order.
523
527
 
@@ -535,9 +539,6 @@ task = CoordinatesTask(
535
539
  result = client.solve(task)
536
540
  print(result["coordinates"]) # [{"x": 140, "y": 110}]
537
541
  ```
538
- For the Yandex SmartCaptcha image challenge, see
539
- [examples/sync/yandex_smartcaptcha_image.py](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/sync/yandex_smartcaptcha_image.py)
540
- (or [examples/async](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/async/yandex_smartcaptcha_image.py)).
541
542
 
542
543
  ### Tencent
543
544
 
@@ -598,6 +599,14 @@ it for every other call:
598
599
  result = client.solve(task, timeout=300)
599
600
  ```
600
601
 
602
+ The [API requires at least 5 seconds between polls](https://captcha-solver.com/en/docs/how-it-works).
603
+ The SDK defaults to 10 seconds. `solve()` waits for `polling_interval` before
604
+ its first poll and between polls.
605
+ Its default 120-second polling window is independent of the API's five-minute
606
+ task lifetime. A local timeout does not cancel a task or mean that the API
607
+ failed to solve it. Use `create_task()` and keep its `task_id` if you need to
608
+ check the same task later; calling `solve()` again creates a new task.
609
+
601
610
  ### Worker language pool
602
611
 
603
612
  Set a default `language_pool` once at construction instead of passing it to every call:
@@ -682,10 +691,10 @@ Avoid importing it by name: it shadows Python's built-in `TimeoutError`.
682
691
  See the dedicated [examples documentation](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/README.md) for the full
683
692
  sync/async example list, setup steps, expected results, and placeholder guidance.
684
693
 
685
- - **Image/click captchas** (`image_to_text.py`, `coordinates.py`,
686
- `yandex_smartcaptcha_image.py`) run end-to-end with nothing but a valid
694
+ - **Image/click captchas** (`image_to_text.py`, `coordinates.py`) run after installing the dependencies and setting a valid
687
695
  `CAPTCHA_API_KEY` -- they read sample images bundled in
688
696
  [examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed.
697
+ `python-dotenv` is optional: install it only if you want the scripts to load a `.env` file.
689
698
  - **Token captchas** (`recaptcha_v2.py`, `recaptcha_v2_enterprise.py`, `recaptcha_v3.py`,
690
699
  `turnstile.py`, `yandex_smartcaptcha.py`, `geetest_v4.py`, `tencent.py`) use
691
700
  placeholder values (`https://example.com/...`, `YOUR_WEBSITE_KEY`, `YOUR_APP_ID`,
@@ -709,12 +718,11 @@ python examples/sync/image_to_text.py
709
718
  python examples/sync/coordinates.py
710
719
  ```
711
720
 
712
- **Verified against the live API** during development, using real target pages and
713
- real proxy credentials in place of the placeholders shown above: every captcha
714
- type in this SDK -- proxyless and with proxy -- returned a real, correctly-shaped
715
- solution when pointed at a genuine target. `geetest_v3.py`'s request shape was
716
- likewise confirmed correct when given a real, freshly-fetched `gt`/`challenge`
717
- pair.
721
+ Live checks with real parameters have exercised all supported task types.
722
+ Individual tasks may return `ERROR_CAPTCHA_UNSOLVABLE` or exceed the client's
723
+ polling timeout. A `ready` response confirms that the service returned a
724
+ solution; the target website must still accept the token with the matching
725
+ page and session parameters.
718
726
 
719
727
  ## Requirements
720
728
 
@@ -74,7 +74,7 @@ client = CaptchaClient("your_api_key")
74
74
 
75
75
  ## Quick Start
76
76
 
77
- Solve a reCAPTCHA v2 in 4 lines.
77
+ Solve a reCAPTCHA v2 with a task object and one `solve()` call.
78
78
 
79
79
  ```python
80
80
  from captcha_solver_api import CaptchaClient
@@ -120,8 +120,8 @@ Constructor.
120
120
  |---|---|---|---|
121
121
  | `client_key` | `str` | required | Your Captcha Solver API key. Raises `ValidationError` if empty. |
122
122
  | `base_url` | `str` | `https://api.captcha-solver.com` | API base URL. Override only for self-hosted/staging deployments. |
123
- | `timeout` | `int` | `120` | Default max seconds `solve()` waits for a solution before raising `CaptchaTimeoutError`. Overridable per call. |
124
- | `polling_interval` | `int` | `3` | Seconds between `getTaskResult` polls inside `solve()`. |
123
+ | `timeout` | `int` | `120` | Polling window after task creation, in seconds. Overridable per call; each HTTP request has a separate 30-second timeout. |
124
+ | `polling_interval` | `int` | `10` | Seconds before the first `getTaskResult` poll and between subsequent polls. |
125
125
  | `language_pool` | `Optional[str]` | `None` | Default worker pool (`"en"` or `"ru"`) applied to every call that doesn't pass its own `language_pool`. |
126
126
 
127
127
  Both clients hold a reusable connection pool (`requests.Session` / `httpx.AsyncClient`)
@@ -155,9 +155,9 @@ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
155
155
  ### `create_task(task, language_pool=None)`
156
156
 
157
157
  Submits `task` and returns its numeric task ID without waiting for a solution.
158
- Same parameters as `solve()`. Use this instead of `solve()` only if you need to
158
+ Accepts `task` and `language_pool`; it does not accept `timeout`. Use this instead of `solve()` if you need to
159
159
  manage polling yourself (e.g. checking on many tasks from a different process).
160
- Raises `ApiError`, `NetworkError`.
160
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
161
161
 
162
162
  ### `get_task_result(task_id)`
163
163
 
@@ -165,12 +165,12 @@ Fetches the current status of a task created with `create_task()`. Always
165
165
  returns a dict with a `status` key (`"processing"` or `"ready"`); when
166
166
  `"ready"`, also has a `solution` dict. This is a single poll, not a wait --
167
167
  call it repeatedly (as `solve()` does) until `status` is `"ready"`.
168
- Raises `ApiError`, `NetworkError`.
168
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
169
169
 
170
170
  ### `get_balance()`
171
171
 
172
172
  Returns the account's current balance (`float`) in the account's currency.
173
- Raises `ApiError`, `NetworkError`.
173
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
174
174
 
175
175
  ## Captcha Types
176
176
 
@@ -240,8 +240,10 @@ task = RecaptchaV2Task(
240
240
  Use this method to solve the Enterprise version of reCAPTCHA v2 and obtain a
241
241
  token for a page that uses `grecaptcha.enterprise`.
242
242
 
243
- `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask`. Same fields
244
- as reCAPTCHA v2, plus:
243
+ `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask` accept
244
+ `websiteURL`, `websiteKey`, `isInvisible`, `apiDomain`, `userAgent`, and `cookies`,
245
+ plus `enterprisePayload`. Use `enterprisePayload={"s": "..."}` for the
246
+ Enterprise `s` value; these classes do not accept `recaptchaDataSValue`.
245
247
 
246
248
  | Parameter | Required | Description |
247
249
  |---|---|---|
@@ -271,8 +273,8 @@ With proxy, use `RecaptchaV2EnterpriseTask` (same proxy fields as reCAPTCHA v2).
271
273
  Use this method to obtain a score-based reCAPTCHA v3 token for a specific site,
272
274
  action, and minimum score. This variant does not use a proxy.
273
275
 
274
- `RecaptchaV3TaskProxyless`. No proxy variant exists -- v3 is score-based and
275
- invisible, so there's no widget/session to pin to a proxy IP.
276
+ `RecaptchaV3TaskProxyless`. This API supports v3 through the service's IPs;
277
+ there is no corresponding task type for your own proxy.
276
278
 
277
279
  | Parameter | Required | Description |
278
280
  |---|---|---|
@@ -317,7 +319,10 @@ that the target page expects in `cf-turnstile-response`.
317
319
  | `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. |
318
320
  | `userAgent` | no | User-Agent to solve with -- the returned token is tied to it, submit with the same one. |
319
321
 
320
- **Response:** `token` -- submit as `cf-turnstile-response`.
322
+ **Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare
323
+ Challenge pages, the solution also includes `userAgent`; use that User-Agent
324
+ when submitting the token. Pass `action`, `data`, and `pagedata` when the page
325
+ provides them. The API field is spelled `pagedata`, all lowercase.
321
326
 
322
327
  ```python
323
328
  from captcha_solver_api import CaptchaClient
@@ -389,7 +394,7 @@ pass the values collected from the target page before creating the task.
389
394
  | `version` | no | `3` (default) or `4`. |
390
395
  | `gt` | v3 only | Public key of the GeeTest widget. |
391
396
  | `challenge` | v3 only | Session-specific challenge value from the page -- must be freshly fetched for every request, it cannot be reused. |
392
- | `initParameters` | v4 only | Extra parameters from the page's `initGeetest` call; for v4 must contain `captcha_id`. |
397
+ | `initParameters` | required for v4; optional for v3 | Extra initialization parameters; for v4 must contain `captcha_id`. |
393
398
  | `geetestApiServerSubdomain` | no | Custom GeeTest API subdomain, if the site uses one. |
394
399
  | `userAgent` | no | User-Agent to solve with. |
395
400
  | `risk_type` | no | Value of the `risk_type` parameter from the captcha-loading request, if present. Dynamic, single-use, and time-limited. |
@@ -435,7 +440,7 @@ for the widget on the target page. Use the coordinates method for image challeng
435
440
 
436
441
  `YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based
437
442
  challenge. For the image challenge instead, use `CoordinatesTask` with
438
- `imgType="smart_captcha"` (see [Coordinates](#coordinates-click-captcha) below).
443
+ [Coordinates](#coordinates-click-captcha) is available for generic click captchas.
439
444
 
440
445
  | Parameter | Required | Description |
441
446
  |---|---|---|
@@ -478,10 +483,9 @@ SmartCaptcha's image challenge. No proxy variant -- the image is submitted direc
478
483
  |---|---|---|
479
484
  | `body` | yes | The captcha image, base64-encoded. |
480
485
  | `comment` | no (recommended) | Hint for the worker, e.g. `"click on the green apple"`. |
481
- | `imgInstructions` | required for `imgType="smart_captcha"` | Instruction image, base64-encoded, showing what to click and in what order. |
486
+ | `imgInstructions` | no | Optional instruction image, base64-encoded. |
482
487
  | `minClicks` | no | Minimum number of clicks expected (default `1`). |
483
488
  | `maxClicks` | no | Maximum number of clicks allowed. |
484
- | `imgType` | no | `"smart_captcha"` to solve a Yandex SmartCaptcha image challenge instead of a generic click captcha. |
485
489
 
486
490
  **Response:** `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click, in order.
487
491
 
@@ -499,9 +503,6 @@ task = CoordinatesTask(
499
503
  result = client.solve(task)
500
504
  print(result["coordinates"]) # [{"x": 140, "y": 110}]
501
505
  ```
502
- For the Yandex SmartCaptcha image challenge, see
503
- [examples/sync/yandex_smartcaptcha_image.py](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/sync/yandex_smartcaptcha_image.py)
504
- (or [examples/async](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/async/yandex_smartcaptcha_image.py)).
505
506
 
506
507
  ### Tencent
507
508
 
@@ -562,6 +563,14 @@ it for every other call:
562
563
  result = client.solve(task, timeout=300)
563
564
  ```
564
565
 
566
+ The [API requires at least 5 seconds between polls](https://captcha-solver.com/en/docs/how-it-works).
567
+ The SDK defaults to 10 seconds. `solve()` waits for `polling_interval` before
568
+ its first poll and between polls.
569
+ Its default 120-second polling window is independent of the API's five-minute
570
+ task lifetime. A local timeout does not cancel a task or mean that the API
571
+ failed to solve it. Use `create_task()` and keep its `task_id` if you need to
572
+ check the same task later; calling `solve()` again creates a new task.
573
+
565
574
  ### Worker language pool
566
575
 
567
576
  Set a default `language_pool` once at construction instead of passing it to every call:
@@ -646,10 +655,10 @@ Avoid importing it by name: it shadows Python's built-in `TimeoutError`.
646
655
  See the dedicated [examples documentation](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/README.md) for the full
647
656
  sync/async example list, setup steps, expected results, and placeholder guidance.
648
657
 
649
- - **Image/click captchas** (`image_to_text.py`, `coordinates.py`,
650
- `yandex_smartcaptcha_image.py`) run end-to-end with nothing but a valid
658
+ - **Image/click captchas** (`image_to_text.py`, `coordinates.py`) run after installing the dependencies and setting a valid
651
659
  `CAPTCHA_API_KEY` -- they read sample images bundled in
652
660
  [examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed.
661
+ `python-dotenv` is optional: install it only if you want the scripts to load a `.env` file.
653
662
  - **Token captchas** (`recaptcha_v2.py`, `recaptcha_v2_enterprise.py`, `recaptcha_v3.py`,
654
663
  `turnstile.py`, `yandex_smartcaptcha.py`, `geetest_v4.py`, `tencent.py`) use
655
664
  placeholder values (`https://example.com/...`, `YOUR_WEBSITE_KEY`, `YOUR_APP_ID`,
@@ -673,12 +682,11 @@ python examples/sync/image_to_text.py
673
682
  python examples/sync/coordinates.py
674
683
  ```
675
684
 
676
- **Verified against the live API** during development, using real target pages and
677
- real proxy credentials in place of the placeholders shown above: every captcha
678
- type in this SDK -- proxyless and with proxy -- returned a real, correctly-shaped
679
- solution when pointed at a genuine target. `geetest_v3.py`'s request shape was
680
- likewise confirmed correct when given a real, freshly-fetched `gt`/`challenge`
681
- pair.
685
+ Live checks with real parameters have exercised all supported task types.
686
+ Individual tasks may return `ERROR_CAPTCHA_UNSOLVABLE` or exceed the client's
687
+ polling timeout. A `ready` response confirms that the service returned a
688
+ solution; the target website must still accept the token with the matching
689
+ page and session parameters.
682
690
 
683
691
  ## Requirements
684
692
 
@@ -0,0 +1 @@
1
+ __version__ = "1.0.2"
@@ -42,7 +42,7 @@ class AsyncCaptchaClient:
42
42
  client_key: str,
43
43
  base_url: str = "https://api.captcha-solver.com",
44
44
  timeout: int = 120,
45
- polling_interval: int = 3,
45
+ polling_interval: int = 10,
46
46
  language_pool: Optional[str] = None,
47
47
  ) -> None:
48
48
  """
@@ -52,8 +52,9 @@ class AsyncCaptchaClient:
52
52
  deployments.
53
53
  timeout: Default max seconds `solve()` waits for a solution before
54
54
  raising `CaptchaTimeoutError`. Can be overridden per call.
55
- polling_interval: Seconds to wait between `getTaskResult` polls
56
- inside `solve()`.
55
+ polling_interval: Seconds to wait before the first `getTaskResult`
56
+ poll and between subsequent polls inside `solve()`. Defaults
57
+ to 10 seconds.
57
58
  language_pool: Default worker pool selector (e.g. `"en"` or `"ru"`)
58
59
  applied to every `create_task()`/`solve()` call that doesn't pass
59
60
  its own `language_pool`. Leave unset to use the account's default
@@ -174,6 +175,7 @@ class AsyncCaptchaClient:
174
175
  Raises:
175
176
  ApiError: The API reports an error for the task.
176
177
  NetworkError: The request failed at the transport level.
178
+ CaptchaTimeoutError: The HTTP request timed out.
177
179
  """
178
180
  payload = {
179
181
  "clientKey": self.client_key,
@@ -196,6 +198,7 @@ class AsyncCaptchaClient:
196
198
  Raises:
197
199
  ApiError: The API key is invalid or the account cannot be resolved.
198
200
  NetworkError: The request failed at the transport level.
201
+ CaptchaTimeoutError: The HTTP request timed out.
199
202
  """
200
203
  payload = {"clientKey": self.client_key}
201
204
  data = await self._request("getBalance", payload)
@@ -235,14 +238,16 @@ class AsyncCaptchaClient:
235
238
 
236
239
  task_id = await self.create_task(task, language_pool=language_pool)
237
240
 
238
- deadline = time.time() + (timeout if timeout is not None else self.timeout)
241
+ deadline = time.monotonic() + (timeout if timeout is not None else self.timeout)
239
242
 
240
- while time.time() < deadline:
243
+ while time.monotonic() < deadline:
244
+ remaining = deadline - time.monotonic()
245
+ await asyncio.sleep(min(self.polling_interval, max(0, remaining)))
246
+ if time.monotonic() >= deadline:
247
+ break
241
248
  result = await self.get_task_result(task_id)
242
249
 
243
250
  if result.get("status") == "ready":
244
251
  return result["solution"]
245
252
 
246
- await asyncio.sleep(self.polling_interval)
247
-
248
253
  raise CaptchaTimeoutError("Task solving timed out.")
@@ -31,7 +31,7 @@ class CaptchaClient:
31
31
  client_key: str,
32
32
  base_url: str = "https://api.captcha-solver.com",
33
33
  timeout: int = 120,
34
- polling_interval: int = 3,
34
+ polling_interval: int = 10,
35
35
  language_pool: Optional[str] = None,
36
36
  ) -> None:
37
37
  """
@@ -41,8 +41,9 @@ class CaptchaClient:
41
41
  deployments.
42
42
  timeout: Default max seconds `solve()` waits for a solution before
43
43
  raising `CaptchaTimeoutError`. Can be overridden per call.
44
- polling_interval: Seconds to wait between `getTaskResult` polls
45
- inside `solve()`.
44
+ polling_interval: Seconds to wait before the first `getTaskResult`
45
+ poll and between subsequent polls inside `solve()`. Defaults
46
+ to 10 seconds.
46
47
  language_pool: Default worker pool selector (e.g. `"en"` or `"ru"`)
47
48
  applied to every `create_task()`/`solve()` call that doesn't pass
48
49
  its own `language_pool`. Leave unset to use the account's default
@@ -160,6 +161,7 @@ class CaptchaClient:
160
161
  Raises:
161
162
  ApiError: The API reports an error for this task (e.g. it expired).
162
163
  NetworkError: The request failed at the transport level.
164
+ CaptchaTimeoutError: The HTTP request timed out.
163
165
  """
164
166
  payload = {
165
167
  "clientKey": self.client_key,
@@ -180,6 +182,7 @@ class CaptchaClient:
180
182
  Raises:
181
183
  ApiError: The API key is invalid or the account can't be resolved.
182
184
  NetworkError: The request failed at the transport level.
185
+ CaptchaTimeoutError: The HTTP request timed out.
183
186
  """
184
187
  payload = {"clientKey": self.client_key}
185
188
  data = self._request("getBalance", payload)
@@ -217,14 +220,16 @@ class CaptchaClient:
217
220
 
218
221
  task_id = self.create_task(task, language_pool=language_pool)
219
222
 
220
- deadline = time.time() + (timeout if timeout is not None else self.timeout)
223
+ deadline = time.monotonic() + (timeout if timeout is not None else self.timeout)
221
224
 
222
- while time.time() < deadline:
225
+ while time.monotonic() < deadline:
226
+ remaining = deadline - time.monotonic()
227
+ time.sleep(min(self.polling_interval, max(0, remaining)))
228
+ if time.monotonic() >= deadline:
229
+ break
223
230
  result = self.get_task_result(task_id)
224
231
 
225
232
  if result.get("status") == "ready":
226
233
  return result["solution"]
227
234
 
228
- time.sleep(self.polling_interval)
229
-
230
235
  raise CaptchaTimeoutError("Task solving timed out.")
@@ -55,6 +55,15 @@ class ProxyMixin:
55
55
  proxy_login: Optional[str] = None,
56
56
  proxy_password: Optional[str] = None,
57
57
  ) -> None:
58
+ """Sets the proxy fields on this task.
59
+
60
+ Args:
61
+ proxy_type: `"http"`, `"socks4"`, or `"socks5"`.
62
+ proxy_address: Proxy IP address or hostname.
63
+ proxy_port: Proxy port.
64
+ proxy_login: Proxy auth username, if required.
65
+ proxy_password: Proxy auth password, if required.
66
+ """
58
67
  self.proxyType = proxy_type
59
68
  self.proxyAddress = proxy_address
60
69
  self.proxyPort = proxy_port
@@ -525,8 +534,7 @@ class GeeTestTask(BaseTask, ProxyMixin):
525
534
 
526
535
 
527
536
  class YandexSmartCaptchaTaskProxyless(BaseTask):
528
- """Yandex SmartCaptcha (token challenge) without proxy. For the image
529
- challenge instead, use `CoordinatesTask` with `imgType="smart_captcha"`.
537
+ """Yandex SmartCaptcha (token challenge) without proxy.
530
538
 
531
539
  Args:
532
540
  websiteURL: Full URL of the page where the widget is located.
@@ -597,20 +605,16 @@ class YandexSmartCaptchaTask(BaseTask, ProxyMixin):
597
605
 
598
606
 
599
607
  class CoordinatesTask(BaseTask):
600
- """Coordinate-based (click) image captcha. Used both for generic
601
- "click on X" captchas and for Yandex SmartCaptcha's image challenge (set
602
- `imgType` accordingly). No proxy variant -- the image is submitted directly.
608
+ """Coordinate-based (click) image captcha. No proxy variant -- the image
609
+ is submitted directly.
603
610
 
604
611
  Args:
605
612
  body: The captcha image, base64-encoded (no `data:image/...;base64,` prefix).
606
613
  comment: Hint for the worker, e.g. `"click on the green apple"`. Recommended
607
614
  for generic click captchas.
608
- imgInstructions: Optional instruction image, base64-encoded, showing what
609
- to click and in what order. Required when `imgType="smart_captcha"`.
615
+ imgInstructions: Optional instruction image, base64-encoded.
610
616
  minClicks: Minimum number of clicks expected (default `1`).
611
617
  maxClicks: Maximum number of clicks allowed.
612
- imgType: Set to `"smart_captcha"` to solve a Yandex SmartCaptcha image
613
- challenge instead of a generic click captcha.
614
618
 
615
619
  Returns (`solution` from `solve()`):
616
620
  `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click,
@@ -626,14 +630,12 @@ class CoordinatesTask(BaseTask):
626
630
  imgInstructions: Optional[str] = None,
627
631
  minClicks: Optional[int] = None,
628
632
  maxClicks: Optional[int] = None,
629
- imgType: Optional[str] = None,
630
633
  ) -> None:
631
634
  self.body = body
632
635
  self.comment = comment
633
636
  self.imgInstructions = imgInstructions
634
637
  self.minClicks = minClicks
635
638
  self.maxClicks = maxClicks
636
- self.imgType = imgType
637
639
 
638
640
 
639
641
  class TencentTaskProxyless(BaseTask):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: captcha-solver-api
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Official Python SDK for solving reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3/v4, Yandex SmartCaptcha, Tencent, and image/click captchas via the Captcha Solver API
5
5
  Author: Captcha Solver Team
6
6
  License-Expression: MIT
@@ -110,7 +110,7 @@ client = CaptchaClient("your_api_key")
110
110
 
111
111
  ## Quick Start
112
112
 
113
- Solve a reCAPTCHA v2 in 4 lines.
113
+ Solve a reCAPTCHA v2 with a task object and one `solve()` call.
114
114
 
115
115
  ```python
116
116
  from captcha_solver_api import CaptchaClient
@@ -156,8 +156,8 @@ Constructor.
156
156
  |---|---|---|---|
157
157
  | `client_key` | `str` | required | Your Captcha Solver API key. Raises `ValidationError` if empty. |
158
158
  | `base_url` | `str` | `https://api.captcha-solver.com` | API base URL. Override only for self-hosted/staging deployments. |
159
- | `timeout` | `int` | `120` | Default max seconds `solve()` waits for a solution before raising `CaptchaTimeoutError`. Overridable per call. |
160
- | `polling_interval` | `int` | `3` | Seconds between `getTaskResult` polls inside `solve()`. |
159
+ | `timeout` | `int` | `120` | Polling window after task creation, in seconds. Overridable per call; each HTTP request has a separate 30-second timeout. |
160
+ | `polling_interval` | `int` | `10` | Seconds before the first `getTaskResult` poll and between subsequent polls. |
161
161
  | `language_pool` | `Optional[str]` | `None` | Default worker pool (`"en"` or `"ru"`) applied to every call that doesn't pass its own `language_pool`. |
162
162
 
163
163
  Both clients hold a reusable connection pool (`requests.Session` / `httpx.AsyncClient`)
@@ -191,9 +191,9 @@ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
191
191
  ### `create_task(task, language_pool=None)`
192
192
 
193
193
  Submits `task` and returns its numeric task ID without waiting for a solution.
194
- Same parameters as `solve()`. Use this instead of `solve()` only if you need to
194
+ Accepts `task` and `language_pool`; it does not accept `timeout`. Use this instead of `solve()` if you need to
195
195
  manage polling yourself (e.g. checking on many tasks from a different process).
196
- Raises `ApiError`, `NetworkError`.
196
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
197
197
 
198
198
  ### `get_task_result(task_id)`
199
199
 
@@ -201,12 +201,12 @@ Fetches the current status of a task created with `create_task()`. Always
201
201
  returns a dict with a `status` key (`"processing"` or `"ready"`); when
202
202
  `"ready"`, also has a `solution` dict. This is a single poll, not a wait --
203
203
  call it repeatedly (as `solve()` does) until `status` is `"ready"`.
204
- Raises `ApiError`, `NetworkError`.
204
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
205
205
 
206
206
  ### `get_balance()`
207
207
 
208
208
  Returns the account's current balance (`float`) in the account's currency.
209
- Raises `ApiError`, `NetworkError`.
209
+ Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`.
210
210
 
211
211
  ## Captcha Types
212
212
 
@@ -276,8 +276,10 @@ task = RecaptchaV2Task(
276
276
  Use this method to solve the Enterprise version of reCAPTCHA v2 and obtain a
277
277
  token for a page that uses `grecaptcha.enterprise`.
278
278
 
279
- `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask`. Same fields
280
- as reCAPTCHA v2, plus:
279
+ `RecaptchaV2EnterpriseTaskProxyless` / `RecaptchaV2EnterpriseTask` accept
280
+ `websiteURL`, `websiteKey`, `isInvisible`, `apiDomain`, `userAgent`, and `cookies`,
281
+ plus `enterprisePayload`. Use `enterprisePayload={"s": "..."}` for the
282
+ Enterprise `s` value; these classes do not accept `recaptchaDataSValue`.
281
283
 
282
284
  | Parameter | Required | Description |
283
285
  |---|---|---|
@@ -307,8 +309,8 @@ With proxy, use `RecaptchaV2EnterpriseTask` (same proxy fields as reCAPTCHA v2).
307
309
  Use this method to obtain a score-based reCAPTCHA v3 token for a specific site,
308
310
  action, and minimum score. This variant does not use a proxy.
309
311
 
310
- `RecaptchaV3TaskProxyless`. No proxy variant exists -- v3 is score-based and
311
- invisible, so there's no widget/session to pin to a proxy IP.
312
+ `RecaptchaV3TaskProxyless`. This API supports v3 through the service's IPs;
313
+ there is no corresponding task type for your own proxy.
312
314
 
313
315
  | Parameter | Required | Description |
314
316
  |---|---|---|
@@ -353,7 +355,10 @@ that the target page expects in `cf-turnstile-response`.
353
355
  | `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. |
354
356
  | `userAgent` | no | User-Agent to solve with -- the returned token is tied to it, submit with the same one. |
355
357
 
356
- **Response:** `token` -- submit as `cf-turnstile-response`.
358
+ **Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare
359
+ Challenge pages, the solution also includes `userAgent`; use that User-Agent
360
+ when submitting the token. Pass `action`, `data`, and `pagedata` when the page
361
+ provides them. The API field is spelled `pagedata`, all lowercase.
357
362
 
358
363
  ```python
359
364
  from captcha_solver_api import CaptchaClient
@@ -425,7 +430,7 @@ pass the values collected from the target page before creating the task.
425
430
  | `version` | no | `3` (default) or `4`. |
426
431
  | `gt` | v3 only | Public key of the GeeTest widget. |
427
432
  | `challenge` | v3 only | Session-specific challenge value from the page -- must be freshly fetched for every request, it cannot be reused. |
428
- | `initParameters` | v4 only | Extra parameters from the page's `initGeetest` call; for v4 must contain `captcha_id`. |
433
+ | `initParameters` | required for v4; optional for v3 | Extra initialization parameters; for v4 must contain `captcha_id`. |
429
434
  | `geetestApiServerSubdomain` | no | Custom GeeTest API subdomain, if the site uses one. |
430
435
  | `userAgent` | no | User-Agent to solve with. |
431
436
  | `risk_type` | no | Value of the `risk_type` parameter from the captcha-loading request, if present. Dynamic, single-use, and time-limited. |
@@ -471,7 +476,7 @@ for the widget on the target page. Use the coordinates method for image challeng
471
476
 
472
477
  `YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based
473
478
  challenge. For the image challenge instead, use `CoordinatesTask` with
474
- `imgType="smart_captcha"` (see [Coordinates](#coordinates-click-captcha) below).
479
+ [Coordinates](#coordinates-click-captcha) is available for generic click captchas.
475
480
 
476
481
  | Parameter | Required | Description |
477
482
  |---|---|---|
@@ -514,10 +519,9 @@ SmartCaptcha's image challenge. No proxy variant -- the image is submitted direc
514
519
  |---|---|---|
515
520
  | `body` | yes | The captcha image, base64-encoded. |
516
521
  | `comment` | no (recommended) | Hint for the worker, e.g. `"click on the green apple"`. |
517
- | `imgInstructions` | required for `imgType="smart_captcha"` | Instruction image, base64-encoded, showing what to click and in what order. |
522
+ | `imgInstructions` | no | Optional instruction image, base64-encoded. |
518
523
  | `minClicks` | no | Minimum number of clicks expected (default `1`). |
519
524
  | `maxClicks` | no | Maximum number of clicks allowed. |
520
- | `imgType` | no | `"smart_captcha"` to solve a Yandex SmartCaptcha image challenge instead of a generic click captcha. |
521
525
 
522
526
  **Response:** `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click, in order.
523
527
 
@@ -535,9 +539,6 @@ task = CoordinatesTask(
535
539
  result = client.solve(task)
536
540
  print(result["coordinates"]) # [{"x": 140, "y": 110}]
537
541
  ```
538
- For the Yandex SmartCaptcha image challenge, see
539
- [examples/sync/yandex_smartcaptcha_image.py](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/sync/yandex_smartcaptcha_image.py)
540
- (or [examples/async](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/async/yandex_smartcaptcha_image.py)).
541
542
 
542
543
  ### Tencent
543
544
 
@@ -598,6 +599,14 @@ it for every other call:
598
599
  result = client.solve(task, timeout=300)
599
600
  ```
600
601
 
602
+ The [API requires at least 5 seconds between polls](https://captcha-solver.com/en/docs/how-it-works).
603
+ The SDK defaults to 10 seconds. `solve()` waits for `polling_interval` before
604
+ its first poll and between polls.
605
+ Its default 120-second polling window is independent of the API's five-minute
606
+ task lifetime. A local timeout does not cancel a task or mean that the API
607
+ failed to solve it. Use `create_task()` and keep its `task_id` if you need to
608
+ check the same task later; calling `solve()` again creates a new task.
609
+
601
610
  ### Worker language pool
602
611
 
603
612
  Set a default `language_pool` once at construction instead of passing it to every call:
@@ -682,10 +691,10 @@ Avoid importing it by name: it shadows Python's built-in `TimeoutError`.
682
691
  See the dedicated [examples documentation](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/README.md) for the full
683
692
  sync/async example list, setup steps, expected results, and placeholder guidance.
684
693
 
685
- - **Image/click captchas** (`image_to_text.py`, `coordinates.py`,
686
- `yandex_smartcaptcha_image.py`) run end-to-end with nothing but a valid
694
+ - **Image/click captchas** (`image_to_text.py`, `coordinates.py`) run after installing the dependencies and setting a valid
687
695
  `CAPTCHA_API_KEY` -- they read sample images bundled in
688
696
  [examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed.
697
+ `python-dotenv` is optional: install it only if you want the scripts to load a `.env` file.
689
698
  - **Token captchas** (`recaptcha_v2.py`, `recaptcha_v2_enterprise.py`, `recaptcha_v3.py`,
690
699
  `turnstile.py`, `yandex_smartcaptcha.py`, `geetest_v4.py`, `tencent.py`) use
691
700
  placeholder values (`https://example.com/...`, `YOUR_WEBSITE_KEY`, `YOUR_APP_ID`,
@@ -709,12 +718,11 @@ python examples/sync/image_to_text.py
709
718
  python examples/sync/coordinates.py
710
719
  ```
711
720
 
712
- **Verified against the live API** during development, using real target pages and
713
- real proxy credentials in place of the placeholders shown above: every captcha
714
- type in this SDK -- proxyless and with proxy -- returned a real, correctly-shaped
715
- solution when pointed at a genuine target. `geetest_v3.py`'s request shape was
716
- likewise confirmed correct when given a real, freshly-fetched `gt`/`challenge`
717
- pair.
721
+ Live checks with real parameters have exercised all supported task types.
722
+ Individual tasks may return `ERROR_CAPTCHA_UNSOLVABLE` or exceed the client's
723
+ polling timeout. A `ready` response confirms that the service returned a
724
+ solution; the target website must still accept the token with the matching
725
+ page and session parameters.
718
726
 
719
727
  ## Requirements
720
728
 
@@ -12,4 +12,5 @@ captcha_solver_api.egg-info/PKG-INFO
12
12
  captcha_solver_api.egg-info/SOURCES.txt
13
13
  captcha_solver_api.egg-info/dependency_links.txt
14
14
  captcha_solver_api.egg-info/requires.txt
15
- captcha_solver_api.egg-info/top_level.txt
15
+ captcha_solver_api.egg-info/top_level.txt
16
+ tests/test_polling.py
@@ -0,0 +1,89 @@
1
+ """The documented polling schedule applies to both clients, including the first poll."""
2
+
3
+ from types import SimpleNamespace
4
+ from unittest.mock import AsyncMock
5
+
6
+ import pytest
7
+
8
+ import captcha_solver_api.client as sync_module
9
+ import captcha_solver_api.async_client as async_module
10
+ from captcha_solver_api import CaptchaClient, AsyncCaptchaClient, CaptchaTimeoutError
11
+ from captcha_solver_api.tasks import RecaptchaV2TaskProxyless
12
+
13
+
14
+ class Clock:
15
+ def __init__(self):
16
+ self.now = 0
17
+
18
+ def monotonic(self):
19
+ return self.now
20
+
21
+ def sleep(self, seconds):
22
+ self.now += seconds
23
+
24
+
25
+ CASES = [
26
+ (None, 25, [10, 20], False),
27
+ (2, 20, [2, 4], False),
28
+ (None, 3, [], True),
29
+ (5, 7, [5], True),
30
+ ]
31
+
32
+
33
+ @pytest.mark.parametrize('interval,timeout,expected_polls,times_out', CASES)
34
+ def test_sync_polling_schedule(monkeypatch, interval, timeout, expected_polls, times_out):
35
+ clock = Clock()
36
+ polls = []
37
+ monkeypatch.setattr(sync_module, 'time', clock)
38
+
39
+ def request(endpoint, payload):
40
+ if endpoint == 'createTask':
41
+ assert clock.now == 0
42
+ return {'errorId': 0, 'taskId': 100}
43
+ assert payload['taskId'] == 100
44
+ polls.append(clock.now)
45
+ if len(polls) == 2:
46
+ return {'errorId': 0, 'status': 'ready', 'solution': {'token': 'done'}}
47
+ return {'errorId': 0, 'status': 'processing'}
48
+
49
+ options = {} if interval is None else {'polling_interval': interval}
50
+ with CaptchaClient('test-key', **options) as client:
51
+ monkeypatch.setattr(client, '_request', request)
52
+ task = RecaptchaV2TaskProxyless('https://example.com', 'site-key')
53
+ if times_out:
54
+ with pytest.raises(CaptchaTimeoutError):
55
+ client.solve(task, timeout=timeout)
56
+ assert clock.now == timeout
57
+ else:
58
+ assert client.solve(task, timeout=timeout) == {'token': 'done'}
59
+ assert polls == expected_polls
60
+
61
+
62
+ @pytest.mark.parametrize('interval,timeout,expected_polls,times_out', CASES)
63
+ async def test_async_polling_schedule(monkeypatch, interval, timeout, expected_polls, times_out):
64
+ clock = Clock()
65
+ polls = []
66
+ monkeypatch.setattr(async_module, 'time', clock)
67
+ monkeypatch.setattr(async_module, 'asyncio', SimpleNamespace(sleep=AsyncMock(side_effect=clock.sleep)))
68
+
69
+ async def request(endpoint, payload):
70
+ if endpoint == 'createTask':
71
+ assert clock.now == 0
72
+ return {'errorId': 0, 'taskId': 100}
73
+ assert payload['taskId'] == 100
74
+ polls.append(clock.now)
75
+ if len(polls) == 2:
76
+ return {'errorId': 0, 'status': 'ready', 'solution': {'token': 'done'}}
77
+ return {'errorId': 0, 'status': 'processing'}
78
+
79
+ options = {} if interval is None else {'polling_interval': interval}
80
+ async with AsyncCaptchaClient('test-key', **options) as client:
81
+ monkeypatch.setattr(client, '_request', request)
82
+ task = RecaptchaV2TaskProxyless('https://example.com', 'site-key')
83
+ if times_out:
84
+ with pytest.raises(CaptchaTimeoutError):
85
+ await client.solve(task, timeout=timeout)
86
+ assert clock.now == timeout
87
+ else:
88
+ assert await client.solve(task, timeout=timeout) == {'token': 'done'}
89
+ assert polls == expected_polls
@@ -1 +0,0 @@
1
- __version__ = "1.0.0"