Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ No API key or internet access is needed:
CI (`.github/workflows/tests.yml`) runs the suite on Python 3.9–3.13 for every push
and pull request and checks that the package builds.

## Editor documentation

Keep Google-style docstrings on public clients, methods, task classes, and
exceptions. Document every argument by its exact signature name, including
defaults, units, accepted values, and whether `None` omits an API field. Methods
should describe their return value and raised SDK exceptions; task classes should
describe the solution fields returned by `solve()`.

Keep constructor settings on the client class as well as `__init__`, so class
hover and constructor signature help both have useful documentation. Include
short examples and keep sync/async documentation aligned (`await` for async
calls). Examples using `client`, `task`, or `task_id` assume those already exist;
async examples belong inside an `async def` function.

Type annotations and `captcha_solver_api/py.typed` must remain in the wheel and
source distribution. To review an editor-facing change, install the built wheel
in a separate environment, inspect `help(CaptchaClient.solve)` and
`help(RecaptchaV2TaskProxyless)`, and check hover/signature help with that environment
selected in the editor.

## Building locally

```bash
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Full API reference (all endpoints, error codes, captcha-type details): **https:/
## Table of Contents

- [Installation](#installation)
- [Editor documentation](#editor-documentation)
- [Configuration](#configuration)
- [Quick Start](#quick-start)
- [Build Faster with AI](#build-faster-with-ai)
Expand Down Expand Up @@ -66,6 +67,31 @@ Or install the latest version from GitHub:
pip install git+https://github.com/captcha-solver-api/python-sdk.git
```

## Editor documentation

The SDK includes Python docstrings (the equivalent of JSDoc), type annotations,
and a `py.typed` marker in the installed package. With Python language support
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.

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
`async def` function. In VS Code, select the Python interpreter where the SDK is
installed and enable the Python and Pylance extensions. In PyCharm, use Quick
Documentation. The exact presentation depends on the editor.

You can also read the same documentation without an editor:

```python
from captcha_solver_api import CaptchaClient
from captcha_solver_api.tasks import RecaptchaV2TaskProxyless

help(CaptchaClient.solve)
help(RecaptchaV2TaskProxyless)
```

## Configuration

The client always takes the API key as an explicit argument -- it does not read
Expand Down
79 changes: 65 additions & 14 deletions captcha_solver_api/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,39 @@


class AsyncCaptchaClient:
"""
Async client for interacting with the Captcha Solver API.
"""Asynchronous client for submitting tasks and awaiting CAPTCHA solutions.

Holds a single, reused `httpx.AsyncClient` connection pool for the
lifetime of the instance (created once, not per request), so repeated
calls -- especially the `getTaskResult` polling inside `solve()` --
reuse the same keep-alive connection instead of paying a fresh TCP/TLS
handshake every time. Close it with `aclose()` when you're done, or use
it as an async context manager:
it as an async context manager.

Args:
client_key: Your Captcha Solver API key. Must not be empty.
base_url: API base URL. Defaults to `https://api.captcha-solver.com`.
timeout: Default polling timeout in seconds, starting after task creation.
Defaults to 120. Each HTTP request has a separate 30-second timeout.
polling_interval: Seconds before the first poll and between polls.
Defaults to 10. Waiting does not block the event loop.
language_pool: Default worker pool, e.g. `"en"` or `"ru"`. `None` uses
the account default. Can be overridden in `solve()` or `create_task()`.

Raises:
ValidationError: `client_key` is empty.

Example:
from captcha_solver_api import AsyncCaptchaClient
from captcha_solver_api.tasks import RecaptchaV2TaskProxyless

