capsolver-core 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.
- capsolver_core/__init__.py +44 -0
- capsolver_core/__main__.py +99 -0
- capsolver_core/browser/__init__.py +6 -0
- capsolver_core/browser/adapter.py +116 -0
- capsolver_core/browser/driver.py +26 -0
- capsolver_core/browser/inject/__init__.py +1 -0
- capsolver_core/browser/inject/cloudflare.py +67 -0
- capsolver_core/browser/inject/recaptcha.py +162 -0
- capsolver_core/capsolver.py +206 -0
- capsolver_core/captcha/__init__.py +20 -0
- capsolver_core/captcha/handler.py +55 -0
- capsolver_core/captcha/handlers/__init__.py +20 -0
- capsolver_core/captcha/handlers/cloudflare.py +83 -0
- capsolver_core/captcha/handlers/recaptcha.py +106 -0
- capsolver_core/captcha/handlers/support.py +30 -0
- capsolver_core/captcha/registry.py +56 -0
- capsolver_core/captcha/types.py +60 -0
- capsolver_core/core/__init__.py +7 -0
- capsolver_core/core/client.py +163 -0
- capsolver_core/core/errors.py +55 -0
- capsolver_core/core/http.py +137 -0
- capsolver_core/core/tasks.py +134 -0
- capsolver_core/core/types.py +157 -0
- capsolver_core/py.typed +0 -0
- capsolver_core-0.1.0.dist-info/METADATA +206 -0
- capsolver_core-0.1.0.dist-info/RECORD +29 -0
- capsolver_core-0.1.0.dist-info/WHEEL +4 -0
- capsolver_core-0.1.0.dist-info/entry_points.txt +2 -0
- capsolver_core-0.1.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""CapsolverClient — the pure-Python token-solving core.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's core/client.ts. Responsible only for
|
|
4
|
+
``/createTask``, ``/getTaskResult`` polling and ``/getBalance``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Callable, NoReturn
|
|
13
|
+
|
|
14
|
+
from capsolver_core.core.errors import CapsolverError, CapsolverTimeoutError, RateLimitError
|
|
15
|
+
from capsolver_core.core.http import FetchHttp
|
|
16
|
+
from capsolver_core.core.types import BalanceResp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WaitOptions:
|
|
21
|
+
"""Per-call overrides for polling behaviour."""
|
|
22
|
+
|
|
23
|
+
timeout: float | None = None
|
|
24
|
+
polling_interval: float | None = None
|
|
25
|
+
cancel_event: asyncio.Event | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CapsolverClientOptions:
|
|
30
|
+
api_key: str = ""
|
|
31
|
+
service: str = "https://api.capsolver.com"
|
|
32
|
+
default_timeout: float = 120.0
|
|
33
|
+
polling_interval: float = 5.0
|
|
34
|
+
request_timeout_ms: int = 30_000
|
|
35
|
+
app_id: str | None = None
|
|
36
|
+
source: str | None = None
|
|
37
|
+
version: str | None = None
|
|
38
|
+
on_error: Callable[[CapsolverError], None] | None = field(default=None, repr=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CapsolverClient:
|
|
42
|
+
"""Token-solving client — create tasks, poll for results, check balance."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, options: CapsolverClientOptions | None = None, **kwargs: Any) -> None:
|
|
45
|
+
opts = options or CapsolverClientOptions(**kwargs)
|
|
46
|
+
if not opts.api_key:
|
|
47
|
+
raise CapsolverError("CapsolverClient: apiKey is required")
|
|
48
|
+
self._options = opts
|
|
49
|
+
self._http = FetchHttp(opts.service)
|
|
50
|
+
|
|
51
|
+
# ── public API ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async def aclose(self) -> None:
|
|
54
|
+
"""Close the underlying HTTP client and release connections."""
|
|
55
|
+
await self._http.aclose()
|
|
56
|
+
|
|
57
|
+
async def get_balance(self) -> BalanceResp:
|
|
58
|
+
res = await self._http.post(
|
|
59
|
+
"/getBalance",
|
|
60
|
+
{"clientKey": self._options.api_key},
|
|
61
|
+
timeout_ms=self._options.request_timeout_ms,
|
|
62
|
+
)
|
|
63
|
+
data = self._unwrap(res.status, res.data, "getBalance failed")
|
|
64
|
+
return BalanceResp.from_dict(data)
|
|
65
|
+
|
|
66
|
+
async def create_task(self, task: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
67
|
+
body: dict[str, Any] = {
|
|
68
|
+
"clientKey": self._options.api_key,
|
|
69
|
+
"task": task,
|
|
70
|
+
}
|
|
71
|
+
if extra.get("app_id") or self._options.app_id:
|
|
72
|
+
body["appId"] = extra.get("app_id") or self._options.app_id
|
|
73
|
+
if extra.get("source") or self._options.source:
|
|
74
|
+
body["source"] = extra.get("source") or self._options.source
|
|
75
|
+
if extra.get("version") or self._options.version:
|
|
76
|
+
body["version"] = extra.get("version") or self._options.version
|
|
77
|
+
|
|
78
|
+
res = await self._http.post(
|
|
79
|
+
"/createTask",
|
|
80
|
+
body,
|
|
81
|
+
timeout_ms=self._options.request_timeout_ms,
|
|
82
|
+
)
|
|
83
|
+
data = self._unwrap(res.status, res.data, "createTask failed")
|
|
84
|
+
if not data.get("taskId"):
|
|
85
|
+
self._fail(CapsolverError("createTask returned an empty taskId"))
|
|
86
|
+
return data
|
|
87
|
+
|
|
88
|
+
async def get_task_solution(self, task_id: str) -> dict[str, Any]:
|
|
89
|
+
res = await self._http.post(
|
|
90
|
+
"/getTaskResult",
|
|
91
|
+
{"clientKey": self._options.api_key, "taskId": task_id},
|
|
92
|
+
timeout_ms=self._options.request_timeout_ms,
|
|
93
|
+
)
|
|
94
|
+
return self._unwrap(res.status, res.data, "getTaskResult failed")
|
|
95
|
+
|
|
96
|
+
async def create_task_result(
|
|
97
|
+
self,
|
|
98
|
+
task: dict[str, Any],
|
|
99
|
+
wait_options: WaitOptions | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Create a task and poll until ``ready``, throwing on ``failed`` or timeout."""
|
|
102
|
+
timeout = (
|
|
103
|
+
wait_options.timeout if wait_options and wait_options.timeout is not None else self._options.default_timeout
|
|
104
|
+
)
|
|
105
|
+
interval = (
|
|
106
|
+
wait_options.polling_interval
|
|
107
|
+
if wait_options and wait_options.polling_interval is not None
|
|
108
|
+
else self._options.polling_interval
|
|
109
|
+
)
|
|
110
|
+
cancel = wait_options.cancel_event if wait_options else None
|
|
111
|
+
|
|
112
|
+
result = await self.create_task(task)
|
|
113
|
+
task_id = result.get("taskId", "")
|
|
114
|
+
started_at = time.monotonic()
|
|
115
|
+
|
|
116
|
+
# Poll immediately before the first sleep — the task may already be ready
|
|
117
|
+
# (e.g. simple captcha types or cached solutions).
|
|
118
|
+
while True:
|
|
119
|
+
if cancel and cancel.is_set():
|
|
120
|
+
self._fail(CapsolverError("Polling aborted", error_code="ABORTED"))
|
|
121
|
+
|
|
122
|
+
if time.monotonic() - started_at > timeout:
|
|
123
|
+
self._fail(CapsolverTimeoutError(timeout, task_id))
|
|
124
|
+
|
|
125
|
+
state = await self.get_task_solution(task_id)
|
|
126
|
+
status = state.get("status")
|
|
127
|
+
|
|
128
|
+
if status == "ready":
|
|
129
|
+
return state
|
|
130
|
+
if status == "failed":
|
|
131
|
+
self._fail(
|
|
132
|
+
CapsolverError(
|
|
133
|
+
state.get("errorDescription") or "Task failed",
|
|
134
|
+
error_id=state.get("errorId"),
|
|
135
|
+
error_code=state.get("errorCode"),
|
|
136
|
+
error_description=state.get("errorDescription"),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
await asyncio.sleep(interval)
|
|
141
|
+
|
|
142
|
+
# ── internals ─────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def _unwrap(self, http_status: int, data: dict[str, Any], fallback_message: str) -> dict[str, Any]:
|
|
145
|
+
if http_status != 200 or (data and (data.get("errorId") or data.get("errorCode"))):
|
|
146
|
+
error_msg = data.get("errorDescription") if data else fallback_message
|
|
147
|
+
message = str(error_msg or fallback_message)
|
|
148
|
+
error_kwargs: dict[str, Any] = {
|
|
149
|
+
"error_id": data.get("errorId") if data else None,
|
|
150
|
+
"error_code": data.get("errorCode") if data else None,
|
|
151
|
+
"error_description": data.get("errorDescription") if data else None,
|
|
152
|
+
"http_status": http_status,
|
|
153
|
+
}
|
|
154
|
+
if http_status == 429:
|
|
155
|
+
self._fail(RateLimitError(message or "Rate limit exceeded", **error_kwargs))
|
|
156
|
+
else:
|
|
157
|
+
self._fail(CapsolverError(message, **error_kwargs))
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
def _fail(self, error: CapsolverError) -> NoReturn:
|
|
161
|
+
if self._options.on_error:
|
|
162
|
+
self._options.on_error(error)
|
|
163
|
+
raise error
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Error types for the CapSolver SDK.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's core/errors.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CapsolverError(Exception):
|
|
12
|
+
"""Raised when the CapSolver API returns an error envelope or an unexpected HTTP status."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
*,
|
|
18
|
+
error_id: int | None = None,
|
|
19
|
+
error_code: str | None = None,
|
|
20
|
+
error_description: str | None = None,
|
|
21
|
+
http_status: int | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.error_id = error_id
|
|
25
|
+
self.error_code = error_code
|
|
26
|
+
self.error_description = error_description
|
|
27
|
+
self.http_status = http_status
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CapsolverTimeoutError(CapsolverError):
|
|
31
|
+
"""Raised when polling exceeds the configured timeout."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, timeout_seconds: float, task_id: str | None = None) -> None:
|
|
34
|
+
super().__init__(f"Timeout of {timeout_seconds}s reached while waiting for task result")
|
|
35
|
+
self.task_id = task_id
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NetworkError(CapsolverError):
|
|
39
|
+
"""Raised when the CapSolver API is unreachable after all retries.
|
|
40
|
+
|
|
41
|
+
Wraps connection-level failures (DNS, TCP, TLS, read timeout) so that
|
|
42
|
+
callers can distinguish "server said no" from "couldn't reach server".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, *, cause: Exception | None = None) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.cause = cause
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RateLimitError(CapsolverError):
|
|
51
|
+
"""Raised when the API returns HTTP 429 (Too Many Requests)."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str = "Rate limit exceeded", **kwargs: Any) -> None:
|
|
54
|
+
kwargs.setdefault("http_status", 429)
|
|
55
|
+
super().__init__(message, **kwargs)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Thin ``httpx`` wrapper around the CapSolver JSON API.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's core/http.ts. Uses ``httpx`` so both sync and
|
|
4
|
+
async callers are supported without pulling in extra dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from capsolver_core.core.errors import NetworkError
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("capsolver_core")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class HttpResp:
|
|
23
|
+
"""Normalized HTTP response."""
|
|
24
|
+
|
|
25
|
+
status: int
|
|
26
|
+
status_text: str
|
|
27
|
+
data: Any
|
|
28
|
+
headers: httpx.Headers = field(repr=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Transient status codes that should trigger an automatic retry.
|
|
32
|
+
_RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FetchHttp:
|
|
36
|
+
"""Minimal async HTTP client for the CapSolver JSON API.
|
|
37
|
+
|
|
38
|
+
Features:
|
|
39
|
+
- Reuses a single ``httpx.AsyncClient`` for connection pooling.
|
|
40
|
+
- Retries on transient HTTP errors (429, 5xx) with exponential backoff.
|
|
41
|
+
- Gracefully handles non-JSON responses instead of crashing.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
base_url: str = "https://api.capsolver.com",
|
|
47
|
+
*,
|
|
48
|
+
max_retries: int = 3,
|
|
49
|
+
retry_backoff: float = 1.0,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.base_url = base_url.rstrip("/")
|
|
52
|
+
self._max_retries = max_retries
|
|
53
|
+
self._retry_backoff = retry_backoff
|
|
54
|
+
self._client: httpx.AsyncClient | None = None
|
|
55
|
+
|
|
56
|
+
def get_url(self, path: str) -> str:
|
|
57
|
+
if not path.startswith("/"):
|
|
58
|
+
path = "/" + path
|
|
59
|
+
return self.base_url + path
|
|
60
|
+
|
|
61
|
+
async def _get_client(self, timeout_s: float) -> httpx.AsyncClient:
|
|
62
|
+
"""Return the shared client, (re)creating it if the timeout changed."""
|
|
63
|
+
if self._client is None or self._client.is_closed:
|
|
64
|
+
self._client = httpx.AsyncClient(timeout=timeout_s)
|
|
65
|
+
return self._client
|
|
66
|
+
|
|
67
|
+
async def aclose(self) -> None:
|
|
68
|
+
"""Close the underlying HTTP client. Safe to call multiple times."""
|
|
69
|
+
if self._client is not None and not self._client.is_closed:
|
|
70
|
+
await self._client.aclose()
|
|
71
|
+
self._client = None
|
|
72
|
+
|
|
73
|
+
async def post(
|
|
74
|
+
self,
|
|
75
|
+
path: str,
|
|
76
|
+
body: Any,
|
|
77
|
+
*,
|
|
78
|
+
timeout_ms: int = 30_000,
|
|
79
|
+
headers: dict[str, str] | None = None,
|
|
80
|
+
) -> HttpResp:
|
|
81
|
+
merged_headers = {"Content-Type": "application/json"}
|
|
82
|
+
if headers:
|
|
83
|
+
merged_headers.update(headers)
|
|
84
|
+
|
|
85
|
+
url = self.get_url(path)
|
|
86
|
+
timeout_s = timeout_ms / 1000.0
|
|
87
|
+
last_exc: Exception | None = None
|
|
88
|
+
|
|
89
|
+
for attempt in range(1, self._max_retries + 1):
|
|
90
|
+
try:
|
|
91
|
+
client = await self._get_client(timeout_s)
|
|
92
|
+
resp = await client.post(url, json=body, headers=merged_headers)
|
|
93
|
+
|
|
94
|
+
# Retry on transient server errors
|
|
95
|
+
if resp.status_code in _RETRY_STATUSES and attempt < self._max_retries:
|
|
96
|
+
delay = self._retry_backoff * (2 ** (attempt - 1))
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Transient HTTP %d on %s (attempt %d/%d), retrying in %.1fs",
|
|
99
|
+
resp.status_code, path, attempt, self._max_retries, delay,
|
|
100
|
+
)
|
|
101
|
+
await asyncio.sleep(delay)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Parse JSON safely — non-JSON bodies (e.g. HTML 502 page)
|
|
105
|
+
# should produce an empty dict, not crash.
|
|
106
|
+
try:
|
|
107
|
+
data = resp.json()
|
|
108
|
+
except (ValueError, TypeError):
|
|
109
|
+
logger.warning(
|
|
110
|
+
"Non-JSON response (HTTP %d) from %s, treating as empty",
|
|
111
|
+
resp.status_code, path,
|
|
112
|
+
)
|
|
113
|
+
data = {}
|
|
114
|
+
|
|
115
|
+
return HttpResp(
|
|
116
|
+
status=resp.status_code,
|
|
117
|
+
status_text=resp.reason_phrase or "",
|
|
118
|
+
data=data,
|
|
119
|
+
headers=resp.headers,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException) as exc:
|
|
123
|
+
last_exc = exc
|
|
124
|
+
if attempt < self._max_retries:
|
|
125
|
+
delay = self._retry_backoff * (2 ** (attempt - 1))
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Network error on %s: %s (attempt %d/%d), retrying in %.1fs",
|
|
128
|
+
path, exc, attempt, self._max_retries, delay,
|
|
129
|
+
)
|
|
130
|
+
await asyncio.sleep(delay)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# All retries exhausted on network errors
|
|
134
|
+
raise NetworkError(
|
|
135
|
+
f"Failed to reach CapSolver API after {self._max_retries} attempts: {last_exc}",
|
|
136
|
+
cause=last_exc,
|
|
137
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Pure task-payload builders.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's core/tasks.ts. Each builder is a pure function
|
|
4
|
+
that takes keyword arguments and returns a plain ``dict`` ready for
|
|
5
|
+
JSON serialization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from capsolver_core.core.types import (
|
|
13
|
+
ReCaptchaV2Task,
|
|
14
|
+
ReCaptchaV3Task,
|
|
15
|
+
CloudflareTask,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── reCAPTCHA v2 ──────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_recaptcha_v2_task(
|
|
23
|
+
*,
|
|
24
|
+
website_url: str,
|
|
25
|
+
website_key: str,
|
|
26
|
+
page_action: str | None = None,
|
|
27
|
+
invisible: bool | None = None,
|
|
28
|
+
enterprise: bool | None = None,
|
|
29
|
+
enterprise_payload: dict[str, Any] | None = None,
|
|
30
|
+
api_domain: str | None = None,
|
|
31
|
+
proxy: str | None = None,
|
|
32
|
+
user_agent: str | None = None,
|
|
33
|
+
) -> ReCaptchaV2Task:
|
|
34
|
+
proxyless = not proxy
|
|
35
|
+
is_enterprise = enterprise
|
|
36
|
+
|
|
37
|
+
if is_enterprise:
|
|
38
|
+
task_type = "ReCaptchaV2EnterpriseTaskProxyLess" if proxyless else "ReCaptchaV2EnterpriseTask"
|
|
39
|
+
else:
|
|
40
|
+
task_type = "ReCaptchaV2TaskProxyLess" if proxyless else "ReCaptchaV2Task"
|
|
41
|
+
|
|
42
|
+
task: ReCaptchaV2Task = {
|
|
43
|
+
"type": task_type,
|
|
44
|
+
"websiteURL": website_url,
|
|
45
|
+
"websiteKey": website_key,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if invisible is not None:
|
|
49
|
+
task["invisible"] = invisible
|
|
50
|
+
if page_action:
|
|
51
|
+
task["pageAction"] = page_action
|
|
52
|
+
if api_domain:
|
|
53
|
+
task["apiDomain"] = api_domain
|
|
54
|
+
if enterprise_payload:
|
|
55
|
+
task["enterprisePayload"] = enterprise_payload
|
|
56
|
+
if proxy:
|
|
57
|
+
task["proxy"] = proxy
|
|
58
|
+
if user_agent:
|
|
59
|
+
task["userAgent"] = user_agent
|
|
60
|
+
|
|
61
|
+
return task
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── reCAPTCHA v3 ──────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_recaptcha_v3_task(
|
|
68
|
+
*,
|
|
69
|
+
website_url: str,
|
|
70
|
+
website_key: str,
|
|
71
|
+
page_action: str | None = None,
|
|
72
|
+
min_score: float | None = None,
|
|
73
|
+
enterprise: bool | None = None,
|
|
74
|
+
enterprise_payload: dict[str, Any] | None = None,
|
|
75
|
+
api_domain: str | None = None,
|
|
76
|
+
proxy: str | None = None,
|
|
77
|
+
) -> ReCaptchaV3Task:
|
|
78
|
+
proxyless = not proxy
|
|
79
|
+
is_enterprise = enterprise
|
|
80
|
+
|
|
81
|
+
if is_enterprise:
|
|
82
|
+
task_type = "ReCaptchaV3EnterpriseTaskProxyLess" if proxyless else "ReCaptchaV3EnterpriseTask"
|
|
83
|
+
else:
|
|
84
|
+
task_type = "ReCaptchaV3TaskProxyLess" if proxyless else "ReCaptchaV3Task"
|
|
85
|
+
|
|
86
|
+
task: ReCaptchaV3Task = {
|
|
87
|
+
"type": task_type,
|
|
88
|
+
"websiteURL": website_url,
|
|
89
|
+
"websiteKey": website_key,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if page_action:
|
|
93
|
+
task["pageAction"] = page_action
|
|
94
|
+
if min_score is not None:
|
|
95
|
+
task["minScore"] = min_score
|
|
96
|
+
if api_domain:
|
|
97
|
+
task["apiDomain"] = api_domain
|
|
98
|
+
if enterprise_payload:
|
|
99
|
+
task["enterprisePayload"] = enterprise_payload
|
|
100
|
+
if proxy:
|
|
101
|
+
task["proxy"] = proxy
|
|
102
|
+
|
|
103
|
+
return task
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Cloudflare Turnstile ──────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_cloudflare_task(
|
|
110
|
+
*,
|
|
111
|
+
website_url: str,
|
|
112
|
+
website_key: str,
|
|
113
|
+
action: str | None = None,
|
|
114
|
+
cdata: str | None = None,
|
|
115
|
+
proxy: str | None = None,
|
|
116
|
+
) -> CloudflareTask:
|
|
117
|
+
task: CloudflareTask = {
|
|
118
|
+
"type": "AntiTurnstileTaskProxyLess" if not proxy else "AntiTurnstileTask",
|
|
119
|
+
"websiteURL": website_url,
|
|
120
|
+
"websiteKey": website_key,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if action or cdata:
|
|
124
|
+
metadata: dict[str, str] = {"type": "turnstile"}
|
|
125
|
+
if action:
|
|
126
|
+
metadata["action"] = action
|
|
127
|
+
if cdata:
|
|
128
|
+
metadata["cdata"] = cdata
|
|
129
|
+
task["metadata"] = metadata
|
|
130
|
+
|
|
131
|
+
if proxy:
|
|
132
|
+
task["proxy"] = proxy
|
|
133
|
+
|
|
134
|
+
return task
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""CapSolver API types — token-mode only.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's core/types.ts. Uses ``str, Enum`` so values can
|
|
4
|
+
be compared directly against raw strings from the API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CaptchaType(str, Enum):
|
|
14
|
+
"""Captcha families the SDK can reason about."""
|
|
15
|
+
|
|
16
|
+
RECAPTCHA_V2 = "reCaptchaV2"
|
|
17
|
+
RECAPTCHA_V3 = "reCaptchaV3"
|
|
18
|
+
CLOUDFLARE = "cloudflare"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Task lifecycle ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskStatus(str, Enum):
|
|
25
|
+
IDLE = "idle"
|
|
26
|
+
READY = "ready"
|
|
27
|
+
PROCESSING = "processing"
|
|
28
|
+
FAILED = "failed"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Cookie ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Cookie:
|
|
35
|
+
__slots__ = ("name", "value")
|
|
36
|
+
|
|
37
|
+
def __init__(self, name: str, value: str) -> None:
|
|
38
|
+
self.name = name
|
|
39
|
+
self.value = value
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, str]:
|
|
42
|
+
return {"name": self.name, "value": self.value}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── API envelope types ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseResp:
|
|
49
|
+
"""Base shape for every CapSolver API response."""
|
|
50
|
+
|
|
51
|
+
__slots__ = ("error_id", "error_code", "error_description", "status", "solution")
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
error_id: int = 0,
|
|
56
|
+
error_code: str = "",
|
|
57
|
+
error_description: str = "",
|
|
58
|
+
status: str | None = None,
|
|
59
|
+
solution: Any = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.error_id = error_id
|
|
62
|
+
self.error_code = error_code
|
|
63
|
+
self.error_description = error_description
|
|
64
|
+
self.status = status
|
|
65
|
+
self.solution = solution
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: dict[str, Any]) -> BaseResp:
|
|
69
|
+
return cls(
|
|
70
|
+
error_id=data.get("errorId", 0),
|
|
71
|
+
error_code=data.get("errorCode", ""),
|
|
72
|
+
error_description=data.get("errorDescription", ""),
|
|
73
|
+
status=data.get("status"),
|
|
74
|
+
solution=data.get("solution"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CreateTaskResp(BaseResp):
|
|
79
|
+
__slots__ = ("task_id",)
|
|
80
|
+
|
|
81
|
+
def __init__(self, task_id: str, **kwargs: Any) -> None:
|
|
82
|
+
super().__init__(**kwargs)
|
|
83
|
+
self.task_id = task_id
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: dict[str, Any]) -> CreateTaskResp:
|
|
87
|
+
return cls(
|
|
88
|
+
task_id=data.get("taskId", ""),
|
|
89
|
+
error_id=data.get("errorId", 0),
|
|
90
|
+
error_code=data.get("errorCode", ""),
|
|
91
|
+
error_description=data.get("errorDescription", ""),
|
|
92
|
+
status=data.get("status"),
|
|
93
|
+
solution=data.get("solution"),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BalanceResp(BaseResp):
|
|
98
|
+
__slots__ = ("balance", "packages")
|
|
99
|
+
|
|
100
|
+
def __init__(self, balance: float = 0.0, packages: list[Any] | None = None, **kwargs: Any) -> None:
|
|
101
|
+
super().__init__(**kwargs)
|
|
102
|
+
self.balance = balance
|
|
103
|
+
self.packages = packages or []
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, data: dict[str, Any]) -> BalanceResp:
|
|
107
|
+
return cls(
|
|
108
|
+
balance=data.get("balance", 0.0),
|
|
109
|
+
packages=data.get("packages", []),
|
|
110
|
+
error_id=data.get("errorId", 0),
|
|
111
|
+
error_code=data.get("errorCode", ""),
|
|
112
|
+
error_description=data.get("errorDescription", ""),
|
|
113
|
+
status=data.get("status"),
|
|
114
|
+
solution=data.get("solution"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── Token-mode task payload types ─────────────────────────────────
|
|
119
|
+
# These are typed dicts so task builders can return plain dicts that
|
|
120
|
+
# serialize directly to JSON without extra conversion.
|
|
121
|
+
|
|
122
|
+
ReCaptchaV2Task = dict[str, Any]
|
|
123
|
+
ReCaptchaV3Task = dict[str, Any]
|
|
124
|
+
CloudflareTask = dict[str, Any]
|
|
125
|
+
AnyTask = dict[str, Any]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── Solution payloads ─────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TokenSolution:
|
|
132
|
+
"""Shared shape for reCAPTCHA / Cloudflare solutions."""
|
|
133
|
+
|
|
134
|
+
__slots__ = ("g_recaptcha_response", "token", "user_agent", "expire_time")
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
g_recaptcha_response: str | None = None,
|
|
139
|
+
token: str | None = None,
|
|
140
|
+
user_agent: str | None = None,
|
|
141
|
+
expire_time: int | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
self.g_recaptcha_response = g_recaptcha_response
|
|
144
|
+
self.token = token
|
|
145
|
+
self.user_agent = user_agent
|
|
146
|
+
self.expire_time = expire_time
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dict(cls, data: dict[str, Any] | None) -> TokenSolution | None:
|
|
150
|
+
if data is None:
|
|
151
|
+
return None
|
|
152
|
+
return cls(
|
|
153
|
+
g_recaptcha_response=data.get("gRecaptchaResponse"),
|
|
154
|
+
token=data.get("token"),
|
|
155
|
+
user_agent=data.get("userAgent"),
|
|
156
|
+
expire_time=data.get("expireTime"),
|
|
157
|
+
)
|
capsolver_core/py.typed
ADDED
|
File without changes
|