Skip to content

Commit 157d23d

Browse files
committed
Upbraded SDK
1 parent ac9b189 commit 157d23d

53 files changed

Lines changed: 2977 additions & 81 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/tests.yml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
python-version: ["3.9", "3.11", "3.13"]
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python ${{ matrix.python-version }}
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
21+
- name: Install package with dev dependencies
22+
run: python -m pip install -e ".[dev]"
23+
24+
- name: Run tests
25+
run: python -m pytest tests/ -v
26+
27+
- name: Verify package builds
28+
run: |
29+
python -m pip install build
30+
python -m build --sdist --wheel

‎README.md‎

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
# Captcha Solver Python SDK
22
![python-examples-banner](assets/repo-banner-python.png)
33

4-
Official Python SDK for the Captcha Solver API. Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, and image captchas with a single method call.
4+
Official Python SDK for the Captcha Solver API. Solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, Yandex SmartCaptcha, Tencent, and image/click captchas with a single method call -- sync or async.
55

66
## Table of Contents
77

88
- [Installation](#installation)
9+
- [Configuration](#configuration)
910
- [Quick Start](#quick-start)
1011
- [Supported CAPTCHA Types](#supported-captcha-types)
1112
- [Usage Examples](#usage-examples)
12-
- [reCAPTCHA v2](#recaptcha-v2)
13+
- [reCAPTCHA v2 with proxy](#recaptcha-v2-with-proxy)
1314
- [reCAPTCHA v2 Enterprise](#recaptcha-v2-enterprise)
1415
- [reCAPTCHA v3](#recaptcha-v3)
1516
- [Cloudflare Turnstile](#cloudflare-turnstile)
1617
- [Image to Text](#image-to-text)
1718
- [GeeTest v3](#geetest-v3)
1819
- [GeeTest v4](#geetest-v4)
20+
- [Yandex SmartCaptcha](#yandex-smartcaptcha)
21+
- [Coordinates (click captcha)](#coordinates-click-captcha)
22+
- [Tencent](#tencent)
1923
- [Check Balance](#check-balance)
20-
- [Custom Timeout](#custom-timeout-and-polling)
24+
- [Custom Timeout and Polling](#custom-timeout-and-polling)
25+
- [Async Client](#async-client)
2126
- [Error Handling](#error-handling)
2227
- [Requirements](#requirements)
2328
- [API Documentation](#api-documentation)
@@ -30,13 +35,18 @@ Official Python SDK for the Captcha Solver API. Solve reCAPTCHA v2, reCAPTCHA v3
3035
pip install git+https://github.com/captcha-solver-api/python-sdk.git
3136
```
3237
## Configuration
33-
Set your API key as an environment variable.
38+
The client always takes the API key as an explicit argument -- it does not read
39+
environment variables on its own. Read `CAPTCHA_API_KEY` yourself and pass it in:
3440
```bash
3541
export CAPTCHA_API_KEY=your_api_key
3642
```
37-
Or pass it directly to the client.
3843
```python
44+
import os
3945
from captcha_sdk import CaptchaClient
46+
client = CaptchaClient(os.getenv("CAPTCHA_API_KEY"))
47+
```
48+
Or just pass the key directly, without an environment variable:
49+
```python
4050
client = CaptchaClient("your_api_key")
4151
```
4252
## Quick Start
@@ -62,6 +72,9 @@ print(result["gRecaptchaResponse"])
6272
| GeeTest v3 | ✅ | ✅ |
6373
| GeeTest v4 | ✅ | ✅ |
6474
| Image to Text | ✅ | ❌ |
75+
| Yandex SmartCaptcha | ✅ | ✅ |
76+
| Coordinates (click captcha) | ✅ | ❌ |
77+
| Tencent | ✅ | ✅ |
6578
## Usage Examples
6679
### reCAPTCHA v2 with proxy
6780
```python
@@ -165,6 +178,49 @@ task = GeeTestTaskProxyless(
165178
result = client.solve(task)
166179
print(result["captcha_output"])
167180
```
181+
### Yandex SmartCaptcha
182+
```python
183+
from captcha_sdk import CaptchaClient
184+
from captcha_sdk.tasks import YandexSmartCaptchaTaskProxyless
185+
client = CaptchaClient("your_api_key")
186+
task = YandexSmartCaptchaTaskProxyless(
187+
websiteURL="https://example.com/login",
188+
websiteKey="Y5Lh0ti..."
189+
)
190+
result = client.solve(task)
191+
print(result["token"])
192+
```
193+
Use `YandexSmartCaptchaTask` instead for the with-proxy variant (same extra `proxyType`/`proxyAddress`/`proxyPort`/`proxyLogin`/`proxyPassword` fields as `RecaptchaV2Task`).
194+
195+
To solve Yandex SmartCaptcha's image challenge instead of the token challenge, use `CoordinatesTask` with `imgType="smart_captcha"` or `imgType="pazl_smart_captcha"` -- see [examples/sync/yandex_smartcaptcha_image.py](examples/sync/yandex_smartcaptcha_image.py) (or [examples/async](examples/async/yandex_smartcaptcha_image.py) for the async version).
196+
197+
### Coordinates (click captcha)
198+
```python
199+
from captcha_sdk import CaptchaClient
200+
from captcha_sdk.tasks import CoordinatesTask
201+
import base64
202+
with open("captcha.png", "rb") as f:
203+
image_base64 = base64.b64encode(f.read()).decode("utf-8")
204+
client = CaptchaClient("your_api_key")
205+
task = CoordinatesTask(
206+
body=image_base64,
207+
comment="click on the green apple"
208+
)
209+
result = client.solve(task)
210+
print(result["coordinates"]) # [{"x": 358, "y": 268}, ...]
211+
```
212+
### Tencent
213+
```python
214+
from captcha_sdk import CaptchaClient
215+
from captcha_sdk.tasks import TencentTaskProxyless
216+
client = CaptchaClient("your_api_key")
217+
task = TencentTaskProxyless(
218+
websiteURL="https://example.com/login",
219+
appId="190014885"
220+
)
221+
result = client.solve(task)
222+
print(result["ticket"])
223+
```
168224
### Check balance
169225
```python
170226
from captcha_sdk import CaptchaClient
@@ -180,12 +236,63 @@ client = CaptchaClient(
180236
polling_interval=5
181237
)
182238
```
239+
A single `solve()` call can also override the client's default timeout, which is handy for
240+
captcha types that reliably take longer to solve (e.g. classic reCAPTCHA v2) without changing
241+
it for every other call:
242+
```python
243+
result = client.solve(task, timeout=300)
244+
```
245+
### Async client
246+
`AsyncCaptchaClient` mirrors `CaptchaClient` method-for-method (`create_task`, `get_task_result`,
247+
`get_balance`, `solve`, same constructor options), just `await`ed and built on `httpx` instead of
248+
`requests`:
249+
```python
250+
import asyncio
251+
from captcha_sdk import AsyncCaptchaClient
252+
from captcha_sdk.tasks import RecaptchaV2TaskProxyless
253+
254+
async def main():
255+
client = AsyncCaptchaClient("your_api_key")
256+
task = RecaptchaV2TaskProxyless(
257+
websiteURL="https://example.com/login",
258+
websiteKey="6Le-xxxxxxxxx"
259+
)
260+
result = await client.solve(task)
261+
print(result["gRecaptchaResponse"])
262+
263+
asyncio.run(main())
264+
```
265+
See [examples/async](examples/async) for more.
266+
267+
#### Solving multiple captchas in parallel
268+
This is the main reason to reach for the async client -- run several `solve()` calls
269+
concurrently instead of waiting for each one in turn:
270+
```python
271+
import asyncio
272+
from captcha_sdk import AsyncCaptchaClient
273+
from captcha_sdk.tasks import RecaptchaV2TaskProxyless, TurnstileTaskProxyless
274+
275+
async def solve_multiple():
276+
client = AsyncCaptchaClient("your_api_key")
277+
278+
task1 = client.solve(RecaptchaV2TaskProxyless(websiteURL="https://site1.com", websiteKey="key1"))
279+
task2 = client.solve(TurnstileTaskProxyless(websiteURL="https://site2.com", websiteKey="key2"))
280+
281+
results = await asyncio.gather(task1, task2, return_exceptions=True)
282+
return results
283+
284+
results = asyncio.run(solve_multiple())
285+
```
286+
This completes in roughly the time of the slowest single captcha, not the sum of all of them.
287+
183288
### Error handling
184289
```python
185-
from captcha_sdk import CaptchaClient, ApiError, TimeoutError, NetworkError
290+
from captcha_sdk import CaptchaClient, ApiError, TimeoutError, NetworkError, ValidationError
186291
client = CaptchaClient("your_api_key")
187292
try:
188293
result = client.solve(task)
294+
except ValidationError as e:
295+
print(f"Invalid argument: {e}")
189296
except ApiError as e:
190297
print(f"API error: {e.error_code} {e.error_description}")
191298
except TimeoutError:

‎captcha_sdk/__init__.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@
1414
"""
1515

1616
from .client import CaptchaClient
17+
from .async_client import AsyncCaptchaClient
1718
from .exceptions import (
1819
CaptchaError,
1920
ApiError,
2021
NetworkError,
2122
TimeoutError,
23+
ValidationError,
2224
)
2325

24-
__version__ = "1.0.0"
26+
__version__ = "1.1.0"
2527

2628
__all__ = [
2729
"CaptchaClient",
30+
"AsyncCaptchaClient",
2831
"CaptchaError",
2932
"ApiError",
3033
"NetworkError",
3134
"TimeoutError",
35+
"ValidationError",
3236
]

‎captcha_sdk/async_client.py‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Async counterpart of captcha_sdk.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 .exceptions import (
16+
ApiError,
17+
NetworkError,
18+
TimeoutError,
19+
ValidationError,
20+
)
21+
22+
23+
class AsyncCaptchaClient:
24+
"""
25+
Async client for interacting with the Captcha Solver API.
26+
27+
Example:
28+
client = AsyncCaptchaClient("YOUR_API_KEY")
29+
result = await client.solve(task)
30+
"""
31+
32+
def __init__(
33+
self,
34+
client_key: str,
35+
base_url: str = "https://api.captcha-solver.com",
36+
timeout: int = 120,
37+
polling_interval: int = 3,
38+
) -> None:
39+
if not client_key:
40+
raise ValidationError("client_key is required")
41+
42+
self.client_key = client_key
43+
self.base_url = base_url.rstrip("/")
44+
self.timeout = timeout
45+
self.polling_interval = polling_interval
46+
47+
async def _request(
48+
self,
49+
endpoint: str,
50+
payload: Dict[str, Any],
51+
) -> Dict[str, Any]:
52+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
53+
54+
try:
55+
async with httpx.AsyncClient(follow_redirects=True) as client:
56+
response = await client.post(
57+
url,
58+
json=payload,
59+
timeout=30,
60+
headers={"Content-Type": "application/json", "Accept": "application/json"},
61+
)
62+
response.raise_for_status()
63+
except httpx.TimeoutException as exc:
64+
raise TimeoutError("Request timed out.") from exc
65+
except httpx.HTTPError as exc:
66+
raise NetworkError(str(exc)) from exc
67+
68+
try:
69+
data = response.json()
70+
except ValueError as exc:
71+
raise NetworkError(f"Non-JSON response from API: {response.text[:200]!r}") from exc
72+
73+
return data
74+
75+
def _ensure_success(self, data: Dict[str, Any]) -> None:
76+
if data.get("errorId", 0) != 0:
77+
raise ApiError(
78+
data.get("errorCode", "UNKNOWN_ERROR"),
79+
data.get("errorDescription", "Unknown API error."),
80+
)
81+
82+
async def create_task(self, task: Any, language_pool: Optional[str] = None) -> int:
83+
payload: Dict[str, Any] = {
84+
"clientKey": self.client_key,
85+
"task": task.to_dict(),
86+
}
87+
if language_pool:
88+
payload["languagePool"] = language_pool
89+
90+
data = await self._request("createTask", payload)
91+
self._ensure_success(data)
92+
return data["taskId"]
93+
94+
async def get_task_result(self, task_id: int) -> Dict[str, Any]:
95+
payload = {
96+
"clientKey": self.client_key,
97+
"taskId": task_id,
98+
}
99+
data = await self._request("getTaskResult", payload)
100+
self._ensure_success(data)
101+
return data
102+
103+
async def get_balance(self) -> float:
104+
payload = {"clientKey": self.client_key}
105+
data = await self._request("getBalance", payload)
106+
self._ensure_success(data)
107+
return data["balance"]
108+
109+
async def solve(
110+
self,
111+
task: Any,
112+
language_pool: Optional[str] = None,
113+
timeout: Optional[int] = None,
114+
) -> Dict[str, Any]:
115+
"""See captcha_sdk.client.CaptchaClient.solve -- same semantics, awaited."""
116+
117+
task_id = await self.create_task(task, language_pool=language_pool)
118+
119+
deadline = time.time() + (timeout if timeout is not None else self.timeout)
120+
121+
while time.time() < deadline:
122+
result = await self.get_task_result(task_id)
123+
124+
if result.get("status") == "ready":
125+
return result["solution"]
126+
127+
await asyncio.sleep(self.polling_interval)
128+
129+
raise TimeoutError("Task solving timed out.")

0 commit comments

Comments
 (0)