diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ff35505..b4fca12 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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 diff --git a/README.md b/README.md index acaf1ce..8695653 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/captcha_solver_api/async_client.py b/captcha_solver_api/async_client.py index b55fc29..faede47 100644 --- a/captcha_solver_api/async_client.py +++ b/captcha_solver_api/async_client.py @@ -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__( @@ -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. @@ -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, @@ -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, @@ -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) @@ -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) diff --git a/captcha_solver_api/client.py b/captcha_solver_api/client.py index d7b7573..6b85021 100644 --- a/captcha_solver_api/client.py +++ b/captcha_solver_api/client.py @@ -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__( @@ -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. @@ -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, @@ -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, @@ -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) @@ -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 @@ -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) diff --git a/captcha_solver_api/exceptions.py b/captcha_solver_api/exceptions.py index 7198085..325d8a2 100644 --- a/captcha_solver_api/exceptions.py +++ b/captcha_solver_api/exceptions.py @@ -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 @@ -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 diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index c9dc260..5df677e 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -86,6 +86,19 @@ class RecaptchaV2TaskProxyless(BaseTask): Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. + + Example: + from captcha_solver_api.tasks import RecaptchaV2TaskProxyless + + task = RecaptchaV2TaskProxyless( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + ) + solution = client.solve(task) + answer = solution["gRecaptchaResponse"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "RecaptchaV2TaskProxyless" @@ -132,6 +145,22 @@ class RecaptchaV2Task(BaseTask, ProxyMixin): Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. + + Example: + from captcha_solver_api.tasks import RecaptchaV2Task + + task = RecaptchaV2Task( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["gRecaptchaResponse"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "RecaptchaV2Task" @@ -180,6 +209,19 @@ class RecaptchaV2EnterpriseTaskProxyless(BaseTask): Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. + + Example: + from captcha_solver_api.tasks import RecaptchaV2EnterpriseTaskProxyless + + task = RecaptchaV2EnterpriseTaskProxyless( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + ) + solution = client.solve(task) + answer = solution["gRecaptchaResponse"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "RecaptchaV2EnterpriseTaskProxyless" @@ -223,6 +265,22 @@ class RecaptchaV2EnterpriseTask(BaseTask, ProxyMixin): Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. + + Example: + from captcha_solver_api.tasks import RecaptchaV2EnterpriseTask + + task = RecaptchaV2EnterpriseTask( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["gRecaptchaResponse"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "RecaptchaV2EnterpriseTask" @@ -270,6 +328,21 @@ class RecaptchaV3TaskProxyless(BaseTask): Returns (`solution` from `solve()`): `gRecaptchaResponse` -- the token to submit as `g-recaptcha-response`. + + Example: + from captcha_solver_api.tasks import RecaptchaV3TaskProxyless + + task = RecaptchaV3TaskProxyless( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + minScore=0.3, + pageAction="login", + ) + solution = client.solve(task) + answer = solution["gRecaptchaResponse"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "RecaptchaV3TaskProxyless" @@ -311,6 +384,19 @@ class TurnstileTaskProxyless(BaseTask): `token` -- the value to submit as `cf-turnstile-response`. `userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP client to this returned User-Agent before invoking the callback. + + Example: + from captcha_solver_api.tasks import TurnstileTaskProxyless + + task = TurnstileTaskProxyless( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + ) + solution = client.solve(task) + answer = solution["token"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "TurnstileTaskProxyless" @@ -356,6 +442,22 @@ class TurnstileTask(BaseTask, ProxyMixin): `token` -- the value to submit as `cf-turnstile-response`. `userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP client to this returned User-Agent before invoking the callback. + + Example: + from captcha_solver_api.tasks import TurnstileTask + + task = TurnstileTask( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["token"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "TurnstileTask" @@ -407,6 +509,18 @@ class ImageToTextTask(BaseTask): Returns (`solution` from `solve()`): `text` -- the recognized text/answer. + + Example: + from captcha_solver_api.tasks import ImageToTextTask + + task = ImageToTextTask( + body="BASE64_IMAGE", + ) + solution = client.solve(task) + answer = solution["text"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "ImageToTextTask" @@ -455,6 +569,20 @@ class GeeTestTaskProxyless(BaseTask): Returns (`solution` from `solve()`): v3: `challenge`, `validate`, `seccode`. v4: `captcha_id`, `lot_number`, `pass_token`, `gen_time`, `captcha_output`. + + Example: + from captcha_solver_api.tasks import GeeTestTaskProxyless + + task = GeeTestTaskProxyless( + websiteURL="https://example.com", + version=4, + initParameters={"captcha_id": "CAPTCHA_ID"}, + ) + solution = client.solve(task) + answer = solution["captcha_output"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "GeeTestTaskProxyless" @@ -505,6 +633,23 @@ class GeeTestTask(BaseTask, ProxyMixin): Returns (`solution` from `solve()`): v3: `challenge`, `validate`, `seccode`. v4: `captcha_id`, `lot_number`, `pass_token`, `gen_time`, `captcha_output`. + + Example: + from captcha_solver_api.tasks import GeeTestTask + + task = GeeTestTask( + websiteURL="https://example.com", + version=4, + initParameters={"captcha_id": "CAPTCHA_ID"}, + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["captcha_output"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "GeeTestTask" @@ -551,6 +696,19 @@ class YandexSmartCaptchaTaskProxyless(BaseTask): Returns (`solution` from `solve()`): `token` -- the value to submit as the SmartCaptcha response token. + + Example: + from captcha_solver_api.tasks import YandexSmartCaptchaTaskProxyless + + task = YandexSmartCaptchaTaskProxyless( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + ) + solution = client.solve(task) + answer = solution["token"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "YandexSmartCaptchaTaskProxyless" @@ -584,6 +742,22 @@ class YandexSmartCaptchaTask(BaseTask, ProxyMixin): Returns (`solution` from `solve()`): `token` -- the value to submit as the SmartCaptcha response token. + + Example: + from captcha_solver_api.tasks import YandexSmartCaptchaTask + + task = YandexSmartCaptchaTask( + websiteURL="https://example.com", + websiteKey="SITE_KEY", + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["token"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "YandexSmartCaptchaTask" @@ -626,6 +800,19 @@ class CoordinatesTask(BaseTask): Returns (`solution` from `solve()`): `coordinates` -- a list of `{"x": int, "y": int}` pixel positions to click, in order. + + Example: + from captcha_solver_api.tasks import CoordinatesTask + + task = CoordinatesTask( + body="BASE64_IMAGE", + comment="Click on the green apple", + ) + solution = client.solve(task) + answer = solution["coordinates"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "CoordinatesTask" @@ -657,6 +844,19 @@ class TencentTaskProxyless(BaseTask): Returns (`solution` from `solve()`): `appid`, `ret`, `ticket`, `randstr` -- pass these to the page's Tencent captcha callback. + + Example: + from captcha_solver_api.tasks import TencentTaskProxyless + + task = TencentTaskProxyless( + websiteURL="https://example.com", + appId="APP_ID", + ) + solution = client.solve(task) + answer = solution["ticket"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "TencentTaskProxyless" @@ -689,6 +889,22 @@ class TencentTask(BaseTask, ProxyMixin): Returns (`solution` from `solve()`): `appid`, `ret`, `ticket`, `randstr` -- pass these to the page's Tencent captcha callback. + + Example: + from captcha_solver_api.tasks import TencentTask + + task = TencentTask( + websiteURL="https://example.com", + appId="APP_ID", + proxyType="http", + proxyAddress="proxy.example.com", + proxyPort=8080, + ) + solution = client.solve(task) + answer = solution["ticket"] + + Note: + Optional arguments default to `None` and are omitted from the request. """ type = "TencentTask"