From 180ce7b0da96be487a25dac2c06b4613830aa3c6 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:34:08 +0300 Subject: [PATCH 01/23] docs: link package metadata to SDK readme --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cb344f3..9fa1f33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ [project.urls] Homepage = "https://captcha-solver.com" -Documentation = "https://captcha-solver.com/en/docs/captcha-types" +Documentation = "https://github.com/captcha-solver-api/python-sdk#readme" Repository = "https://github.com/captcha-solver-api/python-sdk" [tool.setuptools.packages.find] From 8937693d2e8ee8a84811d71661c53c23e9480445 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:35:31 +0300 Subject: [PATCH 02/23] fix: validate required task parameters --- captcha_solver_api/async_client.py | 4 ++++ captcha_solver_api/client.py | 4 ++++ captcha_solver_api/tasks.py | 18 +++++++++++++++ tests/async/test_async_validation.py | 10 ++++++++ tests/sync/test_validation.py | 34 ++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 tests/async/test_async_validation.py create mode 100644 tests/sync/test_validation.py diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index faede47..0638f0e 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -19,6 +19,7 @@ NetworkError, ValidationError, ) +from .tasks import BaseTask class AsyncCaptchaClient: @@ -172,6 +173,9 @@ async def create_task(self, task: Any, language_pool: Optional[str] = None) -> i Example: task_id = await client.create_task(task, language_pool="en") """ + if not isinstance(task, BaseTask): + raise ValidationError("task must be an instance of BaseTask") + payload: Dict[str, Any] = { "clientKey": self.client_key, "task": task.to_dict(), diff --git a/captcha_solver_api/client.py b/captcha_solver_api/client.py index 6b85021..5eb45a6 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -16,6 +16,7 @@ NetworkError, ValidationError, ) +from .tasks import BaseTask class CaptchaClient: @@ -160,6 +161,9 @@ def create_task(self, task: Any, language_pool: Optional[str] = None) -> int: Example: task_id = client.create_task(task, language_pool="en") """ + if not isinstance(task, BaseTask): + raise ValidationError("task must be an instance of BaseTask") + payload: Dict[str, Any] = { "clientKey": self.client_key, "task": task.to_dict(), diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 5df677e..871844e 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -12,8 +12,11 @@ from __future__ import annotations +import inspect from typing import Any, Dict, Optional +from .exceptions import ValidationError + class BaseTask: """Base class for all CAPTCHA tasks.""" @@ -23,12 +26,27 @@ class BaseTask: def to_dict(self) -> Dict[str, Any]: """Serializes the task to the dict shape expected by the `task` field of `createTask`, dropping any fields left as `None`.""" + self._validate_required_fields() result = {"type": self.type} for key, value in self.__dict__.items(): if value is not None: result[key] = value return result + def _validate_required_fields(self) -> None: + """Reject missing values for constructor parameters without defaults.""" + parameters = inspect.signature(type(self).__init__).parameters + missing = [] + for name, parameter in parameters.items(): + if name == "self" or parameter.default is not inspect.Parameter.empty: + continue + value = getattr(self, name, None) + if value is None or (isinstance(value, str) and not value.strip()): + missing.append(name) + if missing: + fields = ", ".join(missing) + raise ValidationError(f"Missing required task parameter(s): {fields}") + class ProxyMixin: """Mixin that adds proxy fields to a task. diff --git a/tests/async/test_async_validation.py b/tests/async/test_async_validation.py new file mode 100644 index 0000000..f07660a --- /dev/null +++ b/tests/async/test_async_validation.py @@ -0,0 +1,10 @@ +import pytest + +from captcha_solver_api import AsyncCaptchaClient, ValidationError + + +@pytest.mark.asyncio +async def test_create_task_rejects_non_task_value(): + async with AsyncCaptchaClient("api-key") as client: + with pytest.raises(ValidationError, match="task must be an instance of BaseTask"): + await client.create_task(None) diff --git a/tests/sync/test_validation.py b/tests/sync/test_validation.py new file mode 100644 index 0000000..2407116 --- /dev/null +++ b/tests/sync/test_validation.py @@ -0,0 +1,34 @@ +import pytest + +from captcha_solver_api import CaptchaClient, ValidationError +from captcha_solver_api.tasks import RecaptchaV2Task, RecaptchaV2TaskProxyless + + +def test_task_rejects_empty_required_fields_before_request(): + task = RecaptchaV2TaskProxyless(websiteURL=" ", websiteKey="") + + with pytest.raises( + ValidationError, + match=r"Missing required task parameter\(s\): websiteURL, websiteKey", + ): + task.to_dict() + + +def test_proxy_task_rejects_empty_required_proxy_fields(): + task = RecaptchaV2Task( + websiteURL="https://example.com", + websiteKey="site-key", + proxyType="", + proxyAddress="", + proxyPort=8080, + ) + + with pytest.raises(ValidationError, match="proxyType, proxyAddress"): + task.to_dict() + + +def test_create_task_rejects_non_task_value(): + client = CaptchaClient("api-key") + + with pytest.raises(ValidationError, match="task must be an instance of BaseTask"): + client.create_task(None) From cecf35f4e77ae2920db3b0caff6de2dcfdf4cd60 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:35:54 +0300 Subject: [PATCH 03/23] docs: clarify unavailable proxy task variants --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8695653..8cc528f 100644 --- a/README.md +++ b/README.md @@ -151,15 +151,18 @@ on the target page. |---|---|---| | reCAPTCHA v2 | ✅ | ✅ | | reCAPTCHA v2 Enterprise | ✅ | ✅ | -| reCAPTCHA v3 | ✅ | ❌ | +| reCAPTCHA v3 | ✅ | — | | Cloudflare Turnstile | ✅ | ✅ | | GeeTest v3 | ✅ | ✅ | | GeeTest v4 | ✅ | ✅ | -| Image to Text | ✅ | ❌ | +| Image to Text | ✅ | — | | Yandex SmartCaptcha | ✅ | ✅ | -| Coordinates (click captcha) | ✅ | ❌ | +| Coordinates (click captcha) | ✅ | — | | Tencent | ✅ | ✅ | +An em dash means that this CAPTCHA type has no separate task method that accepts +your own proxy; it does not indicate that solving the CAPTCHA is unsupported. + ## Client Reference Every method below is available on both `CaptchaClient` (sync, `requests`-based) and From bb4af82002ed748ef539a0222f8a692747b9e0ff Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:36:15 +0300 Subject: [PATCH 04/23] docs: explain py.typed marker --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8cc528f..c9a2a9d 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ enabled in your editor, hover over `CaptchaClient`, `AsyncCaptchaClient`, their methods, or a task class such as `RecaptchaV2TaskProxyless` to read the documentation. Signature help shows argument names, types, and defaults while you type a call. +The `captcha_solver_api/py.typed` file is intentionally empty. It is the standard +[PEP 561](https://peps.python.org/pep-0561/) marker that tells type checkers to use +the inline type annotations shipped with this package. + Docstrings describe parameters, solution fields, exceptions, and usage examples. Task examples assume an existing synchronous `client`; with `AsyncCaptchaClient`, use `solution = await client.solve(task)` instead. Async snippets run inside an From 6b5a3fb54cac0ecc8453fbbac2efa0b5aa4d8cfb Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:38:05 +0300 Subject: [PATCH 05/23] docs: split proxy examples into separate files --- examples/README.md | 23 +++--- examples/async/geetest_v3_proxy.py | 76 +++++++++++++++++++ ...{geetest_v3.py => geetest_v3_proxyless.py} | 23 +----- examples/async/geetest_v4_proxy.py | 61 +++++++++++++++ ...{geetest_v4.py => geetest_v4_proxyless.py} | 25 +----- .../async/recaptcha_v2_enterprise_proxy.py | 62 +++++++++++++++ ...y => recaptcha_v2_enterprise_proxyless.py} | 29 +------ examples/async/recaptcha_v2_proxy.py | 44 +++++++++++ ...aptcha_v2.py => recaptcha_v2_proxyless.py} | 18 ++--- .../async/{tencent.py => tencent_proxy.py} | 21 +---- examples/async/tencent_proxyless.py | 54 +++++++++++++ examples/async/turnstile_proxy.py | 60 +++++++++++++++ .../{turnstile.py => turnstile_proxyless.py} | 27 +------ ...aptcha.py => yandex_smartcaptcha_proxy.py} | 22 +----- .../async/yandex_smartcaptcha_proxyless.py | 54 +++++++++++++ examples/sync/geetest_v3_proxy.py | 76 +++++++++++++++++++ ...{geetest_v3.py => geetest_v3_proxyless.py} | 22 +----- examples/sync/geetest_v4_proxy.py | 62 +++++++++++++++ ...{geetest_v4.py => geetest_v4_proxyless.py} | 24 +----- .../sync/recaptcha_v2_enterprise_proxy.py | 63 +++++++++++++++ ...y => recaptcha_v2_enterprise_proxyless.py} | 28 +------ ...{recaptcha_v2.py => recaptcha_v2_proxy.py} | 21 +---- examples/sync/recaptcha_v2_proxyless.py | 57 ++++++++++++++ .../sync/{tencent.py => tencent_proxy.py} | 21 +---- examples/sync/tencent_proxyless.py | 49 ++++++++++++ examples/sync/turnstile_proxy.py | 61 +++++++++++++++ .../{turnstile.py => turnstile_proxyless.py} | 26 +------ ...aptcha.py => yandex_smartcaptcha_proxy.py} | 22 +----- .../sync/yandex_smartcaptcha_proxyless.py | 49 ++++++++++++ 29 files changed, 860 insertions(+), 320 deletions(-) create mode 100644 examples/async/geetest_v3_proxy.py rename examples/async/{geetest_v3.py => geetest_v3_proxyless.py} (74%) create mode 100644 examples/async/geetest_v4_proxy.py rename examples/async/{geetest_v4.py => geetest_v4_proxyless.py} (66%) create mode 100644 examples/async/recaptcha_v2_enterprise_proxy.py rename examples/async/{recaptcha_v2_enterprise.py => recaptcha_v2_enterprise_proxyless.py} (60%) create mode 100644 examples/async/recaptcha_v2_proxy.py rename examples/async/{recaptcha_v2.py => recaptcha_v2_proxyless.py} (62%) rename examples/async/{tencent.py => tencent_proxy.py} (65%) create mode 100644 examples/async/tencent_proxyless.py create mode 100644 examples/async/turnstile_proxy.py rename examples/async/{turnstile.py => turnstile_proxyless.py} (63%) rename examples/async/{yandex_smartcaptcha.py => yandex_smartcaptcha_proxy.py} (65%) create mode 100644 examples/async/yandex_smartcaptcha_proxyless.py create mode 100644 examples/sync/geetest_v3_proxy.py rename examples/sync/{geetest_v3.py => geetest_v3_proxyless.py} (77%) create mode 100644 examples/sync/geetest_v4_proxy.py rename examples/sync/{geetest_v4.py => geetest_v4_proxyless.py} (70%) create mode 100644 examples/sync/recaptcha_v2_enterprise_proxy.py rename examples/sync/{recaptcha_v2_enterprise.py => recaptcha_v2_enterprise_proxyless.py} (63%) rename examples/sync/{recaptcha_v2.py => recaptcha_v2_proxy.py} (71%) create mode 100644 examples/sync/recaptcha_v2_proxyless.py rename examples/sync/{tencent.py => tencent_proxy.py} (65%) create mode 100644 examples/sync/tencent_proxyless.py create mode 100644 examples/sync/turnstile_proxy.py rename examples/sync/{turnstile.py => turnstile_proxyless.py} (67%) rename examples/sync/{yandex_smartcaptcha.py => yandex_smartcaptcha_proxy.py} (65%) create mode 100644 examples/sync/yandex_smartcaptcha_proxyless.py diff --git a/examples/README.md b/examples/README.md index 00057ac..ec2b645 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,21 +48,26 @@ Replace placeholder values such as `YOUR_WEBSITE_KEY`, `YOUR_APP_ID`, `YOUR_CAPT - [`async`](async) contains asynchronous examples using `AsyncCaptchaClient`. - [`assets`](assets) contains sample images used by image and coordinate examples. -Each supported CAPTCHA type has a matching sync and async script: +Each supported CAPTCHA type has matching sync and async scripts. Types that can +use your own proxy have separate `*_proxyless.py` and `*_proxy.py` files so each +script demonstrates exactly one task variant. | CAPTCHA type | Sync | Async | Main result | |---|---|---|---| | Account balance | [balance.py](sync/balance.py) | [balance.py](async/balance.py) | `float` balance | -| reCAPTCHA v2 | [recaptcha_v2.py](sync/recaptcha_v2.py) | [recaptcha_v2.py](async/recaptcha_v2.py) | `gRecaptchaResponse` | -| reCAPTCHA v2 Enterprise | [recaptcha_v2_enterprise.py](sync/recaptcha_v2_enterprise.py) | [recaptcha_v2_enterprise.py](async/recaptcha_v2_enterprise.py) | `gRecaptchaResponse` | +| reCAPTCHA v2 (proxyless) | [recaptcha_v2_proxyless.py](sync/recaptcha_v2_proxyless.py) | [recaptcha_v2_proxyless.py](async/recaptcha_v2_proxyless.py) | `gRecaptchaResponse` | +| reCAPTCHA v2 (proxy) | [recaptcha_v2_proxy.py](sync/recaptcha_v2_proxy.py) | [recaptcha_v2_proxy.py](async/recaptcha_v2_proxy.py) | `gRecaptchaResponse` | +| reCAPTCHA v2 Enterprise (proxyless) | [recaptcha_v2_enterprise_proxyless.py](sync/recaptcha_v2_enterprise_proxyless.py) | [recaptcha_v2_enterprise_proxyless.py](async/recaptcha_v2_enterprise_proxyless.py) | `gRecaptchaResponse` | +| reCAPTCHA v2 Enterprise (proxy) | [recaptcha_v2_enterprise_proxy.py](sync/recaptcha_v2_enterprise_proxy.py) | [recaptcha_v2_enterprise_proxy.py](async/recaptcha_v2_enterprise_proxy.py) | `gRecaptchaResponse` | | reCAPTCHA v3 | [recaptcha_v3.py](sync/recaptcha_v3.py) | [recaptcha_v3.py](async/recaptcha_v3.py) | `gRecaptchaResponse` | -| Cloudflare Turnstile | [turnstile.py](sync/turnstile.py) | [turnstile.py](async/turnstile.py) | `token` | +| Cloudflare Turnstile (proxyless) | [turnstile_proxyless.py](sync/turnstile_proxyless.py) | [turnstile_proxyless.py](async/turnstile_proxyless.py) | `token` | +| Cloudflare Turnstile (proxy) | [turnstile_proxy.py](sync/turnstile_proxy.py) | [turnstile_proxy.py](async/turnstile_proxy.py) | `token` | | Image to Text | [image_to_text.py](sync/image_to_text.py) | [image_to_text.py](async/image_to_text.py) | `text` | -| GeeTest v3 | [geetest_v3.py](sync/geetest_v3.py) | [geetest_v3.py](async/geetest_v3.py) | `challenge`, `validate`, `seccode` | -| GeeTest v4 | [geetest_v4.py](sync/geetest_v4.py) | [geetest_v4.py](async/geetest_v4.py) | `captcha_output` and related fields | -| Yandex SmartCaptcha token | [yandex_smartcaptcha.py](sync/yandex_smartcaptcha.py) | [yandex_smartcaptcha.py](async/yandex_smartcaptcha.py) | `token` | +| GeeTest v3 (proxyless / proxy) | [proxyless](sync/geetest_v3_proxyless.py) / [proxy](sync/geetest_v3_proxy.py) | [proxyless](async/geetest_v3_proxyless.py) / [proxy](async/geetest_v3_proxy.py) | `challenge`, `validate`, `seccode` | +| GeeTest v4 (proxyless / proxy) | [proxyless](sync/geetest_v4_proxyless.py) / [proxy](sync/geetest_v4_proxy.py) | [proxyless](async/geetest_v4_proxyless.py) / [proxy](async/geetest_v4_proxy.py) | `captcha_output` and related fields | +| Yandex SmartCaptcha (proxyless / proxy) | [proxyless](sync/yandex_smartcaptcha_proxyless.py) / [proxy](sync/yandex_smartcaptcha_proxy.py) | [proxyless](async/yandex_smartcaptcha_proxyless.py) / [proxy](async/yandex_smartcaptcha_proxy.py) | `token` | | Coordinates / click captcha | [coordinates.py](sync/coordinates.py) | [coordinates.py](async/coordinates.py) | `coordinates` | -| Tencent | [tencent.py](sync/tencent.py) | [tencent.py](async/tencent.py) | `appid`, `ret`, `ticket`, `randstr` | +| Tencent (proxyless / proxy) | [proxyless](sync/tencent_proxyless.py) / [proxy](sync/tencent_proxy.py) | [proxyless](async/tencent_proxyless.py) / [proxy](async/tencent_proxy.py) | `appid`, `ret`, `ticket`, `randstr` | ## Running an example @@ -71,7 +76,7 @@ Run commands from the repository root so local imports and bundled assets resolv ```bash python examples/sync/balance.py python examples/sync/image_to_text.py -python examples/async/recaptcha_v2.py +python examples/async/recaptcha_v2_proxyless.py ``` Async scripts create an `AsyncCaptchaClient` and await the same operations as the synchronous client. For several captchas, see the parallel solving pattern in the main [README](../README.md#solving-multiple-captchas-in-parallel). diff --git a/examples/async/geetest_v3_proxy.py b/examples/async/geetest_v3_proxy.py new file mode 100644 index 0000000..1b55cf2 --- /dev/null +++ b/examples/async/geetest_v3_proxy.py @@ -0,0 +1,76 @@ +""" +Example: Solve a GeeTest v3 challenge with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL, gt, and challenge with values from your target page. + Important: the challenge value is dynamic. Fetch a fresh one for each request. + +NOTE: "https://target-site.com/path/to/geetest/init" below is a PLACEHOLDER, not a +real endpoint -- this script will not run end-to-end as-is. Replace it with a request +to your actual target page (or wherever it exposes a fresh `challenge` value) before +running this example. It's here only to illustrate where that fetch belongs in the flow. +""" + +import asyncio +import os +import sys + +import httpx + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import GeeTestTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# GeeTest tasks may take longer. Increase timeout if needed. +solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) + + +async def main(): + """ + Important: the value of the 'challenge' parameter is dynamic. + For each request to the API you need to get a new value from the target page. + Below is an example of fetching it from a demo endpoint. + """ + # Fetch a fresh challenge value from the target page. + # In production, extract this from the page's initGeetest call or network requests. + # "target-site.com" is a placeholder -- point this at your real target before running. + async with httpx.AsyncClient() as client: + resp = await client.get("https://target-site.com/path/to/geetest/init", timeout=30) + challenge = resp.json()["challenge"] + + # --- With proxy example --- + # Solves GeeTest v3 through your own proxy. + try: + result = await solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with GeeTest + gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget + challenge=challenge, # Session-specific value, must be fresh + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) + ) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + + +asyncio.run(main()) diff --git a/examples/async/geetest_v3.py b/examples/async/geetest_v3_proxyless.py similarity index 74% rename from examples/async/geetest_v3.py rename to examples/async/geetest_v3_proxyless.py index 1443530..884da05 100644 --- a/examples/async/geetest_v3.py +++ b/examples/async/geetest_v3_proxyless.py @@ -31,7 +31,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import GeeTestTask, GeeTestTaskProxyless +from captcha_solver_api.tasks import GeeTestTaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -73,25 +73,4 @@ async def main(): except Exception as e: sys.exit(e) - # --- With proxy example --- - # Solves GeeTest v3 through your own proxy. - try: - result = await solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with GeeTest - gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget - challenge=challenge, # Session-specific value, must be fresh - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - - asyncio.run(main()) diff --git a/examples/async/geetest_v4_proxy.py b/examples/async/geetest_v4_proxy.py new file mode 100644 index 0000000..5d9dad3 --- /dev/null +++ b/examples/async/geetest_v4_proxy.py @@ -0,0 +1,61 @@ +""" +Example: Solve a GeeTest v4 challenge with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and captcha_id with values from your target page. + GeeTest v4 drops gt/challenge entirely. It uses captcha_id instead, which + (unlike v3's challenge) is static per site, so it doesn't need to be + re-fetched for every request. +""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import GeeTestTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# GeeTest v4 tasks may take longer. Increase timeout if needed. +solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) + + +async def main(): + # --- With proxy example --- + # Solves GeeTest v4 through your own proxy. + try: + result = await solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget + version=4, # Required: must be 4 for this version + initParameters={ # Required: must contain captcha_id + "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page + }, + # --- Proxy parameters --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) + ) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + + +asyncio.run(main()) diff --git a/examples/async/geetest_v4.py b/examples/async/geetest_v4_proxyless.py similarity index 66% rename from examples/async/geetest_v4.py rename to examples/async/geetest_v4_proxyless.py index 0c5f002..f4c37dd 100644 --- a/examples/async/geetest_v4.py +++ b/examples/async/geetest_v4_proxyless.py @@ -26,7 +26,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import GeeTestTask, GeeTestTaskProxyless +from captcha_solver_api.tasks import GeeTestTaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -56,27 +56,4 @@ async def main(): except Exception as e: sys.exit(e) - # --- With proxy example --- - # Solves GeeTest v4 through your own proxy. - try: - result = await solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget - version=4, # Required: must be 4 for this version - initParameters={ # Required: must contain captcha_id - "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page - }, - # --- Proxy parameters --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - - asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise_proxy.py b/examples/async/recaptcha_v2_enterprise_proxy.py new file mode 100644 index 0000000..23ba4a3 --- /dev/null +++ b/examples/async/recaptcha_v2_enterprise_proxy.py @@ -0,0 +1,62 @@ +""" +Example: Solve a reCAPTCHA v2 Enterprise challenge with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. + If the site uses enterprisePayload, extract and pass it or the token may be rejected. +""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import RecaptchaV2EnterpriseTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = AsyncCaptchaClient(api_key) + + +async def main(): + # --- With proxy example --- + # Solves reCAPTCHA v2 Enterprise through your own proxy. + # Use when the target site is geo-restricted or you need a consistent session. + try: + result = await solver.solve( + RecaptchaV2EnterpriseTask( + websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + isInvisible=False, + # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() + userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent + cookies="foo=bar; baz=1", # Session cookies if needed + ) + ) + # Solution contains the same gRecaptchaResponse token. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + + +asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise.py b/examples/async/recaptcha_v2_enterprise_proxyless.py similarity index 60% rename from examples/async/recaptcha_v2_enterprise.py rename to examples/async/recaptcha_v2_enterprise_proxyless.py index aabb7a8..7cd134a 100644 --- a/examples/async/recaptcha_v2_enterprise.py +++ b/examples/async/recaptcha_v2_enterprise_proxyless.py @@ -24,7 +24,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import RecaptchaV2EnterpriseTask, RecaptchaV2EnterpriseTaskProxyless +from captcha_solver_api.tasks import RecaptchaV2EnterpriseTaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -56,31 +56,4 @@ async def main(): except Exception as e: sys.exit(e) - # --- With proxy example --- - # Solves reCAPTCHA v2 Enterprise through your own proxy. - # Use when the target site is geo-restricted or you need a consistent session. - try: - result = await solver.solve( - RecaptchaV2EnterpriseTask( - websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - isInvisible=False, - # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() - userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent - cookies="foo=bar; baz=1", # Session cookies if needed - ) - ) - # Solution contains the same gRecaptchaResponse token. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - - asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_proxy.py b/examples/async/recaptcha_v2_proxy.py new file mode 100644 index 0000000..0eaf97c --- /dev/null +++ b/examples/async/recaptcha_v2_proxy.py @@ -0,0 +1,44 @@ +"""Example: Solve reCAPTCHA v2 through your own proxy using the async client.""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import RecaptchaV2Task + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") +solver = AsyncCaptchaClient(api_key) + + +async def main(): + task = RecaptchaV2Task( + websiteURL="https://example.com/login", + websiteKey="YOUR_WEBSITE_KEY", + proxyType="http", + proxyAddress="1.2.3.4", + proxyPort=8080, + proxyLogin="user", + proxyPassword="password", + ) + try: + result = await solver.solve(task) + except Exception as exc: + sys.exit(str(exc)) + else: + print("result: " + str(result)) + + +asyncio.run(main()) diff --git a/examples/async/recaptcha_v2.py b/examples/async/recaptcha_v2_proxyless.py similarity index 62% rename from examples/async/recaptcha_v2.py rename to examples/async/recaptcha_v2_proxyless.py index ddbbd27..3206993 100644 --- a/examples/async/recaptcha_v2.py +++ b/examples/async/recaptcha_v2_proxyless.py @@ -1,10 +1,4 @@ -""" -Example: Solve reCAPTCHA v2 with the async client. - -Prerequisites: - Set the CAPTCHA_API_KEY environment variable. - httpx must be installed (it's a package dependency, so `pip install captcha-solver-api` covers it). -""" +"""Example: Solve reCAPTCHA v2 without your own proxy using the async client.""" import asyncio import os @@ -26,20 +20,18 @@ from captcha_solver_api.tasks import RecaptchaV2TaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") - solver = AsyncCaptchaClient(api_key) async def main(): task = RecaptchaV2TaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with the captcha - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page + websiteURL="https://example.com/login", + websiteKey="YOUR_WEBSITE_KEY", ) - try: result = await solver.solve(task) - except Exception as e: - sys.exit(str(e)) + except Exception as exc: + sys.exit(str(exc)) else: print("result: " + str(result)) diff --git a/examples/async/tencent.py b/examples/async/tencent_proxy.py similarity index 65% rename from examples/async/tencent.py rename to examples/async/tencent_proxy.py index 28c9df3..fa176b2 100644 --- a/examples/async/tencent.py +++ b/examples/async/tencent_proxy.py @@ -24,7 +24,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import TencentTask, TencentTaskProxyless +from captcha_solver_api.tasks import TencentTask api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -32,25 +32,6 @@ async def main(): - # --- Proxyless example --- - # Solves Tencent captcha without a proxy. - # The service's own proxies are used to solve the captcha. - # appId is found in the page source code. captchaScript is optional if the site uses the default. - try: - result = await solver.solve( - TencentTaskProxyless( - websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha - appId="YOUR_APP_ID", # appId from page source code (required) - # Optional fields: - captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default - ) - ) - # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} - # Pass all four values together into the page's captcha callback as-is. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - # --- With proxy example --- # Solves Tencent captcha through your own proxy. # Use when the target site is geo-restricted or you need a consistent session. diff --git a/examples/async/tencent_proxyless.py b/examples/async/tencent_proxyless.py new file mode 100644 index 0000000..6d5953f --- /dev/null +++ b/examples/async/tencent_proxyless.py @@ -0,0 +1,54 @@ +""" +Example: Solve a Tencent captcha challenge with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and appId with values from your target page. + Pass captchaScript if the site uses a non-default script URL. +""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import TencentTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = AsyncCaptchaClient(api_key) + + +async def main(): + # --- Proxyless example --- + # Solves Tencent captcha without a proxy. + # The service's own proxies are used to solve the captcha. + # appId is found in the page source code. captchaScript is optional if the site uses the default. + try: + result = await solver.solve( + TencentTaskProxyless( + websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha + appId="YOUR_APP_ID", # appId from page source code (required) + # Optional fields: + captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default + ) + ) + # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} + # Pass all four values together into the page's captcha callback as-is. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + +asyncio.run(main()) diff --git a/examples/async/turnstile_proxy.py b/examples/async/turnstile_proxy.py new file mode 100644 index 0000000..a88b3b6 --- /dev/null +++ b/examples/async/turnstile_proxy.py @@ -0,0 +1,60 @@ +""" +Example: Solve a Cloudflare Turnstile challenge with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. + For Cloudflare Challenge pages, also extract and pass action, data, and pagedata. +""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import TurnstileTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = AsyncCaptchaClient(api_key) + + +async def main(): + # --- With proxy example --- + # Solves Cloudflare Turnstile through your own proxy. + try: + result = await solver.solve( + TurnstileTask( + websiteURL="https://example.com/login", # Full URL of the page with Turnstile + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + # action='login', + # data='custom-cdata-value', + # pagedata='chl-page-data-value', + # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', + ) + ) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + + +asyncio.run(main()) diff --git a/examples/async/turnstile.py b/examples/async/turnstile_proxyless.py similarity index 63% rename from examples/async/turnstile.py rename to examples/async/turnstile_proxyless.py index 40f9154..7414b71 100644 --- a/examples/async/turnstile.py +++ b/examples/async/turnstile_proxyless.py @@ -24,7 +24,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import TurnstileTask, TurnstileTaskProxyless +from captcha_solver_api.tasks import TurnstileTaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -54,29 +54,4 @@ async def main(): except Exception as e: sys.exit(e) - # --- With proxy example --- - # Solves Cloudflare Turnstile through your own proxy. - try: - result = await solver.solve( - TurnstileTask( - websiteURL="https://example.com/login", # Full URL of the page with Turnstile - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - # action='login', - # data='custom-cdata-value', - # pagedata='chl-page-data-value', - # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', - ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - - asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha.py b/examples/async/yandex_smartcaptcha_proxy.py similarity index 65% rename from examples/async/yandex_smartcaptcha.py rename to examples/async/yandex_smartcaptcha_proxy.py index 698eec5..e9bfe65 100644 --- a/examples/async/yandex_smartcaptcha.py +++ b/examples/async/yandex_smartcaptcha_proxy.py @@ -23,7 +23,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import AsyncCaptchaClient -from captcha_solver_api.tasks import YandexSmartCaptchaTask, YandexSmartCaptchaTaskProxyless +from captcha_solver_api.tasks import YandexSmartCaptchaTask api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") @@ -31,26 +31,6 @@ async def main(): - # --- Proxyless example --- - # Solves Yandex SmartCaptcha without a proxy. - # The service's own proxies are used to solve the captcha. - # websiteKey is the sitekey value from the page code or captcha iframe. - try: - result = await solver.solve( - YandexSmartCaptchaTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha - websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page - # Optional fields: - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent - # cookies='session=abc123; token=xyz789', # Session cookies if needed - ) - ) - # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} - # Use solution.token in the smart-token field or pass to your site's backend. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) - # --- With proxy example --- # Solves Yandex SmartCaptcha through your own proxy. # Note: this is the only captcha type where an https proxy is accepted. diff --git a/examples/async/yandex_smartcaptcha_proxyless.py b/examples/async/yandex_smartcaptcha_proxyless.py new file mode 100644 index 0000000..3f2dd04 --- /dev/null +++ b/examples/async/yandex_smartcaptcha_proxyless.py @@ -0,0 +1,54 @@ +""" +Example: Solve a Yandex SmartCaptcha challenge (token-based) with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. +""" + +import asyncio +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import YandexSmartCaptchaTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = AsyncCaptchaClient(api_key) + + +async def main(): + # --- Proxyless example --- + # Solves Yandex SmartCaptcha without a proxy. + # The service's own proxies are used to solve the captcha. + # websiteKey is the sitekey value from the page code or captcha iframe. + try: + result = await solver.solve( + YandexSmartCaptchaTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha + websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page + # Optional fields: + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # cookies='session=abc123; token=xyz789', # Session cookies if needed + ) + ) + # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} + # Use solution.token in the smart-token field or pass to your site's backend. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + +asyncio.run(main()) diff --git a/examples/sync/geetest_v3_proxy.py b/examples/sync/geetest_v3_proxy.py new file mode 100644 index 0000000..e91b441 --- /dev/null +++ b/examples/sync/geetest_v3_proxy.py @@ -0,0 +1,76 @@ +""" +Example: Solve a GeeTest v3 challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL, gt, and challenge with values from your target page. + Important: the challenge value is dynamic. Fetch a fresh one for each request. + +NOTE: "https://target-site.com/path/to/geetest/init" below is a PLACEHOLDER, not a +real endpoint -- this script will not run end-to-end as-is. Replace it with a request +to your actual target page (or wherever it exposes a fresh `challenge` value) before +running this example. It's here only to illustrate where that fetch belongs in the flow. +""" + +import os +import sys + +import requests + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import GeeTestTask + +# in this example we store the API key inside environment variables that can be set like: +# export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS +# set CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Windows +# you can just set the API key directly to its value like: +# api_key="1abc234de56fab7c89012d34e56fa7b8" + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Create a solver instance with your API key. +# GeeTest tasks may take longer. Increase timeout if needed. +solver = CaptchaClient(api_key, timeout=300, polling_interval=10) + +""" +Important: the value of the 'challenge' parameter is dynamic. +For each request to the API you need to get a new value from the target page. +Below is an example of fetching it from a demo endpoint. +""" +# Fetch a fresh challenge value from the target page. +# In production, extract this from the page's initGeetest call or network requests. +# "target-site.com" is a placeholder -- point this at your real target before running. +resp = requests.get("https://target-site.com/path/to/geetest/init", timeout=30) +challenge = resp.json()["challenge"] + +# --- With proxy example --- +# Solves GeeTest v3 through your own proxy. +try: + result = solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with GeeTest + gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget + challenge=challenge, # Session-specific value, must be fresh + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) + ) + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/geetest_v3.py b/examples/sync/geetest_v3_proxyless.py similarity index 77% rename from examples/sync/geetest_v3.py rename to examples/sync/geetest_v3_proxyless.py index 199479e..10503e6 100644 --- a/examples/sync/geetest_v3.py +++ b/examples/sync/geetest_v3_proxyless.py @@ -30,7 +30,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import GeeTestTask, GeeTestTaskProxyless +from captcha_solver_api.tasks import GeeTestTaskProxyless # in this example we store the API key inside environment variables that can be set like: # export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS @@ -75,23 +75,3 @@ print("result: " + str(result)) except Exception as e: sys.exit(e) - -# --- With proxy example --- -# Solves GeeTest v3 through your own proxy. -try: - result = solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with GeeTest - gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget - challenge=challenge, # Session-specific value, must be fresh - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - ) - ) - print("result: " + str(result)) -except Exception as e: - sys.exit(e) diff --git a/examples/sync/geetest_v4_proxy.py b/examples/sync/geetest_v4_proxy.py new file mode 100644 index 0000000..9d05bdf --- /dev/null +++ b/examples/sync/geetest_v4_proxy.py @@ -0,0 +1,62 @@ +""" +Example: Solve a GeeTest v4 challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and captcha_id with values from your target page. + GeeTest v4 drops gt/challenge entirely. It uses captcha_id instead, which + (unlike v3's challenge) is static per site, so it doesn't need to be + re-fetched for every request. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import GeeTestTask + +# in this example we store the API key inside environment variables that can be set like: +# export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS +# set CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Windows +# you can just set the API key directly to its value like: +# api_key="1abc234de56fab7c89012d34e56fa7b8" + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Create a solver instance with your API key. +# GeeTest v4 tasks may take longer. Increase timeout if needed. +solver = CaptchaClient(api_key, timeout=300, polling_interval=10) + +# --- With proxy example --- +# Solves GeeTest v4 through your own proxy. +try: + result = solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget + version=4, # Required: must be 4 for this version + initParameters={ # Required: must contain captcha_id + "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page + }, + # --- Proxy parameters --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) + ) + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/geetest_v4.py b/examples/sync/geetest_v4_proxyless.py similarity index 70% rename from examples/sync/geetest_v4.py rename to examples/sync/geetest_v4_proxyless.py index dc066f0..55cb061 100644 --- a/examples/sync/geetest_v4.py +++ b/examples/sync/geetest_v4_proxyless.py @@ -25,7 +25,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import GeeTestTask, GeeTestTaskProxyless +from captcha_solver_api.tasks import GeeTestTaskProxyless # in this example we store the API key inside environment variables that can be set like: # export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS @@ -59,25 +59,3 @@ print("result: " + str(result)) except Exception as e: sys.exit(e) - -# --- With proxy example --- -# Solves GeeTest v4 through your own proxy. -try: - result = solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget - version=4, # Required: must be 4 for this version - initParameters={ # Required: must contain captcha_id - "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page - }, - # --- Proxy parameters --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - ) - ) - print("result: " + str(result)) -except Exception as e: - sys.exit(e) diff --git a/examples/sync/recaptcha_v2_enterprise_proxy.py b/examples/sync/recaptcha_v2_enterprise_proxy.py new file mode 100644 index 0000000..b2e5ba3 --- /dev/null +++ b/examples/sync/recaptcha_v2_enterprise_proxy.py @@ -0,0 +1,63 @@ +""" +Example: Solve a reCAPTCHA v2 Enterprise challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. + If the site uses enterprisePayload, extract and pass it or the token may be rejected. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import RecaptchaV2EnterpriseTask + +# in this example we store the API key inside environment variables that can be set like: +# export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS +# set CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Windows +# you can just set the API key directly to its value like: +# api_key="1abc234de56fab7c89012d34e56fa7b8" + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Create a solver instance with your API key. +solver = CaptchaClient(api_key) + +# --- With proxy example --- +# Solves reCAPTCHA v2 Enterprise through your own proxy. +# Use when the target site is geo-restricted or you need a consistent session. +try: + result = solver.solve( + RecaptchaV2EnterpriseTask( + websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + isInvisible=False, + # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() + userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent + cookies="foo=bar; baz=1", # Session cookies if needed + ) + ) + # Solution contains the same gRecaptchaResponse token. + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/recaptcha_v2_enterprise.py b/examples/sync/recaptcha_v2_enterprise_proxyless.py similarity index 63% rename from examples/sync/recaptcha_v2_enterprise.py rename to examples/sync/recaptcha_v2_enterprise_proxyless.py index 9ac578a..bd7ebc5 100644 --- a/examples/sync/recaptcha_v2_enterprise.py +++ b/examples/sync/recaptcha_v2_enterprise_proxyless.py @@ -23,7 +23,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import RecaptchaV2EnterpriseTask, RecaptchaV2EnterpriseTaskProxyless +from captcha_solver_api.tasks import RecaptchaV2EnterpriseTaskProxyless # in this example we store the API key inside environment variables that can be set like: # export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS @@ -59,29 +59,3 @@ print("result: " + str(result)) except Exception as e: sys.exit(e) - -# --- With proxy example --- -# Solves reCAPTCHA v2 Enterprise through your own proxy. -# Use when the target site is geo-restricted or you need a consistent session. -try: - result = solver.solve( - RecaptchaV2EnterpriseTask( - websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - isInvisible=False, - # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() - userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent - cookies="foo=bar; baz=1", # Session cookies if needed - ) - ) - # Solution contains the same gRecaptchaResponse token. - print("result: " + str(result)) -except Exception as e: - sys.exit(e) diff --git a/examples/sync/recaptcha_v2.py b/examples/sync/recaptcha_v2_proxy.py similarity index 71% rename from examples/sync/recaptcha_v2.py rename to examples/sync/recaptcha_v2_proxy.py index 9e76fe5..40f6674 100644 --- a/examples/sync/recaptcha_v2.py +++ b/examples/sync/recaptcha_v2_proxy.py @@ -22,7 +22,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import RecaptchaV2Task, RecaptchaV2TaskProxyless +from captcha_solver_api.tasks import RecaptchaV2Task # in this example we store the API key inside environment variables that can be set like: # export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS @@ -37,25 +37,6 @@ # Optional: polling_interval (seconds before and between status checks, default 10) solver = CaptchaClient(api_key) -# --- Proxyless example --- -# Solves reCAPTCHA v2 without a proxy. -# The service uses its own IP addresses. -try: - # Create a task and wait for the solution. - # solve() handles task creation, polling, and returns the solution dict. - result = solver.solve( - RecaptchaV2TaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with the captcha - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page - isInvisible=False, # Set True for invisible reCAPTCHA - ) - ) - # Solution contains {"gRecaptchaResponse": "03AGdBq..."} - # Pass this token to the g-recaptcha-response field or widget callback. - print("result: " + str(result)) -except Exception as e: - sys.exit(e) - # --- With proxy example --- # Solves reCAPTCHA v2 through your own proxy. # Required when the target site is geo-restricted or you need session consistency. diff --git a/examples/sync/recaptcha_v2_proxyless.py b/examples/sync/recaptcha_v2_proxyless.py new file mode 100644 index 0000000..2670021 --- /dev/null +++ b/examples/sync/recaptcha_v2_proxyless.py @@ -0,0 +1,57 @@ +""" +Example: Solve a reCAPTCHA v2 challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + +# in this example we store the API key inside environment variables that can be set like: +# export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS +# set CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Windows +# you can just set the API key directly to its value like: +# api_key="1abc234de56fab7c89012d34e56fa7b8" + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Create a solver instance with your API key. +# Optional: timeout (max seconds to wait for solution, default 120) +# Optional: polling_interval (seconds before and between status checks, default 10) +solver = CaptchaClient(api_key) + +# --- Proxyless example --- +# Solves reCAPTCHA v2 without a proxy. +# The service uses its own IP addresses. +try: + # Create a task and wait for the solution. + # solve() handles task creation, polling, and returns the solution dict. + result = solver.solve( + RecaptchaV2TaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with the captcha + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page + isInvisible=False, # Set True for invisible reCAPTCHA + ) + ) + # Solution contains {"gRecaptchaResponse": "03AGdBq..."} + # Pass this token to the g-recaptcha-response field or widget callback. + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/tencent.py b/examples/sync/tencent_proxy.py similarity index 65% rename from examples/sync/tencent.py rename to examples/sync/tencent_proxy.py index 86fe48d..1314907 100644 --- a/examples/sync/tencent.py +++ b/examples/sync/tencent_proxy.py @@ -23,31 +23,12 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import TencentTask, TencentTaskProxyless +from captcha_solver_api.tasks import TencentTask api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") solver = CaptchaClient(api_key) -# --- Proxyless example --- -# Solves Tencent captcha without a proxy. -# The service's own proxies are used to solve the captcha. -# appId is found in the page source code. captchaScript is optional if the site uses the default. -try: - result = solver.solve( - TencentTaskProxyless( - websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha - appId="YOUR_APP_ID", # appId from page source code (required) - # Optional fields: - captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default - ) - ) - # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} - # Pass all four values together into the page's captcha callback as-is. - print("result: " + str(result)) -except Exception as e: - sys.exit(e) - # --- With proxy example --- # Solves Tencent captcha through your own proxy. # Use when the target site is geo-restricted or you need a consistent session. diff --git a/examples/sync/tencent_proxyless.py b/examples/sync/tencent_proxyless.py new file mode 100644 index 0000000..fd620bc --- /dev/null +++ b/examples/sync/tencent_proxyless.py @@ -0,0 +1,49 @@ +""" +Example: Solve a Tencent captcha challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and appId with values from your target page. + Pass captchaScript if the site uses a non-default script URL. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import TencentTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = CaptchaClient(api_key) + +# --- Proxyless example --- +# Solves Tencent captcha without a proxy. +# The service's own proxies are used to solve the captcha. +# appId is found in the page source code. captchaScript is optional if the site uses the default. +try: + result = solver.solve( + TencentTaskProxyless( + websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha + appId="YOUR_APP_ID", # appId from page source code (required) + # Optional fields: + captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default + ) + ) + # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} + # Pass all four values together into the page's captcha callback as-is. + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/turnstile_proxy.py b/examples/sync/turnstile_proxy.py new file mode 100644 index 0000000..29d61ac --- /dev/null +++ b/examples/sync/turnstile_proxy.py @@ -0,0 +1,61 @@ +""" +Example: Solve a Cloudflare Turnstile challenge. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. + For Cloudflare Challenge pages, also extract and pass action, data, and pagedata. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import TurnstileTask + +# in this example we store the API key inside environment variables that can be set like: +# export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS +# set CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Windows +# you can just set the API key directly to its value like: +# api_key="1abc234de56fab7c89012d34e56fa7b8" + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Create a solver instance with your API key. +solver = CaptchaClient(api_key) + +# --- With proxy example --- +# Solves Cloudflare Turnstile through your own proxy. +try: + result = solver.solve( + TurnstileTask( + websiteURL="https://example.com/login", # Full URL of the page with Turnstile + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + # action='login', + # data='custom-cdata-value', + # pagedata='chl-page-data-value', + # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', + ) + ) + print("result: " + str(result)) +except Exception as e: + sys.exit(e) diff --git a/examples/sync/turnstile.py b/examples/sync/turnstile_proxyless.py similarity index 67% rename from examples/sync/turnstile.py rename to examples/sync/turnstile_proxyless.py index bbc3743..258402d 100644 --- a/examples/sync/turnstile.py +++ b/examples/sync/turnstile_proxyless.py @@ -23,7 +23,7 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import TurnstileTask, TurnstileTaskProxyless +from captcha_solver_api.tasks import TurnstileTaskProxyless # in this example we store the API key inside environment variables that can be set like: # export CAPTCHA_API_KEY=1abc234de56fab7c89012d34e56fa7b8 on Linux or macOS @@ -57,27 +57,3 @@ print("result: " + str(result)) except Exception as e: sys.exit(e) - -# --- With proxy example --- -# Solves Cloudflare Turnstile through your own proxy. -try: - result = solver.solve( - TurnstileTask( - websiteURL="https://example.com/login", # Full URL of the page with Turnstile - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - # action='login', - # data='custom-cdata-value', - # pagedata='chl-page-data-value', - # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', - ) - ) - print("result: " + str(result)) -except Exception as e: - sys.exit(e) diff --git a/examples/sync/yandex_smartcaptcha.py b/examples/sync/yandex_smartcaptcha_proxy.py similarity index 65% rename from examples/sync/yandex_smartcaptcha.py rename to examples/sync/yandex_smartcaptcha_proxy.py index 11b328f..08a2fc5 100644 --- a/examples/sync/yandex_smartcaptcha.py +++ b/examples/sync/yandex_smartcaptcha_proxy.py @@ -22,32 +22,12 @@ load_dotenv(os.path.join(repo_root, ".env")) from captcha_solver_api import CaptchaClient -from captcha_solver_api.tasks import YandexSmartCaptchaTask, YandexSmartCaptchaTaskProxyless +from captcha_solver_api.tasks import YandexSmartCaptchaTask api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") solver = CaptchaClient(api_key) -# --- Proxyless example --- -# Solves Yandex SmartCaptcha without a proxy. -# The service's own proxies are used to solve the captcha. -# websiteKey is the sitekey value from the page code or captcha iframe. -try: - result = solver.solve( - YandexSmartCaptchaTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha - websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page - # Optional fields: - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent - # cookies='session=abc123; token=xyz789', # Session cookies if needed - ) - ) - # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} - # Use solution.token in the smart-token field or pass to your site's backend. - print("result: " + str(result)) -except Exception as e: - sys.exit(e) - # --- With proxy example --- # Solves Yandex SmartCaptcha through your own proxy. # Note: this is the only captcha type where an https proxy is accepted. diff --git a/examples/sync/yandex_smartcaptcha_proxyless.py b/examples/sync/yandex_smartcaptcha_proxyless.py new file mode 100644 index 0000000..af1b089 --- /dev/null +++ b/examples/sync/yandex_smartcaptcha_proxyless.py @@ -0,0 +1,49 @@ +""" +Example: Solve a Yandex SmartCaptcha challenge (token-based). + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. +""" + +import os +import sys + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import YandexSmartCaptchaTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +solver = CaptchaClient(api_key) + +# --- Proxyless example --- +# Solves Yandex SmartCaptcha without a proxy. +# The service's own proxies are used to solve the captcha. +# websiteKey is the sitekey value from the page code or captcha iframe. +try: + result = solver.solve( + YandexSmartCaptchaTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha + websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page + # Optional fields: + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # cookies='session=abc123; token=xyz789', # Session cookies if needed + ) + ) + # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} + # Use solution.token in the smart-token field or pass to your site's backend. + print("result: " + str(result)) +except Exception as e: + sys.exit(e) From b36877c1497cc8ed8265910676c4e9472d2c24cd Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:38:32 +0300 Subject: [PATCH 06/23] docs: add security policy --- SECURITY.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..aea455c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policy + +## Supported versions + +Security fixes are provided for the latest released version of +`captcha-solver-api`. Before reporting a vulnerability, verify that it is still +present after upgrading to the latest version available on PyPI. + +| Version | Supported | +|---|---| +| Latest release | Yes | +| Older releases | No | + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately through +[GitHub Security Advisories](https://github.com/captcha-solver-api/python-sdk/security/advisories/new). +Do not open a public issue or pull request for an undisclosed vulnerability. + +Include the following information when possible: + +- the affected SDK version and Python version; +- a clear description of the impact; +- steps or a minimal example that reproduces the problem; +- any known mitigations or suggested fixes. + +You should receive an acknowledgement within five business days. We will keep +you informed while the report is investigated and will coordinate disclosure +after a fix is available. Please allow a reasonable amount of time for the +issue to be addressed before making it public. + +## Scope + +This policy covers the Python SDK in this repository. Vulnerabilities in the +Captcha Solver service or website should be reported through the security +channel of the corresponding project. From 413a2ea072dc35e9bea6abf25d0f9a747b42f5c3 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:40:18 +0300 Subject: [PATCH 07/23] chore: set version to 1.0.5 --- captcha_solver_api/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captcha_solver_api/_version.py b/captcha_solver_api/_version.py index 92192ee..68cdeee 100644 --- a/captcha_solver_api/_version.py +++ b/captcha_solver_api/_version.py @@ -1 +1 @@ -__version__ = "1.0.4" +__version__ = "1.0.5" From edaab93c6a4bcbe093e2f1d96de977815ded0e46 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:54:09 +0300 Subject: [PATCH 08/23] fix: format async proxyless examples --- examples/async/geetest_v3_proxyless.py | 1 + examples/async/geetest_v4_proxyless.py | 1 + examples/async/recaptcha_v2_enterprise_proxyless.py | 1 + examples/async/tencent_proxyless.py | 1 + examples/async/turnstile_proxyless.py | 1 + examples/async/yandex_smartcaptcha_proxyless.py | 1 + 6 files changed, 6 insertions(+) diff --git a/examples/async/geetest_v3_proxyless.py b/examples/async/geetest_v3_proxyless.py index 884da05..1759891 100644 --- a/examples/async/geetest_v3_proxyless.py +++ b/examples/async/geetest_v3_proxyless.py @@ -73,4 +73,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) diff --git a/examples/async/geetest_v4_proxyless.py b/examples/async/geetest_v4_proxyless.py index f4c37dd..8a91357 100644 --- a/examples/async/geetest_v4_proxyless.py +++ b/examples/async/geetest_v4_proxyless.py @@ -56,4 +56,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise_proxyless.py b/examples/async/recaptcha_v2_enterprise_proxyless.py index 7cd134a..133e94a 100644 --- a/examples/async/recaptcha_v2_enterprise_proxyless.py +++ b/examples/async/recaptcha_v2_enterprise_proxyless.py @@ -56,4 +56,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) diff --git a/examples/async/tencent_proxyless.py b/examples/async/tencent_proxyless.py index 6d5953f..f260fd3 100644 --- a/examples/async/tencent_proxyless.py +++ b/examples/async/tencent_proxyless.py @@ -51,4 +51,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) diff --git a/examples/async/turnstile_proxyless.py b/examples/async/turnstile_proxyless.py index 7414b71..d59e8d4 100644 --- a/examples/async/turnstile_proxyless.py +++ b/examples/async/turnstile_proxyless.py @@ -54,4 +54,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha_proxyless.py b/examples/async/yandex_smartcaptcha_proxyless.py index 3f2dd04..8e980d6 100644 --- a/examples/async/yandex_smartcaptcha_proxyless.py +++ b/examples/async/yandex_smartcaptcha_proxyless.py @@ -51,4 +51,5 @@ async def main(): except Exception as e: sys.exit(e) + asyncio.run(main()) From bda2216f7fb4c136bdd42b72514e8bc182e2754a Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Wed, 28 Oct 2026 15:55:43 +0300 Subject: [PATCH 09/23] ci: run Python matrix on Ubuntu 22.04 --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 16e814f..aeed544 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,10 @@ jobs: # to catch packaging errors early. Skipped if lint fails to save CI minutes. test: needs: lint - runs-on: ubuntu-latest + # Python 3.9 is not available through setup-python on Ubuntu 26.04, + # which ubuntu-latest currently resolves to. Keep the compatibility + # matrix on an image that provides every supported Python version. + runs-on: ubuntu-22.04 strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] From ef6134ce603e1a43e8376cb43f4ed62adbdbfeca Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Thu, 29 Oct 2026 01:25:54 +0300 Subject: [PATCH 10/23] fix: tighten validation and document required task parameters - reject whitespace-only client_key in both clients - document ValidationError in solve() and create_task() docstrings and README - mark every task parameter as Required or Optional in docstrings - replace em dash in proxy support table with a plain dash - update README references to renamed example files Co-Authored-By: Claude Opus 5.5 --- README.md | 23 +-- captcha_solver_api/async_client.py | 10 +- captcha_solver_api/client.py | 10 +- captcha_solver_api/tasks.py | 260 +++++++++++++-------------- examples/README.md | 2 +- tests/async/test_async_validation.py | 5 + tests/sync/test_validation.py | 5 + 7 files changed, 168 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index c9a2a9d..5f54dc3 100644 --- a/README.md +++ b/README.md @@ -155,16 +155,16 @@ on the target page. |---|---|---| | reCAPTCHA v2 | ✅ | ✅ | | reCAPTCHA v2 Enterprise | ✅ | ✅ | -| reCAPTCHA v3 | ✅ | — | +| reCAPTCHA v3 | ✅ | - | | Cloudflare Turnstile | ✅ | ✅ | | GeeTest v3 | ✅ | ✅ | | GeeTest v4 | ✅ | ✅ | -| Image to Text | ✅ | — | +| Image to Text | ✅ | - | | Yandex SmartCaptcha | ✅ | ✅ | -| Coordinates (click captcha) | ✅ | — | +| Coordinates (click captcha) | ✅ | - | | Tencent | ✅ | ✅ | -An em dash means that this CAPTCHA type has no separate task method that accepts +A dash (`-`) means that this CAPTCHA type has no separate task class that accepts your own proxy; it does not indicate that solving the CAPTCHA is unsupported. ## Client Reference @@ -214,14 +214,17 @@ solution -- wraps `create_task()` + `get_task_result()` so you don't poll by han Returns the `solution` dict once `status` is `"ready"` -- its shape depends on the task type (see [Captcha Types](#captcha-types)). -Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`. +Raises `ValidationError` before any request if `task` is not a task object or a +required task parameter is missing or empty. Otherwise raises `ApiError`, +`CaptchaTimeoutError`, or `NetworkError`. ### `create_task(task, language_pool=None)` Submits `task` and returns its numeric task ID without waiting for a solution. Accepts `task` and `language_pool`; it does not accept `timeout`. Use this instead of `solve()` if you need to manage polling yourself (e.g. checking on many tasks from a different process). -Raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`. +Raises `ValidationError` before any request if `task` is invalid (same rules as +`solve()`). Otherwise raises `ApiError`, `CaptchaTimeoutError`, or `NetworkError`. ### `get_task_result(task_id)` @@ -486,7 +489,7 @@ print(result["validate"]) print(result["seccode"]) ``` `challenge` is session-specific and expires quickly, so it can't be hardcoded -into a static example -- see [examples/sync/geetest_v3.py](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/sync/geetest_v3.py) +into a static example -- see [examples/sync/geetest_v3_proxyless.py](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/sync/geetest_v3_proxyless.py) for where the fetch belongs in the flow. ```python @@ -729,12 +732,12 @@ sync/async example list, setup steps, expected results, and placeholder guidance `CAPTCHA_API_KEY` -- they read sample images bundled in [examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed. `python-dotenv` is optional: install it only if you want the scripts to load a `.env` file. -- **Token captchas** (`recaptcha_v2.py`, `recaptcha_v2_enterprise.py`, `recaptcha_v3.py`, - `turnstile.py`, `yandex_smartcaptcha.py`, `geetest_v4.py`, `tencent.py`) use +- **Token captchas** (`recaptcha_v2_*.py`, `recaptcha_v2_enterprise_*.py`, `recaptcha_v3.py`, + `turnstile_*.py`, `yandex_smartcaptcha_*.py`, `geetest_v4_*.py`, `tencent_*.py`) use placeholder values (`https://example.com/...`, `YOUR_WEBSITE_KEY`, `YOUR_APP_ID`, `YOUR_CAPTCHA_ID`) -- replace these with the real values from your own target page before running. -- **`geetest_v3.py`** additionally needs `challenge` fetched fresh for every +- **`geetest_v3_*.py`** additionally needs `challenge` fetched fresh for every request -- it's single-use and expires within seconds, so it can't be hardcoded into a static example. `"https://target-site.com/path/to/geetest/init"` is a placeholder; replace it with a request to your own target's equivalent diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index 0638f0e..90267bf 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -43,7 +43,7 @@ class AsyncCaptchaClient: the account default. Can be overridden in `solve()` or `create_task()`. Raises: - ValidationError: `client_key` is empty. + ValidationError: `client_key` is missing, empty, or whitespace. Example: from captcha_solver_api import AsyncCaptchaClient @@ -85,9 +85,9 @@ def __init__( pool. Raises: - ValidationError: `client_key` is empty. + ValidationError: `client_key` is missing, empty, or whitespace. """ - if not client_key: + if not isinstance(client_key, str) or not client_key.strip(): raise ValidationError("client_key is required") self.client_key = client_key @@ -166,6 +166,8 @@ async def create_task(self, task: Any, language_pool: Optional[str] = None) -> i The numeric task ID to pass to `get_task_result()`. Raises: + ValidationError: `task` is not a task object, or a required task + parameter is missing or empty. Raised before any request is sent. ApiError: The API rejected the task or its parameters. NetworkError: The request failed at the transport level. CaptchaTimeoutError: The HTTP request timed out. @@ -277,6 +279,8 @@ async def solve( or the README's method reference. Raises: + ValidationError: `task` is not a task object, or a required task + parameter is missing or empty. Raised before any request is sent. ApiError: The API rejected the task or reported a solving error. CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request timed out. diff --git a/captcha_solver_api/client.py b/captcha_solver_api/client.py index 5eb45a6..a2fe4e5 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -33,7 +33,7 @@ class CaptchaClient: the account default. Can be overridden in `solve()` or `create_task()`. Raises: - ValidationError: `client_key` is empty. + ValidationError: `client_key` is missing, empty, or whitespace. Example: from captcha_solver_api import CaptchaClient @@ -75,9 +75,9 @@ def __init__( pool. Raises: - ValidationError: `client_key` is empty. + ValidationError: `client_key` is missing, empty, or whitespace. """ - if not client_key: + if not isinstance(client_key, str) or not client_key.strip(): raise ValidationError("client_key is required") self.client_key = client_key @@ -154,6 +154,8 @@ def create_task(self, task: Any, language_pool: Optional[str] = None) -> int: The numeric task ID to pass to `get_task_result()`. Raises: + ValidationError: `task` is not a task object, or a required task + parameter is missing or empty. Raised before any request is sent. ApiError: The API rejected the task (bad key, bad parameters, etc). NetworkError: The request failed at the transport level. CaptchaTimeoutError: The HTTP request itself timed out (not the solve). @@ -255,6 +257,8 @@ def solve( or the README's method reference. Raises: + ValidationError: `task` is not a task object, or a required task + parameter is missing or empty. Raised before any request is sent. ApiError: The API rejected the task or reported an error while solving. CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request timed out. diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 871844e..0184719 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -52,11 +52,11 @@ class ProxyMixin: """Mixin that adds proxy fields to a task. Args (via `set_proxy`, or pass directly to the task's `__init__`): - proxy_type: `"http"`, `"socks4"`, or `"socks5"`. - proxy_address: Proxy IP address or hostname. - proxy_port: Proxy port. - proxy_login: Proxy auth username, if required. - proxy_password: Proxy auth password, if required. + proxy_type: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxy_address: Required. Proxy IP address or hostname. + proxy_port: Required. Proxy port. + proxy_login: Optional. Proxy auth username, if required. + proxy_password: Optional. Proxy auth password, if required. """ proxyType: str @@ -76,11 +76,11 @@ def set_proxy( """Sets the proxy fields on this task. Args: - proxy_type: `"http"`, `"socks4"`, or `"socks5"`. - proxy_address: Proxy IP address or hostname. - proxy_port: Proxy port. - proxy_login: Proxy auth username, if required. - proxy_password: Proxy auth password, if required. + proxy_type: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxy_address: Required. Proxy IP address or hostname. + proxy_port: Required. Proxy port. + proxy_login: Optional. Proxy auth username, if required. + proxy_password: Optional. Proxy auth password, if required. """ self.proxyType = proxy_type self.proxyAddress = proxy_address @@ -93,14 +93,14 @@ class RecaptchaV2TaskProxyless(BaseTask): """reCAPTCHA v2 without proxy -- the service solves using its own IPs. Args: - websiteURL: Full URL of the page where the captcha is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - isInvisible: `True` for invisible reCAPTCHA v2. - recaptchaDataSValue: The `data-s` value, found on Google Search/YouTube pages. - apiDomain: Non-default domain the widget's script is served from, if any. - userAgent: User-Agent to solve with. Recommended to match the agent that + websiteURL: Required. Full URL of the page where the captcha is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + isInvisible: Optional. `True` for invisible reCAPTCHA v2. + recaptchaDataSValue: Optional. The `data-s` value, found on Google Search/YouTube pages. + apiDomain: Optional. Non-default domain the widget's script is served from, if any. + userAgent: Optional. User-Agent to solve with. Recommended to match the agent that will submit the resulting token. - cookies: Session cookies to use while solving, if the page requires them. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. @@ -148,18 +148,18 @@ class RecaptchaV2Task(BaseTask, ProxyMixin): browser/bot that will submit the token. Args: - websiteURL: Full URL of the page where the captcha is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - isInvisible: `True` for invisible reCAPTCHA v2. - recaptchaDataSValue: The `data-s` value, found on Google Search/YouTube pages. - apiDomain: Non-default domain the widget's script is served from, if any. - userAgent: User-Agent to solve with. - cookies: Session cookies to use while solving, if the page requires them. + websiteURL: Required. Full URL of the page where the captcha is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + isInvisible: Optional. `True` for invisible reCAPTCHA v2. + recaptchaDataSValue: Optional. The `data-s` value, found on Google Search/YouTube pages. + apiDomain: Optional. Non-default domain the widget's script is served from, if any. + userAgent: Optional. User-Agent to solve with. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. @@ -216,14 +216,14 @@ class RecaptchaV2EnterpriseTaskProxyless(BaseTask): """reCAPTCHA v2 Enterprise without proxy. Args: - websiteURL: Full URL of the page where the captcha is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - enterprisePayload: Extra parameters passed to `grecaptcha.enterprise.render` + websiteURL: Required. Full URL of the page where the captcha is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + enterprisePayload: Optional. Extra parameters passed to `grecaptcha.enterprise.render` on the page, if any (e.g. `{"s": "..."}`). - isInvisible: `True` for invisible reCAPTCHA v2 Enterprise. - apiDomain: Non-default domain, defaults to `google.com`. - userAgent: User-Agent to solve with. - cookies: Session cookies to use while solving, if the page requires them. + isInvisible: Optional. `True` for invisible reCAPTCHA v2 Enterprise. + apiDomain: Optional. Non-default domain, defaults to `google.com`. + userAgent: Optional. User-Agent to solve with. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. @@ -267,19 +267,19 @@ class RecaptchaV2EnterpriseTask(BaseTask, ProxyMixin): """reCAPTCHA v2 Enterprise solved through your own proxy. Args: - websiteURL: Full URL of the page where the captcha is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - enterprisePayload: Extra parameters passed to `grecaptcha.enterprise.render` + websiteURL: Required. Full URL of the page where the captcha is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + enterprisePayload: Optional. Extra parameters passed to `grecaptcha.enterprise.render` on the page, if any (e.g. `{"s": "..."}`). - isInvisible: `True` for invisible reCAPTCHA v2 Enterprise. - apiDomain: Non-default domain, defaults to `google.com`. - userAgent: User-Agent to solve with. - cookies: Session cookies to use while solving, if the page requires them. + isInvisible: Optional. `True` for invisible reCAPTCHA v2 Enterprise. + apiDomain: Optional. Non-default domain, defaults to `google.com`. + userAgent: Optional. User-Agent to solve with. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. @@ -337,12 +337,12 @@ class RecaptchaV3TaskProxyless(BaseTask): and invisible, so there's no widget/session to keep pinned to a proxy IP. Args: - websiteURL: Full URL of the page where the captcha is located. - websiteKey: Site key for the v3 widget. - minScore: Minimum acceptable token score to return, e.g. `0.3`, `0.7`, `0.9`. - pageAction: The `action` parameter passed to `grecaptcha.execute()` on the page. - isEnterprise: `True` if this is reCAPTCHA v3 Enterprise. - apiDomain: Non-standard domain the widget's script is served from, if any. + websiteURL: Required. Full URL of the page where the captcha is located. + websiteKey: Required. Site key for the v3 widget. + minScore: Required. Minimum acceptable token score to return, e.g. `0.3`, `0.7`, `0.9`. + pageAction: Optional. The `action` parameter passed to `grecaptcha.execute()` on the page. + isEnterprise: Optional. `True` if this is reCAPTCHA v3 Enterprise. + apiDomain: Optional. Non-standard domain the widget's script is served from, if any. Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. @@ -386,15 +386,15 @@ class TurnstileTaskProxyless(BaseTask): """Cloudflare Turnstile without proxy. Args: - websiteURL: Full URL of the page where the widget is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - action: Value of the widget's `data-action` attribute, if set. - data: Custom payload from the widget's `data-cdata` attribute, if set. - pagedata: Value of the `chlPageData` parameter, needed for some Cloudflare + websiteURL: Required. Full URL of the page where the widget is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + action: Optional. Value of the widget's `data-action` attribute, if set. + data: Optional. Custom payload from the widget's `data-cdata` attribute, if set. + pagedata: Optional. Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. Named lowercase (not `pageData`) to match the API field name exactly -- Cloudflare-specific fields are the one place this API doesn't camelCase. - userAgent: User-Agent of the browser that loaded the challenge. Pass it + userAgent: Optional. User-Agent of the browser that loaded the challenge. Pass it together with `action`, `data`, and `pagedata` for Cloudflare Challenge pages. It is not needed for a standalone widget. @@ -440,19 +440,19 @@ class TurnstileTask(BaseTask, ProxyMixin): """Cloudflare Turnstile solved through your own proxy. Args: - websiteURL: Full URL of the page where the widget is located. - websiteKey: Value of the widget's `data-sitekey` attribute. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - action: Value of the widget's `data-action` attribute, if set. - data: Custom payload from the widget's `data-cdata` attribute, if set. - pagedata: Value of the `chlPageData` parameter, needed for some Cloudflare + websiteURL: Required. Full URL of the page where the widget is located. + websiteKey: Required. Value of the widget's `data-sitekey` attribute. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + action: Optional. Value of the widget's `data-action` attribute, if set. + data: Optional. Custom payload from the widget's `data-cdata` attribute, if set. + pagedata: Optional. Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. Named lowercase (not `pageData`) to match the API field name exactly. - userAgent: User-Agent of the browser that loaded the challenge. Pass it + userAgent: Optional. User-Agent of the browser that loaded the challenge. Pass it for Cloudflare Challenge pages; it is not needed for a standalone widget. @@ -513,17 +513,17 @@ class ImageToTextTask(BaseTask): session or target site is involved. Args: - body: The captcha image, base64-encoded (no `data:image/...;base64,` prefix). - phrase: `True` if the answer is multiple words. - case: `True` if the answer is case-sensitive. - numeric: Character set hint -- `0` unspecified, `1` digits only, `2` letters + body: Required. The captcha image, base64-encoded (no `data:image/...;base64,` prefix). + phrase: Optional. `True` if the answer is multiple words. + case: Optional. `True` if the answer is case-sensitive. + numeric: Optional. Character set hint -- `0` unspecified, `1` digits only, `2` letters only, `3` any with digits, `4` any with letters. - math: `True` if the image contains a math expression to evaluate rather + math: Optional. `True` if the image contains a math expression to evaluate rather than text to transcribe. - minLength: Minimum expected answer length. - maxLength: Maximum expected answer length. - comment: Free-text hint for the worker (e.g. what kind of answer is expected). - imgInstructions: Optional supplementary instruction image, base64-encoded. + minLength: Optional. Minimum expected answer length. + maxLength: Optional. Maximum expected answer length. + comment: Optional. Free-text hint for the worker (e.g. what kind of answer is expected). + imgInstructions: Optional. Optional supplementary instruction image, base64-encoded. Returns (`solution` from `solve()`): `text` -- the recognized text/answer. @@ -572,16 +572,16 @@ class GeeTestTaskProxyless(BaseTask): instead. Args: - websiteURL: Full URL of the page where the widget is located. - version: `3` (default) or `4`. - gt: Public key of the GeeTest widget. Required for v3. - challenge: Session-specific challenge value from the page. Required for + websiteURL: Required. Full URL of the page where the widget is located. + version: Optional. `3` (default) or `4`. + gt: Optional. Public key of the GeeTest widget. Required for v3. + challenge: Optional. Session-specific challenge value from the page. Required for v3, and must be freshly fetched for every request -- it cannot be reused. - initParameters: Extra parameters from the page's `initGeetest` call. For + initParameters: Optional. Extra parameters from the page's `initGeetest` call. For v4, must contain `captcha_id`. - geetestApiServerSubdomain: Custom GeeTest API subdomain, if the site uses one. - userAgent: User-Agent to solve with. - risk_type: Value of the `risk_type` parameter from the captcha-loading + geetestApiServerSubdomain: Optional. Custom GeeTest API subdomain, if the site uses one. + userAgent: Optional. User-Agent to solve with. + risk_type: Optional. Value of the `risk_type` parameter from the captcha-loading request, if present. Dynamic, single-use, and time-limited. Returns (`solution` from `solve()`): @@ -631,21 +631,21 @@ class GeeTestTask(BaseTask, ProxyMixin): for the version-specific fields. Args: - websiteURL: Full URL of the page where the widget is located. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - version: `3` (default) or `4`. - gt: Public key of the GeeTest widget. Required for v3. - challenge: Session-specific challenge value from the page, must be fresh. + websiteURL: Required. Full URL of the page where the widget is located. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + version: Optional. `3` (default) or `4`. + gt: Optional. Public key of the GeeTest widget. Required for v3. + challenge: Optional. Session-specific challenge value from the page, must be fresh. Required for v3. - initParameters: Extra parameters from the page's `initGeetest` call. For + initParameters: Optional. Extra parameters from the page's `initGeetest` call. For v4, must contain `captcha_id`. - geetestApiServerSubdomain: Custom GeeTest API subdomain, if the site uses one. - userAgent: User-Agent to solve with. - risk_type: Value of the `risk_type` parameter from the captcha-loading + geetestApiServerSubdomain: Optional. Custom GeeTest API subdomain, if the site uses one. + userAgent: Optional. User-Agent to solve with. + risk_type: Optional. Value of the `risk_type` parameter from the captcha-loading request, if present. Dynamic, single-use, and time-limited. Returns (`solution` from `solve()`): @@ -707,10 +707,10 @@ class YandexSmartCaptchaTaskProxyless(BaseTask): """Yandex SmartCaptcha (token challenge) without proxy. Args: - websiteURL: Full URL of the page where the widget is located. - websiteKey: The `sitekey` value from the page source or captcha iframe. - userAgent: User-Agent to solve with. - cookies: Session cookies to use while solving, if the page requires them. + websiteURL: Required. Full URL of the page where the widget is located. + websiteKey: Required. The `sitekey` value from the page source or captcha iframe. + userAgent: Optional. User-Agent to solve with. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `token` -- the value to submit as the SmartCaptcha response token. @@ -748,15 +748,15 @@ class YandexSmartCaptchaTask(BaseTask, ProxyMixin): """Yandex SmartCaptcha (token challenge) solved through your own proxy. Args: - websiteURL: Full URL of the page where the widget is located. - websiteKey: The `sitekey` value from the page source or captcha iframe. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - userAgent: User-Agent to solve with. - cookies: Session cookies to use while solving, if the page requires them. + websiteURL: Required. Full URL of the page where the widget is located. + websiteKey: Required. The `sitekey` value from the page source or captcha iframe. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + userAgent: Optional. User-Agent to solve with. + cookies: Optional. Session cookies to use while solving, if the page requires them. Returns (`solution` from `solve()`): `token` -- the value to submit as the SmartCaptcha response token. @@ -808,12 +808,12 @@ class CoordinatesTask(BaseTask): is submitted directly. Args: - body: The captcha image, base64-encoded (no `data:image/...;base64,` prefix). - comment: Hint for the worker, e.g. `"click on the green apple"`. Recommended + body: Required. The captcha image, base64-encoded (no `data:image/...;base64,` prefix). + comment: Optional. Hint for the worker, e.g. `"click on the green apple"`. Recommended for generic click captchas. - imgInstructions: Optional instruction image, base64-encoded. - minClicks: Minimum number of clicks expected (default `1`). - maxClicks: Maximum number of clicks allowed. + imgInstructions: Optional. Optional instruction image, base64-encoded. + minClicks: Optional. Minimum number of clicks expected (default `1`). + maxClicks: Optional. Maximum number of clicks allowed. Returns (`solution` from `solve()`): `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click, @@ -854,9 +854,9 @@ class TencentTaskProxyless(BaseTask): """Tencent captcha without proxy. Args: - websiteURL: Full URL of the page where the captcha is located. - appId: Value of the `appId` parameter found in the page source. - captchaScript: URL of the Tencent captcha script, if the page uses a + websiteURL: Required. Full URL of the page where the captcha is located. + appId: Required. Value of the `appId` parameter found in the page source. + captchaScript: Optional. URL of the Tencent captcha script, if the page uses a non-default one. Returns (`solution` from `solve()`): @@ -894,14 +894,14 @@ class TencentTask(BaseTask, ProxyMixin): """Tencent captcha solved through your own proxy. Args: - websiteURL: Full URL of the page where the captcha is located. - appId: Value of the `appId` parameter found in the page source. - proxyType: `"http"`, `"socks4"`, or `"socks5"`. - proxyAddress: Proxy IP address or hostname. - proxyPort: Proxy port. - proxyLogin: Proxy auth username, if required. - proxyPassword: Proxy auth password, if required. - captchaScript: URL of the Tencent captcha script, if the page uses a + websiteURL: Required. Full URL of the page where the captcha is located. + appId: Required. Value of the `appId` parameter found in the page source. + proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyAddress: Required. Proxy IP address or hostname. + proxyPort: Required. Proxy port. + proxyLogin: Optional. Proxy auth username, if required. + proxyPassword: Optional. Proxy auth password, if required. + captchaScript: Optional. URL of the Tencent captcha script, if the page uses a non-default one. Returns (`solution` from `solve()`): diff --git a/examples/README.md b/examples/README.md index ec2b645..a7bd7f4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -85,7 +85,7 @@ Async scripts create an `AsyncCaptchaClient` and await the same operations as th - Image examples can use the bundled files in `examples/assets`; they do not require a target website. - Token examples use placeholder site keys and URLs. Replace them with values from a page you are authorized to test. -- `geetest_v3.py` needs a fresh `challenge` for every request; it expires quickly and must not be hardcoded. +- `geetest_v3_*.py` need a fresh `challenge` for every request; it expires quickly and must not be hardcoded. - Files using `*Task` instead of `*TaskProxyless` demonstrate solving through your own proxy. Supply valid proxy settings for your account. - For task parameters and response formats, see the [CAPTCHA type documentation](https://captcha-solver.com/en/docs/captcha-types). - For client methods and errors, see the [main README](../README.md#client-reference). diff --git a/tests/async/test_async_validation.py b/tests/async/test_async_validation.py index f07660a..9db1fcc 100644 --- a/tests/async/test_async_validation.py +++ b/tests/async/test_async_validation.py @@ -8,3 +8,8 @@ async def test_create_task_rejects_non_task_value(): async with AsyncCaptchaClient("api-key") as client: with pytest.raises(ValidationError, match="task must be an instance of BaseTask"): await client.create_task(None) + + +def test_async_client_rejects_whitespace_client_key(): + with pytest.raises(ValidationError, match="client_key is required"): + AsyncCaptchaClient(" ") diff --git a/tests/sync/test_validation.py b/tests/sync/test_validation.py index 2407116..6958a55 100644 --- a/tests/sync/test_validation.py +++ b/tests/sync/test_validation.py @@ -32,3 +32,8 @@ def test_create_task_rejects_non_task_value(): with pytest.raises(ValidationError, match="task must be an instance of BaseTask"): client.create_task(None) + + +def test_client_rejects_whitespace_client_key(): + with pytest.raises(ValidationError, match="client_key is required"): + CaptchaClient(" ") From e16259a075ff273a61d8e83b97e62f0f97d4f713 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Thu, 29 Oct 2026 13:23:43 +0300 Subject: [PATCH 11/23] fix: validate version-specific GeeTest parameters --- captcha_solver_api/tasks.py | 33 ++++++++++++++++++++ tests/async/test_async_validation.py | 30 ++++++++++++++++++ tests/sync/test_geetest.py | 46 +++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 0184719..73c8c60 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -46,6 +46,10 @@ def _validate_required_fields(self) -> None: if missing: fields = ", ".join(missing) raise ValidationError(f"Missing required task parameter(s): {fields}") + self._validate_specific_fields() + + def _validate_specific_fields(self) -> None: + """Validate task-specific requirements that depend on other fields.""" class ProxyMixin: @@ -566,6 +570,29 @@ def __init__( self.imgInstructions = imgInstructions +def _validate_geetest_fields(task: Any) -> None: + """Validate fields whose required status depends on the GeeTest version.""" + version = 3 if task.version is None else task.version + if version not in (3, 4): + raise ValidationError("GeeTest version must be 3 or 4") + + if version == 3: + missing = [ + name + for name in ("gt", "challenge") + if not isinstance(getattr(task, name, None), str) or not getattr(task, name).strip() + ] + if missing: + fields = ", ".join(missing) + raise ValidationError(f"Missing required GeeTest v3 parameter(s): {fields}") + return + + init_parameters = task.initParameters + captcha_id = init_parameters.get("captcha_id") if isinstance(init_parameters, dict) else None + if not isinstance(captcha_id, str) or not captcha_id.strip(): + raise ValidationError("Missing required GeeTest v4 parameter: initParameters.captcha_id") + + class GeeTestTaskProxyless(BaseTask): """GeeTest v3 or v4 without proxy. Set `version=4` for v4 (and provide `initParameters["captcha_id"]`); v3 is the default and needs `gt`/`challenge` @@ -605,6 +632,9 @@ class GeeTestTaskProxyless(BaseTask): type = "GeeTestTaskProxyless" + def _validate_specific_fields(self) -> None: + _validate_geetest_fields(self) + def __init__( self, websiteURL: str, @@ -672,6 +702,9 @@ class GeeTestTask(BaseTask, ProxyMixin): type = "GeeTestTask" + def _validate_specific_fields(self) -> None: + _validate_geetest_fields(self) + def __init__( self, websiteURL: str, diff --git a/tests/async/test_async_validation.py b/tests/async/test_async_validation.py index 9db1fcc..c53bae1 100644 --- a/tests/async/test_async_validation.py +++ b/tests/async/test_async_validation.py @@ -1,6 +1,9 @@ +from unittest.mock import AsyncMock, patch + import pytest from captcha_solver_api import AsyncCaptchaClient, ValidationError +from captcha_solver_api.tasks import GeeTestTaskProxyless @pytest.mark.asyncio @@ -13,3 +16,30 @@ async def test_create_task_rejects_non_task_value(): def test_async_client_rejects_whitespace_client_key(): with pytest.raises(ValidationError, match="client_key is required"): AsyncCaptchaClient(" ") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "task, message", + [ + ( + GeeTestTaskProxyless(websiteURL="https://example.com"), + r"Missing required GeeTest v3 parameter\(s\): gt, challenge", + ), + ( + GeeTestTaskProxyless( + websiteURL="https://example.com", + version=4, + initParameters={}, + ), + r"Missing required GeeTest v4 parameter: initParameters\.captcha_id", + ), + ], +) +async def test_create_task_rejects_invalid_geetest_before_request(task, message): + async with AsyncCaptchaClient("api-key") as client: + with patch.object(client, "_request", new_callable=AsyncMock) as request: + with pytest.raises(ValidationError, match=message): + await client.create_task(task) + + request.assert_not_awaited() diff --git a/tests/sync/test_geetest.py b/tests/sync/test_geetest.py index 3e12023..9467a65 100644 --- a/tests/sync/test_geetest.py +++ b/tests/sync/test_geetest.py @@ -4,11 +4,55 @@ from unittest.mock import patch -from captcha_solver_api import CaptchaClient +import pytest + +from captcha_solver_api import CaptchaClient, ValidationError from captcha_solver_api.tasks import GeeTestTask, GeeTestTaskProxyless class TestGeeTest: + @pytest.mark.parametrize( + "task", + [ + GeeTestTaskProxyless(websiteURL="https://example.com"), + GeeTestTask( + websiteURL="https://example.com", + proxyType="http", + proxyAddress="1.2.3.4", + proxyPort=8080, + ), + ], + ) + def test_v3_requires_gt_and_challenge(self, task): + with pytest.raises( + ValidationError, + match=r"Missing required GeeTest v3 parameter\(s\): gt, challenge", + ): + task.to_dict() + + @pytest.mark.parametrize("init_parameters", [None, {}, {"captcha_id": " "}]) + def test_v4_requires_captcha_id(self, init_parameters): + task = GeeTestTaskProxyless( + websiteURL="https://example.com", + version=4, + initParameters=init_parameters, + ) + + with pytest.raises( + ValidationError, + match=r"Missing required GeeTest v4 parameter: initParameters\.captcha_id", + ): + task.to_dict() + + def test_rejects_unsupported_version(self): + task = GeeTestTaskProxyless( + websiteURL="https://example.com", + version=5, + ) + + with pytest.raises(ValidationError, match="GeeTest version must be 3 or 4"): + task.to_dict() + def test_v3_to_dict(self): task = GeeTestTaskProxyless( websiteURL="https://example.com", From c14ef5aca127ee9c24f90b2f6fe38d0a8f17aa8a Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 00:56:56 +0300 Subject: [PATCH 12/23] docs: align task docs and examples after merging main - document YandexSmartCaptchaImageTask in the shared docstring format - add Yandex SmartCaptcha Image to the supported types table - clarify version-specific GeeTest parameters in docstrings - restore comments in async reCAPTCHA v2 examples - replace em dashes in README Co-Authored-By: Claude Opus 5.5 --- README.md | 5 ++- captcha_solver_api/tasks.py | 54 ++++++++++++++++-------- examples/async/recaptcha_v2_proxy.py | 43 +++++++++++++------ examples/async/recaptcha_v2_proxyless.py | 33 +++++++++++---- 4 files changed, 92 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 92cc533..26708c6 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ on the target page. | GeeTest v4 | ✅ | ✅ | | Image to Text | ✅ | - | | Yandex SmartCaptcha | ✅ | ✅ | +| Yandex SmartCaptcha Image | ✅ | - | | Coordinates (click captcha) | ✅ | - | | Tencent | ✅ | ✅ | @@ -807,8 +808,8 @@ Full API reference: https://captcha-solver.com/en/docs/captcha-types - [Selenium Python examples](https://github.com/captcha-solver-api/captcha-solver-selenium-python-examples) - [JavaScript examples](https://github.com/captcha-solver-api/javascript-examples) - [Tencent CAPTCHA automation examples](https://github.com/captcha-solver-api/How-to-Automate-Tencent-CAPTCHA) -- [Cloudflare Turnstile Puppeteer Demo](https://github.com/captcha-solver-api/cloudflare-turnstile-puppeteer-demo) — a working browser automation example for Cloudflare Turnstile. -- [How to Automate Tencent CAPTCHA](https://captcha-solver.com/en/blog/how-to-automate-tencent-captcha) — a step-by-step guide with Python and JavaScript SDK examples. +- [Cloudflare Turnstile Puppeteer Demo](https://github.com/captcha-solver-api/cloudflare-turnstile-puppeteer-demo) - a working browser automation example for Cloudflare Turnstile. +- [How to Automate Tencent CAPTCHA](https://captcha-solver.com/en/blog/how-to-automate-tencent-captcha) - a step-by-step guide with Python and JavaScript SDK examples. ## License diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 651479d..56d3752 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -600,12 +600,12 @@ class GeeTestTaskProxyless(BaseTask): Args: websiteURL: Required. Full URL of the page where the widget is located. - version: Optional. `3` (default) or `4`. - gt: Optional. Public key of the GeeTest widget. Required for v3. - challenge: Optional. Session-specific challenge value from the page. Required for - v3, and must be freshly fetched for every request -- it cannot be reused. - initParameters: Optional. Extra parameters from the page's `initGeetest` call. For - v4, must contain `captcha_id`. + version: Optional. `3` (default) or `4`. Must be `4` for GeeTest v4. + gt: Required for v3. Public key of the GeeTest widget. + challenge: Required for v3. Session-specific challenge value from the page. Must be + freshly fetched for every request -- it cannot be reused. + initParameters: Required for v4. Parameters from the page's `initGeetest` call; + must contain `captcha_id`. Optional for v3. geetestApiServerSubdomain: Optional. Custom GeeTest API subdomain, if the site uses one. userAgent: Optional. User-Agent to solve with. risk_type: Optional. Value of the `risk_type` parameter from the captcha-loading @@ -667,12 +667,12 @@ class GeeTestTask(BaseTask, ProxyMixin): proxyPort: Required. Proxy port. proxyLogin: Optional. Proxy auth username, if required. proxyPassword: Optional. Proxy auth password, if required. - version: Optional. `3` (default) or `4`. - gt: Optional. Public key of the GeeTest widget. Required for v3. - challenge: Optional. Session-specific challenge value from the page, must be fresh. - Required for v3. - initParameters: Optional. Extra parameters from the page's `initGeetest` call. For - v4, must contain `captcha_id`. + version: Optional. `3` (default) or `4`. Must be `4` for GeeTest v4. + gt: Required for v3. Public key of the GeeTest widget. + challenge: Required for v3. Session-specific challenge value from the page, must be + fresh. + initParameters: Required for v4. Parameters from the page's `initGeetest` call; + must contain `captcha_id`. Optional for v3. geetestApiServerSubdomain: Optional. Custom GeeTest API subdomain, if the site uses one. userAgent: Optional. User-Agent to solve with. risk_type: Optional. Value of the `risk_type` parameter from the captcha-loading @@ -884,15 +884,33 @@ def __init__( class YandexSmartCaptchaImageTask(BaseTask): - """Image-based Yandex SmartCaptcha using API v2 ``SmartCaptchaTask``. + """Image-based Yandex SmartCaptcha, sent as API type `SmartCaptchaTask`. No proxy + variant exists for this type -- the images are submitted directly. Args: - image: Main SmartCaptcha challenge image, base64-encoded. - imgInstructions: Click-order instruction image, base64-encoded. - comment: Optional text hint for the worker. + image: Required. Main SmartCaptcha challenge image, base64-encoded. + imgInstructions: Required. Click-order instruction image, base64-encoded. + comment: Optional. Text hint for the worker, e.g. `"click the symbols in order"`. - Returns (``solution`` from ``solve()``): - ``coordinates`` -- an ordered list of ``{"x": int, "y": int}`` points. + Returns (`solution` from `solve()`): + `coordinates` -- an ordered list of `{"x": int, "y": int}` points to click. + + Example: + import base64 + + from captcha_solver_api.tasks import YandexSmartCaptchaImageTask + + with open("captcha.png", "rb") as f: + image = base64.b64encode(f.read()).decode("utf-8") + with open("instructions.png", "rb") as f: + instructions = base64.b64encode(f.read()).decode("utf-8") + + task = YandexSmartCaptchaImageTask(image=image, imgInstructions=instructions) + solution = client.solve(task) + points = solution["coordinates"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "SmartCaptchaTask" diff --git a/examples/async/recaptcha_v2_proxy.py b/examples/async/recaptcha_v2_proxy.py index 0eaf97c..38d70e4 100644 --- a/examples/async/recaptcha_v2_proxy.py +++ b/examples/async/recaptcha_v2_proxy.py @@ -1,4 +1,11 @@ -"""Example: Solve reCAPTCHA v2 through your own proxy using the async client.""" +""" +Example: Solve a reCAPTCHA v2 challenge through your own proxy with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. + Replace the proxy placeholders with your own proxy settings. +""" import asyncio import os @@ -20,25 +27,33 @@ from captcha_solver_api.tasks import RecaptchaV2Task api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Optional: timeout (max seconds to wait for solution, default 120) +# Optional: polling_interval (seconds before and between status checks, default 10) solver = AsyncCaptchaClient(api_key) async def main(): - task = RecaptchaV2Task( - websiteURL="https://example.com/login", - websiteKey="YOUR_WEBSITE_KEY", - proxyType="http", - proxyAddress="1.2.3.4", - proxyPort=8080, - proxyLogin="user", - proxyPassword="password", - ) + # --- With proxy example --- + # Solves reCAPTCHA v2 through your own proxy. + # Use when the target site is geo-restricted or you need session consistency. try: - result = await solver.solve(task) - except Exception as exc: - sys.exit(str(exc)) - else: + result = await solver.solve( + RecaptchaV2Task( + websiteURL="https://example.com/login", # Full URL of the page with captcha + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) + ) + # Solution contains the same gRecaptchaResponse token. print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_proxyless.py b/examples/async/recaptcha_v2_proxyless.py index 3206993..ed7bc76 100644 --- a/examples/async/recaptcha_v2_proxyless.py +++ b/examples/async/recaptcha_v2_proxyless.py @@ -1,4 +1,10 @@ -"""Example: Solve reCAPTCHA v2 without your own proxy using the async client.""" +""" +Example: Solve a reCAPTCHA v2 challenge without a proxy with the async client. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + Replace websiteURL and websiteKey with values from your target page. +""" import asyncio import os @@ -20,20 +26,29 @@ from captcha_solver_api.tasks import RecaptchaV2TaskProxyless api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + +# Optional: timeout (max seconds to wait for solution, default 120) +# Optional: polling_interval (seconds before and between status checks, default 10) solver = AsyncCaptchaClient(api_key) async def main(): - task = RecaptchaV2TaskProxyless( - websiteURL="https://example.com/login", - websiteKey="YOUR_WEBSITE_KEY", - ) + # --- Proxyless example --- + # Solves reCAPTCHA v2 without a proxy. The service uses its own IP addresses. try: - result = await solver.solve(task) - except Exception as exc: - sys.exit(str(exc)) - else: + # solve() handles task creation, polling, and returns the solution dict. + result = await solver.solve( + RecaptchaV2TaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with the captcha + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page + isInvisible=False, # Set True for invisible reCAPTCHA + ) + ) + # Solution contains {"gRecaptchaResponse": "03AGdBq..."} + # Pass this token to the g-recaptcha-response field or widget callback. print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) From b6d324eaa9b4aafe7160d0df7bd2fb040a902098 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:02:15 +0300 Subject: [PATCH 13/23] docs: close the async client in every async example Use `async with AsyncCaptchaClient(...)` so each example closes its HTTP connection pool, as recommended in the README. Co-Authored-By: Claude Opus 5.5 --- examples/async/balance.py | 17 ++-- examples/async/coordinates.py | 85 +++++++++---------- examples/async/geetest_v3_proxy.py | 53 ++++++------ examples/async/geetest_v3_proxyless.py | 55 ++++++------ examples/async/geetest_v4_proxy.py | 45 +++++----- examples/async/geetest_v4_proxyless.py | 43 +++++----- examples/async/image_to_text.py | 29 +++---- .../async/recaptcha_v2_enterprise_proxy.py | 51 ++++++----- .../recaptcha_v2_enterprise_proxyless.py | 47 +++++----- examples/async/recaptcha_v2_proxy.py | 45 +++++----- examples/async/recaptcha_v2_proxyless.py | 37 ++++---- examples/async/recaptcha_v3.py | 43 +++++----- examples/async/tencent_proxy.py | 43 +++++----- examples/async/tencent_proxyless.py | 37 ++++---- examples/async/turnstile_proxy.py | 47 +++++----- examples/async/turnstile_proxyless.py | 43 +++++----- examples/async/yandex_smartcaptcha_proxy.py | 47 +++++----- .../async/yandex_smartcaptcha_proxyless.py | 39 +++++---- 18 files changed, 394 insertions(+), 412 deletions(-) diff --git a/examples/async/balance.py b/examples/async/balance.py index 01a670a..3c81311 100644 --- a/examples/async/balance.py +++ b/examples/async/balance.py @@ -32,17 +32,16 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # Get the current account balance. - # Returns a float with the available amount in your account currency. - try: - balance = await solver.get_balance() - print("Balance: " + str(balance)) - except Exception as e: - sys.exit(e) + async with AsyncCaptchaClient(api_key) as solver: + # Get the current account balance. + # Returns a float with the available amount in your account currency. + try: + balance = await solver.get_balance() + print("Balance: " + str(balance)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/coordinates.py b/examples/async/coordinates.py index 1814002..70663b6 100644 --- a/examples/async/coordinates.py +++ b/examples/async/coordinates.py @@ -31,57 +31,56 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Basic example --- - # Solves a simple click-based captcha (examples/assets/fruit-click.png) with - # a hint for the worker. The worker will click on the specified points on the image. - try: - # Read and encode the captcha image to base64. - # The body must be a pure base64 string without the data:image/...;base64, prefix. - with open(os.path.join(assets_dir, "fruit-click.png"), "rb") as f: - body = b64encode(f.read()).decode("utf-8") + async with AsyncCaptchaClient(api_key) as solver: + # --- Basic example --- + # Solves a simple click-based captcha (examples/assets/fruit-click.png) with + # a hint for the worker. The worker will click on the specified points on the image. + try: + # Read and encode the captcha image to base64. + # The body must be a pure base64 string without the data:image/...;base64, prefix. + with open(os.path.join(assets_dir, "fruit-click.png"), "rb") as f: + body = b64encode(f.read()).decode("utf-8") - result = await solver.solve( - CoordinatesTask( - body=body, # Base64-encoded captcha image (required) - comment="click on the green apple", # Text hint for the worker + result = await solver.solve( + CoordinatesTask( + body=body, # Base64-encoded captcha image (required) + comment="click on the green apple", # Text hint for the worker + ) ) - ) - # Solution contains {"coordinates": [{"x": 140, "y": 110}]} - # Click on each coordinate in order. Coordinates are pixel positions. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"coordinates": [{"x": 140, "y": 110}]} + # Click on each coordinate in order. Coordinates are pixel positions. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) - # --- Advanced example --- - # Solves a captcha (examples/assets/traffic-lights.png) with instruction image - # and click count limits. - try: - with open(os.path.join(assets_dir, "traffic-lights.png"), "rb") as f: - body = b64encode(f.read()).decode("utf-8") + # --- Advanced example --- + # Solves a captcha (examples/assets/traffic-lights.png) with instruction image + # and click count limits. + try: + with open(os.path.join(assets_dir, "traffic-lights.png"), "rb") as f: + body = b64encode(f.read()).decode("utf-8") - # Read and encode an optional instruction image. - # This image helps the worker understand what to click. - with open(os.path.join(assets_dir, "traffic-lights-instructions.png"), "rb") as f: - img_instructions = b64encode(f.read()).decode("utf-8") + # Read and encode an optional instruction image. + # This image helps the worker understand what to click. + with open(os.path.join(assets_dir, "traffic-lights-instructions.png"), "rb") as f: + img_instructions = b64encode(f.read()).decode("utf-8") - result = await solver.solve( - CoordinatesTask( - body=body, # Base64-encoded captcha image - comment="click on all traffic lights", # Text hint for the worker - imgInstructions=img_instructions, # Optional instruction image - minClicks=1, # Minimum number of clicks (default 1) - maxClicks=3, # Maximum number of clicks allowed + result = await solver.solve( + CoordinatesTask( + body=body, # Base64-encoded captcha image + comment="click on all traffic lights", # Text hint for the worker + imgInstructions=img_instructions, # Optional instruction image + minClicks=1, # Minimum number of clicks (default 1) + maxClicks=3, # Maximum number of clicks allowed + ) ) - ) - # Solution contains coordinates for all requested clicks, e.g. - # {"coordinates": [{"x": 110, "y": 150}, {"x": 430, "y": 130}]} - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains coordinates for all requested clicks, e.g. + # {"coordinates": [{"x": 110, "y": 150}, {"x": 430, "y": 130}]} + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/geetest_v3_proxy.py b/examples/async/geetest_v3_proxy.py index 1b55cf2..ce6bc1e 100644 --- a/examples/async/geetest_v3_proxy.py +++ b/examples/async/geetest_v3_proxy.py @@ -35,9 +35,6 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# GeeTest tasks may take longer. Increase timeout if needed. -solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) - async def main(): """ @@ -45,32 +42,34 @@ async def main(): For each request to the API you need to get a new value from the target page. Below is an example of fetching it from a demo endpoint. """ - # Fetch a fresh challenge value from the target page. - # In production, extract this from the page's initGeetest call or network requests. - # "target-site.com" is a placeholder -- point this at your real target before running. - async with httpx.AsyncClient() as client: - resp = await client.get("https://target-site.com/path/to/geetest/init", timeout=30) - challenge = resp.json()["challenge"] + # GeeTest tasks may take longer. Increase timeout if needed. + async with AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) as solver: + # Fetch a fresh challenge value from the target page. + # In production, extract this from the page's initGeetest call or network requests. + # "target-site.com" is a placeholder -- point this at your real target before running. + async with httpx.AsyncClient() as client: + resp = await client.get("https://target-site.com/path/to/geetest/init", timeout=30) + challenge = resp.json()["challenge"] - # --- With proxy example --- - # Solves GeeTest v3 through your own proxy. - try: - result = await solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with GeeTest - gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget - challenge=challenge, # Session-specific value, must be fresh - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) + # --- With proxy example --- + # Solves GeeTest v3 through your own proxy. + try: + result = await solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with GeeTest + gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget + challenge=challenge, # Session-specific value, must be fresh + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/geetest_v3_proxyless.py b/examples/async/geetest_v3_proxyless.py index 1759891..1d9ffec 100644 --- a/examples/async/geetest_v3_proxyless.py +++ b/examples/async/geetest_v3_proxyless.py @@ -35,9 +35,6 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# GeeTest tasks may take longer. Increase timeout if needed. -solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) - async def main(): """ @@ -45,33 +42,35 @@ async def main(): For each request to the API you need to get a new value from the target page. Below is an example of fetching it from a demo endpoint. """ - # Fetch a fresh challenge value from the target page. - # In production, extract this from the page's initGeetest call or network requests. - # "target-site.com" is a placeholder -- point this at your real target before running. - async with httpx.AsyncClient() as client: - resp = await client.get("https://target-site.com/path/to/geetest/init", timeout=30) - challenge = resp.json()["challenge"] + # GeeTest tasks may take longer. Increase timeout if needed. + async with AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) as solver: + # Fetch a fresh challenge value from the target page. + # In production, extract this from the page's initGeetest call or network requests. + # "target-site.com" is a placeholder -- point this at your real target before running. + async with httpx.AsyncClient() as client: + resp = await client.get("https://target-site.com/path/to/geetest/init", timeout=30) + challenge = resp.json()["challenge"] - # --- Proxyless example --- - # Solves GeeTest v3 without a proxy. - # v3 is the default version, so the version field can be omitted. - try: - result = await solver.solve( - GeeTestTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with GeeTest - gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget - challenge=challenge, # Session-specific value, must be fresh - # Optional fields - # geetestApiServerSubdomain='api-na.geetest.com', # Custom API subdomain - # initParameters={...}, # Extra params from initGeetest call - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # --- Proxyless example --- + # Solves GeeTest v3 without a proxy. + # v3 is the default version, so the version field can be omitted. + try: + result = await solver.solve( + GeeTestTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with GeeTest + gt="f2ae6cadcf7886856696c46d84d109d1", # Public key of the GeeTest widget + challenge=challenge, # Session-specific value, must be fresh + # Optional fields + # geetestApiServerSubdomain='api-na.geetest.com', # Custom API subdomain + # initParameters={...}, # Extra params from initGeetest call + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + ) ) - ) - # Solution contains {"challenge": "...", "validate": "...", "seccode": "..."} - # Pass solution.validate and solution.seccode to the page's GeeTest callback. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"challenge": "...", "validate": "...", "seccode": "..."} + # Pass solution.validate and solution.seccode to the page's GeeTest callback. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/geetest_v4_proxy.py b/examples/async/geetest_v4_proxy.py index 5d9dad3..506af84 100644 --- a/examples/async/geetest_v4_proxy.py +++ b/examples/async/geetest_v4_proxy.py @@ -30,32 +30,31 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# GeeTest v4 tasks may take longer. Increase timeout if needed. -solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) - async def main(): - # --- With proxy example --- - # Solves GeeTest v4 through your own proxy. - try: - result = await solver.solve( - GeeTestTask( - websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget - version=4, # Required: must be 4 for this version - initParameters={ # Required: must contain captcha_id - "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page - }, - # --- Proxy parameters --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) + # GeeTest v4 tasks may take longer. Increase timeout if needed. + async with AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) as solver: + # --- With proxy example --- + # Solves GeeTest v4 through your own proxy. + try: + result = await solver.solve( + GeeTestTask( + websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget + version=4, # Required: must be 4 for this version + initParameters={ # Required: must contain captcha_id + "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page + }, + # --- Proxy parameters --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/geetest_v4_proxyless.py b/examples/async/geetest_v4_proxyless.py index 8a91357..5668b8d 100644 --- a/examples/async/geetest_v4_proxyless.py +++ b/examples/async/geetest_v4_proxyless.py @@ -30,31 +30,30 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# GeeTest v4 tasks may take longer. Increase timeout if needed. -solver = AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) - async def main(): - # --- Proxyless example --- - # Solves GeeTest v4 without a proxy. - # v4 drops gt/challenge. The widget is identified by captcha_id inside initParameters. - try: - result = await solver.solve( - GeeTestTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget - version=4, # Required: must be 4 for this version - initParameters={ # Required: must contain captcha_id - "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page - }, - # Optional fields - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # GeeTest v4 tasks may take longer. Increase timeout if needed. + async with AsyncCaptchaClient(api_key, timeout=300, polling_interval=10) as solver: + # --- Proxyless example --- + # Solves GeeTest v4 without a proxy. + # v4 drops gt/challenge. The widget is identified by captcha_id inside initParameters. + try: + result = await solver.solve( + GeeTestTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with the GeeTest widget + version=4, # Required: must be 4 for this version + initParameters={ # Required: must contain captcha_id + "captcha_id": "YOUR_CAPTCHA_ID", # Static site identifier, from that page + }, + # Optional fields + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + ) ) - ) - # Solution contains {"captcha_id": "...", "lot_number": "...", "pass_token": "...", "gen_time": "...", "captcha_output": "..."} - # Pass these values together into the page's GeeTest v4 callback as-is. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"captcha_id": "...", "lot_number": "...", "pass_token": "...", "gen_time": "...", "captcha_output": "..."} + # Pass these values together into the page's GeeTest v4 callback as-is. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/image_to_text.py b/examples/async/image_to_text.py index 189a083..a5ccd53 100644 --- a/examples/async/image_to_text.py +++ b/examples/async/image_to_text.py @@ -29,23 +29,22 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # Base64 body must not include the data:image/...;base64, prefix. - with open(os.path.join(assets_dir, "captcha-digits.png"), "rb") as f: - body = b64encode(f.read()).decode("utf-8") - - task = ImageToTextTask(body=body, numeric=1, minLength=4, maxLength=6) - - try: - result = await solver.solve(task) - except Exception as e: - sys.exit(str(e)) - else: - # Solution contains {"text": "58204"} - print("result: " + str(result)) + async with AsyncCaptchaClient(api_key) as solver: + # Base64 body must not include the data:image/...;base64, prefix. + with open(os.path.join(assets_dir, "captcha-digits.png"), "rb") as f: + body = b64encode(f.read()).decode("utf-8") + + task = ImageToTextTask(body=body, numeric=1, minLength=4, maxLength=6) + + try: + result = await solver.solve(task) + except Exception as e: + sys.exit(str(e)) + else: + # Solution contains {"text": "58204"} + print("result: " + str(result)) asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise_proxy.py b/examples/async/recaptcha_v2_enterprise_proxy.py index 23ba4a3..4389924 100644 --- a/examples/async/recaptcha_v2_enterprise_proxy.py +++ b/examples/async/recaptcha_v2_enterprise_proxy.py @@ -28,35 +28,34 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- With proxy example --- - # Solves reCAPTCHA v2 Enterprise through your own proxy. - # Use when the target site is geo-restricted or you need a consistent session. - try: - result = await solver.solve( - RecaptchaV2EnterpriseTask( - websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - isInvisible=False, - # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() - userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent - cookies="foo=bar; baz=1", # Session cookies if needed + async with AsyncCaptchaClient(api_key) as solver: + # --- With proxy example --- + # Solves reCAPTCHA v2 Enterprise through your own proxy. + # Use when the target site is geo-restricted or you need a consistent session. + try: + result = await solver.solve( + RecaptchaV2EnterpriseTask( + websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + isInvisible=False, + # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() + userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", # Browser User-Agent + cookies="foo=bar; baz=1", # Session cookies if needed + ) ) - ) - # Solution contains the same gRecaptchaResponse token. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains the same gRecaptchaResponse token. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise_proxyless.py b/examples/async/recaptcha_v2_enterprise_proxyless.py index 133e94a..10d157f 100644 --- a/examples/async/recaptcha_v2_enterprise_proxyless.py +++ b/examples/async/recaptcha_v2_enterprise_proxyless.py @@ -28,33 +28,32 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Proxyless example --- - # Solves reCAPTCHA v2 Enterprise without a proxy. - # Enterprise captchas are loaded via the reCAPTCHA Enterprise API. - # If the site passes extra parameters to grecaptcha.enterprise.render(), - # you must pass them as enterprisePayload or the token will be rejected. - try: - result = await solver.solve( - RecaptchaV2EnterpriseTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - isInvisible=False, # Set True for invisible reCAPTCHA - # Optional fields (pass only if the target site requires them) - # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() - # apiDomain='recaptcha.net', # Set if site loads captcha from recaptcha.net - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent - # cookies='session=abc123; token=xyz789', # Session cookies if needed + async with AsyncCaptchaClient(api_key) as solver: + # --- Proxyless example --- + # Solves reCAPTCHA v2 Enterprise without a proxy. + # Enterprise captchas are loaded via the reCAPTCHA Enterprise API. + # If the site passes extra parameters to grecaptcha.enterprise.render(), + # you must pass them as enterprisePayload or the token will be rejected. + try: + result = await solver.solve( + RecaptchaV2EnterpriseTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the Enterprise-protected page + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + isInvisible=False, # Set True for invisible reCAPTCHA + # Optional fields (pass only if the target site requires them) + # enterprisePayload={'s': 'value-from-page'}, # Extra params from grecaptcha.enterprise.render() + # apiDomain='recaptcha.net', # Set if site loads captcha from recaptcha.net + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # cookies='session=abc123; token=xyz789', # Session cookies if needed + ) ) - ) - # Solution contains {"gRecaptchaResponse": "03AGdBq..."} - # Pass this token to the g-recaptcha-response field or widget callback. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"gRecaptchaResponse": "03AGdBq..."} + # Pass this token to the g-recaptcha-response field or widget callback. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_proxy.py b/examples/async/recaptcha_v2_proxy.py index 38d70e4..6cd87d9 100644 --- a/examples/async/recaptcha_v2_proxy.py +++ b/examples/async/recaptcha_v2_proxy.py @@ -28,32 +28,31 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# Optional: timeout (max seconds to wait for solution, default 120) -# Optional: polling_interval (seconds before and between status checks, default 10) -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- With proxy example --- - # Solves reCAPTCHA v2 through your own proxy. - # Use when the target site is geo-restricted or you need session consistency. - try: - result = await solver.solve( - RecaptchaV2Task( - websiteURL="https://example.com/login", # Full URL of the page with captcha - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) + # Optional: timeout (max seconds to wait for solution, default 120) + # Optional: polling_interval (seconds before and between status checks, default 10) + async with AsyncCaptchaClient(api_key) as solver: + # --- With proxy example --- + # Solves reCAPTCHA v2 through your own proxy. + # Use when the target site is geo-restricted or you need session consistency. + try: + result = await solver.solve( + RecaptchaV2Task( + websiteURL="https://example.com/login", # Full URL of the page with captcha + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + ) ) - ) - # Solution contains the same gRecaptchaResponse token. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains the same gRecaptchaResponse token. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_proxyless.py b/examples/async/recaptcha_v2_proxyless.py index ed7bc76..01d2626 100644 --- a/examples/async/recaptcha_v2_proxyless.py +++ b/examples/async/recaptcha_v2_proxyless.py @@ -27,28 +27,27 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# Optional: timeout (max seconds to wait for solution, default 120) -# Optional: polling_interval (seconds before and between status checks, default 10) -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Proxyless example --- - # Solves reCAPTCHA v2 without a proxy. The service uses its own IP addresses. - try: - # solve() handles task creation, polling, and returns the solution dict. - result = await solver.solve( - RecaptchaV2TaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with the captcha - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page - isInvisible=False, # Set True for invisible reCAPTCHA + # Optional: timeout (max seconds to wait for solution, default 120) + # Optional: polling_interval (seconds before and between status checks, default 10) + async with AsyncCaptchaClient(api_key) as solver: + # --- Proxyless example --- + # Solves reCAPTCHA v2 without a proxy. The service uses its own IP addresses. + try: + # solve() handles task creation, polling, and returns the solution dict. + result = await solver.solve( + RecaptchaV2TaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with the captcha + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value on that page + isInvisible=False, # Set True for invisible reCAPTCHA + ) ) - ) - # Solution contains {"gRecaptchaResponse": "03AGdBq..."} - # Pass this token to the g-recaptcha-response field or widget callback. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"gRecaptchaResponse": "03AGdBq..."} + # Pass this token to the g-recaptcha-response field or widget callback. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/recaptcha_v3.py b/examples/async/recaptcha_v3.py index 8af759b..d4bb2a7 100644 --- a/examples/async/recaptcha_v3.py +++ b/examples/async/recaptcha_v3.py @@ -29,31 +29,30 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -# reCAPTCHA v3 tasks may take longer to solve. Increase timeout if needed. -solver = AsyncCaptchaClient(api_key, timeout=180) - async def main(): - # reCAPTCHA v3 returns a score instead of a pass/fail challenge. - # The higher the minScore you request, the harder and longer the task takes. - # minScore values: 0.3 (fastest), 0.7 (balanced), 0.9 (highest, slowest). - try: - result = await solver.solve( - RecaptchaV3TaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with the v3 widget - websiteKey="YOUR_WEBSITE_KEY", # Site key of the v3 widget on that page - minScore=0.3, # Minimum acceptable score (0.3, 0.7, or 0.9) - # Optional fields (pass if the site uses them, increases token acceptance) - pageAction="homepage", # Action set by that page in grecaptcha.execute() - # isEnterprise=True, # Set True for reCAPTCHA v3 Enterprise - # apiDomain='www.recaptcha.net', # Set if site loads from recaptcha.net + # reCAPTCHA v3 tasks may take longer to solve. Increase timeout if needed. + async with AsyncCaptchaClient(api_key, timeout=180) as solver: + # reCAPTCHA v3 returns a score instead of a pass/fail challenge. + # The higher the minScore you request, the harder and longer the task takes. + # minScore values: 0.3 (fastest), 0.7 (balanced), 0.9 (highest, slowest). + try: + result = await solver.solve( + RecaptchaV3TaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with the v3 widget + websiteKey="YOUR_WEBSITE_KEY", # Site key of the v3 widget on that page + minScore=0.3, # Minimum acceptable score (0.3, 0.7, or 0.9) + # Optional fields (pass if the site uses them, increases token acceptance) + pageAction="homepage", # Action set by that page in grecaptcha.execute() + # isEnterprise=True, # Set True for reCAPTCHA v3 Enterprise + # apiDomain='www.recaptcha.net', # Set if site loads from recaptcha.net + ) ) - ) - # Solution contains {"gRecaptchaResponse": "03AGdBq..."} - # Pass this token to the g-recaptcha-response field or grecaptcha callback. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"gRecaptchaResponse": "03AGdBq..."} + # Pass this token to the g-recaptcha-response field or grecaptcha callback. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/tencent_proxy.py b/examples/async/tencent_proxy.py index fa176b2..c92644e 100644 --- a/examples/async/tencent_proxy.py +++ b/examples/async/tencent_proxy.py @@ -28,31 +28,30 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- With proxy example --- - # Solves Tencent captcha through your own proxy. - # Use when the target site is geo-restricted or you need a consistent session. - try: - result = await solver.solve( - TencentTask( - websiteURL="https://example.com/register", # Full URL of the page with captcha - appId="YOUR_APP_ID", # appId from page source code (required) - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", + async with AsyncCaptchaClient(api_key) as solver: + # --- With proxy example --- + # Solves Tencent captcha through your own proxy. + # Use when the target site is geo-restricted or you need a consistent session. + try: + result = await solver.solve( + TencentTask( + websiteURL="https://example.com/register", # Full URL of the page with captcha + appId="YOUR_APP_ID", # appId from page source code (required) + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", + ) ) - ) - # Solution contains the same appid, ret, ticket, and randstr values. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains the same appid, ret, ticket, and randstr values. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/tencent_proxyless.py b/examples/async/tencent_proxyless.py index f260fd3..a840450 100644 --- a/examples/async/tencent_proxyless.py +++ b/examples/async/tencent_proxyless.py @@ -28,28 +28,27 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Proxyless example --- - # Solves Tencent captcha without a proxy. - # The service's own proxies are used to solve the captcha. - # appId is found in the page source code. captchaScript is optional if the site uses the default. - try: - result = await solver.solve( - TencentTaskProxyless( - websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha - appId="YOUR_APP_ID", # appId from page source code (required) - # Optional fields: - captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default + async with AsyncCaptchaClient(api_key) as solver: + # --- Proxyless example --- + # Solves Tencent captcha without a proxy. + # The service's own proxies are used to solve the captcha. + # appId is found in the page source code. captchaScript is optional if the site uses the default. + try: + result = await solver.solve( + TencentTaskProxyless( + websiteURL="https://example.com/register", # Full URL of the page using Tencent captcha + appId="YOUR_APP_ID", # appId from page source code (required) + # Optional fields: + captchaScript="https://captchacdn.tencentcloudcs.com/TCaptcha-global.js", # Custom script URL if non-default + ) ) - ) - # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} - # Pass all four values together into the page's captcha callback as-is. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"appid": "...", "ret": 0, "ticket": "...", "randstr": "..."} + # Pass all four values together into the page's captcha callback as-is. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/turnstile_proxy.py b/examples/async/turnstile_proxy.py index a88b3b6..f46b797 100644 --- a/examples/async/turnstile_proxy.py +++ b/examples/async/turnstile_proxy.py @@ -28,33 +28,32 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- With proxy example --- - # Solves Cloudflare Turnstile through your own proxy. - try: - result = await solver.solve( - TurnstileTask( - websiteURL="https://example.com/login", # Full URL of the page with Turnstile - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, socks4, or socks5 - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # --- Optional fields --- - # action='login', - # data='custom-cdata-value', - # pagedata='chl-page-data-value', - # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', + async with AsyncCaptchaClient(api_key) as solver: + # --- With proxy example --- + # Solves Cloudflare Turnstile through your own proxy. + try: + result = await solver.solve( + TurnstileTask( + websiteURL="https://example.com/login", # Full URL of the page with Turnstile + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, socks4, or socks5 + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # --- Optional fields --- + # action='login', + # data='custom-cdata-value', + # pagedata='chl-page-data-value', + # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', + ) ) - ) - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/turnstile_proxyless.py b/examples/async/turnstile_proxyless.py index d59e8d4..76dd7f2 100644 --- a/examples/async/turnstile_proxyless.py +++ b/examples/async/turnstile_proxyless.py @@ -28,31 +28,30 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Proxyless example --- - # Solves Cloudflare Turnstile without a proxy. - # The token is tied to the User-Agent. If you pass userAgent, use the same - # User-Agent in your browser or bot when submitting the token. - try: - result = await solver.solve( - TurnstileTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page with a Turnstile widget - websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value - # Optional fields (pass only if the target site sets them) - # action='login', # Value of data-action attribute - # data='custom-cdata-value', # Value of data-cdata attribute - # pagedata='chl-page-data-value', # Value of chlPageData parameter - # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', # Must match the browser submitting the token + async with AsyncCaptchaClient(api_key) as solver: + # --- Proxyless example --- + # Solves Cloudflare Turnstile without a proxy. + # The token is tied to the User-Agent. If you pass userAgent, use the same + # User-Agent in your browser or bot when submitting the token. + try: + result = await solver.solve( + TurnstileTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page with a Turnstile widget + websiteKey="YOUR_WEBSITE_KEY", # data-sitekey attribute value + # Optional fields (pass only if the target site sets them) + # action='login', # Value of data-action attribute + # data='custom-cdata-value', # Value of data-cdata attribute + # pagedata='chl-page-data-value', # Value of chlPageData parameter + # userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', # Must match the browser submitting the token + ) ) - ) - # Solution contains {"token": "0.zxcv..."} - # Pass this token to the widget callback or cf-turnstile-response field. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"token": "0.zxcv..."} + # Pass this token to the widget callback or cf-turnstile-response field. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha_proxy.py b/examples/async/yandex_smartcaptcha_proxy.py index e9bfe65..5cae19b 100644 --- a/examples/async/yandex_smartcaptcha_proxy.py +++ b/examples/async/yandex_smartcaptcha_proxy.py @@ -27,33 +27,32 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- With proxy example --- - # Solves Yandex SmartCaptcha through your own proxy. - # Note: this is the only captcha type where an https proxy is accepted. - try: - result = await solver.solve( - YandexSmartCaptchaTask( - websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha - websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page - # --- Proxy parameters (replace with your own -- these are placeholders) --- - proxyType="http", # http, https, socks4, or socks5 (https is accepted only for this type) - proxyAddress="1.2.3.4", # Proxy IP address - proxyPort=8080, # Proxy port - proxyLogin="user", # Login for proxy authorization (optional) - proxyPassword="password", # Password for proxy authorization (optional) - # Optional fields: - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent - # cookies='session=abc123; token=xyz789', # Session cookies if needed + async with AsyncCaptchaClient(api_key) as solver: + # --- With proxy example --- + # Solves Yandex SmartCaptcha through your own proxy. + # Note: this is the only captcha type where an https proxy is accepted. + try: + result = await solver.solve( + YandexSmartCaptchaTask( + websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha + websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page + # --- Proxy parameters (replace with your own -- these are placeholders) --- + proxyType="http", # http, https, socks4, or socks5 (https is accepted only for this type) + proxyAddress="1.2.3.4", # Proxy IP address + proxyPort=8080, # Proxy port + proxyLogin="user", # Login for proxy authorization (optional) + proxyPassword="password", # Password for proxy authorization (optional) + # Optional fields: + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # cookies='session=abc123; token=xyz789', # Session cookies if needed + ) ) - ) - # Solution contains the same token. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains the same token. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha_proxyless.py b/examples/async/yandex_smartcaptcha_proxyless.py index 8e980d6..9537797 100644 --- a/examples/async/yandex_smartcaptcha_proxyless.py +++ b/examples/async/yandex_smartcaptcha_proxyless.py @@ -27,29 +27,28 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(api_key) - async def main(): - # --- Proxyless example --- - # Solves Yandex SmartCaptcha without a proxy. - # The service's own proxies are used to solve the captcha. - # websiteKey is the sitekey value from the page code or captcha iframe. - try: - result = await solver.solve( - YandexSmartCaptchaTaskProxyless( - websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha - websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page - # Optional fields: - # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent - # cookies='session=abc123; token=xyz789', # Session cookies if needed + async with AsyncCaptchaClient(api_key) as solver: + # --- Proxyless example --- + # Solves Yandex SmartCaptcha without a proxy. + # The service's own proxies are used to solve the captcha. + # websiteKey is the sitekey value from the page code or captcha iframe. + try: + result = await solver.solve( + YandexSmartCaptchaTaskProxyless( + websiteURL="https://example.com/login", # Full URL of the page using SmartCaptcha + websiteKey="YOUR_WEBSITE_KEY", # sitekey from that page + # Optional fields: + # userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', # Browser User-Agent + # cookies='session=abc123; token=xyz789', # Session cookies if needed + ) ) - ) - # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} - # Use solution.token in the smart-token field or pass to your site's backend. - print("result: " + str(result)) - except Exception as e: - sys.exit(e) + # Solution contains {"token": "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..."} + # Use solution.token in the smart-token field or pass to your site's backend. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) From b7927e9fb1592a8c98499a4f9bc74d78384b45a7 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:02:32 +0300 Subject: [PATCH 14/23] docs: expand async Image to Text example Show the same basic, advanced, and language pool scenarios as the sync example, with comments for each task parameter. Co-Authored-By: Claude Opus 5.5 --- examples/async/image_to_text.py | 70 ++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/examples/async/image_to_text.py b/examples/async/image_to_text.py index a5ccd53..db6fb9a 100644 --- a/examples/async/image_to_text.py +++ b/examples/async/image_to_text.py @@ -3,7 +3,7 @@ Prerequisites: Set the CAPTCHA_API_KEY environment variable. - Ready to run as-is: sample captcha image is bundled in examples/assets/. + Ready to run as-is: sample captcha images are bundled in examples/assets/. """ import asyncio @@ -30,21 +30,73 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") +def encode_image(filename): + """Read an image from examples/assets and return it as a base64 string. + + The body must be a pure base64 string without the data:image/...;base64, prefix. + """ + with open(os.path.join(assets_dir, filename), "rb") as f: + return b64encode(f.read()).decode("utf-8") + + async def main(): + # Image to Text tasks are usually fast. Default timeout is fine. async with AsyncCaptchaClient(api_key) as solver: - # Base64 body must not include the data:image/...;base64, prefix. - with open(os.path.join(assets_dir, "captcha-digits.png"), "rb") as f: - body = b64encode(f.read()).decode("utf-8") + digits_body = encode_image("captcha-digits.png") - task = ImageToTextTask(body=body, numeric=1, minLength=4, maxLength=6) + # --- Basic example --- + # Solves a simple 5-digit image captcha (examples/assets/captcha-digits.png) + # with character set hints. + try: + result = await solver.solve( + ImageToTextTask( + body=digits_body, # Base64-encoded image (required) + numeric=1, # 1 = digits only + minLength=4, # Minimum expected answer length + maxLength=6, # Maximum expected answer length + ) + ) + # Solution contains {"text": "58204"} + # Submit solution.text to the target form field. + print("result: " + str(result)) + except Exception as e: + sys.exit(e) + # --- Advanced example --- + # Solves a math captcha (examples/assets/captcha-math.png) with comment and + # instruction image (examples/assets/captcha-math-instructions.png). try: - result = await solver.solve(task) + result = await solver.solve( + ImageToTextTask( + body=encode_image("captcha-math.png"), # Base64-encoded captcha image + # Optional fields (pass only if needed by the captcha type) + phrase=False, # True if answer has multiple words + case=True, # True if answer is case-sensitive + numeric=0, # 0 = not specified, 1 = digits, 2 = letters, 3 = any with digits, 4 = any with letters + math=True, # True if image is a math expression to solve + minLength=1, # Minimum answer length + maxLength=10, # Maximum answer length + comment="Enter the result of the equation", # Text hint for the worker + # Optional instruction image for the worker + imgInstructions=encode_image("captcha-math-instructions.png"), + ) + ) + print("result: " + str(result)) except Exception as e: - sys.exit(str(e)) - else: - # Solution contains {"text": "58204"} + sys.exit(e) + + # --- With language pool --- + # The languagePool parameter selects the worker pool by language. + # Pass it to solve() as a top-level parameter, not inside the task. + # Accepted values: "en" (English) or "ru" (Russian). + try: + result = await solver.solve( + ImageToTextTask(body=digits_body, numeric=1, minLength=4, maxLength=6), + language_pool="en", # Picks English-speaking worker pool + ) print("result: " + str(result)) + except Exception as e: + sys.exit(e) asyncio.run(main()) From f37a1133989e1d4c4e2f17627f94c0a449600a6b Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:02:41 +0300 Subject: [PATCH 15/23] docs: list https proxy type for Yandex SmartCaptcha Co-Authored-By: Claude Opus 5.5 --- captcha_solver_api/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 56d3752..d4e6824 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -783,7 +783,8 @@ class YandexSmartCaptchaTask(BaseTask, ProxyMixin): Args: websiteURL: Required. Full URL of the page where the widget is located. websiteKey: Required. The `sitekey` value from the page source or captcha iframe. - proxyType: Required. `"http"`, `"socks4"`, or `"socks5"`. + proxyType: Required. `"http"`, `"https"`, `"socks4"`, or `"socks5"`. `"https"` + is accepted only for this CAPTCHA type. proxyAddress: Required. Proxy IP address or hostname. proxyPort: Required. Proxy port. proxyLogin: Optional. Proxy auth username, if required. From ae7e970a798997a66b2245bc9a8e2f8d39f87bba Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:02:47 +0300 Subject: [PATCH 16/23] docs: replace em dashes in contributor docs Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 10 +++++----- DEVELOPMENT.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 99a5357..7cd70bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,11 @@ Python SDK for the Captcha Solver API, published on PyPI as `captcha-solver-api` ## Project layout -- `captcha_solver_api/` — package: `client.py` (sync), `async_client.py` (async), `tasks.py`, `exceptions.py` -- `tests/sync/`, `tests/async/` — unit tests, HTTP is mocked -- `tests/integration/` — real HTTP requests to a local stub server (`api_server` fixture) -- `examples/sync/`, `examples/async/` — runnable examples -- `DEVELOPMENT.md` — setup, tests, release process +- `captcha_solver_api/` - package: `client.py` (sync), `async_client.py` (async), `tasks.py`, `exceptions.py` +- `tests/sync/`, `tests/async/` - unit tests, HTTP is mocked +- `tests/integration/` - real HTTP requests to a local stub server (`api_server` fixture) +- `examples/sync/`, `examples/async/` - runnable examples +- `DEVELOPMENT.md` - setup, tests, release process ## Commands diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b4fca12..c09a7a7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -27,8 +27,8 @@ python -m pytest -k turnstile # filter by name No API key or internet access is needed: -- `tests/sync/`, `tests/async/` — unit tests, HTTP calls are mocked. -- `tests/integration/` — the client sends real HTTP requests to a server started on +- `tests/sync/`, `tests/async/` - unit tests, HTTP calls are mocked. +- `tests/integration/` - the client sends real HTTP requests to a server started on `127.0.0.1` by the `api_server` fixture, which records path, headers and body. Use it to check what actually goes over the wire (e.g. the `X-SDK` header). From 47c604ccc45f20d7e40e75e5760f276bd91caf56 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:10:06 +0300 Subject: [PATCH 17/23] feat: retry transient polling failures in solve() A single network error or HTTP timeout on getTaskResult used to abort solve() and lose an already created, paid task. Retry such failures on the next poll, up to `poll_retries` (default 3) consecutive times. API errors and createTask are not retried. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +++ captcha_solver_api/async_client.py | 18 ++++- captcha_solver_api/client.py | 18 ++++- tests/async/test_async_polling_retries.py | 91 +++++++++++++++++++++++ tests/sync/test_polling_retries.py | 91 +++++++++++++++++++++++ 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 tests/async/test_async_polling_retries.py create mode 100644 tests/sync/test_polling_retries.py diff --git a/README.md b/README.md index 26708c6..87faa4c 100644 --- a/README.md +++ b/README.md @@ -673,6 +673,16 @@ task lifetime. A local timeout does not cancel a task or mean that the API failed to solve it. Use `create_task()` and keep its `task_id` if you need to check the same task later; calling `solve()` again creates a new task. +While polling, `solve()` retries short network failures and HTTP timeouts of +`getTaskResult` on the next poll, so a brief outage does not lose an already +created task. After `poll_retries` (default 3) consecutive failures the error is +raised. API errors are not retried, and `createTask` is never retried because +that could create and pay for a second task. To change the limit: + +```python +client.poll_retries = 5 +``` + ### Worker language pool Set a default `language_pool` once at construction instead of passing it to every call: diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index 90267bf..1ff7362 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -58,6 +58,10 @@ class AsyncCaptchaClient: token = solution["gRecaptchaResponse"] """ + #: Consecutive `getTaskResult` network failures or HTTP timeouts that + #: `solve()` tolerates before raising. Each retry happens on the next poll. + poll_retries: int = 3 + def __init__( self, client_key: str, @@ -285,6 +289,8 @@ async def solve( CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request timed out. NetworkError: A request failed or the API returned an invalid response. + While polling, up to `poll_retries` consecutive network failures or + HTTP timeouts are retried on the next poll before this is raised. Example: from captcha_solver_api.tasks import RecaptchaV2TaskProxyless @@ -300,13 +306,23 @@ async def solve( task_id = await self.create_task(task, language_pool=language_pool) deadline = time.monotonic() + (timeout if timeout is not None else self.timeout) + poll_failures = 0 while time.monotonic() < deadline: remaining = deadline - time.monotonic() await asyncio.sleep(min(self.polling_interval, max(0, remaining))) if time.monotonic() >= deadline: break - result = await self.get_task_result(task_id) + try: + result = await self.get_task_result(task_id) + except (NetworkError, CaptchaTimeoutError): + # getTaskResult is safe to repeat: retry a short transport + # failure on the next poll instead of losing a paid task. + poll_failures += 1 + if poll_failures > self.poll_retries: + raise + continue + poll_failures = 0 if result.get("status") == "ready": solution = result["solution"] diff --git a/captcha_solver_api/client.py b/captcha_solver_api/client.py index a2fe4e5..a70cc0e 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -48,6 +48,10 @@ class CaptchaClient: token = solution["gRecaptchaResponse"] """ + #: Consecutive `getTaskResult` network failures or HTTP timeouts that + #: `solve()` tolerates before raising. Each retry happens on the next poll. + poll_retries: int = 3 + def __init__( self, client_key: str, @@ -263,6 +267,8 @@ def solve( CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request timed out. NetworkError: A request failed or the API returned an invalid response. + While polling, up to `poll_retries` consecutive network failures or + HTTP timeouts are retried on the next poll before this is raised. Example: from captcha_solver_api.tasks import RecaptchaV2TaskProxyless @@ -278,13 +284,23 @@ def solve( task_id = self.create_task(task, language_pool=language_pool) deadline = time.monotonic() + (timeout if timeout is not None else self.timeout) + poll_failures = 0 while time.monotonic() < deadline: remaining = deadline - time.monotonic() time.sleep(min(self.polling_interval, max(0, remaining))) if time.monotonic() >= deadline: break - result = self.get_task_result(task_id) + try: + result = self.get_task_result(task_id) + except (NetworkError, CaptchaTimeoutError): + # getTaskResult is safe to repeat: retry a short transport + # failure on the next poll instead of losing a paid task. + poll_failures += 1 + if poll_failures > self.poll_retries: + raise + continue + poll_failures = 0 if result.get("status") == "ready": solution = result["solution"] diff --git a/tests/async/test_async_polling_retries.py b/tests/async/test_async_polling_retries.py new file mode 100644 index 0000000..df6435c --- /dev/null +++ b/tests/async/test_async_polling_retries.py @@ -0,0 +1,91 @@ +""" +Tests for retrying transient getTaskResult failures inside AsyncCaptchaClient.solve(). Mirrors tests/sync/test_polling_retries.py. +""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from captcha_solver_api import ApiError, AsyncCaptchaClient, CaptchaTimeoutError, NetworkError +from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + +TASK = RecaptchaV2TaskProxyless(websiteURL="https://example.com", websiteKey="site-key") +CREATED = {"errorId": 0, "taskId": 100} +READY = {"errorId": 0, "status": "ready", "solution": {"gRecaptchaResponse": "token"}} + + +def make_client(): + return AsyncCaptchaClient("test_key", timeout=5, polling_interval=0.01) + + +async def test_solve_retries_network_errors_and_http_timeouts_while_polling(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [ + CREATED, + NetworkError("502 Bad Gateway"), + CaptchaTimeoutError("Request timed out."), + READY, + ] + result = await client.solve(TASK) + + assert result == {"gRecaptchaResponse": "token"} + assert mock_request.call_count == 4 + + +async def test_solve_raises_after_too_many_consecutive_poll_failures(): + client = make_client() + failures = [NetworkError("connection reset")] * (client.poll_retries + 1) + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED, *failures] + with pytest.raises(NetworkError, match="connection reset"): + await client.solve(TASK) + + assert mock_request.call_count == 1 + client.poll_retries + 1 + + +async def test_successful_poll_resets_the_failure_counter(): + client = make_client() + client.poll_retries = 1 + processing = {"errorId": 0, "status": "processing"} + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [ + CREATED, + NetworkError("blip"), + processing, + NetworkError("blip"), + READY, + ] + result = await client.solve(TASK) + + assert result == {"gRecaptchaResponse": "token"} + + +async def test_solve_does_not_retry_api_errors_while_polling(): + client = make_client() + api_error = { + "errorId": 1, + "errorCode": "ERROR_CAPTCHA_UNSOLVABLE", + "errorDescription": "Captcha could not be solved", + } + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED, api_error, READY] + with pytest.raises(ApiError): + await client.solve(TASK) + + assert mock_request.call_count == 2 + + +async def test_solve_does_not_retry_create_task(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [NetworkError("down"), CREATED, READY] + with pytest.raises(NetworkError): + await client.solve(TASK) + + assert mock_request.call_count == 1 diff --git a/tests/sync/test_polling_retries.py b/tests/sync/test_polling_retries.py new file mode 100644 index 0000000..cde3a73 --- /dev/null +++ b/tests/sync/test_polling_retries.py @@ -0,0 +1,91 @@ +""" +Tests for retrying transient getTaskResult failures inside CaptchaClient.solve(). +""" + +from unittest.mock import patch + +import pytest + +from captcha_solver_api import ApiError, CaptchaClient, CaptchaTimeoutError, NetworkError +from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + +TASK = RecaptchaV2TaskProxyless(websiteURL="https://example.com", websiteKey="site-key") +CREATED = {"errorId": 0, "taskId": 100} +READY = {"errorId": 0, "status": "ready", "solution": {"gRecaptchaResponse": "token"}} + + +def make_client(): + return CaptchaClient("test_key", timeout=5, polling_interval=0.01) + + +def test_solve_retries_network_errors_and_http_timeouts_while_polling(): + client = make_client() + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [ + CREATED, + NetworkError("502 Bad Gateway"), + CaptchaTimeoutError("Request timed out."), + READY, + ] + result = client.solve(TASK) + + assert result == {"gRecaptchaResponse": "token"} + assert mock_request.call_count == 4 + + +def test_solve_raises_after_too_many_consecutive_poll_failures(): + client = make_client() + failures = [NetworkError("connection reset")] * (client.poll_retries + 1) + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED, *failures] + with pytest.raises(NetworkError, match="connection reset"): + client.solve(TASK) + + assert mock_request.call_count == 1 + client.poll_retries + 1 + + +def test_successful_poll_resets_the_failure_counter(): + client = make_client() + client.poll_retries = 1 + processing = {"errorId": 0, "status": "processing"} + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [ + CREATED, + NetworkError("blip"), + processing, + NetworkError("blip"), + READY, + ] + result = client.solve(TASK) + + assert result == {"gRecaptchaResponse": "token"} + + +def test_solve_does_not_retry_api_errors_while_polling(): + client = make_client() + api_error = { + "errorId": 1, + "errorCode": "ERROR_CAPTCHA_UNSOLVABLE", + "errorDescription": "Captcha could not be solved", + } + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED, api_error, READY] + with pytest.raises(ApiError): + client.solve(TASK) + + assert mock_request.call_count == 2 + + +def test_solve_does_not_retry_create_task(): + client = make_client() + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [NetworkError("down"), CREATED, READY] + with pytest.raises(NetworkError): + client.solve(TASK) + + assert mock_request.call_count == 1 From 7f81a4d30ffba433e07376540ff98ba3f06c264a Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:12:13 +0300 Subject: [PATCH 18/23] feat: expose task_id on errors raised after task creation Errors from solve() polling and get_task_result() now carry `task_id`, so a timed-out task can still be checked with get_task_result() and the ID can be logged or sent to support. The polling timeout message also names the task. `task_id` is None for errors raised before a task exists. Co-Authored-By: Claude Opus 5.5 --- README.md | 19 ++++- captcha_solver_api/async_client.py | 23 +++++- captcha_solver_api/client.py | 23 +++++- captcha_solver_api/exceptions.py | 28 ++++++- tests/async/test_async_error_task_id.py | 105 ++++++++++++++++++++++++ tests/sync/test_error_task_id.py | 104 +++++++++++++++++++++++ 6 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 tests/async/test_async_error_task_id.py create mode 100644 tests/sync/test_error_task_id.py diff --git a/README.md b/README.md index 87faa4c..3c61896 100644 --- a/README.md +++ b/README.md @@ -752,13 +752,26 @@ try: except ValidationError as e: print(f"Invalid argument: {e}") except ApiError as e: - print(f"API error: {e.error_code} {e.error_description}") -except CaptchaTimeoutError: - print("Task timed out") + print(f"API error: {e.error_code} {e.error_description} (task {e.task_id})") +except CaptchaTimeoutError as e: + print(f"Task {e.task_id} timed out") except NetworkError as e: print(f"Network error: {e}") ``` +Every SDK error has a `task_id` attribute. It is set when the error happens +after the task was created (while polling in `solve()` or in `get_task_result()`) +and is `None` otherwise. A polling timeout does not cancel the task, so you can +keep checking it: + +```python +try: + result = client.solve(task) +except CaptchaTimeoutError as e: + if e.task_id is not None: + status = client.get_task_result(e.task_id) +``` + `TimeoutError` is still exported as a deprecated alias of `CaptchaTimeoutError`. Avoid importing it by name: it shadows Python's built-in `TimeoutError`. diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index 1ff7362..45454cd 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -15,6 +15,7 @@ from ._version import __version__ from .exceptions import ( ApiError, + CaptchaError, CaptchaTimeoutError, NetworkError, ValidationError, @@ -226,8 +227,12 @@ async def get_task_result(self, task_id: int) -> Dict[str, Any]: "clientKey": self.client_key, "taskId": task_id, } - data = await self._request("getTaskResult", payload) - self._ensure_success(data) + try: + data = await self._request("getTaskResult", payload) + self._ensure_success(data) + except CaptchaError as exc: + exc.task_id = task_id + raise return data async def get_balance(self) -> float: @@ -292,6 +297,9 @@ async def solve( While polling, up to `poll_retries` consecutive network failures or HTTP timeouts are retried on the next poll before this is raised. + Errors raised after the task is created have `task_id` set, so the + task can still be checked with `get_task_result(exc.task_id)`. + Example: from captcha_solver_api.tasks import RecaptchaV2TaskProxyless @@ -327,7 +335,14 @@ async def solve( if result.get("status") == "ready": solution = result["solution"] if not isinstance(solution, dict): - raise NetworkError("API returned a ready task without a solution object") + raise NetworkError( + "API returned a ready task without a solution object", + task_id=task_id, + ) return solution - raise CaptchaTimeoutError("Task solving timed out.") + raise CaptchaTimeoutError( + f"Task {task_id} was not solved within the polling timeout. It may still " + f"be solved later: check it with get_task_result({task_id}).", + task_id=task_id, + ) diff --git a/captcha_solver_api/client.py b/captcha_solver_api/client.py index a70cc0e..5e42c8a 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -12,6 +12,7 @@ from ._version import __version__ from .exceptions import ( ApiError, + CaptchaError, CaptchaTimeoutError, NetworkError, ValidationError, @@ -211,8 +212,12 @@ def get_task_result(self, task_id: int) -> Dict[str, Any]: "clientKey": self.client_key, "taskId": task_id, } - data = self._request("getTaskResult", payload) - self._ensure_success(data) + try: + data = self._request("getTaskResult", payload) + self._ensure_success(data) + except CaptchaError as exc: + exc.task_id = task_id + raise return data def get_balance(self) -> float: @@ -270,6 +275,9 @@ def solve( While polling, up to `poll_retries` consecutive network failures or HTTP timeouts are retried on the next poll before this is raised. + Errors raised after the task is created have `task_id` set, so the + task can still be checked with `get_task_result(exc.task_id)`. + Example: from captcha_solver_api.tasks import RecaptchaV2TaskProxyless @@ -305,7 +313,14 @@ def solve( if result.get("status") == "ready": solution = result["solution"] if not isinstance(solution, dict): - raise NetworkError("API returned a ready task without a solution object") + raise NetworkError( + "API returned a ready task without a solution object", + task_id=task_id, + ) return solution - raise CaptchaTimeoutError("Task solving timed out.") + raise CaptchaTimeoutError( + f"Task {task_id} was not solved within the polling timeout. It may still " + f"be solved later: check it with get_task_result({task_id}).", + task_id=task_id, + ) diff --git a/captcha_solver_api/exceptions.py b/captcha_solver_api/exceptions.py index 325d8a2..33e43d1 100644 --- a/captcha_solver_api/exceptions.py +++ b/captcha_solver_api/exceptions.py @@ -2,14 +2,33 @@ Custom exceptions used by the Captcha Solver SDK. """ +from typing import Optional + class CaptchaError(Exception): """Base exception for all SDK errors. Catch this to handle `ApiError`, `NetworkError`, `CaptchaTimeoutError`, and `ValidationError` together. + + Attributes: + task_id: ID of the task the error relates to, or `None` if it was raised + before a task was created (for example, by `createTask` or + `getBalance`). Errors from `solve()` and `get_task_result()` set it, + so you can keep polling the same task with `get_task_result()` or + mention it when contacting support. + + Example: + try: + solution = client.solve(task) + except CaptchaTimeoutError as exc: + print("Task", exc.task_id, "is not ready yet") """ + def __init__(self, *args: object, task_id: Optional[int] = None) -> None: + super().__init__(*args) + self.task_id = task_id + class NetworkError(CaptchaError): """An HTTP request failed or the API returned an invalid response. @@ -51,10 +70,15 @@ class ApiError(CaptchaError): print(exc.error_code, exc.error_description) """ - def __init__(self, error_code: str, error_description: str) -> None: + def __init__( + self, + error_code: str, + error_description: str, + task_id: Optional[int] = None, + ) -> None: self.error_code = error_code self.error_description = error_description - super().__init__(f"{error_code}: {error_description}") + super().__init__(f"{error_code}: {error_description}", task_id=task_id) class ValidationError(CaptchaError): diff --git a/tests/async/test_async_error_task_id.py b/tests/async/test_async_error_task_id.py new file mode 100644 index 0000000..8d2a599 --- /dev/null +++ b/tests/async/test_async_error_task_id.py @@ -0,0 +1,105 @@ +""" +Tests for the task_id attribute on errors raised by AsyncCaptchaClient. +Mirrors tests/sync/test_error_task_id.py. +""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from captcha_solver_api import ( + ApiError, + AsyncCaptchaClient, + CaptchaError, + CaptchaTimeoutError, + NetworkError, + ValidationError, +) +from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + +TASK = RecaptchaV2TaskProxyless(websiteURL="https://example.com", websiteKey="site-key") +CREATED = {"errorId": 0, "taskId": 100} +PROCESSING = {"errorId": 0, "status": "processing"} +API_ERROR = { + "errorId": 1, + "errorCode": "ERROR_CAPTCHA_UNSOLVABLE", + "errorDescription": "Captcha could not be solved", +} + + +def make_client(**kwargs): + return AsyncCaptchaClient("test_key", polling_interval=0.01, **kwargs) + + +def test_task_id_defaults_to_none(): + assert CaptchaError("boom").task_id is None + assert ApiError("CODE", "description").task_id is None + + +async def test_polling_timeout_has_task_id(): + client = make_client(timeout=0.05) + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED] + [PROCESSING] * 20 + with pytest.raises(CaptchaTimeoutError, match="Task 100") as exc_info: + await client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +async def test_api_error_while_polling_has_task_id(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED, API_ERROR] + with pytest.raises(ApiError) as exc_info: + await client.solve(TASK) + + assert exc_info.value.task_id == 100 + assert exc_info.value.error_code == "ERROR_CAPTCHA_UNSOLVABLE" + + +async def test_network_error_after_retries_has_task_id(): + client = make_client() + client.poll_retries = 0 + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED, NetworkError("connection reset")] + with pytest.raises(NetworkError) as exc_info: + await client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +async def test_ready_task_without_solution_object_has_task_id(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock) as mock_request: + mock_request.side_effect = [CREATED, {"errorId": 0, "status": "ready", "solution": "x"}] + with pytest.raises(NetworkError) as exc_info: + await client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +async def test_get_task_result_error_has_task_id(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock, return_value=API_ERROR): + with pytest.raises(ApiError) as exc_info: + await client.get_task_result(42) + + assert exc_info.value.task_id == 42 + + +async def test_errors_before_task_creation_have_no_task_id(): + client = make_client() + + with patch.object(client, "_request", new_callable=AsyncMock, return_value=API_ERROR): + with pytest.raises(ApiError) as exc_info: + await client.solve(TASK) + assert exc_info.value.task_id is None + + with pytest.raises(ValidationError) as validation_info: + await client.solve(RecaptchaV2TaskProxyless(websiteURL="", websiteKey="site-key")) + assert validation_info.value.task_id is None diff --git a/tests/sync/test_error_task_id.py b/tests/sync/test_error_task_id.py new file mode 100644 index 0000000..b28acaf --- /dev/null +++ b/tests/sync/test_error_task_id.py @@ -0,0 +1,104 @@ +""" +Tests for the task_id attribute on errors raised by CaptchaClient. +""" + +from unittest.mock import patch + +import pytest + +from captcha_solver_api import ( + ApiError, + CaptchaClient, + CaptchaError, + CaptchaTimeoutError, + NetworkError, + ValidationError, +) +from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + +TASK = RecaptchaV2TaskProxyless(websiteURL="https://example.com", websiteKey="site-key") +CREATED = {"errorId": 0, "taskId": 100} +PROCESSING = {"errorId": 0, "status": "processing"} +API_ERROR = { + "errorId": 1, + "errorCode": "ERROR_CAPTCHA_UNSOLVABLE", + "errorDescription": "Captcha could not be solved", +} + + +def make_client(**kwargs): + return CaptchaClient("test_key", polling_interval=0.01, **kwargs) + + +def test_task_id_defaults_to_none(): + assert CaptchaError("boom").task_id is None + assert ApiError("CODE", "description").task_id is None + + +def test_polling_timeout_has_task_id(): + client = make_client(timeout=0.05) + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED] + [PROCESSING] * 20 + with pytest.raises(CaptchaTimeoutError, match="Task 100") as exc_info: + client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +def test_api_error_while_polling_has_task_id(): + client = make_client() + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED, API_ERROR] + with pytest.raises(ApiError) as exc_info: + client.solve(TASK) + + assert exc_info.value.task_id == 100 + assert exc_info.value.error_code == "ERROR_CAPTCHA_UNSOLVABLE" + + +def test_network_error_after_retries_has_task_id(): + client = make_client() + client.poll_retries = 0 + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED, NetworkError("connection reset")] + with pytest.raises(NetworkError) as exc_info: + client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +def test_ready_task_without_solution_object_has_task_id(): + client = make_client() + + with patch.object(client, "_request") as mock_request: + mock_request.side_effect = [CREATED, {"errorId": 0, "status": "ready", "solution": "x"}] + with pytest.raises(NetworkError) as exc_info: + client.solve(TASK) + + assert exc_info.value.task_id == 100 + + +def test_get_task_result_error_has_task_id(): + client = make_client() + + with patch.object(client, "_request", return_value=API_ERROR): + with pytest.raises(ApiError) as exc_info: + client.get_task_result(42) + + assert exc_info.value.task_id == 42 + + +def test_errors_before_task_creation_have_no_task_id(): + client = make_client() + + with patch.object(client, "_request", return_value=API_ERROR): + with pytest.raises(ApiError) as exc_info: + client.solve(TASK) + assert exc_info.value.task_id is None + + with pytest.raises(ValidationError) as validation_info: + client.solve(RecaptchaV2TaskProxyless(websiteURL="", websiteKey="site-key")) + assert validation_info.value.task_id is None From 3ea01bae73597ec6c470d5499855b66c7aaf0e45 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:47:33 +0300 Subject: [PATCH 19/23] ci: add dependency audit, bandit, and CodeQL checks Run pip-audit on the runtime dependencies, bandit on the package source, and CodeQL security queries on every pull request, on pushes to main, and weekly so newly published advisories are caught without code changes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/security.yml | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..9e897fc --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,72 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + # New advisories are published without any change in this repository, + # so re-check the dependencies weekly as well (Mondays, 06:00 UTC). + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + # Known vulnerabilities in the runtime dependencies that users install + # with the package. pip-audit resolves them from pyproject.toml in an + # isolated environment, so the result matches a fresh `pip install`. + dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install pip-audit + run: python -m pip install pip-audit + + - name: Audit runtime dependencies + run: pip-audit . + + # Static analysis of the SDK source for insecure patterns, such as disabled + # TLS verification, requests without timeouts, or eval/exec. + bandit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install bandit + run: python -m pip install bandit + + - name: Scan package source + run: bandit -r captcha_solver_api + + # GitHub CodeQL analysis. Results appear under Security -> Code scanning. + # Do not also enable CodeQL "default setup" in the repository settings: + # GitHub rejects uploads from this workflow while default setup is on. + codeql: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + queries: security-extended + + - name: Analyze + uses: github/codeql-action/analyze@v3 From abb3e4752eca86a7d812c7ed6c6f1bc25c6e3f98 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:47:33 +0300 Subject: [PATCH 20/23] ci: configure Dependabot for pip and GitHub Actions Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..83ab292 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# Dependabot opens pull requests that update dependencies. Security updates +# for vulnerable dependencies also require "Dependabot alerts" and +# "Dependabot security updates" to be enabled in the repository settings. +version: 2 +updates: + # Runtime and dev dependencies declared in pyproject.toml. + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + commit-message: + prefix: build + + # Versions of the actions used in .github/workflows. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: ci From 8334e2dfb1e9a53aa4b1037ab1810af9ae233c48 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:52:41 +0300 Subject: [PATCH 21/23] build: raise minimum requests and httpx versions requests 2.28-2.30 can leak the Proxy-Authorization header on redirects (CVE-2023-32681), which matters for proxy tasks, and later 2.32.x releases fix further advisories. Require requests>=2.32.4, the newest line that still supports Python 3.9, and httpx>=0.27. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9fa1f33..63203d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "requests>=2.28", - "httpx>=0.24", + "requests>=2.32.4", + "httpx>=0.27", ] [project.optional-dependencies] From 423df2dd1ce8907df07aed9b3f37b169a0982f9e Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 01:55:24 +0300 Subject: [PATCH 22/23] ci: drop CodeQL job in favor of default setup The repository already has CodeQL default setup enabled, and GitHub rejects SARIF uploads from advanced workflows while it is on. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/security.yml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9e897fc..2a7cfb8 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +# CodeQL is not run here: the repository uses CodeQL default setup +# (Settings -> Code security), and GitHub rejects results from a workflow +# while default setup is enabled. + jobs: # Known vulnerabilities in the runtime dependencies that users install # with the package. pip-audit resolves them from pyproject.toml in an @@ -50,23 +54,3 @@ jobs: - name: Scan package source run: bandit -r captcha_solver_api - - # GitHub CodeQL analysis. Results appear under Security -> Code scanning. - # Do not also enable CodeQL "default setup" in the repository settings: - # GitHub rejects uploads from this workflow while default setup is on. - codeql: - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - steps: - - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: python - queries: security-extended - - - name: Analyze - uses: github/codeql-action/analyze@v3 From f3738dd05ff45f22b9c4811f720dca2bad011e84 Mon Sep 17 00:00:00 2001 From: dzmitry-duboyski Date: Wed, 4 Nov 2026 02:09:10 +0300 Subject: [PATCH 23/23] chore: set version to 1.1.0 Co-Authored-By: Claude Opus 5.5 --- captcha_solver_api/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captcha_solver_api/_version.py b/captcha_solver_api/_version.py index 68cdeee..6849410 100644 --- a/captcha_solver_api/_version.py +++ b/captcha_solver_api/_version.py @@ -1 +1 @@ -__version__ = "1.0.5" +__version__ = "1.1.0"