capmonster-client 0.1.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.
- capmonster/__init__.py +79 -0
- capmonster/client.py +299 -0
- capmonster/errors.py +96 -0
- capmonster/tasks.py +230 -0
- capmonster/types.py +76 -0
- capmonster_client-0.1.0.dist-info/METADATA +211 -0
- capmonster_client-0.1.0.dist-info/RECORD +9 -0
- capmonster_client-0.1.0.dist-info/WHEEL +4 -0
- capmonster_client-0.1.0.dist-info/licenses/LICENSE +15 -0
capmonster/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""CapMonster Cloud recognition API client."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_SOFT_ID, CapMonster
|
|
4
|
+
from .errors import CAPMONSTER_ERRORS, CapMonsterError
|
|
5
|
+
from .tasks import (
|
|
6
|
+
alibaba,
|
|
7
|
+
altcha,
|
|
8
|
+
amazon,
|
|
9
|
+
basilisk,
|
|
10
|
+
binance,
|
|
11
|
+
castle,
|
|
12
|
+
complex_image,
|
|
13
|
+
datadome,
|
|
14
|
+
friendly_captcha,
|
|
15
|
+
funcaptcha,
|
|
16
|
+
geetest,
|
|
17
|
+
hcaptcha,
|
|
18
|
+
hunt,
|
|
19
|
+
image_to_text,
|
|
20
|
+
imperva,
|
|
21
|
+
mtcaptcha,
|
|
22
|
+
prosopo,
|
|
23
|
+
recaptcha_v2,
|
|
24
|
+
recaptcha_v2_enterprise,
|
|
25
|
+
recaptcha_v3,
|
|
26
|
+
recaptcha_v3_enterprise,
|
|
27
|
+
temu,
|
|
28
|
+
tendi,
|
|
29
|
+
tspd,
|
|
30
|
+
turnstile,
|
|
31
|
+
yidun,
|
|
32
|
+
)
|
|
33
|
+
from .types import (
|
|
34
|
+
TASK_CLASSES,
|
|
35
|
+
TASK_TYPES,
|
|
36
|
+
CapMonsterBalance,
|
|
37
|
+
RawApiResponse,
|
|
38
|
+
TaskResult,
|
|
39
|
+
TaskStatus,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"CAPMONSTER_ERRORS",
|
|
44
|
+
"DEFAULT_SOFT_ID",
|
|
45
|
+
"TASK_CLASSES",
|
|
46
|
+
"TASK_TYPES",
|
|
47
|
+
"CapMonster",
|
|
48
|
+
"CapMonsterBalance",
|
|
49
|
+
"CapMonsterError",
|
|
50
|
+
"RawApiResponse",
|
|
51
|
+
"TaskResult",
|
|
52
|
+
"TaskStatus",
|
|
53
|
+
"alibaba",
|
|
54
|
+
"altcha",
|
|
55
|
+
"amazon",
|
|
56
|
+
"basilisk",
|
|
57
|
+
"binance",
|
|
58
|
+
"castle",
|
|
59
|
+
"complex_image",
|
|
60
|
+
"datadome",
|
|
61
|
+
"friendly_captcha",
|
|
62
|
+
"funcaptcha",
|
|
63
|
+
"geetest",
|
|
64
|
+
"hcaptcha",
|
|
65
|
+
"hunt",
|
|
66
|
+
"image_to_text",
|
|
67
|
+
"imperva",
|
|
68
|
+
"mtcaptcha",
|
|
69
|
+
"prosopo",
|
|
70
|
+
"recaptcha_v2",
|
|
71
|
+
"recaptcha_v2_enterprise",
|
|
72
|
+
"recaptcha_v3",
|
|
73
|
+
"recaptcha_v3_enterprise",
|
|
74
|
+
"temu",
|
|
75
|
+
"tendi",
|
|
76
|
+
"tspd",
|
|
77
|
+
"turnstile",
|
|
78
|
+
"yidun",
|
|
79
|
+
]
|
capmonster/client.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Client for the CapMonster Cloud recognition API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import CapMonsterError, describe_error
|
|
12
|
+
|
|
13
|
+
_DEFAULT_BASE_URL = "https://api.capmonster.cloud"
|
|
14
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
15
|
+
|
|
16
|
+
# Developer software id sent as `softId` on createTask.
|
|
17
|
+
#
|
|
18
|
+
# TODO: CapMonster Cloud's developer program is pending approval, so no
|
|
19
|
+
# softId has been issued yet. Leave this empty. When a numeric id is
|
|
20
|
+
# assigned, set it here. Requests omit `softId` until this constant or the
|
|
21
|
+
# client `soft_id` argument is a non-empty value.
|
|
22
|
+
#
|
|
23
|
+
# The createTask field name is `softId` (same name as the official client).
|
|
24
|
+
# https://docs.capmonster.cloud/docs/api/methods/create-task/
|
|
25
|
+
DEFAULT_SOFT_ID = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CapMonster:
|
|
29
|
+
"""Client for the CapMonster Cloud recognition API.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
client_key: CapMonster Cloud client key.
|
|
33
|
+
base_url: Custom API base URL (defaults to `https://api.capmonster.cloud`).
|
|
34
|
+
timeout: Request timeout in seconds (defaults to 30.0).
|
|
35
|
+
soft_id: Developer software id. Empty until the developer program assigns one.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
>>> from capmonster import CapMonster
|
|
39
|
+
>>> with CapMonster("your_client_key") as client:
|
|
40
|
+
... balance = client.get_balance()
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
client_key: str,
|
|
46
|
+
*,
|
|
47
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
48
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
49
|
+
soft_id: str | int | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not client_key or not client_key.strip():
|
|
52
|
+
raise CapMonsterError("Client key is required", 1, "MISSING_CLIENT_KEY")
|
|
53
|
+
|
|
54
|
+
self._client_key = client_key.strip()
|
|
55
|
+
self._soft_id: str | int = DEFAULT_SOFT_ID if soft_id is None else soft_id
|
|
56
|
+
self._http = httpx.Client(
|
|
57
|
+
base_url=base_url.rstrip("/"),
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
headers={
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"Accept": "application/json",
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
"""Close the underlying HTTP client."""
|
|
67
|
+
self._http.close()
|
|
68
|
+
|
|
69
|
+
def __enter__(self) -> CapMonster:
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *exc: object) -> None:
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def get_balance(self) -> dict[str, float]:
|
|
76
|
+
"""Retrieve the available account balance.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with a ``balance`` amount.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> with CapMonster("your_key") as client:
|
|
83
|
+
... balance = client.get_balance()
|
|
84
|
+
... print(balance["balance"])
|
|
85
|
+
"""
|
|
86
|
+
response = self._post("/getBalance", {"clientKey": self._client_key})
|
|
87
|
+
return {"balance": float(response.get("balance", 0.0))}
|
|
88
|
+
|
|
89
|
+
def create_task(
|
|
90
|
+
self,
|
|
91
|
+
task: Mapping[str, Any],
|
|
92
|
+
*,
|
|
93
|
+
soft_id: str | int | None = None,
|
|
94
|
+
callback_url: str | None = None,
|
|
95
|
+
) -> int:
|
|
96
|
+
"""Submit a new captcha recognition task.
|
|
97
|
+
|
|
98
|
+
``softId`` is included only when ``DEFAULT_SOFT_ID`` or ``soft_id`` is set.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
task: Task dictionary (must include ``type``).
|
|
102
|
+
soft_id: Override the client software id for this call.
|
|
103
|
+
callback_url: Optional URL that receives the getTaskResult payload.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The submitted numeric task ID.
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
>>> with CapMonster("your_key") as client:
|
|
110
|
+
... task_id = client.create_task({
|
|
111
|
+
... "type": "TurnstileTask",
|
|
112
|
+
... "websiteURL": "https://example.com",
|
|
113
|
+
... "websiteKey": "0x4AAAAAAA...",
|
|
114
|
+
... })
|
|
115
|
+
"""
|
|
116
|
+
payload: dict[str, Any] = {
|
|
117
|
+
"clientKey": self._client_key,
|
|
118
|
+
"task": dict(task),
|
|
119
|
+
}
|
|
120
|
+
chosen = self._soft_id if soft_id is None else soft_id
|
|
121
|
+
if has_soft_id(chosen):
|
|
122
|
+
payload["softId"] = format_soft_id(chosen)
|
|
123
|
+
if callback_url:
|
|
124
|
+
payload["callbackUrl"] = callback_url
|
|
125
|
+
|
|
126
|
+
response = self._post("/createTask", payload)
|
|
127
|
+
task_id = normalize_task_id(response.get("taskId"))
|
|
128
|
+
if task_id is None:
|
|
129
|
+
raise CapMonsterError(
|
|
130
|
+
"Response missing taskId",
|
|
131
|
+
response.get("errorId", 0),
|
|
132
|
+
response.get("errorCode"),
|
|
133
|
+
200,
|
|
134
|
+
response,
|
|
135
|
+
)
|
|
136
|
+
return task_id
|
|
137
|
+
|
|
138
|
+
def get_task_result(self, task_id: int | str) -> dict[str, Any]:
|
|
139
|
+
"""Query the status and solution of a submitted task.
|
|
140
|
+
|
|
141
|
+
``CAPTCHA_NOT_READY`` is returned as ``{"status": "processing"}``.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
task_id: Task identifier returned by ``create_task``.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dictionary containing ``status`` and optional ``solution``.
|
|
148
|
+
"""
|
|
149
|
+
normalized = normalize_task_id(task_id)
|
|
150
|
+
if normalized is None:
|
|
151
|
+
raise CapMonsterError(f"Invalid taskId: {task_id}", 1, "WRONG_CAPTCHA_ID")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
response = self._post(
|
|
155
|
+
"/getTaskResult",
|
|
156
|
+
{"clientKey": self._client_key, "taskId": normalized},
|
|
157
|
+
)
|
|
158
|
+
except CapMonsterError as err:
|
|
159
|
+
if err.error_code == "CAPTCHA_NOT_READY":
|
|
160
|
+
return {"status": "processing", "solution": {}}
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"status": response.get("status", "processing"),
|
|
165
|
+
"solution": response.get("solution", {}),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def wait_for_result(
|
|
169
|
+
self,
|
|
170
|
+
task_id: int | str,
|
|
171
|
+
*,
|
|
172
|
+
timeout: float = 120.0,
|
|
173
|
+
interval: float = 3.0,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Poll ``get_task_result`` until the solution is ready.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
task_id: Task identifier.
|
|
179
|
+
timeout: Maximum wait time in seconds (defaults to 120.0).
|
|
180
|
+
interval: Delay between poll attempts in seconds (defaults to 3.0).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The solution dictionary.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
CapMonsterError: If polling times out or the task fails.
|
|
187
|
+
"""
|
|
188
|
+
start_time = time.monotonic()
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
res = self.get_task_result(task_id)
|
|
192
|
+
if res.get("status") == "ready" and res.get("solution"):
|
|
193
|
+
solution = res["solution"]
|
|
194
|
+
if isinstance(solution, dict):
|
|
195
|
+
return solution
|
|
196
|
+
return {"value": solution}
|
|
197
|
+
|
|
198
|
+
if time.monotonic() - start_time >= timeout:
|
|
199
|
+
raise CapMonsterError(
|
|
200
|
+
"Timed out waiting for captcha solution",
|
|
201
|
+
0,
|
|
202
|
+
"TIMEOUT",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
time.sleep(interval)
|
|
206
|
+
|
|
207
|
+
def solve(
|
|
208
|
+
self,
|
|
209
|
+
task: Mapping[str, Any],
|
|
210
|
+
*,
|
|
211
|
+
timeout: float = 120.0,
|
|
212
|
+
interval: float = 3.0,
|
|
213
|
+
soft_id: str | int | None = None,
|
|
214
|
+
callback_url: str | None = None,
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""Submit a task and wait for its solution.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
task: Task dictionary.
|
|
220
|
+
timeout: Maximum wait time in seconds.
|
|
221
|
+
interval: Delay between poll attempts in seconds.
|
|
222
|
+
soft_id: Override the client software id for this call.
|
|
223
|
+
callback_url: Optional result callback URL.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The solution dictionary.
|
|
227
|
+
"""
|
|
228
|
+
task_id = self.create_task(task, soft_id=soft_id, callback_url=callback_url)
|
|
229
|
+
return self.wait_for_result(task_id, timeout=timeout, interval=interval)
|
|
230
|
+
|
|
231
|
+
def _post(self, path: str, json_data: Any) -> dict[str, Any]:
|
|
232
|
+
try:
|
|
233
|
+
res = self._http.post(path, json=json_data)
|
|
234
|
+
except httpx.HTTPError as err:
|
|
235
|
+
raise CapMonsterError(str(err), 0) from err
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
data = res.json()
|
|
239
|
+
except ValueError:
|
|
240
|
+
data = None
|
|
241
|
+
|
|
242
|
+
if res.is_error:
|
|
243
|
+
error_id = data.get("errorId", 0) if isinstance(data, dict) else 0
|
|
244
|
+
error_code = data.get("errorCode") if isinstance(data, dict) else None
|
|
245
|
+
error_description = data.get("errorDescription") if isinstance(data, dict) else None
|
|
246
|
+
if isinstance(data, dict) and (error_description or error_code or error_id):
|
|
247
|
+
error_msg = describe_error(error_code, error_description, error_id)
|
|
248
|
+
else:
|
|
249
|
+
error_msg = res.text or f"HTTP {res.status_code}"
|
|
250
|
+
raise CapMonsterError(error_msg, error_id, error_code, res.status_code, data)
|
|
251
|
+
|
|
252
|
+
if not isinstance(data, dict):
|
|
253
|
+
raise CapMonsterError(
|
|
254
|
+
"Unexpected non-JSON response from CapMonster Cloud API",
|
|
255
|
+
0,
|
|
256
|
+
None,
|
|
257
|
+
res.status_code,
|
|
258
|
+
res.text,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if data.get("errorId", 0) != 0:
|
|
262
|
+
error_id = int(data.get("errorId", 0))
|
|
263
|
+
error_code = data.get("errorCode")
|
|
264
|
+
msg = describe_error(error_code, data.get("errorDescription"), error_id)
|
|
265
|
+
raise CapMonsterError(msg, error_id, error_code, res.status_code, data)
|
|
266
|
+
|
|
267
|
+
return data
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def has_soft_id(value: str | int | None) -> bool:
|
|
271
|
+
"""True when a softId should be sent. Empty string and 0 are treated as unset."""
|
|
272
|
+
if value is None:
|
|
273
|
+
return False
|
|
274
|
+
if isinstance(value, str):
|
|
275
|
+
return value.strip() != ""
|
|
276
|
+
return value != 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def format_soft_id(value: str | int) -> str | int:
|
|
280
|
+
"""CapMonster expects a numeric softId. Numeric strings are sent as integers."""
|
|
281
|
+
if isinstance(value, int):
|
|
282
|
+
return value
|
|
283
|
+
trimmed = value.strip()
|
|
284
|
+
if trimmed.isdigit():
|
|
285
|
+
return int(trimmed)
|
|
286
|
+
return trimmed
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def normalize_task_id(task_id: Any) -> int | None:
|
|
290
|
+
"""Return a positive integer task id, or None when the value cannot be used."""
|
|
291
|
+
if task_id is None or task_id == "":
|
|
292
|
+
return None
|
|
293
|
+
try:
|
|
294
|
+
value = int(task_id)
|
|
295
|
+
except (TypeError, ValueError):
|
|
296
|
+
return None
|
|
297
|
+
if value <= 0:
|
|
298
|
+
return None
|
|
299
|
+
return value
|
capmonster/errors.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Error types for the CapMonster Cloud API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
# Documented codes: https://docs.capmonster.cloud/docs/api/api-errors/
|
|
8
|
+
CAPMONSTER_ERRORS: dict[str, str] = {
|
|
9
|
+
"ERROR_KEY_DOES_NOT_EXIST": "API key not found or provided in an incorrect format",
|
|
10
|
+
"ERROR_PROXY_CREDENTIALS_INVALID_CHARACTER": (
|
|
11
|
+
"Proxy login or password contains an invalid character"
|
|
12
|
+
),
|
|
13
|
+
"ERROR_ZERO_BALANCE": "Insufficient funds on the account to solve captchas",
|
|
14
|
+
"ERROR_TOO_BIG_CAPTCHA_FILESIZE": (
|
|
15
|
+
"The uploaded image exceeds the maximum allowed size of 50 KB"
|
|
16
|
+
),
|
|
17
|
+
"ERROR_ZERO_CAPTCHA_FILESIZE": "The image size is too small (less than 100 bytes)",
|
|
18
|
+
"ERROR_NO_SUCH_CAPCHA_ID": "Captcha task id was not found",
|
|
19
|
+
"WRONG_CAPTCHA_ID": "Captcha task id is missing, expired, or invalid",
|
|
20
|
+
"ERROR_CAPTCHA_UNSOLVABLE": (
|
|
21
|
+
"The captcha could not be solved. Unsolved tasks are not charged"
|
|
22
|
+
),
|
|
23
|
+
"CAPTCHA_NOT_READY": "The task is still being solved",
|
|
24
|
+
"ERROR_IP_NOT_ALLOWED": "Requests from this IP with the current key are denied",
|
|
25
|
+
"ERROR_IP_BANNED": "The IP address has been temporarily blocked due to invalid requests",
|
|
26
|
+
"ERROR_NO_SUCH_METHOD": "An incorrect task type was specified",
|
|
27
|
+
"ERROR_TOO_MUCH_REQUESTS": "The allowed request limit has been exceeded",
|
|
28
|
+
"ERROR_DOMAIN_NOT_ALLOWED": "Solving captchas from this domain is forbidden",
|
|
29
|
+
"ERROR_TOKEN_EXPIRED": "The provided token or challenge has expired",
|
|
30
|
+
"ERROR_RECAPTCHA_INVALID_SITEKEY": "An invalid websiteKey was provided",
|
|
31
|
+
"ERROR_RECAPTCHA_INVALID_DOMAIN": (
|
|
32
|
+
"The domain does not match the sitekey, or websiteURL is not a full http(s) URL"
|
|
33
|
+
),
|
|
34
|
+
"ERROR_RECAPTCHA_TIMEOUT": "The captcha solving time has expired",
|
|
35
|
+
"ERROR_IP_BLOCKED": "Access from this IP is blocked due to a high number of failed requests",
|
|
36
|
+
"ERROR_PROXY_CONNECT_REFUSED": "The proxy server refused the connection",
|
|
37
|
+
"ERROR_PROXY_BANNED": "The specified proxy is blocked by the target website",
|
|
38
|
+
"ERROR_PROXY_MISSING": "Proxy parameters are missing or proxyType is incorrect",
|
|
39
|
+
"ERROR_PROXY_NOT_AUTHORISED": "Proxy login or password is incorrect",
|
|
40
|
+
"ERROR_PROXY_READ_TIMEOUT": "The response timed out while using the proxy",
|
|
41
|
+
"ERROR_TASK_NOT_SUPPORTED": "The specified task type is unsupported or invalid",
|
|
42
|
+
"ERROR_TASK_ABSENT": "The task object is missing or the JSON is invalid",
|
|
43
|
+
"ERROR_WRONG_USERAGENT": "An invalid User-Agent was provided",
|
|
44
|
+
"ERROR_SERVICE_NOT_AVAILABLE": "The service is temporarily unavailable",
|
|
45
|
+
"ERROR_INVALID_TASK": "The task is syntactically valid but contains invalid data",
|
|
46
|
+
"TIMEOUT": "Polling timed out before the captcha was solved",
|
|
47
|
+
"MISSING_CLIENT_KEY": "Client key is required",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CapMonsterError(Exception):
|
|
52
|
+
"""Exception raised for API errors or failed requests.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
message: Human-readable error description.
|
|
56
|
+
error_id: Numeric error ID returned by CapMonster Cloud (0 is success).
|
|
57
|
+
error_code: String code (e.g. `ERROR_KEY_DOES_NOT_EXIST`).
|
|
58
|
+
status_code: HTTP status code or 0 for client/transport errors.
|
|
59
|
+
response: Raw response body, if available.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
message: str,
|
|
65
|
+
error_id: int = 0,
|
|
66
|
+
error_code: str | None = None,
|
|
67
|
+
status_code: int = 0,
|
|
68
|
+
response: Any = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__(message)
|
|
71
|
+
self.message = message
|
|
72
|
+
self.error_id = error_id
|
|
73
|
+
self.error_code = error_code
|
|
74
|
+
self.status_code = status_code
|
|
75
|
+
self.response = response
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return (
|
|
79
|
+
f"CapMonsterError(message={self.message!r}, "
|
|
80
|
+
f"error_id={self.error_id}, error_code={self.error_code!r})"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def describe_error(
|
|
85
|
+
error_code: str | None,
|
|
86
|
+
error_description: str | None,
|
|
87
|
+
error_id: int,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Prefer the API description, then the local catalog, then the raw code."""
|
|
90
|
+
if error_description:
|
|
91
|
+
return error_description
|
|
92
|
+
if error_code and error_code in CAPMONSTER_ERRORS:
|
|
93
|
+
return CAPMONSTER_ERRORS[error_code]
|
|
94
|
+
if error_code:
|
|
95
|
+
return error_code
|
|
96
|
+
return f"API error #{error_id}"
|
capmonster/tasks.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Task builders for documented CapMonster Cloud captcha types.
|
|
2
|
+
|
|
3
|
+
Each helper sets `type` (and `class`, when the docs require `CustomTask`)
|
|
4
|
+
and forwards the remaining API fields unchanged. Field names match the JSON
|
|
5
|
+
documented at https://docs.capmonster.cloud/docs/captchas/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _task(task_type: str, fields: dict[str, Any], task_class: str | None = None) -> dict[str, Any]:
|
|
14
|
+
task: dict[str, Any] = {"type": task_type}
|
|
15
|
+
if task_class is not None:
|
|
16
|
+
task["class"] = task_class
|
|
17
|
+
task.update(fields)
|
|
18
|
+
return task
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def recaptcha_v2(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
22
|
+
"""reCAPTCHA v2 (`RecaptchaV2Task`)."""
|
|
23
|
+
return _task(
|
|
24
|
+
"RecaptchaV2Task",
|
|
25
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def recaptcha_v2_enterprise(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
30
|
+
"""reCAPTCHA v2 Enterprise (`RecaptchaV2EnterpriseTask`)."""
|
|
31
|
+
return _task(
|
|
32
|
+
"RecaptchaV2EnterpriseTask",
|
|
33
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def recaptcha_v3(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
38
|
+
"""reCAPTCHA v3 (`RecaptchaV3TaskProxyless`). Pass isEnterprise=True for v3 Enterprise."""
|
|
39
|
+
return _task(
|
|
40
|
+
"RecaptchaV3TaskProxyless",
|
|
41
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def recaptcha_v3_enterprise(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
46
|
+
"""reCAPTCHA v3 Enterprise (`RecaptchaV3EnterpriseTask`)."""
|
|
47
|
+
return _task(
|
|
48
|
+
"RecaptchaV3EnterpriseTask",
|
|
49
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def turnstile(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
54
|
+
"""Cloudflare Turnstile, Challenge, and Waiting Room (`TurnstileTask`)."""
|
|
55
|
+
return _task("TurnstileTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def geetest(website_url: str, gt: str, **extra: Any) -> dict[str, Any]:
|
|
59
|
+
"""GeeTest v3 or v4 (`GeeTestTask`). Pass version and challenge via extra."""
|
|
60
|
+
return _task("GeeTestTask", {"websiteURL": website_url, "gt": gt, **extra})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def image_to_text(body: str, **extra: Any) -> dict[str, Any]:
|
|
64
|
+
"""ImageToTextTask. `body` is a single-line Base64 image."""
|
|
65
|
+
return _task("ImageToTextTask", {"body": body, **extra})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def complex_image(class_name: str, **extra: Any) -> dict[str, Any]:
|
|
69
|
+
"""ComplexImageTask. `class_name` is recaptcha, hcaptcha, funcaptcha, or recognition."""
|
|
70
|
+
return _task("ComplexImageTask", extra, class_name)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def datadome(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
74
|
+
"""DataDome (`CustomTask` / `DataDome`). Requires your own proxy in extra."""
|
|
75
|
+
return _task(
|
|
76
|
+
"CustomTask",
|
|
77
|
+
{"websiteURL": website_url, "metadata": metadata, **extra},
|
|
78
|
+
"DataDome",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tendi(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
83
|
+
"""TenDI / Tencent captcha (`CustomTask` / `TenDI`).
|
|
84
|
+
|
|
85
|
+
CapMonster documents this captcha as TenDI, not Tenable.
|
|
86
|
+
"""
|
|
87
|
+
return _task(
|
|
88
|
+
"CustomTask",
|
|
89
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
90
|
+
"TenDI",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def amazon(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
95
|
+
"""Amazon / AWS WAF (`AmazonTask`)."""
|
|
96
|
+
return _task("AmazonTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def binance(website_url: str, website_key: str, validate_id: str, **extra: Any) -> dict[str, Any]:
|
|
100
|
+
"""Binance captcha (`BinanceTask`)."""
|
|
101
|
+
return _task(
|
|
102
|
+
"BinanceTask",
|
|
103
|
+
{
|
|
104
|
+
"websiteURL": website_url,
|
|
105
|
+
"websiteKey": website_key,
|
|
106
|
+
"validateId": validate_id,
|
|
107
|
+
**extra,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def funcaptcha(website_url: str, website_public_key: str, **extra: Any) -> dict[str, Any]:
|
|
113
|
+
"""Arkose Labs FunCaptcha (`FunCaptchaTask`)."""
|
|
114
|
+
return _task(
|
|
115
|
+
"FunCaptchaTask",
|
|
116
|
+
{"websiteURL": website_url, "websitePublicKey": website_public_key, **extra},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def hcaptcha(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
121
|
+
"""hCaptcha (`HCaptchaTask`)."""
|
|
122
|
+
return _task("HCaptchaTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def basilisk(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
126
|
+
"""Basilisk (`CustomTask` / `Basilisk`)."""
|
|
127
|
+
return _task(
|
|
128
|
+
"CustomTask",
|
|
129
|
+
{"websiteURL": website_url, "websiteKey": website_key, **extra},
|
|
130
|
+
"Basilisk",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def imperva(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
135
|
+
"""Imperva / Incapsula (`CustomTask` / `Imperva`)."""
|
|
136
|
+
return _task(
|
|
137
|
+
"CustomTask",
|
|
138
|
+
{"websiteURL": website_url, "metadata": metadata, **extra},
|
|
139
|
+
"Imperva",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def prosopo(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
144
|
+
"""Prosopo (`ProsopoTask`)."""
|
|
145
|
+
return _task("ProsopoTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def yidun(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
149
|
+
"""Yidun (`YidunTask`)."""
|
|
150
|
+
return _task("YidunTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def mtcaptcha(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
|
|
154
|
+
"""MTCaptcha (`MTCaptchaTask`)."""
|
|
155
|
+
return _task("MTCaptchaTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def altcha(
|
|
159
|
+
website_url: str,
|
|
160
|
+
website_key: str,
|
|
161
|
+
metadata: dict[str, Any],
|
|
162
|
+
**extra: Any,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
"""Altcha (`CustomTask` / `altcha`)."""
|
|
165
|
+
return _task(
|
|
166
|
+
"CustomTask",
|
|
167
|
+
{"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
|
|
168
|
+
"altcha",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def friendly_captcha(
|
|
173
|
+
website_url: str,
|
|
174
|
+
website_key: str,
|
|
175
|
+
metadata: dict[str, Any],
|
|
176
|
+
**extra: Any,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Friendly Captcha (`CustomTask` / `friendly`)."""
|
|
179
|
+
return _task(
|
|
180
|
+
"CustomTask",
|
|
181
|
+
{"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
|
|
182
|
+
"friendly",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def temu(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
187
|
+
"""Temu (`CustomTask` / `Temu`)."""
|
|
188
|
+
return _task("CustomTask", {"websiteURL": website_url, "metadata": metadata, **extra}, "Temu")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def castle(
|
|
192
|
+
website_url: str,
|
|
193
|
+
website_key: str,
|
|
194
|
+
metadata: dict[str, Any],
|
|
195
|
+
**extra: Any,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Castle (`CustomTask` / `Castle`)."""
|
|
198
|
+
return _task(
|
|
199
|
+
"CustomTask",
|
|
200
|
+
{"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
|
|
201
|
+
"Castle",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def tspd(
|
|
206
|
+
website_url: str,
|
|
207
|
+
user_agent: str,
|
|
208
|
+
metadata: dict[str, Any],
|
|
209
|
+
**extra: Any,
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""TSPD (`CustomTask` / `tspd`)."""
|
|
212
|
+
return _task(
|
|
213
|
+
"CustomTask",
|
|
214
|
+
{"websiteURL": website_url, "userAgent": user_agent, "metadata": metadata, **extra},
|
|
215
|
+
"tspd",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def hunt(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
220
|
+
"""Hunt CAPTCHA (`CustomTask` / `HUNT`)."""
|
|
221
|
+
return _task("CustomTask", {"websiteURL": website_url, "metadata": metadata, **extra}, "HUNT")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def alibaba(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
225
|
+
"""Alibaba captcha (`CustomTask` / `alibaba`)."""
|
|
226
|
+
return _task(
|
|
227
|
+
"CustomTask",
|
|
228
|
+
{"websiteURL": website_url, "metadata": metadata, **extra},
|
|
229
|
+
"alibaba",
|
|
230
|
+
)
|
capmonster/types.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Type definitions for the CapMonster Cloud API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
ProxyType = Literal["http", "https", "socks4", "socks5"]
|
|
8
|
+
TaskStatus = Literal["processing", "ready"]
|
|
9
|
+
|
|
10
|
+
# Documented `type` values. Custom captchas also set `class` (see TASK_CLASSES).
|
|
11
|
+
TASK_TYPES: dict[str, str] = {
|
|
12
|
+
"recaptcha_v2": "RecaptchaV2Task",
|
|
13
|
+
"recaptcha_v2_proxyless": "RecaptchaV2TaskProxyless",
|
|
14
|
+
"no_captcha": "NoCaptchaTask",
|
|
15
|
+
"no_captcha_proxyless": "NoCaptchaTaskProxyless",
|
|
16
|
+
"recaptcha_v2_enterprise": "RecaptchaV2EnterpriseTask",
|
|
17
|
+
"recaptcha_v3": "RecaptchaV3TaskProxyless",
|
|
18
|
+
"recaptcha_v3_enterprise": "RecaptchaV3EnterpriseTask",
|
|
19
|
+
"turnstile": "TurnstileTask",
|
|
20
|
+
"geetest": "GeeTestTask",
|
|
21
|
+
"image_to_text": "ImageToTextTask",
|
|
22
|
+
"complex_image": "ComplexImageTask",
|
|
23
|
+
"custom": "CustomTask",
|
|
24
|
+
"funcaptcha": "FunCaptchaTask",
|
|
25
|
+
"hcaptcha": "HCaptchaTask",
|
|
26
|
+
"hcaptcha_proxyless": "HCaptchaTaskProxyless",
|
|
27
|
+
"amazon": "AmazonTask",
|
|
28
|
+
"binance": "BinanceTask",
|
|
29
|
+
"prosopo": "ProsopoTask",
|
|
30
|
+
"yidun": "YidunTask",
|
|
31
|
+
"mtcaptcha": "MTCaptchaTask",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# `class` values for CustomTask and ComplexImageTask.
|
|
35
|
+
TASK_CLASSES: dict[str, str] = {
|
|
36
|
+
"datadome": "DataDome",
|
|
37
|
+
"tendi": "TenDI",
|
|
38
|
+
"basilisk": "Basilisk",
|
|
39
|
+
"imperva": "Imperva",
|
|
40
|
+
"altcha": "altcha",
|
|
41
|
+
"friendly": "friendly",
|
|
42
|
+
"temu": "Temu",
|
|
43
|
+
"castle": "Castle",
|
|
44
|
+
"tspd": "tspd",
|
|
45
|
+
"hunt": "HUNT",
|
|
46
|
+
"alibaba": "alibaba",
|
|
47
|
+
"complex_recaptcha": "recaptcha",
|
|
48
|
+
"complex_hcaptcha": "hcaptcha",
|
|
49
|
+
"complex_funcaptcha": "funcaptcha",
|
|
50
|
+
"complex_recognition": "recognition",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CapMonsterBalance(TypedDict):
|
|
55
|
+
"""Account balance details."""
|
|
56
|
+
|
|
57
|
+
balance: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TaskResult(TypedDict, total=False):
|
|
61
|
+
"""Result returned by getTaskResult."""
|
|
62
|
+
|
|
63
|
+
status: TaskStatus
|
|
64
|
+
solution: dict[str, Any]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RawApiResponse(TypedDict, total=False):
|
|
68
|
+
"""Envelope returned by CapMonster Cloud endpoints."""
|
|
69
|
+
|
|
70
|
+
errorId: int
|
|
71
|
+
errorCode: str | None
|
|
72
|
+
errorDescription: str | None
|
|
73
|
+
taskId: int | str
|
|
74
|
+
status: TaskStatus
|
|
75
|
+
solution: Any
|
|
76
|
+
balance: float
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: capmonster-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the CapMonster Cloud captcha recognition API
|
|
5
|
+
Project-URL: Repository, https://github.com/TysikGG/api-kit
|
|
6
|
+
Project-URL: Documentation, https://docs.capmonster.cloud
|
|
7
|
+
License-Expression: ISC
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# capmonster-client
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/capmonster-client/)
|
|
16
|
+
[](https://pypi.org/project/capmonster-client/)
|
|
17
|
+
[](https://opensource.org/licenses/ISC)
|
|
18
|
+
|
|
19
|
+
Typed Python client for the [CapMonster Cloud API](https://docs.capmonster.cloud). Supports reCAPTCHA v2/v3/Enterprise, Cloudflare Turnstile (including Challenge and Waiting Room), GeeTest, ImageToText, ComplexImage, DataDome, Amazon AWS WAF, TenDI (Tencent), Binance, and the other task types documented by CapMonster Cloud. Built on [HTTPX](https://www.python-httpx.org).
|
|
20
|
+
|
|
21
|
+
Official API documentation: <https://docs.capmonster.cloud>
|
|
22
|
+
|
|
23
|
+
The import package is `capmonster`. The distribution name is `capmonster-client` so it does not collide with other projects on PyPI.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- 🧩 **Documented task types** — Helpers for reCAPTCHA v2/v3/Enterprise, Turnstile, GeeTest, ImageToText, ComplexImage, DataDome, Amazon WAF, TenDI, Binance, FunCaptcha, hCaptcha, Basilisk, Imperva, Prosopo, Yidun, MTCaptcha, Altcha, Friendly Captcha, Temu, Castle, TSPD, Hunt, and Alibaba.
|
|
30
|
+
- ⏳ **Automated polling** — `wait_for_result()` and `solve()` poll until the solution is ready. `CAPTCHA_NOT_READY` stays in the processing state.
|
|
31
|
+
- 💰 **Balance tracking** — `get_balance()` reads the account balance and does not spend credit.
|
|
32
|
+
- 🏷️ **Developer id** — `softId` is omitted until you set it. CapMonster's developer program is still pending, so `DEFAULT_SOFT_ID` is empty.
|
|
33
|
+
- 🐍 **Modern Python** — Type annotations, context manager support (`with CapMonster(...) as client:`).
|
|
34
|
+
- 🚨 **Structured errors** — `CapMonsterError` carries `error_id` and `error_code` for every documented API error.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install capmonster-client
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.10 or higher.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick Start (Safe Check)
|
|
49
|
+
|
|
50
|
+
The quick check only inspects your account balance. It does not submit tasks or spend credits.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from capmonster import CapMonster
|
|
55
|
+
|
|
56
|
+
with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
|
|
57
|
+
balance = client.get_balance()
|
|
58
|
+
print(f"Balance: {balance['balance']}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Usage Guide
|
|
64
|
+
|
|
65
|
+
### 1. High-Level Solve (Recommended)
|
|
66
|
+
|
|
67
|
+
`client.solve()` submits the task and waits until the solution is ready:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import os
|
|
71
|
+
from capmonster import CapMonster, turnstile
|
|
72
|
+
|
|
73
|
+
with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
|
|
74
|
+
solution = client.solve(turnstile("https://example.com/", "0x4AAAAAAA..."))
|
|
75
|
+
print("Turnstile token:", solution["token"])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Solving reCAPTCHA v2
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from capmonster import CapMonster, recaptcha_v2
|
|
82
|
+
|
|
83
|
+
with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
|
|
84
|
+
solution = client.solve(recaptcha_v2(
|
|
85
|
+
"https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high",
|
|
86
|
+
"6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd",
|
|
87
|
+
))
|
|
88
|
+
print(solution["gRecaptchaResponse"])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Manual Task Lifecycle
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from capmonster import CapMonster, turnstile
|
|
95
|
+
|
|
96
|
+
with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
|
|
97
|
+
task_id = client.create_task(turnstile("https://example.com/", "0x4AAAAAAA..."))
|
|
98
|
+
result = client.get_task_result(task_id)
|
|
99
|
+
if result["status"] == "ready":
|
|
100
|
+
print("Solution:", result["solution"])
|
|
101
|
+
else:
|
|
102
|
+
print("Task is still processing...")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You can also pass a raw `dict`. `type` is required. Custom captchas (DataDome, TenDI, and the others listed below) also require `class`.
|
|
106
|
+
|
|
107
|
+
### 4. Developer software id
|
|
108
|
+
|
|
109
|
+
`create_task` sends `softId` only when it is set:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from capmonster import DEFAULT_SOFT_ID, CapMonster
|
|
113
|
+
|
|
114
|
+
print(DEFAULT_SOFT_ID) # ""
|
|
115
|
+
|
|
116
|
+
with CapMonster(os.environ["CAPMONSTER_API_KEY"], soft_id=12345) as client:
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
TODO: replace `DEFAULT_SOFT_ID` in `src/capmonster/client.py` once CapMonster assigns a numeric id.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Task Types
|
|
125
|
+
|
|
126
|
+
| Captcha | `type` | `class` | Helper |
|
|
127
|
+
| :--- | :--- | :--- | :--- |
|
|
128
|
+
| reCAPTCHA v2 | `RecaptchaV2Task` | | `recaptcha_v2` |
|
|
129
|
+
| reCAPTCHA v2 Enterprise | `RecaptchaV2EnterpriseTask` | | `recaptcha_v2_enterprise` |
|
|
130
|
+
| reCAPTCHA v3 | `RecaptchaV3TaskProxyless` | | `recaptcha_v3` |
|
|
131
|
+
| reCAPTCHA v3 Enterprise | `RecaptchaV3EnterpriseTask` | | `recaptcha_v3_enterprise` |
|
|
132
|
+
| Cloudflare Turnstile / Challenge / Waiting Room | `TurnstileTask` | | `turnstile` |
|
|
133
|
+
| GeeTest v3 / v4 | `GeeTestTask` | | `geetest` |
|
|
134
|
+
| ImageToText | `ImageToTextTask` | | `image_to_text` |
|
|
135
|
+
| ComplexImage (click, grid, recognition) | `ComplexImageTask` | `recaptcha`, `hcaptcha`, `funcaptcha`, `recognition` | `complex_image` |
|
|
136
|
+
| DataDome | `CustomTask` | `DataDome` | `datadome` |
|
|
137
|
+
| TenDI (Tencent) | `CustomTask` | `TenDI` | `tendi` |
|
|
138
|
+
| Amazon AWS WAF | `AmazonTask` | | `amazon` |
|
|
139
|
+
| Binance | `BinanceTask` | | `binance` |
|
|
140
|
+
| FunCaptcha | `FunCaptchaTask` | | `funcaptcha` |
|
|
141
|
+
| hCaptcha | `HCaptchaTask` | | `hcaptcha` |
|
|
142
|
+
| Basilisk | `CustomTask` | `Basilisk` | `basilisk` |
|
|
143
|
+
| Imperva (Incapsula) | `CustomTask` | `Imperva` | `imperva` |
|
|
144
|
+
| Prosopo | `ProsopoTask` | | `prosopo` |
|
|
145
|
+
| Yidun | `YidunTask` | | `yidun` |
|
|
146
|
+
| MTCaptcha | `MTCaptchaTask` | | `mtcaptcha` |
|
|
147
|
+
| Altcha | `CustomTask` | `altcha` | `altcha` |
|
|
148
|
+
| Friendly Captcha | `CustomTask` | `friendly` | `friendly_captcha` |
|
|
149
|
+
| Temu | `CustomTask` | `Temu` | `temu` |
|
|
150
|
+
| Castle | `CustomTask` | `Castle` | `castle` |
|
|
151
|
+
| TSPD | `CustomTask` | `tspd` | `tspd` |
|
|
152
|
+
| Hunt | `CustomTask` | `HUNT` | `hunt` |
|
|
153
|
+
| Alibaba | `CustomTask` | `alibaba` | `alibaba` |
|
|
154
|
+
|
|
155
|
+
Legacy aliases `NoCaptchaTask`, `NoCaptchaTaskProxyless`, `RecaptchaV2TaskProxyless`, and `HCaptchaTaskProxyless` are accepted on the `type` field. Current docs use `RecaptchaV2Task` with proxy fields omitted when CapMonster's own proxies are enough.
|
|
156
|
+
|
|
157
|
+
TenDI is the Tencent captcha documented by CapMonster. It is not a Tenable product.
|
|
158
|
+
|
|
159
|
+
Parameter reference: <https://docs.capmonster.cloud/docs/captchas/>
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
When the API returns an error, the client raises `CapMonsterError`:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from capmonster import CapMonster, CapMonsterError
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with CapMonster("invalid_key") as client:
|
|
172
|
+
client.get_balance()
|
|
173
|
+
except CapMonsterError as err:
|
|
174
|
+
print(f"CapMonster error #{err.error_id} [{err.error_code}]: {err.message}")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Common error codes (full list: <https://docs.capmonster.cloud/docs/api/api-errors/>):
|
|
178
|
+
|
|
179
|
+
- `ERROR_KEY_DOES_NOT_EXIST` — Account key does not exist or is malformed.
|
|
180
|
+
- `ERROR_ZERO_BALANCE` — Account has no funds. Returned by `create_task`, not by `get_balance`.
|
|
181
|
+
- `ERROR_CAPTCHA_UNSOLVABLE` — The captcha could not be solved. Unsolved tasks are not charged.
|
|
182
|
+
- `CAPTCHA_NOT_READY` — Still solving. `get_task_result` maps this to `status: "processing"`.
|
|
183
|
+
- `ERROR_TASK_NOT_SUPPORTED` / `ERROR_NO_SUCH_METHOD` — Unknown `type`.
|
|
184
|
+
- `ERROR_INVALID_TASK` / `ERROR_TASK_ABSENT` — Task payload is missing or invalid.
|
|
185
|
+
- `ERROR_TOO_MUCH_REQUESTS` — Poll `get_task_result` no more than once every 2 seconds. The default interval is 3 seconds.
|
|
186
|
+
- `TIMEOUT` — `wait_for_result()` exceeded the configured timeout.
|
|
187
|
+
|
|
188
|
+
`CAPMONSTER_ERRORS` maps every documented code to a short description. The API `errorDescription` is used when CapMonster sends one.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Live Tests
|
|
193
|
+
|
|
194
|
+
Unit tests mock HTTP and always run. The live suite calls the real API and runs only when `CAPMONSTER_API_KEY` is set. It checks balance, an invalid key, and `ERROR_ZERO_BALANCE` on `create_task`.
|
|
195
|
+
|
|
196
|
+
`create_task` is sent only when the account balance is exactly `0`. A funded key skips that assertion so the suite does not spend credit.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cd capmonster/python
|
|
200
|
+
uv sync
|
|
201
|
+
uv run pytest -m "not live"
|
|
202
|
+
CAPMONSTER_API_KEY=your_key uv run pytest -m live
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Do not commit API keys.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[ISC](LICENSE) © api-kit contributors
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
capmonster/__init__.py,sha256=srbljr3wN9rR6cTQzvH6uGbuOj6GntzMiHVdtgmxyZI,1373
|
|
2
|
+
capmonster/client.py,sha256=2Jnynz_BaANaHPZSPN8MzzJ0WWR4D1EptgsYfPcvAoY,9968
|
|
3
|
+
capmonster/errors.py,sha256=1I7sycm5oDCjKl8DJDe49A-ITEEj_tJBlNdYufl3vRk,4274
|
|
4
|
+
capmonster/tasks.py,sha256=uM_b241tD1xEoZ8j_futb_bERhmjnLIryYedrSNGvg0,7665
|
|
5
|
+
capmonster/types.py,sha256=2SipfWVTmUfwdfO8oksEoyDR9qL0lov2gihEmviFc1Y,2163
|
|
6
|
+
capmonster_client-0.1.0.dist-info/METADATA,sha256=uypg5KE1UyAgiBZLnZQIGO_jjwY2Ahof0P6fWbVfPlc,8256
|
|
7
|
+
capmonster_client-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
capmonster_client-0.1.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
|
|
9
|
+
capmonster_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 api-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|