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 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..2a7cfb8 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,56 @@ +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 + +# 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 + # 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 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). diff --git a/README.md b/README.md index e23adae..3c61896 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 @@ -151,15 +155,19 @@ 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) | ✅ | ❌ | +| Yandex SmartCaptcha Image | ✅ | - | +| Coordinates (click captcha) | ✅ | - | | Tencent | ✅ | ✅ | +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 Every method below is available on both `CaptchaClient` (sync, `requests`-based) and @@ -207,14 +215,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)` @@ -479,7 +490,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 @@ -662,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: @@ -731,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`. @@ -751,12 +785,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 @@ -797,8 +831,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/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. diff --git a/captcha_solver_api/_version.py b/captcha_solver_api/_version.py index 92192ee..6849410 100644 --- a/captcha_solver_api/_version.py +++ b/captcha_solver_api/_version.py @@ -1 +1 @@ -__version__ = "1.0.4" +__version__ = "1.1.0" diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index faede47..45454cd 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -15,10 +15,12 @@ from ._version import __version__ from .exceptions import ( ApiError, + CaptchaError, CaptchaTimeoutError, NetworkError, ValidationError, ) +from .tasks import BaseTask class AsyncCaptchaClient: @@ -42,7 +44,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 @@ -57,6 +59,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, @@ -84,9 +90,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 @@ -165,6 +171,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. @@ -172,6 +180,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(), @@ -216,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: @@ -273,10 +288,17 @@ 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. 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. + + 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 @@ -292,18 +314,35 @@ 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"] 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 6b85021..5e42c8a 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -12,10 +12,12 @@ from ._version import __version__ from .exceptions import ( ApiError, + CaptchaError, CaptchaTimeoutError, NetworkError, ValidationError, ) +from .tasks import BaseTask class CaptchaClient: @@ -32,7 +34,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 @@ -47,6 +49,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, @@ -74,9 +80,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 @@ -153,6 +159,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). @@ -160,6 +168,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(), @@ -201,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: @@ -251,10 +266,17 @@ 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. 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. + + 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 @@ -270,18 +292,35 @@ 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"] 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/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 0f7a500..d4e6824 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,22 +26,41 @@ 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}") + self._validate_specific_fields() + + def _validate_specific_fields(self) -> None: + """Validate task-specific requirements that depend on other fields.""" + 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 @@ -58,11 +80,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 @@ -75,14 +97,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`. @@ -130,18 +152,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`. @@ -198,14 +220,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`. @@ -249,19 +271,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`. @@ -319,12 +341,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`. @@ -368,15 +390,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. @@ -422,19 +444,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. @@ -495,17 +517,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. @@ -548,22 +570,45 @@ 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` 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 - v3, and must be freshly fetched for every request -- it cannot be reused. - initParameters: 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 + websiteURL: Required. Full URL of the page where the widget is located. + 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 request, if present. Dynamic, single-use, and time-limited. Returns (`solution` from `solve()`): @@ -587,6 +632,9 @@ class GeeTestTaskProxyless(BaseTask): type = "GeeTestTaskProxyless" + def _validate_specific_fields(self) -> None: + _validate_geetest_fields(self) + def __init__( self, websiteURL: str, @@ -613,21 +661,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. - Required for v3. - initParameters: 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 + 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`. 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 request, if present. Dynamic, single-use, and time-limited. Returns (`solution` from `solve()`): @@ -654,6 +702,9 @@ class GeeTestTask(BaseTask, ProxyMixin): type = "GeeTestTask" + def _validate_specific_fields(self) -> None: + _validate_geetest_fields(self) + def __init__( self, websiteURL: str, @@ -689,10 +740,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. @@ -730,15 +781,16 @@ 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"`, `"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. + 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. @@ -790,12 +842,12 @@ class CoordinatesTask(BaseTask): is submitted directly. Use this task for generic click captchas. 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, @@ -833,15 +885,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 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") - Returns (``solution`` from ``solve()``): - ``coordinates`` -- an ordered list of ``{"x": int, "y": int}`` points. + 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" @@ -861,9 +931,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()`): @@ -901,14 +971,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 43772f8..ef4b00b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,22 +48,27 @@ 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` | | Yandex SmartCaptcha image | [yandex_smartcaptcha_image.py](sync/yandex_smartcaptcha_image.py) | [yandex_smartcaptcha_image.py](async/yandex_smartcaptcha_image.py) | `coordinates` | | 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 @@ -72,7 +77,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). @@ -81,7 +86,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/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.py b/examples/async/geetest_v3.py deleted file mode 100644 index 1443530..0000000 --- a/examples/async/geetest_v3.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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, GeeTestTaskProxyless - -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"] - - # --- 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) - - # --- 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_proxy.py b/examples/async/geetest_v3_proxy.py new file mode 100644 index 0000000..ce6bc1e --- /dev/null +++ b/examples/async/geetest_v3_proxy.py @@ -0,0 +1,75 @@ +""" +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") + + +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. + """ + # 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) + ) + ) + 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 new file mode 100644 index 0000000..1d9ffec --- /dev/null +++ b/examples/async/geetest_v3_proxyless.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 GeeTestTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +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. + """ + # 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 + ) + ) + # 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.py b/examples/async/geetest_v4.py deleted file mode 100644 index 0c5f002..0000000 --- a/examples/async/geetest_v4.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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, GeeTestTaskProxyless - -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 - ) - ) - # 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) - - # --- 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_proxy.py b/examples/async/geetest_v4_proxy.py new file mode 100644 index 0000000..506af84 --- /dev/null +++ b/examples/async/geetest_v4_proxy.py @@ -0,0 +1,60 @@ +""" +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") + + +async def main(): + # 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) + + +asyncio.run(main()) diff --git a/examples/async/geetest_v4_proxyless.py b/examples/async/geetest_v4_proxyless.py new file mode 100644 index 0000000..5668b8d --- /dev/null +++ b/examples/async/geetest_v4_proxyless.py @@ -0,0 +1,59 @@ +""" +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 GeeTestTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + # 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) + + +asyncio.run(main()) diff --git a/examples/async/image_to_text.py b/examples/async/image_to_text.py index 189a083..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 @@ -29,23 +29,74 @@ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") -solver = AsyncCaptchaClient(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(): - # 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)) + # Image to Text tasks are usually fast. Default timeout is fine. + async with AsyncCaptchaClient(api_key) as solver: + digits_body = encode_image("captcha-digits.png") + + # --- 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( + 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(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()) diff --git a/examples/async/recaptcha_v2.py b/examples/async/recaptcha_v2.py deleted file mode 100644 index ddbbd27..0000000 --- a/examples/async/recaptcha_v2.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -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). -""" - -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 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 - ) - - try: - result = await solver.solve(task) - except Exception as e: - sys.exit(str(e)) - else: - print("result: " + str(result)) - - -asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise.py b/examples/async/recaptcha_v2_enterprise.py deleted file mode 100644 index aabb7a8..0000000 --- a/examples/async/recaptcha_v2_enterprise.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -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, RecaptchaV2EnterpriseTaskProxyless - -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 - ) - ) - # 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 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_proxy.py b/examples/async/recaptcha_v2_enterprise_proxy.py new file mode 100644 index 0000000..4389924 --- /dev/null +++ b/examples/async/recaptcha_v2_enterprise_proxy.py @@ -0,0 +1,61 @@ +""" +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") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_enterprise_proxyless.py b/examples/async/recaptcha_v2_enterprise_proxyless.py new file mode 100644 index 0000000..10d157f --- /dev/null +++ b/examples/async/recaptcha_v2_enterprise_proxyless.py @@ -0,0 +1,59 @@ +""" +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 RecaptchaV2EnterpriseTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + 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) + + +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..6cd87d9 --- /dev/null +++ b/examples/async/recaptcha_v2_proxy.py @@ -0,0 +1,58 @@ +""" +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 +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") + + +async def main(): + # 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) + + +asyncio.run(main()) diff --git a/examples/async/recaptcha_v2_proxyless.py b/examples/async/recaptcha_v2_proxyless.py new file mode 100644 index 0000000..01d2626 --- /dev/null +++ b/examples/async/recaptcha_v2_proxyless.py @@ -0,0 +1,53 @@ +""" +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 +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 RecaptchaV2TaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + # 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) + + +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.py b/examples/async/tencent.py deleted file mode 100644 index 28c9df3..0000000 --- a/examples/async/tencent.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -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 TencentTask, 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) - - # --- 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) - - -asyncio.run(main()) diff --git a/examples/async/tencent_proxy.py b/examples/async/tencent_proxy.py new file mode 100644 index 0000000..c92644e --- /dev/null +++ b/examples/async/tencent_proxy.py @@ -0,0 +1,57 @@ +""" +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 TencentTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/tencent_proxyless.py b/examples/async/tencent_proxyless.py new file mode 100644 index 0000000..a840450 --- /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") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/turnstile.py b/examples/async/turnstile.py deleted file mode 100644 index 40f9154..0000000 --- a/examples/async/turnstile.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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, TurnstileTaskProxyless - -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 - ) - ) - # 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) - - # --- 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_proxy.py b/examples/async/turnstile_proxy.py new file mode 100644 index 0000000..f46b797 --- /dev/null +++ b/examples/async/turnstile_proxy.py @@ -0,0 +1,59 @@ +""" +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") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/turnstile_proxyless.py b/examples/async/turnstile_proxyless.py new file mode 100644 index 0000000..76dd7f2 --- /dev/null +++ b/examples/async/turnstile_proxyless.py @@ -0,0 +1,57 @@ +""" +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 TurnstileTaskProxyless + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha.py b/examples/async/yandex_smartcaptcha.py deleted file mode 100644 index 698eec5..0000000 --- a/examples/async/yandex_smartcaptcha.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -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 YandexSmartCaptchaTask, 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) - - # --- 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) - - -asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha_proxy.py b/examples/async/yandex_smartcaptcha_proxy.py new file mode 100644 index 0000000..5cae19b --- /dev/null +++ b/examples/async/yandex_smartcaptcha_proxy.py @@ -0,0 +1,58 @@ +""" +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 YandexSmartCaptchaTask + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + + +async def main(): + 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) + + +asyncio.run(main()) diff --git a/examples/async/yandex_smartcaptcha_proxyless.py b/examples/async/yandex_smartcaptcha_proxyless.py new file mode 100644 index 0000000..9537797 --- /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") + + +async def main(): + 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) + + +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) diff --git a/pyproject.toml b/pyproject.toml index cb344f3..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] @@ -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] 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/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/async/test_async_validation.py b/tests/async/test_async_validation.py new file mode 100644 index 0000000..c53bae1 --- /dev/null +++ b/tests/async/test_async_validation.py @@ -0,0 +1,45 @@ +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 +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(" ") + + +@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_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 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", 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 diff --git a/tests/sync/test_validation.py b/tests/sync/test_validation.py new file mode 100644 index 0000000..6958a55 --- /dev/null +++ b/tests/sync/test_validation.py @@ -0,0 +1,39 @@ +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) + + +def test_client_rejects_whitespace_client_key(): + with pytest.raises(ValidationError, match="client_key is required"): + CaptchaClient(" ")