task = RecaptchaV2TaskProxyless(
websiteURL="https://example.com",
websiteKey="SITE_KEY",
)
async with AsyncCaptchaClient("YOUR_API_KEY") as client:
result = await client.solve(task)
solution = await client.solve(task)
token = solution["gRecaptchaResponse"]
"""

def __init__(
Expand All @@ -45,13 +65,16 @@ def __init__(
polling_interval: int = 10,
language_pool: Optional[str] = None,
) -> None:
"""
"""Create an async client with a reusable HTTP connection pool.

Args:
client_key: Your Captcha Solver API key.
base_url: API base URL. Override only for self-hosted or staging
deployments.
timeout: Default max seconds `solve()` waits for a solution before
raising `CaptchaTimeoutError`. Can be overridden per call.
timeout: Default polling timeout in seconds, starting after task
creation. Defaults to 120 and can be overridden per `solve()` call.
Each HTTP request has a separate 30-second timeout, so this is
not a strict deadline for the entire call.
polling_interval: Seconds to wait before the first `getTaskResult`
poll and between subsequent polls inside `solve()`. Defaults
to 10 seconds.
Expand Down Expand Up @@ -145,6 +168,9 @@ async def create_task(self, task: Any, language_pool: Optional[str] = None) -> i
ApiError: The API rejected the task or its parameters.
NetworkError: The request failed at the transport level.
CaptchaTimeoutError: The HTTP request timed out.

Example:
task_id = await client.create_task(task, language_pool="en")
"""
payload: Dict[str, Any] = {
"clientKey": self.client_key,
Expand All @@ -171,13 +197,20 @@ async def get_task_result(self, task_id: int) -> Dict[str, Any]:
task_id: The ID returned by `create_task()`.

Returns:
The raw API response with `status`; ready responses also contain a
solution dictionary.
The raw API response with `status` (`"processing"` or `"ready"`).
Ready responses also contain a `solution` dict, e.g.
`{"gRecaptchaResponse": "..."}` for reCAPTCHA or `{"text": "..."}`
for `ImageToTextTask`.

Raises:
ApiError: The API reports an error for the task.
NetworkError: The request failed at the transport level.
CaptchaTimeoutError: The HTTP request timed out.

Example:
result = await client.get_task_result(task_id)
if result["status"] == "ready":
solution = result["solution"]
"""
payload = {
"clientKey": self.client_key,
Expand All @@ -201,6 +234,9 @@ async def get_balance(self) -> float:
ApiError: The API key is invalid or the account cannot be resolved.
NetworkError: The request failed at the transport level.
CaptchaTimeoutError: The HTTP request timed out.

Example:
balance = await client.get_balance()
"""
payload = {"clientKey": self.client_key}
data = await self._request("getBalance", payload)
Expand All @@ -226,16 +262,31 @@ async def solve(
task: A task object from `captcha_solver_api.tasks`.
language_pool: Optional worker pool selector. Falls back to the
client's configured pool.
timeout: Maximum polling time for this call, in seconds. Overrides
the client's default timeout.
timeout: Polling timeout in seconds, starting after task creation.
`None` uses the client's default (120 unless configured otherwise).
Each HTTP request has a separate 30-second timeout, so this is
not a strict deadline for the entire call.

Returns:
The solution dictionary once the task status becomes `"ready"`.
The `solution` dict once `status` is `"ready"`. Its shape depends on
the task type -- see the per-type docstrings in `captcha_solver_api.tasks`
or the README's method reference.

Raises:
ApiError: The API rejected the task or reported a solving error.
CaptchaTimeoutError: No solution was ready before the deadline.
NetworkError: A request failed at the transport level.
CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request
timed out.
NetworkError: A request failed or the API returned an invalid response.

Example:
from captcha_solver_api.tasks import RecaptchaV2TaskProxyless

task = RecaptchaV2TaskProxyless(
websiteURL="https://example.com",
websiteKey="SITE_KEY",
)
solution = await client.solve(task, timeout=180)
token = solution["gRecaptchaResponse"]
"""

task_id = await self.create_task(task, language_pool=language_pool)
Expand Down
69 changes: 58 additions & 11 deletions captcha_solver_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,32 @@


class CaptchaClient:
"""
Main client for interacting with the Captcha Solver API.
"""Synchronous client for submitting tasks and waiting for CAPTCHA solutions.

Args:
client_key: Your Captcha Solver API key. Must not be empty.
base_url: API base URL. Defaults to `https://api.captcha-solver.com`.
timeout: Default polling timeout in seconds, starting after task creation.
Defaults to 120. Each HTTP request has a separate 30-second timeout.
polling_interval: Seconds before the first poll and between polls.
Defaults to 10.
language_pool: Default worker pool, e.g. `"en"` or `"ru"`. `None` uses
the account default. Can be overridden in `solve()` or `create_task()`.

Raises:
ValidationError: `client_key` is empty.

Example:
client = CaptchaClient("YOUR_API_KEY")
from captcha_solver_api import CaptchaClient
from captcha_solver_api.tasks import RecaptchaV2TaskProxyless

task = RecaptchaV2TaskProxyless(
websiteURL="https://example.com",
websiteKey="SITE_KEY",
)
with CaptchaClient("YOUR_API_KEY") as client:
solution = client.solve(task)
token = solution["gRecaptchaResponse"]
"""

def __init__(
Expand All @@ -34,13 +55,16 @@ def __init__(
polling_interval: int = 10,
language_pool: Optional[str] = None,
) -> None:
"""
"""Create a client with a reusable HTTP connection pool.

Args:
client_key: Your Captcha Solver API key.
base_url: API base URL. Override only for self-hosted or staging
deployments.
timeout: Default max seconds `solve()` waits for a solution before
raising `CaptchaTimeoutError`. Can be overridden per call.
timeout: Default polling timeout in seconds, starting after task
creation. Defaults to 120 and can be overridden per `solve()` call.
Each HTTP request has a separate 30-second timeout, so this is
not a strict deadline for the entire call.
polling_interval: Seconds to wait before the first `getTaskResult`
poll and between subsequent polls inside `solve()`. Defaults
to 10 seconds.
Expand Down Expand Up @@ -132,6 +156,9 @@ def create_task(self, task: Any, language_pool: Optional[str] = None) -> int:
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).

Example:
task_id = client.create_task(task, language_pool="en")
"""
payload: Dict[str, Any] = {
"clientKey": self.client_key,
Expand Down Expand Up @@ -164,6 +191,11 @@ def get_task_result(self, task_id: int) -> Dict[str, Any]:
ApiError: The API reports an error for this task (e.g. it expired).
NetworkError: The request failed at the transport level.
CaptchaTimeoutError: The HTTP request timed out.

Example:
result = client.get_task_result(task_id)
if result["status"] == "ready":
solution = result["solution"]
"""
payload = {
"clientKey": self.client_key,
Expand All @@ -185,6 +217,9 @@ def get_balance(self) -> float:
ApiError: The API key is invalid or the account can't be resolved.
NetworkError: The request failed at the transport level.
CaptchaTimeoutError: The HTTP request timed out.

Example:
balance = client.get_balance()
"""
payload = {"clientKey": self.client_key}
data = self._request("getBalance", payload)
Expand All @@ -205,9 +240,10 @@ def solve(
task: One of the task objects from `captcha_solver_api.tasks`.
language_pool: Worker pool selector, e.g. `"en"` or `"ru"`. Falls back
to the client's `language_pool` (set at construction) when omitted.
timeout: Overrides the client's default polling timeout for this call
only (useful for captcha types that reliably take longer, e.g.
classic reCAPTCHA v2), in seconds.
timeout: Polling timeout in seconds, starting after task creation.
`None` uses the client's default (120 unless configured otherwise).
Each HTTP request has a separate 30-second timeout, so this is
not a strict deadline for the entire call.

Returns:
The `solution` dict once `status` is `"ready"`. Its shape depends on
Expand All @@ -216,8 +252,19 @@ def solve(

Raises:
ApiError: The API rejected the task or reported an error while solving.
CaptchaTimeoutError: No solution was ready before the deadline.
NetworkError: A request failed at the transport level.
CaptchaTimeoutError: Polling exceeded its deadline or an HTTP request
timed out.
NetworkError: A request failed or the API returned an invalid response.

Example:
from captcha_solver_api.tasks import RecaptchaV2TaskProxyless

task = RecaptchaV2TaskProxyless(
websiteURL="https://example.com",
websiteKey="SITE_KEY",
)
solution = client.solve(task, timeout=180)
token = solution["gRecaptchaResponse"]
"""

task_id = self.create_task(task, language_pool=language_pool)
Expand Down
36 changes: 32 additions & 4 deletions captcha_solver_api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@


class CaptchaError(Exception):
"""Base exception for all SDK errors."""
"""Base exception for all SDK errors.

Catch this to handle `ApiError`, `NetworkError`, `CaptchaTimeoutError`, and
`ValidationError` together.
"""


class NetworkError(CaptchaError):
"""Raised when a network request fails."""
"""An HTTP request failed or the API returned an invalid response.

Includes HTTP error statuses, non-JSON responses, unexpected response
shapes, and ready tasks whose solution is not a dictionary.
"""


class CaptchaTimeoutError(CaptchaError):
"""Raised when the operation exceeds the configured timeout."""
"""An HTTP request timed out or the task's polling deadline was reached.

`solve(timeout=...)` overrides the polling timeout; each HTTP request uses
a separate 30-second timeout. `TimeoutError` is a deprecated alias of this
class; prefer `CaptchaTimeoutError` to avoid shadowing Python's built-in.
"""


# Deprecated alias kept for backward compatibility. It shadows the built-in
Expand All @@ -21,7 +34,22 @@ class CaptchaTimeoutError(CaptchaError):


class ApiError(CaptchaError):
"""Raised when the API returns an error."""
"""The API returned a nonzero `errorId`.

Args:
error_code: Machine-readable API `errorCode`.
error_description: Human-readable API `errorDescription`.

Attributes:
error_code: Error code to use when deciding how to handle the failure.
error_description: Explanation supplied by the API.

Example:
try:
solution = client.solve(task)
except ApiError as exc:
print(exc.error_code, exc.error_description)
"""

def __init__(self, error_code: str, error_description: str) -> None:
self.error_code = error_code
Expand Down
Loading
Loading