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.
@@ -0,0 +1,206 @@
1
+ """Capsolver — the public, agent-facing entry point.
2
+
3
+ Mirrors the Node SDK's capsolver.ts. Wires a ``CapsolverClient``
4
+ (token-solving core) to a ``HandlerRegistry`` of per-captcha handlers.
5
+
6
+ The API key is optional at construction so read-only calls like
7
+ ``get_supported_captchas()`` work without one; it is required (and
8
+ validated lazily) the first time a solve is attempted.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ from capsolver_core.core.client import CapsolverClient, CapsolverClientOptions, WaitOptions
17
+ from capsolver_core.core.errors import CapsolverError
18
+ from capsolver_core.core.types import CaptchaType, BalanceResp
19
+ from capsolver_core.captcha.handler import CaptchaHandler
20
+ from capsolver_core.captcha.handlers import default_handlers
21
+ from capsolver_core.captcha.registry import HandlerRegistry
22
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
23
+ from capsolver_core.browser.adapter import to_driver
24
+
25
+
26
+ @dataclass
27
+ class SolveOnPageOptions:
28
+ """Options for ``Capsolver.solve_on_page``."""
29
+
30
+ autofill: bool = True
31
+ throw_on_error: bool = False
32
+ timeout: float | None = None
33
+ polling_interval: float | None = None
34
+
35
+
36
+ @dataclass
37
+ class SolveOnPageResult:
38
+ """Per-captcha result from ``solve_on_page``."""
39
+
40
+ info: CaptchaInfo
41
+ solution: Solution | None = None
42
+ filled: bool | None = None
43
+ error: str | None = None
44
+
45
+
46
+ class Capsolver:
47
+ """Main SDK entry point — detect, solve, and autofill captchas."""
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ api_key: str = "",
53
+ service: str = "https://api.capsolver.com",
54
+ default_timeout: float = 120.0,
55
+ polling_interval: float = 5.0,
56
+ request_timeout_ms: int = 30_000,
57
+ app_id: str | None = None,
58
+ source: str | None = None,
59
+ version: str | None = None,
60
+ on_error: Any = None,
61
+ handlers: list[CaptchaHandler] | None = None,
62
+ ) -> None:
63
+ self._client_options = CapsolverClientOptions(
64
+ api_key=api_key,
65
+ service=service,
66
+ default_timeout=default_timeout,
67
+ polling_interval=polling_interval,
68
+ request_timeout_ms=request_timeout_ms,
69
+ app_id=app_id,
70
+ source=source,
71
+ version=version,
72
+ on_error=on_error,
73
+ )
74
+ self._registry = HandlerRegistry()
75
+ self._client: CapsolverClient | None = None
76
+
77
+ for h in handlers if handlers is not None else default_handlers():
78
+ self._registry.register(h)
79
+
80
+ # ── resource management ───────────────────────────────────────
81
+
82
+ async def aclose(self) -> None:
83
+ """Close the underlying HTTP client and release connections."""
84
+ if self._client is not None:
85
+ await self._client.aclose()
86
+
87
+ async def __aenter__(self) -> Capsolver:
88
+ return self
89
+
90
+ async def __aexit__(self, *exc: Any) -> None:
91
+ await self.aclose()
92
+
93
+ # ── registry access ───────────────────────────────────────────
94
+
95
+ def register(self, handler: CaptchaHandler) -> Capsolver:
96
+ """Add or replace a captcha handler."""
97
+ self._registry.register(handler)
98
+ return self
99
+
100
+ def get_supported_captchas(self) -> list[str]:
101
+ """Names of every registered handler."""
102
+ return self._registry.list()
103
+
104
+ def get_handler(self, key: str | CaptchaType) -> CaptchaHandler | None:
105
+ """Resolve a handler by name or captcha type."""
106
+ return self._registry.resolve(key)
107
+
108
+ # ── solving ───────────────────────────────────────────────────
109
+
110
+ async def solve(self, info: CaptchaInfo, wait_options: WaitOptions | None = None) -> Solution:
111
+ """Solve a captcha from its normalized info (token mode)."""
112
+ if not info or not info.type:
113
+ raise CapsolverError("CaptchaInfo.type is required to pick a handler")
114
+
115
+ handler = self._registry.resolve(info.type)
116
+ if not handler:
117
+ raise CapsolverError(f"No handler registered for captcha type: {info.type}")
118
+
119
+ return await handler.solve(info, self._get_client(), wait_options)
120
+
121
+ # ── browser-aware methods ─────────────────────────────────────
122
+
123
+ async def detect(self, page: Any) -> list[CaptchaType]:
124
+ """Which captcha types are present on a page. Returns ``[]`` when none.
125
+
126
+ Derived from :meth:`get_captcha_info` so the reported type reflects the
127
+ actual widget — e.g. reCAPTCHA v3 is returned as ``RECAPTCHA_V3``, not
128
+ the handler's canonical ``RECAPTCHA_V2`` family. Order-preserving and
129
+ de-duplicated.
130
+ """
131
+ driver = to_driver(page)
132
+ found: list[CaptchaType] = []
133
+ for info in await self.get_captcha_info(driver):
134
+ if info.type not in found:
135
+ found.append(info.type)
136
+ return found
137
+
138
+ async def get_captcha_info(self, page: Any) -> list[CaptchaInfo]:
139
+ """Structured params for every captcha on the page. Returns ``[]`` when none."""
140
+ driver = to_driver(page)
141
+ infos: list[CaptchaInfo] = []
142
+ for handler in self._registry.handlers():
143
+ try:
144
+ result = await handler.get_captcha_info(driver)
145
+ infos.extend(result)
146
+ except Exception:
147
+ pass
148
+ return infos
149
+
150
+ async def solve_on_page(
151
+ self,
152
+ page: Any,
153
+ options: SolveOnPageOptions | None = None,
154
+ ) -> list[SolveOnPageResult]:
155
+ """One-shot: detect → solve → (optionally) autofill.
156
+
157
+ Per-captcha errors are collected unless ``throw_on_error`` is set.
158
+ """
159
+ opts = options or SolveOnPageOptions()
160
+ driver = to_driver(page)
161
+ infos = await self.get_captcha_info(driver)
162
+
163
+ wait_opts = WaitOptions(timeout=opts.timeout, polling_interval=opts.polling_interval)
164
+
165
+ results: list[SolveOnPageResult] = []
166
+ for info in infos:
167
+ result = SolveOnPageResult(info=info)
168
+ try:
169
+ handler = self._registry.resolve(info.type)
170
+ if not handler:
171
+ raise CapsolverError(f"No handler registered for captcha type: {info.type}")
172
+
173
+ result.solution = await handler.solve(info, self._get_client(), wait_opts)
174
+
175
+ if opts.autofill and hasattr(handler, "fill") and handler.fill is not None:
176
+ try:
177
+ result.filled = await handler.fill(driver, result.solution, info)
178
+ except Exception:
179
+ result.filled = False
180
+ except Exception as e:
181
+ if opts.throw_on_error:
182
+ raise
183
+ result.error = str(e)
184
+ results.append(result)
185
+ return results
186
+
187
+ # ── account ───────────────────────────────────────────────────
188
+
189
+ async def get_balance(self) -> BalanceResp:
190
+ """Account balance (requires an API key)."""
191
+ return await self._get_client().get_balance()
192
+
193
+ # ── internals ─────────────────────────────────────────────────
194
+
195
+ def _get_client(self) -> CapsolverClient:
196
+ """Lazily construct the client, validating the API key on first use."""
197
+ if self._client is None:
198
+ if not self._client_options.api_key:
199
+ raise CapsolverError("Capsolver: apiKey is required to solve. Pass it to the constructor.")
200
+ self._client = CapsolverClient(self._client_options)
201
+ return self._client
202
+
203
+
204
+ def create_capsolver(**kwargs: Any) -> Capsolver:
205
+ """Convenience factory mirroring ``Capsolver(...)``."""
206
+ return Capsolver(**kwargs)
@@ -0,0 +1,20 @@
1
+ """Captcha layer — handler protocol, registry, built-in handlers."""
2
+
3
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
4
+ from capsolver_core.captcha.handler import CaptchaHandler
5
+ from capsolver_core.captcha.registry import HandlerRegistry
6
+ from capsolver_core.captcha.handlers import (
7
+ RecaptchaHandler,
8
+ CloudflareHandler,
9
+ default_handlers,
10
+ )
11
+
12
+ __all__ = [
13
+ "CaptchaInfo",
14
+ "Solution",
15
+ "CaptchaHandler",
16
+ "HandlerRegistry",
17
+ "RecaptchaHandler",
18
+ "CloudflareHandler",
19
+ "default_handlers",
20
+ ]
@@ -0,0 +1,55 @@
1
+ """CaptchaHandler — the per-captcha plugin contract.
2
+
3
+ Mirrors the Node SDK's captcha/handler.ts. Uses ``Protocol`` for
4
+ structural subtyping so third-party handlers don't need to inherit.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Protocol, runtime_checkable
10
+
11
+ from capsolver_core.core.types import CaptchaType
12
+ from capsolver_core.core.client import CapsolverClient, WaitOptions
13
+ from capsolver_core.browser.driver import PageDriver
14
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
15
+
16
+
17
+ @runtime_checkable
18
+ class CaptchaHandler(Protocol):
19
+ """Per-captcha-family plugin interface."""
20
+
21
+ @property
22
+ def type(self) -> CaptchaType:
23
+ """Canonical captcha family this handler serves."""
24
+ ...
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ """Short, stable name used for registration."""
29
+ ...
30
+
31
+ @property
32
+ def aliases(self) -> tuple[CaptchaType, ...]:
33
+ """Extra captcha types this handler also serves."""
34
+ ...
35
+
36
+ async def solve(
37
+ self,
38
+ info: CaptchaInfo,
39
+ client: CapsolverClient,
40
+ wait_options: WaitOptions | None = None,
41
+ ) -> Solution:
42
+ """Token-mode solve: build the task, poll, normalize the result."""
43
+ ...
44
+
45
+ async def detect(self, page: PageDriver) -> bool:
46
+ """Is this captcha present on the page?"""
47
+ ...
48
+
49
+ async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
50
+ """Extract structured info for every instance on the page."""
51
+ ...
52
+
53
+ async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
54
+ """Write a solved token back into the page (autofill)."""
55
+ ...
@@ -0,0 +1,20 @@
1
+ """Built-in captcha handlers."""
2
+
3
+ from capsolver_core.captcha.handlers.recaptcha import RecaptchaHandler
4
+ from capsolver_core.captcha.handlers.cloudflare import CloudflareHandler
5
+ from capsolver_core.captcha.handler import CaptchaHandler
6
+
7
+
8
+ def default_handlers() -> list[CaptchaHandler]:
9
+ """Fresh instances of every built-in token-mode handler."""
10
+ return [
11
+ RecaptchaHandler(),
12
+ CloudflareHandler(),
13
+ ]
14
+
15
+
16
+ __all__ = [
17
+ "RecaptchaHandler",
18
+ "CloudflareHandler",
19
+ "default_handlers",
20
+ ]
@@ -0,0 +1,83 @@
1
+ """Cloudflare Turnstile handler (token mode).
2
+
3
+ Mirrors the Node SDK's captcha/handlers/cloudflare.ts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from capsolver_core.core.types import CaptchaType, TokenSolution
11
+ from capsolver_core.core.client import CapsolverClient, WaitOptions
12
+ from capsolver_core.core.tasks import build_cloudflare_task
13
+ from capsolver_core.browser.driver import PageDriver
14
+ from capsolver_core.browser.inject.cloudflare import (
15
+ DETECT_CLOUDFLARE_JS,
16
+ GET_CLOUDFLARE_INFOS_JS,
17
+ FILL_CLOUDFLARE_JS,
18
+ )
19
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
20
+ from capsolver_core.captcha.handlers.support import require_url_and_key, to_solution
21
+
22
+
23
+ class CloudflareHandler:
24
+ """Cloudflare Turnstile token-mode handler."""
25
+
26
+ @property
27
+ def type(self) -> CaptchaType:
28
+ return CaptchaType.CLOUDFLARE
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return "cloudflare"
33
+
34
+ @property
35
+ def aliases(self) -> tuple[CaptchaType, ...]:
36
+ return ()
37
+
38
+ async def solve(
39
+ self,
40
+ info: CaptchaInfo,
41
+ client: CapsolverClient,
42
+ wait_options: WaitOptions | None = None,
43
+ ) -> Solution:
44
+ website_url, website_key = require_url_and_key(info)
45
+ task = build_cloudflare_task(
46
+ website_url=website_url,
47
+ website_key=website_key,
48
+ action=info.page_action,
49
+ cdata=info.cdata,
50
+ proxy=info.proxy,
51
+ )
52
+ res = await client.create_task_result(task, wait_options)
53
+ return to_solution(CaptchaType.CLOUDFLARE, TokenSolution.from_dict(res.get("solution")))
54
+
55
+ async def detect(self, page: PageDriver) -> bool:
56
+ return bool(await page.evaluate(DETECT_CLOUDFLARE_JS))
57
+
58
+ async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
59
+ url = await page.url()
60
+ raws: list[dict[str, Any]] = await page.evaluate(GET_CLOUDFLARE_INFOS_JS)
61
+ infos: list[CaptchaInfo] = []
62
+ for r in raws:
63
+ if not r.get("websiteKey"):
64
+ continue
65
+ infos.append(
66
+ CaptchaInfo(
67
+ type=CaptchaType.CLOUDFLARE,
68
+ website_url=url,
69
+ website_key=r["websiteKey"],
70
+ page_action=r.get("action") or None,
71
+ cdata=r.get("cdata") or None,
72
+ container_id=r.get("containerId"),
73
+ )
74
+ )
75
+ return infos
76
+
77
+ async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
78
+ return bool(
79
+ await page.evaluate(
80
+ FILL_CLOUDFLARE_JS,
81
+ {"token": solution.token, "containerId": info.container_id},
82
+ )
83
+ )
@@ -0,0 +1,106 @@
1
+ """reCAPTCHA handler — covers v2 and v3 (incl. enterprise) in token mode.
2
+
3
+ Mirrors the Node SDK's captcha/handlers/recaptcha.ts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from capsolver_core.core.types import CaptchaType, TokenSolution
11
+ from capsolver_core.core.client import CapsolverClient, WaitOptions
12
+ from capsolver_core.core.tasks import build_recaptcha_v2_task, build_recaptcha_v3_task
13
+ from capsolver_core.browser.driver import PageDriver
14
+ from capsolver_core.browser.inject.recaptcha import (
15
+ DETECT_RECAPTCHA_JS,
16
+ GET_RECAPTCHA_INFOS_JS,
17
+ FILL_RECAPTCHA_JS,
18
+ )
19
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
20
+ from capsolver_core.captcha.handlers.support import require_url_and_key, to_solution
21
+
22
+
23
+ class RecaptchaHandler:
24
+ """reCAPTCHA v2 + v3 (incl. enterprise) token-mode handler."""
25
+
26
+ @property
27
+ def type(self) -> CaptchaType:
28
+ return CaptchaType.RECAPTCHA_V2
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return "recaptcha"
33
+
34
+ @property
35
+ def aliases(self) -> tuple[CaptchaType, ...]:
36
+ return (CaptchaType.RECAPTCHA_V3,)
37
+
38
+ async def solve(
39
+ self,
40
+ info: CaptchaInfo,
41
+ client: CapsolverClient,
42
+ wait_options: WaitOptions | None = None,
43
+ ) -> Solution:
44
+ website_url, website_key = require_url_and_key(info)
45
+ enterprise_payload = {"s": info.s} if info.s else None
46
+ is_v3 = info.version == "v3" or info.type == CaptchaType.RECAPTCHA_V3
47
+
48
+ if is_v3:
49
+ task = build_recaptcha_v3_task(
50
+ website_url=website_url,
51
+ website_key=website_key,
52
+ page_action=info.page_action,
53
+ min_score=info.min_score,
54
+ enterprise=info.enterprise,
55
+ enterprise_payload=enterprise_payload,
56
+ proxy=info.proxy,
57
+ )
58
+ else:
59
+ task = build_recaptcha_v2_task(
60
+ website_url=website_url,
61
+ website_key=website_key,
62
+ invisible=info.invisible,
63
+ page_action=info.page_action,
64
+ enterprise=info.enterprise,
65
+ enterprise_payload=enterprise_payload,
66
+ proxy=info.proxy,
67
+ user_agent=info.user_agent,
68
+ )
69
+
70
+ res = await client.create_task_result(task, wait_options)
71
+ return to_solution(info.type, TokenSolution.from_dict(res.get("solution")))
72
+
73
+ async def detect(self, page: PageDriver) -> bool:
74
+ return bool(await page.evaluate(DETECT_RECAPTCHA_JS))
75
+
76
+ async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
77
+ url = await page.url()
78
+ raws: list[dict[str, Any]] = await page.evaluate(GET_RECAPTCHA_INFOS_JS)
79
+ infos: list[CaptchaInfo] = []
80
+ for r in raws:
81
+ if not r.get("sitekey"):
82
+ continue
83
+ infos.append(
84
+ CaptchaInfo(
85
+ type=CaptchaType.RECAPTCHA_V3 if r.get("version") == "v3" else CaptchaType.RECAPTCHA_V2,
86
+ version=r.get("version"),
87
+ website_url=url,
88
+ website_key=r["sitekey"],
89
+ page_action=r.get("action") or None,
90
+ invisible=r.get("invisible"),
91
+ enterprise=r.get("enterprise"),
92
+ s=r.get("s") or None,
93
+ container_id=r.get("containerId"),
94
+ callback=r.get("callback"),
95
+ binded_button_id=r.get("bindedButtonId"),
96
+ )
97
+ )
98
+ return infos
99
+
100
+ async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
101
+ return bool(
102
+ await page.evaluate(
103
+ FILL_RECAPTCHA_JS,
104
+ {"token": solution.token, "containerId": info.container_id, "callback": info.callback},
105
+ )
106
+ )
@@ -0,0 +1,30 @@
1
+ """Shared helpers for token-mode handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from capsolver_core.core.errors import CapsolverError
6
+ from capsolver_core.core.types import CaptchaType, TokenSolution
7
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
8
+
9
+
10
+ def require_url_and_key(info: CaptchaInfo) -> tuple[str, str]:
11
+ """Ensure the fields every token task needs are present."""
12
+ if not info.website_url:
13
+ raise CapsolverError("CaptchaInfo.website_url is required to solve")
14
+ if not info.website_key:
15
+ raise CapsolverError("CaptchaInfo.website_key is required to solve")
16
+ return info.website_url, info.website_key
17
+
18
+
19
+ def to_solution(captcha_type: CaptchaType, raw: TokenSolution | None) -> Solution:
20
+ """Normalize an API solution payload into the SDK ``Solution`` shape."""
21
+ token = (raw.g_recaptcha_response if raw else None) or (raw.token if raw else None)
22
+ if not token:
23
+ raise CapsolverError("Solver returned an empty token", error_code="EMPTY_TOKEN")
24
+ return Solution(
25
+ captcha_type=captcha_type,
26
+ token=token,
27
+ raw=raw,
28
+ expire_time=raw.expire_time if raw else None,
29
+ user_agent=raw.user_agent if raw else None,
30
+ )
@@ -0,0 +1,56 @@
1
+ """HandlerRegistry — register/lookup captcha handlers.
2
+
3
+ Mirrors the Node SDK's captcha/registry.ts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import builtins
9
+
10
+ from capsolver_core.core.types import CaptchaType
11
+ from capsolver_core.captcha.handler import CaptchaHandler
12
+
13
+
14
+ class HandlerRegistry:
15
+ """Index handlers by short name and by every ``CaptchaType`` they serve."""
16
+
17
+ def __init__(self) -> None:
18
+ self._by_name: dict[str, CaptchaHandler] = {}
19
+ self._by_type: dict[CaptchaType, CaptchaHandler] = {}
20
+
21
+ def register(self, handler: CaptchaHandler) -> HandlerRegistry:
22
+ self._by_name[handler.name] = handler
23
+ self._by_type[handler.type] = handler
24
+ for alias in handler.aliases:
25
+ self._by_type[alias] = handler
26
+ return self
27
+
28
+ def get(self, name: str) -> CaptchaHandler | None:
29
+ return self._by_name.get(name)
30
+
31
+ def get_by_type(self, captcha_type: CaptchaType) -> CaptchaHandler | None:
32
+ return self._by_type.get(captcha_type)
33
+
34
+ def resolve(self, key: str | CaptchaType) -> CaptchaHandler | None:
35
+ """Look up by name string or ``CaptchaType``."""
36
+ if isinstance(key, CaptchaType):
37
+ return self._by_type.get(key)
38
+ # Try by name first
39
+ handler = self._by_name.get(key)
40
+ if handler:
41
+ return handler
42
+ # Try converting string to CaptchaType (e.g. "reCaptchaV2")
43
+ try:
44
+ return self._by_type.get(CaptchaType(key))
45
+ except (ValueError, KeyError):
46
+ return None
47
+
48
+ def list(self) -> builtins.list[str]:
49
+ """Registered handler names, in insertion order."""
50
+ return builtins.list(self._by_name.keys())
51
+
52
+ def handlers(self) -> builtins.list[CaptchaHandler]:
53
+ return builtins.list(self._by_name.values())
54
+
55
+ def has(self, name: str) -> bool:
56
+ return name in self._by_name
@@ -0,0 +1,60 @@
1
+ """Contract types shared between detection and solving.
2
+
3
+ Mirrors the Node SDK's captcha/types.ts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from capsolver_core.core.types import CaptchaType, TokenSolution
12
+
13
+
14
+ @dataclass
15
+ class CaptchaInfo:
16
+ """Normalized, browser-agnostic description of a captcha on a page."""
17
+
18
+ type: CaptchaType
19
+ website_url: str = ""
20
+ website_key: str = ""
21
+
22
+ # reCAPTCHA-specific
23
+ version: str | None = None # "v2" | "v3"
24
+ page_action: str | None = None
25
+ invisible: bool | None = None
26
+ enterprise: bool | None = None
27
+ s: str | None = None # Enterprise ``s`` token
28
+ min_score: float | None = None # reCAPTCHA v3
29
+
30
+ # Cloudflare Turnstile
31
+ cdata: str | None = None
32
+
33
+ # Generic
34
+ proxy: str | None = None
35
+ user_agent: str | None = None
36
+
37
+ # DOM hooks (Phase 3 autofill)
38
+ container_id: str | None = None
39
+ callback: str | None = None
40
+ binded_button_id: str | None = None
41
+
42
+ # Carry-through for fields not yet modelled
43
+ extra: dict[str, Any] = field(default_factory=dict)
44
+
45
+ def __post_init__(self) -> None:
46
+ if not self.website_url:
47
+ raise ValueError("CaptchaInfo.website_url is required")
48
+ if not self.website_key:
49
+ raise ValueError("CaptchaInfo.website_key is required")
50
+
51
+
52
+ @dataclass
53
+ class Solution:
54
+ """Normalized solve result."""
55
+
56
+ captcha_type: CaptchaType
57
+ token: str
58
+ raw: TokenSolution | None = None
59
+ expire_time: int | None = None
60
+ user_agent: str | None = None
@@ -0,0 +1,7 @@
1
+ """Core layer — HTTP, errors, task builders, client, types."""
2
+
3
+ from capsolver_core.core.types import * # noqa: F401,F403
4
+ from capsolver_core.core.errors import * # noqa: F401,F403
5
+ from capsolver_core.core.http import * # noqa: F401,F403
6
+ from capsolver_core.core.tasks import * # noqa: F401,F403
7
+ from capsolver_core.core.client import * # noqa: F401,F403