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,44 @@
1
+ """CapSolver SDK for Python — detect, solve and autofill captchas via the CapSolver API."""
2
+
3
+ from capsolver_core.capsolver import Capsolver, create_capsolver
4
+ from capsolver_core.core.types import CaptchaType
5
+ from capsolver_core.core.errors import CapsolverError, CapsolverTimeoutError, NetworkError, RateLimitError
6
+ from capsolver_core.core.client import CapsolverClient, WaitOptions
7
+ from capsolver_core.captcha.types import CaptchaInfo, Solution
8
+ from capsolver_core.captcha.handler import CaptchaHandler
9
+ from capsolver_core.captcha.registry import HandlerRegistry
10
+ from capsolver_core.captcha.handlers import (
11
+ RecaptchaHandler,
12
+ CloudflareHandler,
13
+ default_handlers,
14
+ )
15
+ from capsolver_core.browser.driver import PageDriver
16
+ from capsolver_core.browser.adapter import from_playwright_page, to_driver
17
+
18
+ __all__ = [
19
+ # Main entry
20
+ "Capsolver",
21
+ "create_capsolver",
22
+ # Core
23
+ "CaptchaType",
24
+ "CapsolverError",
25
+ "CapsolverTimeoutError",
26
+ "NetworkError",
27
+ "RateLimitError",
28
+ "CapsolverClient",
29
+ "WaitOptions",
30
+ # Captcha
31
+ "CaptchaInfo",
32
+ "Solution",
33
+ "CaptchaHandler",
34
+ "HandlerRegistry",
35
+ "RecaptchaHandler",
36
+ "CloudflareHandler",
37
+ "default_handlers",
38
+ # Browser
39
+ "PageDriver",
40
+ "from_playwright_page",
41
+ "to_driver",
42
+ ]
43
+
44
+ __version__ = "0.1.0"
@@ -0,0 +1,99 @@
1
+ """CLI entry point for ``python -m capsolver_core`` and the ``capsolver`` console script.
2
+
3
+ Usage:
4
+ capsolver info # show version, Python, optional deps
5
+ capsolver balance # check API balance (needs CAPSOLVER_API_KEY)
6
+ capsolver list-types # list supported captcha types
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from importlib.metadata import version as pkg_version
15
+
16
+
17
+ def _cmd_info(_args: argparse.Namespace) -> None:
18
+ """Print SDK version, Python version, and optional-dependency availability."""
19
+ print(f"capsolver-core {pkg_version('capsolver-core')}")
20
+ print(f"python {sys.version.split()[0]}")
21
+
22
+ optionals = {"playwright": "playwright"}
23
+ for label, module in optionals.items():
24
+ try:
25
+ mod_ver = pkg_version(module)
26
+ print(f"{label:<14} {mod_ver}")
27
+ except Exception:
28
+ print(f"{label:<14} not installed")
29
+
30
+
31
+ def _cmd_balance(args: argparse.Namespace) -> None:
32
+ """Check API balance — requires CAPSOLVER_API_KEY."""
33
+ import asyncio
34
+ import os
35
+
36
+ key = args.api_key or os.environ.get("CAPSOLVER_API_KEY", "")
37
+ if not key:
38
+ print("Error: CAPSOLVER_API_KEY is not set.", file=sys.stderr)
39
+ sys.exit(1)
40
+
41
+ from capsolver_core import Capsolver
42
+
43
+ async def _run() -> None:
44
+ cs = Capsolver(api_key=key)
45
+ balance = await cs.get_balance()
46
+ data = {"balance": balance.balance, "packages": balance.packages}
47
+ print(json.dumps(data, indent=2, ensure_ascii=False))
48
+
49
+ asyncio.run(_run())
50
+
51
+
52
+ def _cmd_list_types(_args: argparse.Namespace) -> None:
53
+ """List all supported captcha types."""
54
+ from capsolver_core import Capsolver, CaptchaType
55
+
56
+ cs = Capsolver()
57
+ handlers = cs.get_supported_captchas()
58
+ all_types = [t.value for t in CaptchaType]
59
+
60
+ print("All captcha types:")
61
+ for t in all_types:
62
+ marker = " *" if t in handlers else ""
63
+ print(f" {t}{marker}")
64
+
65
+ if handlers:
66
+ print(f"\nRegistered handlers ({len(handlers)}): {', '.join(handlers)}")
67
+ print("\n(* = handler registered)")
68
+
69
+
70
+ def main() -> None:
71
+ parser = argparse.ArgumentParser(
72
+ prog="capsolver",
73
+ description="CapSolver SDK — diagnostics and info CLI.",
74
+ )
75
+ sub = parser.add_subparsers(dest="command")
76
+
77
+ sub.add_parser("info", help="Show version and environment info.")
78
+
79
+ bal = sub.add_parser("balance", help="Check CapSolver account balance.")
80
+ bal.add_argument("--api-key", default=None, help="API key (default: CAPSOLVER_API_KEY env).")
81
+
82
+ sub.add_parser("list-types", help="List supported captcha types.")
83
+
84
+ args = parser.parse_args()
85
+
86
+ if args.command is None:
87
+ parser.print_help()
88
+ sys.exit(0)
89
+
90
+ dispatch = {
91
+ "info": _cmd_info,
92
+ "balance": _cmd_balance,
93
+ "list-types": _cmd_list_types,
94
+ }
95
+ dispatch[args.command](args)
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -0,0 +1,6 @@
1
+ """Browser layer — page driver, adapters, and inject scripts."""
2
+
3
+ from capsolver_core.browser.driver import PageDriver
4
+ from capsolver_core.browser.adapter import from_playwright_page, to_driver
5
+
6
+ __all__ = ["PageDriver", "from_playwright_page", "to_driver"]
@@ -0,0 +1,116 @@
1
+ """Page adapters.
2
+
3
+ Mirrors the Node SDK's browser/adapter.ts. Wraps a Playwright ``Page``
4
+ (or any structurally-compatible object) as a ``PageDriver``.
5
+
6
+ The SDK deliberately does **not** import Playwright at module level so
7
+ it stays dependency-free at import time — callers pass their own page.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Protocol, cast, runtime_checkable
13
+
14
+ from capsolver_core.browser.driver import PageDriver
15
+
16
+
17
+ @runtime_checkable
18
+ class EvaluatablePage(Protocol):
19
+ """The minimal surface the SDK needs from a Playwright/Selenium page."""
20
+
21
+ async def evaluate(self, expression: str, arg: Any = None) -> Any: ...
22
+ def url(self) -> str: ...
23
+
24
+
25
+ class _PlaywrightDriver:
26
+ """Wraps a Playwright ``Page`` as a ``PageDriver``."""
27
+
28
+ def __init__(self, page: Any) -> None:
29
+ self._page = page
30
+
31
+ async def evaluate(self, script: str, arg: Any = None) -> Any:
32
+ if arg is not None:
33
+ return await self._page.evaluate(script, arg)
34
+ return await self._page.evaluate(script)
35
+
36
+ async def url(self) -> str:
37
+ return cast(str, self._page.url)
38
+
39
+ async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
40
+ opts: dict[str, Any] = {}
41
+ if timeout is not None:
42
+ opts["timeout"] = timeout
43
+ await self._page.wait_for_selector(selector, **opts)
44
+
45
+
46
+ class _GenericDriver:
47
+ """Wraps any object that exposes ``evaluate`` and ``url`` (async or sync)."""
48
+
49
+ def __init__(self, page: Any) -> None:
50
+ self._page = page
51
+
52
+ async def evaluate(self, script: str, arg: Any = None) -> Any:
53
+ fn = self._page.evaluate
54
+ import asyncio
55
+
56
+ if arg is not None:
57
+ result = fn(script, arg)
58
+ else:
59
+ result = fn(script)
60
+ if asyncio.iscoroutine(result):
61
+ return await result
62
+ return result
63
+
64
+ async def url(self) -> str:
65
+ result = self._page.url
66
+ import asyncio
67
+
68
+ if asyncio.iscoroutine(result):
69
+ return cast(str, await result)
70
+ if callable(result):
71
+ r = result()
72
+ if asyncio.iscoroutine(r):
73
+ return cast(str, await r)
74
+ return cast(str, r)
75
+ return cast(str, result)
76
+
77
+ async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
78
+ fn = getattr(self._page, "wait_for_selector", None)
79
+ if fn is None:
80
+ return
81
+ import asyncio
82
+
83
+ opts: dict[str, Any] = {}
84
+ if timeout is not None:
85
+ opts["timeout"] = timeout
86
+ result = fn(selector, **opts)
87
+ if asyncio.iscoroutine(result):
88
+ await result
89
+
90
+
91
+ def from_playwright_page(page: Any) -> PageDriver:
92
+ """Wrap a Playwright ``Page`` as a ``PageDriver``."""
93
+ return _PlaywrightDriver(page)
94
+
95
+
96
+ def from_generic_page(page: Any) -> PageDriver:
97
+ """Wrap any evaluate/url-compatible object as a ``PageDriver``."""
98
+ return _GenericDriver(page)
99
+
100
+
101
+ def to_driver(page: Any) -> PageDriver:
102
+ """Normalize a page-like object into a ``PageDriver``.
103
+
104
+ If it already satisfies ``PageDriver`` it is returned as-is;
105
+ otherwise it is wrapped with the generic adapter.
106
+ """
107
+ # A raw Playwright ``Page`` exposes ``url`` as a string property, whereas a
108
+ # ``PageDriver`` exposes it as an async method. Detect the Playwright page
109
+ # FIRST: a raw page also structurally satisfies the runtime-checkable
110
+ # PageDriver protocol, so the isinstance check below would otherwise return
111
+ # it unwrapped — and ``await page.url()`` would then fail on the string.
112
+ if hasattr(page, "url") and not callable(getattr(page, "url", None)):
113
+ return _PlaywrightDriver(page)
114
+ if isinstance(page, PageDriver):
115
+ return page
116
+ return _GenericDriver(page)
@@ -0,0 +1,26 @@
1
+ """PageDriver — the browser abstraction handlers use to read the DOM.
2
+
3
+ Mirrors the Node SDK's browser/driver.ts. Uses ``Protocol`` so any
4
+ object with the right shape works without inheritance.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class PageDriver(Protocol):
14
+ """Minimal browser page interface."""
15
+
16
+ async def evaluate(self, script: str, arg: Any = None) -> Any:
17
+ """Run a JavaScript snippet in the page context and return its result."""
18
+ ...
19
+
20
+ async def url(self) -> str:
21
+ """The page's current URL."""
22
+ ...
23
+
24
+ async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
25
+ """Optionally wait for a selector to appear."""
26
+ ...
@@ -0,0 +1 @@
1
+ """Inject script package — JS snippets for in-page captcha detection/fill."""
@@ -0,0 +1,67 @@
1
+ """In-page Cloudflare Turnstile scripts (ported from the Node SDK's cloudflare.inject.ts).
2
+
3
+ Self-contained JS snippets — see recaptcha.py for the pattern.
4
+ """
5
+
6
+ # ── Cheap presence check ──────────────────────────────────────────
7
+
8
+ DETECT_CLOUDFLARE_JS = """
9
+ () => {
10
+ // Match Cloudflare-specific markers only. A bare [data-sitekey] is NOT
11
+ // Cloudflare-exclusive (reCAPTCHA/hCaptcha use it too) and would cause a
12
+ // reCAPTCHA page to be mis-detected as Cloudflare.
13
+ return !!document.querySelector(
14
+ 'input[name="cf-turnstile-response"], .cf-turnstile, iframe[src*="challenges.cloudflare.com"]'
15
+ );
16
+ }
17
+ """
18
+
19
+ # ── Extract info for Cloudflare Turnstile widgets on the page ─────
20
+
21
+ GET_CLOUDFLARE_INFOS_JS = """
22
+ () => {
23
+ // Only consider Cloudflare-specific containers — never a bare
24
+ // [data-sitekey], which would grab a reCAPTCHA/hCaptcha sitekey and
25
+ // build an invalid Turnstile task.
26
+ const widget =
27
+ document.querySelector('.cf-turnstile[data-sitekey]') ||
28
+ document.querySelector('.cf-turnstile');
29
+
30
+ let websiteKey = widget ? widget.getAttribute('data-sitekey') : null;
31
+ let action = widget ? widget.getAttribute('data-action') : null;
32
+ let cdata = widget ? widget.getAttribute('data-cdata') : null;
33
+
34
+ // Fall back to the widget iframe's query string.
35
+ if (!websiteKey) {
36
+ const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]');
37
+ if (iframe && iframe.src) {
38
+ const params = new URL(iframe.src.replace('#', '?')).searchParams;
39
+ websiteKey = params.get('sitekey');
40
+ action = action || params.get('action');
41
+ cdata = cdata || params.get('cData');
42
+ }
43
+ }
44
+
45
+ // Identify a container for fill / autofill.
46
+ const input = document.querySelector('input[name="cf-turnstile-response"]');
47
+ const container = (input ? input.parentElement : widget) || null;
48
+ if (container && !container.id) container.id = 'cloudflare-container-' + Date.now();
49
+
50
+ if (!websiteKey) return [];
51
+ return [{ websiteKey, action, cdata, containerId: container ? container.id : null }];
52
+ }
53
+ """
54
+
55
+ # ── Write the token back into the hidden input ────────────────────
56
+
57
+ FILL_CLOUDFLARE_JS = """
58
+ (args) => {
59
+ const scope = args.containerId ? document.getElementById(args.containerId) : document;
60
+ const input =
61
+ (scope ? scope.querySelector('input[name="cf-turnstile-response"]') : null) ||
62
+ document.querySelector('input[name="cf-turnstile-response"]');
63
+ if (!input) return false;
64
+ input.value = args.token;
65
+ return true;
66
+ }
67
+ """
@@ -0,0 +1,162 @@
1
+ """In-page reCAPTCHA scripts (ported from the Node SDK's recaptcha.inject.ts).
2
+
3
+ Each constant is a self-contained JavaScript snippet that runs inside the
4
+ page via ``page.evaluate``. No imports, no module-scope references — only
5
+ the page's ``window``/``document`` and their own arguments.
6
+ """
7
+
8
+ # ── Cheap presence check ──────────────────────────────────────────
9
+
10
+ DETECT_RECAPTCHA_JS = """
11
+ () => {
12
+ const cfg = window.___grecaptcha_cfg;
13
+ return !!(cfg && cfg.clients && Object.keys(cfg.clients).length > 0);
14
+ }
15
+ """
16
+
17
+ # ── Extract a descriptor for every reCAPTCHA widget on the page ───
18
+
19
+ GET_RECAPTCHA_INFOS_JS = """
20
+ () => {
21
+ function getSaParam() {
22
+ const reCap = document.querySelector('iframe[title="reCAPTCHA"]');
23
+ const src = reCap ? reCap.getAttribute('src') : null;
24
+ if (!src) return null;
25
+ return new URL(src).searchParams.get('sa');
26
+ }
27
+
28
+ function getWidgetInfo(widget) {
29
+ const info = {
30
+ captchaType: 'reCaptcha',
31
+ version: 'v2',
32
+ sitekey: null,
33
+ action: null,
34
+ s: null,
35
+ callback: null,
36
+ enterprise: !!(window.grecaptcha && window.grecaptcha.enterprise),
37
+ containerId: null,
38
+ bindedButtonId: null,
39
+ invisible: false,
40
+ };
41
+
42
+ // Detect the v3 badge.
43
+ let isBadge = false;
44
+ badge: for (const f in widget) {
45
+ if (typeof widget[f] !== 'object') continue;
46
+ for (const g in widget[f]) {
47
+ if (widget[f][g] && widget[f][g].classList && widget[f][g].classList.contains('grecaptcha-badge')) {
48
+ isBadge = true;
49
+ break badge;
50
+ }
51
+ }
52
+ }
53
+ if (isBadge) {
54
+ info.version = 'v3';
55
+ info.captchaType = 'reCaptcha3';
56
+ for (const h in widget) {
57
+ const i = widget[h];
58
+ if (typeof i !== 'object') continue;
59
+ for (const j in i) {
60
+ if (typeof i[j] !== 'string') continue;
61
+ if (i[j] === 'fullscreen') {
62
+ info.version = 'v2';
63
+ info.captchaType = 'reCaptcha';
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ // Find the container element id.
70
+ let candidate = null;
71
+ for (const k in widget) {
72
+ if (widget[k] && widget[k].nodeType) {
73
+ if (widget[k].id) {
74
+ info.containerId = widget[k].id;
75
+ } else if (widget[k].dataset && widget[k].dataset.sitekey) {
76
+ widget[k].id = 'recaptcha-container-' + Date.now();
77
+ info.containerId = widget[k].id;
78
+ } else if (!candidate) {
79
+ candidate = widget[k];
80
+ } else if (widget[k].isSameNode(candidate)) {
81
+ widget[k].id = 'recaptcha-container-' + Date.now();
82
+ info.containerId = widget[k].id;
83
+ break;
84
+ }
85
+ }
86
+ }
87
+
88
+ // Find sitekey / action / s / callback / bind / size.
89
+ for (const k1 in widget) {
90
+ const obj = widget[k1];
91
+ if (typeof obj !== 'object') continue;
92
+ for (const k2 in obj) {
93
+ if (obj[k2] === null || typeof obj[k2] !== 'object') continue;
94
+ if (obj[k2].sitekey === undefined || obj[k2].action === undefined) continue;
95
+ for (const k3 in obj[k2]) {
96
+ if (k3 === 'sitekey') info.sitekey = obj[k2][k3];
97
+ if (k3 === 'action') info.action = obj[k2][k3];
98
+ if (k3 === 's') info.s = obj[k2][k3];
99
+ if (k3 === 'callback' || k3 === 'promise-callback') info.callback = obj[k2][k3];
100
+ if (k3 === 'bind' && obj[k2][k3]) {
101
+ const bind = obj[k2][k3];
102
+ if (typeof bind === 'string') {
103
+ info.bindedButtonId = bind;
104
+ } else {
105
+ if (bind.id === undefined) bind.id = 'recaptchaBindedElement' + widget.id;
106
+ info.bindedButtonId = bind.id;
107
+ }
108
+ }
109
+ if (k3 === 'size' && obj[k2][k3] === 'invisible') info.invisible = true;
110
+ }
111
+ }
112
+ }
113
+
114
+ // Stash a function callback under a global key so fill can invoke it.
115
+ if (typeof info.callback === 'function') {
116
+ const callbackKey = 'reCaptchaWidgetCallback' + widget.id;
117
+ window[callbackKey] = info.callback;
118
+ info.callback = callbackKey;
119
+ }
120
+
121
+ if (info.captchaType === 'reCaptcha') info.action = getSaParam();
122
+
123
+ return info;
124
+ }
125
+
126
+ const cfg = window.___grecaptcha_cfg;
127
+ if (!cfg || !cfg.clients) return [];
128
+
129
+ const infos = [];
130
+ for (const widgetId in cfg.clients) {
131
+ infos.push(getWidgetInfo(cfg.clients[widgetId]));
132
+ }
133
+ return infos;
134
+ }
135
+ """
136
+
137
+ # ── Write the token back into the response textarea and fire the callback ─
138
+
139
+ FILL_RECAPTCHA_JS = """
140
+ (args) => {
141
+ let textarea = null;
142
+ if (args.containerId) {
143
+ textarea = document.querySelector('#' + args.containerId + ' textarea[name=g-recaptcha-response]');
144
+ }
145
+ if (!textarea) {
146
+ textarea = document.querySelector('textarea[name=g-recaptcha-response]');
147
+ }
148
+ if (textarea) {
149
+ textarea.innerHTML = args.token;
150
+ textarea.value = args.token;
151
+ }
152
+
153
+ if (args.callback && typeof window[args.callback] === 'function') {
154
+ try {
155
+ window[args.callback](args.token);
156
+ } catch (e) {
157
+ /* callback threw — token is still set in the textarea */
158
+ }
159
+ }
160
+ return !!textarea;
161
+ }
162
+ """