captcha-solver-api 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ """
2
+ Official Python SDK for the Captcha Solver API.
3
+
4
+ Example:
5
+ from captcha_solver_api import CaptchaClient
6
+ from captcha_solver_api.tasks import RecaptchaV2TaskProxyless
7
+
8
+ client = CaptchaClient("YOUR_API_KEY")
9
+ task = RecaptchaV2TaskProxyless(
10
+ websiteURL="https://example.com",
11
+ websiteKey="6Le-xxxxxxxxx"
12
+ )
13
+ result = client.solve(task)
14
+ """
15
+
16
+ from .client import CaptchaClient
17
+ from .async_client import AsyncCaptchaClient
18
+ from .exceptions import (
19
+ CaptchaError,
20
+ ApiError,
21
+ NetworkError,
22
+ CaptchaTimeoutError,
23
+ TimeoutError,
24
+ ValidationError,
25
+ )
26
+
27
+ from ._version import __version__
28
+
29
+ __all__ = [
30
+ "CaptchaClient",
31
+ "AsyncCaptchaClient",
32
+ "CaptchaError",
33
+ "ApiError",
34
+ "NetworkError",
35
+ "CaptchaTimeoutError",
36
+ "TimeoutError",
37
+ "ValidationError",
38
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,248 @@
1
+ """
2
+ Async counterpart of captcha_solver_api.client.CaptchaClient -- same endpoints
3
+ (createTask / getTaskResult / getBalance), same method names/arguments,
4
+ `await`ed. Requires httpx.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+
13
+ import httpx
14
+
15
+ from ._version import __version__
16
+ from .exceptions import (
17
+ ApiError,
18
+ NetworkError,
19
+ CaptchaTimeoutError,
20
+ ValidationError,
21
+ )
22
+
23
+
24
+ class AsyncCaptchaClient:
25
+ """
26
+ Async client for interacting with the Captcha Solver API.
27
+
28
+ Holds a single, reused `httpx.AsyncClient` connection pool for the
29
+ lifetime of the instance (created once, not per request), so repeated
30
+ calls -- especially the `getTaskResult` polling inside `solve()` --
31
+ reuse the same keep-alive connection instead of paying a fresh TCP/TLS
32
+ handshake every time. Close it with `aclose()` when you're done, or use
33
+ it as an async context manager:
34
+
35
+ Example:
36
+ async with AsyncCaptchaClient("YOUR_API_KEY") as client:
37
+ result = await client.solve(task)
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ client_key: str,
43
+ base_url: str = "https://api.captcha-solver.com",
44
+ timeout: int = 120,
45
+ polling_interval: int = 3,
46
+ language_pool: Optional[str] = None,
47
+ ) -> None:
48
+ """
49
+ Args:
50
+ client_key: Your Captcha Solver API key.
51
+ base_url: API base URL. Override only for self-hosted or staging
52
+ deployments.
53
+ timeout: Default max seconds `solve()` waits for a solution before
54
+ raising `CaptchaTimeoutError`. Can be overridden per call.
55
+ polling_interval: Seconds to wait between `getTaskResult` polls
56
+ inside `solve()`.
57
+ language_pool: Default worker pool selector (e.g. `"en"` or `"ru"`)
58
+ applied to every `create_task()`/`solve()` call that doesn't pass
59
+ its own `language_pool`. Leave unset to use the account's default
60
+ pool.
61
+
62
+ Raises:
63
+ ValidationError: `client_key` is empty.
64
+ """
65
+ if not client_key:
66
+ raise ValidationError("client_key is required")
67
+
68
+ self.client_key = client_key
69
+ self.base_url = base_url.rstrip("/")
70
+ self.timeout = timeout
71
+ self.polling_interval = polling_interval
72
+ self.language_pool = language_pool
73
+
74
+ self._client = httpx.AsyncClient(
75
+ follow_redirects=True,
76
+ headers={
77
+ "Content-Type": "application/json",
78
+ "Accept": "application/json",
79
+ "X-SDK": f"python-sdk/{__version__}",
80
+ },
81
+ )
82
+
83
+ async def aclose(self) -> None:
84
+ """Closes the underlying connection pool. Call this when you're done with
85
+ the client, or use it as an async context manager
86
+ (`async with AsyncCaptchaClient(...) as c:`) to have it closed automatically."""
87
+ await self._client.aclose()
88
+
89
+ async def __aenter__(self) -> "AsyncCaptchaClient":
90
+ return self
91
+
92
+ async def __aexit__(self, *exc_info: Any) -> None:
93
+ await self.aclose()
94
+
95
+ async def _request(
96
+ self,
97
+ endpoint: str,
98
+ payload: Dict[str, Any],
99
+ ) -> Dict[str, Any]:
100
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
101
+
102
+ try:
103
+ response = await self._client.post(url, json=payload, timeout=30)
104
+ response.raise_for_status()
105
+ except httpx.TimeoutException as exc:
106
+ raise CaptchaTimeoutError("Request timed out.") from exc
107
+ except httpx.HTTPError as exc:
108
+ raise NetworkError(str(exc)) from exc
109
+
110
+ try:
111
+ data = response.json()
112
+ except ValueError as exc:
113
+ raise NetworkError(f"Non-JSON response from API: {response.text[:200]!r}") from exc
114
+
115
+ return data
116
+
117
+ def _ensure_success(self, data: Dict[str, Any]) -> None:
118
+ if data.get("errorId", 0) != 0:
119
+ raise ApiError(
120
+ data.get("errorCode", "UNKNOWN_ERROR"),
121
+ data.get("errorDescription", "Unknown API error."),
122
+ )
123
+
124
+ async def create_task(self, task: Any, language_pool: Optional[str] = None) -> int:
125
+ """Submit a CAPTCHA task and return its ID without waiting for a solution.
126
+
127
+ This calls the API `createTask` endpoint. Use `solve()` for the usual
128
+ submit-and-wait flow, or use this method when polling must be managed
129
+ separately, for example when checking several tasks from another process.
130
+ See the `createTask` and CAPTCHA type details in the
131
+ `https://captcha-solver.com/en/docs/captcha-types` documentation.
132
+
133
+ Args:
134
+ task: A task object from `captcha_solver_api.tasks`.
135
+ language_pool: Optional worker pool selector such as `"en"` or
136
+ `"ru"`. Falls back to the client's configured pool.
137
+
138
+ Returns:
139
+ The numeric task ID to pass to `get_task_result()`.
140
+
141
+ Raises:
142
+ ApiError: The API rejected the task or its parameters.
143
+ NetworkError: The request failed at the transport level.
144
+ CaptchaTimeoutError: The HTTP request timed out.
145
+ """
146
+ payload: Dict[str, Any] = {
147
+ "clientKey": self.client_key,
148
+ "task": task.to_dict(),
149
+ }
150
+ pool = language_pool if language_pool is not None else self.language_pool
151
+ if pool:
152
+ payload["languagePool"] = pool
153
+
154
+ data = await self._request("createTask", payload)
155
+ self._ensure_success(data)
156
+ return data["taskId"]
157
+
158
+ async def get_task_result(self, task_id: int) -> Dict[str, Any]:
159
+ """Fetch the current status of a previously submitted task.
160
+
161
+ This performs one asynchronous poll of `getTaskResult`; it does not
162
+ wait until the task is ready. Call it repeatedly until the response has
163
+ `status == "ready"`, or use `solve()` to handle polling automatically.
164
+ See `https://captcha-solver.com/en/docs/captcha-types` for the response
165
+ fields returned by each CAPTCHA type.
166
+
167
+ Args:
168
+ task_id: The ID returned by `create_task()`.
169
+
170
+ Returns:
171
+ The raw API response with `status`; ready responses also contain a
172
+ solution dictionary.
173
+
174
+ Raises:
175
+ ApiError: The API reports an error for the task.
176
+ NetworkError: The request failed at the transport level.
177
+ """
178
+ payload = {
179
+ "clientKey": self.client_key,
180
+ "taskId": task_id,
181
+ }
182
+ data = await self._request("getTaskResult", payload)
183
+ self._ensure_success(data)
184
+ return data
185
+
186
+ async def get_balance(self) -> float:
187
+ """Fetch the account's current balance asynchronously.
188
+
189
+ Calls the `getBalance` endpoint. See the API documentation at
190
+ `https://captcha-solver.com/en/docs/captcha-types` for account and API
191
+ requirements.
192
+
193
+ Returns:
194
+ The available balance in the account's currency.
195
+
196
+ Raises:
197
+ ApiError: The API key is invalid or the account cannot be resolved.
198
+ NetworkError: The request failed at the transport level.
199
+ """
200
+ payload = {"clientKey": self.client_key}
201
+ data = await self._request("getBalance", payload)
202
+ self._ensure_success(data)
203
+ return data["balance"]
204
+
205
+ async def solve(
206
+ self,
207
+ task: Any,
208
+ language_pool: Optional[str] = None,
209
+ timeout: Optional[int] = None,
210
+ ) -> Dict[str, Any]:
211
+ """Submit a task and asynchronously poll until its solution is ready.
212
+
213
+ This is the main entry point for the async client. It combines
214
+ `create_task()` and repeated `get_task_result()` calls, waiting between
215
+ polls without blocking the event loop. For several concurrent solves,
216
+ create multiple coroutines and await them with `asyncio.gather()`.
217
+ See the CAPTCHA-specific request and response formats at
218
+ `https://captcha-solver.com/en/docs/captcha-types`.
219
+
220
+ Args:
221
+ task: A task object from `captcha_solver_api.tasks`.
222
+ language_pool: Optional worker pool selector. Falls back to the
223
+ client's configured pool.
224
+ timeout: Maximum polling time for this call, in seconds. Overrides
225
+ the client's default timeout.
226
+
227
+ Returns:
228
+ The solution dictionary once the task status becomes `"ready"`.
229
+
230
+ Raises:
231
+ ApiError: The API rejected the task or reported a solving error.
232
+ CaptchaTimeoutError: No solution was ready before the deadline.
233
+ NetworkError: A request failed at the transport level.
234
+ """
235
+
236
+ task_id = await self.create_task(task, language_pool=language_pool)
237
+
238
+ deadline = time.time() + (timeout if timeout is not None else self.timeout)
239
+
240
+ while time.time() < deadline:
241
+ result = await self.get_task_result(task_id)
242
+
243
+ if result.get("status") == "ready":
244
+ return result["solution"]
245
+
246
+ await asyncio.sleep(self.polling_interval)
247
+
248
+ raise CaptchaTimeoutError("Task solving timed out.")
@@ -0,0 +1,230 @@
1
+ """
2
+ Main API client for the Captcha Solver service.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from typing import Any, Dict, Optional
9
+
10
+ import requests
11
+
12
+ from ._version import __version__
13
+ from .exceptions import (
14
+ ApiError,
15
+ NetworkError,
16
+ CaptchaTimeoutError,
17
+ ValidationError,
18
+ )
19
+
20
+
21
+ class CaptchaClient:
22
+ """
23
+ Main client for interacting with the Captcha Solver API.
24
+
25
+ Example:
26
+ client = CaptchaClient("YOUR_API_KEY")
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ client_key: str,
32
+ base_url: str = "https://api.captcha-solver.com",
33
+ timeout: int = 120,
34
+ polling_interval: int = 3,
35
+ language_pool: Optional[str] = None,
36
+ ) -> None:
37
+ """
38
+ Args:
39
+ client_key: Your Captcha Solver API key.
40
+ base_url: API base URL. Override only for self-hosted or staging
41
+ deployments.
42
+ timeout: Default max seconds `solve()` waits for a solution before
43
+ raising `CaptchaTimeoutError`. Can be overridden per call.
44
+ polling_interval: Seconds to wait between `getTaskResult` polls
45
+ inside `solve()`.
46
+ language_pool: Default worker pool selector (e.g. `"en"` or `"ru"`)
47
+ applied to every `create_task()`/`solve()` call that doesn't pass
48
+ its own `language_pool`. Leave unset to use the account's default
49
+ pool.
50
+
51
+ Raises:
52
+ ValidationError: `client_key` is empty.
53
+ """
54
+ if not client_key:
55
+ raise ValidationError("client_key is required")
56
+
57
+ self.client_key = client_key
58
+ self.base_url = base_url.rstrip("/")
59
+ self.timeout = timeout
60
+ self.polling_interval = polling_interval
61
+ self.language_pool = language_pool
62
+
63
+ self.session = requests.Session()
64
+ self.session.headers.update(
65
+ {
66
+ "Content-Type": "application/json",
67
+ "Accept": "application/json",
68
+ "X-SDK": f"python-sdk/{__version__}",
69
+ }
70
+ )
71
+
72
+ def close(self) -> None:
73
+ """Closes the underlying connection pool. Call this when you're done with
74
+ the client, or use it as a context manager (`with CaptchaClient(...) as c:`)
75
+ to have it closed automatically."""
76
+ self.session.close()
77
+
78
+ def __enter__(self) -> "CaptchaClient":
79
+ return self
80
+
81
+ def __exit__(self, *exc_info: Any) -> None:
82
+ self.close()
83
+
84
+ def _request(
85
+ self,
86
+ endpoint: str,
87
+ payload: Dict[str, Any],
88
+ ) -> Dict[str, Any]:
89
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
90
+
91
+ try:
92
+ response = self.session.post(url, json=payload, timeout=30)
93
+ response.raise_for_status()
94
+ except requests.exceptions.Timeout as exc:
95
+ raise CaptchaTimeoutError("Request timed out.") from exc
96
+ except requests.exceptions.RequestException as exc:
97
+ raise NetworkError(str(exc)) from exc
98
+
99
+ try:
100
+ data = response.json()
101
+ except ValueError as exc:
102
+ raise NetworkError(f"Non-JSON response from API: {response.text[:200]!r}") from exc
103
+
104
+ return data
105
+
106
+ def _ensure_success(self, data: Dict[str, Any]) -> None:
107
+ if data.get("errorId", 0) != 0:
108
+ raise ApiError(
109
+ data.get("errorCode", "UNKNOWN_ERROR"),
110
+ data.get("errorDescription", "Unknown API error."),
111
+ )
112
+
113
+ def create_task(self, task: Any, language_pool: Optional[str] = None) -> int:
114
+ """Submits a captcha task and returns its task ID without waiting for a solution.
115
+
116
+ Calls the `createTask` endpoint. Prefer `solve()` unless you need to manage
117
+ polling yourself (e.g. to check on many tasks from a different process).
118
+
119
+ Args:
120
+ task: One of the task objects from `captcha_solver_api.tasks` (e.g.
121
+ `RecaptchaV2TaskProxyless`, `ImageToTextTask`).
122
+ language_pool: Worker pool selector, e.g. `"en"` or `"ru"`. Falls back
123
+ to the client's `language_pool` (set at construction) when omitted.
124
+
125
+ Returns:
126
+ The numeric task ID to pass to `get_task_result()`.
127
+
128
+ Raises:
129
+ ApiError: The API rejected the task (bad key, bad parameters, etc).
130
+ NetworkError: The request failed at the transport level.
131
+ CaptchaTimeoutError: The HTTP request itself timed out (not the solve).
132
+ """
133
+ payload: Dict[str, Any] = {
134
+ "clientKey": self.client_key,
135
+ "task": task.to_dict(),
136
+ }
137
+ pool = language_pool if language_pool is not None else self.language_pool
138
+ if pool:
139
+ payload["languagePool"] = pool
140
+
141
+ data = self._request("createTask", payload)
142
+ self._ensure_success(data)
143
+ return data["taskId"]
144
+
145
+ def get_task_result(self, task_id: int) -> Dict[str, Any]:
146
+ """Fetches the current status of a task created with `create_task()`.
147
+
148
+ Calls the `getTaskResult` endpoint. This is a single poll, not a wait --
149
+ call it repeatedly (as `solve()` does) until `status` is `"ready"`.
150
+
151
+ Args:
152
+ task_id: The ID returned by `create_task()`.
153
+
154
+ Returns:
155
+ The raw API response. Always has a `status` key (`"processing"` or
156
+ `"ready"`); when `"ready"`, also has a `solution` dict whose shape
157
+ depends on the task type (e.g. `{"gRecaptchaResponse": "..."}` for
158
+ reCAPTCHA, `{"text": "..."}` for `ImageToTextTask`).
159
+
160
+ Raises:
161
+ ApiError: The API reports an error for this task (e.g. it expired).
162
+ NetworkError: The request failed at the transport level.
163
+ """
164
+ payload = {
165
+ "clientKey": self.client_key,
166
+ "taskId": task_id,
167
+ }
168
+ data = self._request("getTaskResult", payload)
169
+ self._ensure_success(data)
170
+ return data
171
+
172
+ def get_balance(self) -> float:
173
+ """Fetches the account's current balance.
174
+
175
+ Calls the `getBalance` endpoint.
176
+
177
+ Returns:
178
+ The available balance, in the account's currency.
179
+
180
+ Raises:
181
+ ApiError: The API key is invalid or the account can't be resolved.
182
+ NetworkError: The request failed at the transport level.
183
+ """
184
+ payload = {"clientKey": self.client_key}
185
+ data = self._request("getBalance", payload)
186
+ self._ensure_success(data)
187
+ return data["balance"]
188
+
189
+ def solve(
190
+ self,
191
+ task: Any,
192
+ language_pool: Optional[str] = None,
193
+ timeout: Optional[int] = None,
194
+ ) -> Dict[str, Any]:
195
+ """Submits `task` and polls until it's solved. This is the main entry point --
196
+ it wraps `create_task()` and `get_task_result()` so you don't have to poll
197
+ by hand.
198
+
199
+ Args:
200
+ task: One of the task objects from `captcha_solver_api.tasks`.
201
+ language_pool: Worker pool selector, e.g. `"en"` or `"ru"`. Falls back
202
+ to the client's `language_pool` (set at construction) when omitted.
203
+ timeout: Overrides the client's default polling timeout for this call
204
+ only (useful for captcha types that reliably take longer, e.g.
205
+ classic reCAPTCHA v2), in seconds.
206
+
207
+ Returns:
208
+ The `solution` dict once `status` is `"ready"`. Its shape depends on
209
+ the task type -- see the per-type docstrings in `captcha_solver_api.tasks`
210
+ or the README's method reference.
211
+
212
+ Raises:
213
+ ApiError: The API rejected the task or reported an error while solving.
214
+ CaptchaTimeoutError: No solution was ready before the deadline.
215
+ NetworkError: A request failed at the transport level.
216
+ """
217
+
218
+ task_id = self.create_task(task, language_pool=language_pool)
219
+
220
+ deadline = time.time() + (timeout if timeout is not None else self.timeout)
221
+
222
+ while time.time() < deadline:
223
+ result = self.get_task_result(task_id)
224
+
225
+ if result.get("status") == "ready":
226
+ return result["solution"]
227
+
228
+ time.sleep(self.polling_interval)
229
+
230
+ raise CaptchaTimeoutError("Task solving timed out.")
@@ -0,0 +1,41 @@
1
+ """
2
+ Custom exceptions used by the Captcha Solver SDK.
3
+ """
4
+
5
+
6
+ class CaptchaError(Exception):
7
+ """Base exception for all SDK errors."""
8
+
9
+ pass
10
+
11
+
12
+ class NetworkError(CaptchaError):
13
+ """Raised when a network request fails."""
14
+
15
+ pass
16
+
17
+
18
+ class CaptchaTimeoutError(CaptchaError):
19
+ """Raised when the operation exceeds the configured timeout."""
20
+
21
+ pass
22
+
23
+
24
+ # Deprecated alias kept for backward compatibility. It shadows the built-in
25
+ # ``TimeoutError`` when imported by name, so prefer ``CaptchaTimeoutError``.
26
+ TimeoutError = CaptchaTimeoutError
27
+
28
+
29
+ class ApiError(CaptchaError):
30
+ """Raised when the API returns an error."""
31
+
32
+ def __init__(self, error_code: str, error_description: str) -> None:
33
+ self.error_code = error_code
34
+ self.error_description = error_description
35
+ super().__init__(f"{error_code}: {error_description}")
36
+
37
+
38
+ class ValidationError(CaptchaError):
39
+ """Raised for client-side argument problems caught before any request is sent."""
40
+
41
+ pass
File without changes