capskip 1.0.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.
capskip/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """CapSkip Python SDK — local captcha solver client."""
2
+
3
+ from .api import ApiClient
4
+ from .async_api import AsyncApiClient
5
+ from .async_solver import AsyncCapSkip
6
+ from .exceptions import (
7
+ ApiException,
8
+ CapSkipError,
9
+ NetworkException,
10
+ SolverExceptions,
11
+ TimeoutException,
12
+ ValidationException,
13
+ )
14
+ from .solver import CapSkip
15
+
16
+ __all__ = [
17
+ 'CapSkip',
18
+ 'AsyncCapSkip',
19
+ 'ApiClient',
20
+ 'AsyncApiClient',
21
+ 'CapSkipError',
22
+ 'SolverExceptions',
23
+ 'ValidationException',
24
+ 'NetworkException',
25
+ 'ApiException',
26
+ 'TimeoutException',
27
+ ]
28
+
29
+ __version__ = '1.0.0'
capskip/_api_params.py ADDED
@@ -0,0 +1,118 @@
1
+ """CapSkip API parameter validation (https://capskip.com/api-docs/)."""
2
+
3
+ from .exceptions import ValidationException
4
+
5
+ NORMAL_SUBMIT = frozenset({'method', 'body', 'json', 'file'})
6
+
7
+ RECAPTCHA_V2_SUBMIT = frozenset({
8
+ 'method', 'googlekey', 'pageurl', 'enterprise', 'invisible', 'data-s', 'json',
9
+ 'proxy', 'proxytype',
10
+ })
11
+
12
+ RECAPTCHA_V3_SUBMIT = frozenset({
13
+ 'method', 'version', 'googlekey', 'pageurl', 'enterprise', 'action', 'min_score',
14
+ 'json', 'proxy', 'proxytype',
15
+ })
16
+
17
+ TURNSTILE_SUBMIT = frozenset({
18
+ 'method', 'sitekey', 'pageurl', 'action', 'data', 'pagedata', 'json',
19
+ 'proxy', 'proxytype',
20
+ })
21
+
22
+ _PARAM_ALIASES = {
23
+ 'url': 'pageurl',
24
+ 'score': 'min_score',
25
+ 'minScore': 'min_score',
26
+ 'datas': 'data-s',
27
+ 'data_s': 'data-s',
28
+ }
29
+
30
+
31
+ def apply_param_aliases(params: dict) -> dict:
32
+ out = dict(params)
33
+ for alias, api_name in _PARAM_ALIASES.items():
34
+ if alias in out:
35
+ if api_name in out and out[alias] != out[api_name]:
36
+ raise ValidationException(
37
+ f"Conflicting parameters: {alias!r} and {api_name!r}"
38
+ )
39
+ out[api_name] = out.pop(alias)
40
+ return out
41
+
42
+
43
+ def apply_proxy(params: dict) -> dict:
44
+ out = dict(params)
45
+ proxy = out.pop('proxy', None)
46
+ if not proxy:
47
+ return out
48
+ if isinstance(proxy, dict):
49
+ if 'uri' not in proxy or 'type' not in proxy:
50
+ raise ValidationException("proxy dict must contain 'type' and 'uri' keys")
51
+ out['proxy'] = proxy['uri']
52
+ out['proxytype'] = proxy['type']
53
+ else:
54
+ out['proxy'] = proxy
55
+ if 'proxytype' not in out:
56
+ out['proxytype'] = 'HTTP'
57
+ return out
58
+
59
+
60
+ def _unknown_keys(params: dict, allowed: frozenset) -> set:
61
+ return set(params.keys()) - allowed - {'key', 'file', 'files'}
62
+
63
+
64
+ def validate_normal_submit(params: dict) -> None:
65
+ unknown = _unknown_keys(params, NORMAL_SUBMIT)
66
+ if unknown:
67
+ raise ValidationException(
68
+ f"Unsupported parameters for image captcha: {sorted(unknown)}. "
69
+ f"CapSkip only supports: method, file/body, json."
70
+ )
71
+ if 'proxy' in params or 'proxytype' in params:
72
+ raise ValidationException(
73
+ "Proxy is not supported for image captcha. "
74
+ "Use proxy only with reCAPTCHA or Turnstile."
75
+ )
76
+
77
+
78
+ def validate_recaptcha_submit(params: dict, version: str) -> None:
79
+ version = (version or 'v2').lower()
80
+ if version == 'v3':
81
+ allowed = RECAPTCHA_V3_SUBMIT
82
+ if params.get('invisible'):
83
+ raise ValidationException("invisible is only supported for reCAPTCHA v2.")
84
+ else:
85
+ allowed = RECAPTCHA_V2_SUBMIT
86
+ if params.get('version') == 'v3':
87
+ raise ValidationException("Use version='v3' for reCAPTCHA v3.")
88
+ for key in ('action', 'min_score'):
89
+ if key in params:
90
+ raise ValidationException(f"{key!r} is only supported for reCAPTCHA v3.")
91
+
92
+ unknown = _unknown_keys(params, allowed)
93
+ if unknown:
94
+ raise ValidationException(
95
+ f"Unsupported parameters for reCAPTCHA {version}: {sorted(unknown)}."
96
+ )
97
+
98
+
99
+ def validate_turnstile_submit(params: dict) -> None:
100
+ unknown = _unknown_keys(params, TURNSTILE_SUBMIT)
101
+ if unknown:
102
+ raise ValidationException(
103
+ f"Unsupported parameters for Turnstile: {sorted(unknown)}."
104
+ )
105
+
106
+
107
+ def prepare_submit_params(params: dict, captcha_type: str, version: str = 'v2') -> dict:
108
+ params = apply_param_aliases(params)
109
+ params = apply_proxy(params)
110
+
111
+ if captcha_type == 'normal':
112
+ validate_normal_submit(params)
113
+ elif captcha_type == 'recaptcha':
114
+ validate_recaptcha_submit(params, version)
115
+ elif captcha_type == 'turnstile':
116
+ validate_turnstile_submit(params)
117
+
118
+ return params
capskip/api.py ADDED
@@ -0,0 +1,56 @@
1
+ import requests
2
+
3
+ from .exceptions import ApiException, NetworkException
4
+
5
+
6
+ class ApiClient:
7
+ """Low-level HTTP client for CapSkip in.php / res.php endpoints."""
8
+
9
+ def __init__(self, host='127.0.0.1', port=8080):
10
+ self.host = host
11
+ self.port = port
12
+
13
+ @property
14
+ def base_url(self):
15
+ return f'http://{self.host}:{self.port}'
16
+
17
+ def in_(self, files=None, **kwargs):
18
+ files = files or {}
19
+ try:
20
+ url = f'{self.base_url}/in.php'
21
+ if files:
22
+ opened = {key: open(path, 'rb') for key, path in files.items()}
23
+ try:
24
+ resp = requests.post(url, data=kwargs, files=opened)
25
+ finally:
26
+ for f in opened.values():
27
+ f.close()
28
+ elif 'file' in kwargs:
29
+ with open(kwargs.pop('file'), 'rb') as f:
30
+ resp = requests.post(url, data=kwargs, files={'file': f})
31
+ else:
32
+ resp = requests.post(url, data=kwargs)
33
+ except requests.RequestException as e:
34
+ raise NetworkException(e) from e
35
+
36
+ if resp.status_code != 200:
37
+ raise NetworkException(f'bad response: {resp.status_code}')
38
+
39
+ text = resp.content.decode('utf-8')
40
+ if 'ERROR' in text:
41
+ raise ApiException(text)
42
+ return text
43
+
44
+ def res(self, **kwargs):
45
+ try:
46
+ resp = requests.get(f'{self.base_url}/res.php', params=kwargs)
47
+ except requests.RequestException as e:
48
+ raise NetworkException(e) from e
49
+
50
+ if resp.status_code != 200:
51
+ raise NetworkException(f'bad response: {resp.status_code}')
52
+
53
+ text = resp.content.decode('utf-8')
54
+ if 'ERROR' in text:
55
+ raise ApiException(text)
56
+ return text
capskip/async_api.py ADDED
@@ -0,0 +1,63 @@
1
+ from contextlib import AsyncExitStack
2
+
3
+ import aiofiles
4
+ import httpx
5
+
6
+ from .exceptions import ApiException, NetworkException
7
+
8
+
9
+ class AsyncApiClient:
10
+ """Async HTTP client for CapSkip in.php / res.php endpoints."""
11
+
12
+ def __init__(self, host='127.0.0.1', port=8080):
13
+ self.host = host
14
+ self.port = port
15
+
16
+ @property
17
+ def base_url(self):
18
+ return f'http://{self.host}:{self.port}'
19
+
20
+ async def in_(self, files=None, **kwargs):
21
+ files = files or {}
22
+ url = f'{self.base_url}/in.php'
23
+ try:
24
+ async with httpx.AsyncClient(follow_redirects=True) as client:
25
+ if files:
26
+ async with AsyncExitStack() as stack:
27
+ file_objects = {}
28
+ for key, path in files.items():
29
+ handle = await stack.enter_async_context(aiofiles.open(path, 'rb'))
30
+ file_objects[key] = await handle.read()
31
+ resp = await client.post(url, data=kwargs, files=file_objects)
32
+ elif 'file' in kwargs:
33
+ path = kwargs.pop('file')
34
+ async with aiofiles.open(path, 'rb') as handle:
35
+ content = await handle.read()
36
+ resp = await client.post(url, data=kwargs, files={'file': content})
37
+ else:
38
+ resp = await client.post(url, data=kwargs)
39
+ except httpx.RequestError as e:
40
+ raise NetworkException(e) from e
41
+
42
+ if resp.status_code != 200:
43
+ raise NetworkException(f'bad response: {resp.status_code}')
44
+
45
+ text = resp.content.decode('utf-8')
46
+ if 'ERROR' in text:
47
+ raise ApiException(text)
48
+ return text
49
+
50
+ async def res(self, **kwargs):
51
+ try:
52
+ async with httpx.AsyncClient(follow_redirects=True) as client:
53
+ resp = await client.get(f'{self.base_url}/res.php', params=kwargs)
54
+ except httpx.RequestError as e:
55
+ raise NetworkException(e) from e
56
+
57
+ if resp.status_code != 200:
58
+ raise NetworkException(f'bad response: {resp.status_code}')
59
+
60
+ text = resp.content.decode('utf-8')
61
+ if 'ERROR' in text:
62
+ raise ApiException(text)
63
+ return text
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ import os
3
+ import time
4
+ from base64 import b64encode
5
+
6
+ import httpx
7
+
8
+ from .async_api import AsyncApiClient
9
+ from ._api_params import apply_param_aliases, apply_proxy, prepare_submit_params
10
+ from .exceptions import ApiException, NetworkException, TimeoutException, ValidationException, SolverExceptions
11
+ from .solver import INITIAL_POLLING_INTERVAL, _apply_poll_result, _next_poll_interval, _parse_poll_response
12
+
13
+
14
+ class AsyncCapSkip:
15
+ """Async client for the CapSkip local captcha solver."""
16
+
17
+ def __init__(self,
18
+ apiKey='capskip',
19
+ host='127.0.0.1',
20
+ port=8080,
21
+ defaultTimeout=120,
22
+ recaptchaTimeout=300,
23
+ pollingInterval=5):
24
+
25
+ self.API_KEY = apiKey
26
+ self.default_timeout = defaultTimeout
27
+ self.recaptcha_timeout = recaptchaTimeout
28
+ self.polling_interval = pollingInterval
29
+ self.api_client = AsyncApiClient(host=host, port=port)
30
+ self.exceptions = SolverExceptions
31
+
32
+ async def normal(self, file, **kwargs):
33
+ unsupported = set(kwargs) - {'json'}
34
+ if unsupported:
35
+ raise ValidationException(
36
+ f"Unsupported parameters for image captcha: {sorted(unsupported)}. "
37
+ f"Only json is supported besides the image input."
38
+ )
39
+ method = await self.get_method(file)
40
+ return await self.solve(**method, **kwargs)
41
+
42
+ async def recaptcha(self, sitekey, url, version='v2', enterprise=0, **kwargs):
43
+ params = {
44
+ 'googlekey': sitekey,
45
+ 'url': url,
46
+ 'method': 'userrecaptcha',
47
+ 'enterprise': enterprise,
48
+ **kwargs,
49
+ }
50
+ if str(version).lower() == 'v3':
51
+ params['version'] = 'v3'
52
+ return await self.solve(timeout=self.recaptcha_timeout, **params)
53
+
54
+ async def turnstile(self, sitekey, url, **kwargs):
55
+ return await self.solve(
56
+ sitekey=sitekey,
57
+ url=url,
58
+ method='turnstile',
59
+ poll_json=1,
60
+ **kwargs,
61
+ )
62
+
63
+ async def solve(self, timeout=0, polling_interval=0, poll_json=0, **kwargs):
64
+ poll_json = int(kwargs.pop('poll_json', poll_json) or 0)
65
+ captcha_id = await self.send(**kwargs)
66
+ result = {'captchaId': captcha_id}
67
+ timeout = float(timeout or self.default_timeout)
68
+ sleep = float(polling_interval or self.polling_interval)
69
+ polled = await self.wait_result(captcha_id, timeout, sleep, json=poll_json)
70
+ return _apply_poll_result(result, polled)
71
+
72
+ async def wait_result(self, id_, timeout, polling_interval, json=0):
73
+ deadline = time.time() + timeout
74
+ interval = min(INITIAL_POLLING_INTERVAL, polling_interval)
75
+ while time.time() < deadline:
76
+ try:
77
+ return await self.get_result(id_, json=json)
78
+ except NetworkException:
79
+ await asyncio.sleep(interval)
80
+ interval = _next_poll_interval(interval, polling_interval)
81
+ raise TimeoutException(f'timeout {timeout} exceeded')
82
+
83
+ async def get_method(self, file):
84
+ if not file:
85
+ raise ValidationException('File required')
86
+ if file.startswith('data:'):
87
+ return {'method': 'base64', 'body': file.split(',', 1)[1]}
88
+ if '.' not in file and len(file) > 50:
89
+ return {'method': 'base64', 'body': file}
90
+ if file.startswith('http'):
91
+ async with httpx.AsyncClient(follow_redirects=True) as client:
92
+ resp = await client.get(file)
93
+ if resp.status_code != 200:
94
+ raise ValidationException(f'File could not be downloaded from url: {file}')
95
+ return {'method': 'base64', 'body': b64encode(resp.content).decode('utf-8')}
96
+ if not os.path.exists(file):
97
+ raise ValidationException(f'File not found: {file}')
98
+ return {'method': 'post', 'file': file}
99
+
100
+ async def send(self, **kwargs):
101
+ params = self._prepare_send_params({**kwargs, 'key': self.API_KEY})
102
+ files = params.pop('files', {})
103
+ response = await self.api_client.in_(files=files, **params)
104
+ if not response.startswith('OK|'):
105
+ raise ApiException(f'cannot recognize response {response}')
106
+ return response[3:]
107
+
108
+ async def get_result(self, id_, json=0):
109
+ query = {'key': self.API_KEY, 'action': 'get', 'id': id_}
110
+ if json:
111
+ query['json'] = 1
112
+ response = await self.api_client.res(**query)
113
+ return _parse_poll_response(response, json_mode=int(json or 0))
114
+
115
+ def _prepare_send_params(self, params: dict) -> dict:
116
+ method = params.get('method')
117
+ if method in ('post', 'base64'):
118
+ return prepare_submit_params(params, 'normal')
119
+ if method == 'userrecaptcha':
120
+ return prepare_submit_params(params, 'recaptcha', params.get('version', 'v2'))
121
+ if method == 'turnstile':
122
+ return prepare_submit_params(params, 'turnstile')
123
+ return apply_proxy(apply_param_aliases(params))
capskip/exceptions.py ADDED
@@ -0,0 +1,21 @@
1
+ class CapSkipError(Exception):
2
+ """Base exception for all CapSkip SDK errors."""
3
+
4
+
5
+ SolverExceptions = CapSkipError
6
+
7
+
8
+ class ValidationException(CapSkipError):
9
+ """Invalid or unsupported parameters."""
10
+
11
+
12
+ class NetworkException(CapSkipError):
13
+ """Connection failure or captcha not ready."""
14
+
15
+
16
+ class ApiException(CapSkipError):
17
+ """CapSkip API returned an error."""
18
+
19
+
20
+ class TimeoutException(CapSkipError):
21
+ """Polling exceeded the configured timeout."""
capskip/solver.py ADDED
@@ -0,0 +1,163 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from base64 import b64encode
5
+
6
+ import requests
7
+
8
+ from .api import ApiClient
9
+ from ._api_params import apply_param_aliases, apply_proxy, prepare_submit_params
10
+ from .exceptions import ApiException, NetworkException, TimeoutException, ValidationException, SolverExceptions
11
+
12
+ # First poll fires this soon after submitting, then the interval backs off
13
+ # (doubling) up to the configured pollingInterval ceiling. Keeps latency low for
14
+ # fast local solves (e.g. image captchas) without hammering on slow ones.
15
+ INITIAL_POLLING_INTERVAL = 0.25
16
+
17
+
18
+ def _next_poll_interval(interval: float, ceiling: float) -> float:
19
+ return min(interval * 2, ceiling)
20
+
21
+
22
+ def _parse_poll_response(response: str, json_mode: int = 0):
23
+ if json_mode:
24
+ try:
25
+ data = json.loads(response)
26
+ except json.JSONDecodeError as exc:
27
+ raise ApiException(f'invalid JSON response: {response}') from exc
28
+
29
+ if data.get('status') == 0 and data.get('request') == 'CAPCHA_NOT_READY':
30
+ raise NetworkException
31
+
32
+ if data.get('status') != 1:
33
+ raise ApiException(f'cannot recognize response {data}')
34
+
35
+ return data
36
+
37
+ if response == 'CAPCHA_NOT_READY':
38
+ raise NetworkException
39
+
40
+ if not response.startswith('OK|'):
41
+ raise ApiException(f'cannot recognize response {response}')
42
+
43
+ return response[3:]
44
+
45
+
46
+ def _apply_poll_result(result: dict, polled) -> dict:
47
+ if isinstance(polled, dict):
48
+ result['code'] = polled.get('request', '')
49
+ user_agent = polled.get('useragent') or polled.get('userAgent')
50
+ if user_agent:
51
+ result['userAgent'] = user_agent
52
+ else:
53
+ result['code'] = polled
54
+ return result
55
+
56
+
57
+ class CapSkip:
58
+ """Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile)."""
59
+
60
+ def __init__(self,
61
+ apiKey='capskip',
62
+ host='127.0.0.1',
63
+ port=8080,
64
+ defaultTimeout=120,
65
+ recaptchaTimeout=300,
66
+ pollingInterval=5):
67
+
68
+ self.API_KEY = apiKey
69
+ self.default_timeout = defaultTimeout
70
+ self.recaptcha_timeout = recaptchaTimeout
71
+ self.polling_interval = pollingInterval
72
+ self.api_client = ApiClient(host=host, port=port)
73
+ self.exceptions = SolverExceptions
74
+
75
+ def normal(self, file, **kwargs):
76
+ unsupported = set(kwargs) - {'json'}
77
+ if unsupported:
78
+ raise ValidationException(
79
+ f"Unsupported parameters for image captcha: {sorted(unsupported)}. "
80
+ f"Only json is supported besides the image input."
81
+ )
82
+ return self.solve(**self.get_method(file), **kwargs)
83
+
84
+ def recaptcha(self, sitekey, url, version='v2', enterprise=0, **kwargs):
85
+ params = {
86
+ 'googlekey': sitekey,
87
+ 'url': url,
88
+ 'method': 'userrecaptcha',
89
+ 'enterprise': enterprise,
90
+ **kwargs,
91
+ }
92
+ if str(version).lower() == 'v3':
93
+ params['version'] = 'v3'
94
+ return self.solve(timeout=self.recaptcha_timeout, **params)
95
+
96
+ def turnstile(self, sitekey, url, **kwargs):
97
+ return self.solve(
98
+ sitekey=sitekey,
99
+ url=url,
100
+ method='turnstile',
101
+ poll_json=1,
102
+ **kwargs,
103
+ )
104
+
105
+ def solve(self, timeout=0, polling_interval=0, poll_json=0, **kwargs):
106
+ poll_json = int(kwargs.pop('poll_json', poll_json) or 0)
107
+ captcha_id = self.send(**kwargs)
108
+ result = {'captchaId': captcha_id}
109
+ timeout = float(timeout or self.default_timeout)
110
+ sleep = float(polling_interval or self.polling_interval)
111
+ polled = self.wait_result(captcha_id, timeout, sleep, json=poll_json)
112
+ return _apply_poll_result(result, polled)
113
+
114
+ def wait_result(self, id_, timeout, polling_interval, json=0):
115
+ deadline = time.time() + timeout
116
+ interval = min(INITIAL_POLLING_INTERVAL, polling_interval)
117
+ while time.time() < deadline:
118
+ try:
119
+ return self.get_result(id_, json=json)
120
+ except NetworkException:
121
+ time.sleep(interval)
122
+ interval = _next_poll_interval(interval, polling_interval)
123
+ raise TimeoutException(f'timeout {timeout} exceeded')
124
+
125
+ def get_method(self, file):
126
+ if not file:
127
+ raise ValidationException('File required')
128
+ if file.startswith('data:'):
129
+ return {'method': 'base64', 'body': file.split(',', 1)[1]}
130
+ if '.' not in file and len(file) > 50:
131
+ return {'method': 'base64', 'body': file}
132
+ if file.startswith('http'):
133
+ resp = requests.get(file)
134
+ if resp.status_code != 200:
135
+ raise ValidationException(f'File could not be downloaded from url: {file}')
136
+ return {'method': 'base64', 'body': b64encode(resp.content).decode('utf-8')}
137
+ if not os.path.exists(file):
138
+ raise ValidationException(f'File not found: {file}')
139
+ return {'method': 'post', 'file': file}
140
+
141
+ def send(self, **kwargs):
142
+ params = self._prepare_send_params({**kwargs, 'key': self.API_KEY})
143
+ files = params.pop('files', {})
144
+ response = self.api_client.in_(files=files, **params)
145
+ if not response.startswith('OK|'):
146
+ raise ApiException(f'cannot recognize response {response}')
147
+ return response[3:]
148
+
149
+ def get_result(self, id_, json=0):
150
+ query = {'key': self.API_KEY, 'action': 'get', 'id': id_}
151
+ if json:
152
+ query['json'] = 1
153
+ return _parse_poll_response(self.api_client.res(**query), json_mode=int(json or 0))
154
+
155
+ def _prepare_send_params(self, params: dict) -> dict:
156
+ method = params.get('method')
157
+ if method in ('post', 'base64'):
158
+ return prepare_submit_params(params, 'normal')
159
+ if method == 'userrecaptcha':
160
+ return prepare_submit_params(params, 'recaptcha', params.get('version', 'v2'))
161
+ if method == 'turnstile':
162
+ return prepare_submit_params(params, 'turnstile')
163
+ return apply_proxy(apply_param_aliases(params))
@@ -0,0 +1,301 @@
1
+ Metadata-Version: 2.4
2
+ Name: capskip
3
+ Version: 1.0.0
4
+ Summary: Python SDK for the CapSkip local captcha solver
5
+ Author-email: CapSkip <support@capskip.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://capskip.com
8
+ Project-URL: Documentation, https://github.com/capskip/capskip-python/blob/main/docs/GETTING_STARTED.md
9
+ Project-URL: Repository, https://github.com/capskip/capskip-python
10
+ Project-URL: Bug Tracker, https://github.com/capskip/capskip-python/issues
11
+ Project-URL: Changelog, https://github.com/capskip/capskip-python/blob/main/CHANGELOG.md
12
+ Keywords: captcha,captcha-solver,recaptcha,cloudflare,turnstile,capskip,automation
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests>=2.28
26
+ Requires-Dist: httpx>=0.24
27
+ Requires-Dist: aiofiles>=23.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
31
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # CapSkip Python SDK
35
+
36
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
38
+ [![Tests](https://github.com/capskip/capskip-python/actions/workflows/ci.yml/badge.svg)](https://github.com/capskip/capskip-python/actions/workflows/ci.yml)
39
+
40
+ Official Python client for the [CapSkip](https://capskip.com) **local** captcha solver.
41
+
42
+ CapSkip runs on your machine and exposes a standard captcha-solver HTTP API (the familiar `in.php` / `res.php` endpoints). This SDK wraps that API with clean, familiar method names, so you can solve captchas locally — no cloud service and no per-solve API fees beyond your CapSkip license.
43
+
44
+ ---
45
+
46
+ ## Quick start (5 minutes)
47
+
48
+ ### 1. Install CapSkip
49
+
50
+ Download and run the CapSkip desktop app from [capskip.com](https://capskip.com). Leave it running in the background.
51
+
52
+ In CapSkip settings, note:
53
+
54
+ - **API port** (default: `8080`)
55
+ - **API key** (optional — if validation is disabled, any string works)
56
+
57
+ ### 2. Install the SDK
58
+
59
+ ```bash
60
+ pip install capskip
61
+ ```
62
+
63
+ Or from source:
64
+
65
+ ```bash
66
+ git clone https://github.com/capskip/capskip-python.git
67
+ cd capskip-python
68
+ pip install -e .
69
+ ```
70
+
71
+ ### 3. Solve your first captcha
72
+
73
+ ```python
74
+ from capskip import CapSkip
75
+
76
+ solver = CapSkip(host="127.0.0.1", port=8080)
77
+
78
+ result = solver.recaptcha(
79
+ sitekey="YOUR_SITEKEY",
80
+ url="https://example.com/page-with-recaptcha",
81
+ )
82
+
83
+ print(result["code"]) # g-recaptcha-response token
84
+ ```
85
+
86
+ > **Prerequisite:** CapSkip must be running before you call the SDK. If you see a connection error, see [Troubleshooting](docs/TROUBLESHOOTING.md).
87
+
88
+ ---
89
+
90
+ ## Supported captcha types
91
+
92
+ | Type | SDK method |
93
+ |---|---|
94
+ | Image CAPTCHA (distorted text) | `solver.normal(file)` |
95
+ | reCAPTCHA v2 (checkbox) | `solver.recaptcha(sitekey, url)` |
96
+ | reCAPTCHA v2 Invisible | `solver.recaptcha(..., invisible=1)` |
97
+ | reCAPTCHA v2 Enterprise | `solver.recaptcha(..., enterprise=1)` |
98
+ | reCAPTCHA v3 | `solver.recaptcha(..., version="v3")` |
99
+ | reCAPTCHA v3 Enterprise | `solver.recaptcha(..., version="v3", enterprise=1)` |
100
+ | Cloudflare Turnstile (widget) | `solver.turnstile(sitekey, url)` |
101
+ | Cloudflare Turnstile (challenge page) | `solver.turnstile(..., data=..., pagedata=...)` |
102
+
103
+ ---
104
+
105
+ ## Documentation
106
+
107
+ | Guide | Description |
108
+ |---|---|
109
+ | [Tutorial](docs/TUTORIAL.md) | Complete walkthrough of every captcha type, sync and async |
110
+ | [Getting Started](docs/GETTING_STARTED.md) | Full setup: CapSkip app, SDK install, first script |
111
+ | [API Reference](docs/API_REFERENCE.md) | All classes, methods, parameters, and return values |
112
+ | [Examples](examples/) | Ready-to-run scripts for every captcha type |
113
+ | [Troubleshooting](docs/TROUBLESHOOTING.md) | Connection errors, timeouts, proxy issues |
114
+ | [Contributing](CONTRIBUTING.md) | Development setup, tests, pull requests |
115
+ | [Changelog](CHANGELOG.md) | Release history |
116
+
117
+ ---
118
+
119
+ ## Configuration
120
+
121
+ ```python
122
+ from capskip import CapSkip
123
+
124
+ solver = CapSkip(
125
+ apiKey="capskip", # your CapSkip API key (or any string if validation is off)
126
+ host="127.0.0.1", # CapSkip host
127
+ port=8080, # CapSkip port from app settings
128
+ defaultTimeout=120, # seconds — image captcha polling timeout
129
+ recaptchaTimeout=300, # seconds — reCAPTCHA / Turnstile polling timeout
130
+ pollingInterval=5, # max seconds between res.php polls (starts at 0.25s, backs off to this)
131
+ )
132
+ ```
133
+
134
+ Use environment variables in production:
135
+
136
+ ```bash
137
+ # Linux / macOS
138
+ export CAPSKIP_API_KEY="your-key"
139
+ export CAPSKIP_HOST="127.0.0.1"
140
+ export CAPSKIP_PORT="8080"
141
+ ```
142
+
143
+ ```powershell
144
+ # Windows PowerShell
145
+ $env:CAPSKIP_API_KEY = "your-key"
146
+ $env:CAPSKIP_HOST = "127.0.0.1"
147
+ $env:CAPSKIP_PORT = "8080"
148
+ ```
149
+
150
+ ```python
151
+ import os
152
+ from capskip import CapSkip
153
+
154
+ solver = CapSkip(
155
+ apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
156
+ host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
157
+ port=int(os.getenv("CAPSKIP_PORT", "8080")),
158
+ )
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Usage examples
164
+
165
+ ### Image captcha
166
+
167
+ ```python
168
+ result = solver.normal("captcha.png")
169
+ result = solver.normal("https://example.com/captcha.jpg")
170
+ result = solver.normal("data:image/png;base64,iVBORw0KGgo...")
171
+ print(result["code"])
172
+ ```
173
+
174
+ ### reCAPTCHA v2 / v3
175
+
176
+ ```python
177
+ # reCAPTCHA v2
178
+ result = solver.recaptcha(sitekey="...", url="https://example.com")
179
+
180
+ # reCAPTCHA v3
181
+ result = solver.recaptcha(
182
+ sitekey="...",
183
+ url="https://example.com",
184
+ version="v3",
185
+ action="submit",
186
+ score=0.7,
187
+ )
188
+ ```
189
+
190
+ ### Cloudflare Turnstile
191
+
192
+ ```python
193
+ result = solver.turnstile(
194
+ sitekey="0x4AAAAAAA...",
195
+ url="https://example.com",
196
+ )
197
+ ```
198
+
199
+ ### With a proxy (reCAPTCHA & Turnstile only)
200
+
201
+ ```python
202
+ # Proxy is not supported for image captcha
203
+ result = solver.recaptcha(
204
+ sitekey="...",
205
+ url="https://example.com",
206
+ proxy={"type": "HTTPS", "uri": "user:pass@1.2.3.4:3128"},
207
+ )
208
+ result = solver.turnstile(
209
+ sitekey="...",
210
+ url="https://example.com",
211
+ proxy={"type": "HTTP", "uri": "1.2.3.4:3128"},
212
+ )
213
+ ```
214
+
215
+ ### Async (parallel solving)
216
+
217
+ ```python
218
+ import asyncio
219
+ from capskip import AsyncCapSkip
220
+
221
+ async def main():
222
+ solver = AsyncCapSkip()
223
+ r1, r2 = await asyncio.gather(
224
+ solver.recaptcha(sitekey="...", url="https://a.com"),
225
+ solver.turnstile(sitekey="...", url="https://b.com"),
226
+ )
227
+ print(r1["code"], r2["code"])
228
+
229
+ asyncio.run(main())
230
+ ```
231
+
232
+ More examples: [`examples/`](examples/)
233
+
234
+ ---
235
+
236
+ ## Return value
237
+
238
+ Every solve method returns:
239
+
240
+ ```python
241
+ {
242
+ "captchaId": "12345", # internal ID from CapSkip
243
+ "code": "TOKEN_OR_TEXT" # solution — text for image, token for reCAPTCHA/Turnstile
244
+ "userAgent": "..." # Turnstile only — use when submitting challenge-page tokens
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Error handling
251
+
252
+ ```python
253
+ from capskip import CapSkip, ValidationException, NetworkException, ApiException, TimeoutException
254
+
255
+ try:
256
+ result = solver.recaptcha(sitekey="...", url="...")
257
+ except ValidationException:
258
+ pass # invalid parameters
259
+ except NetworkException:
260
+ pass # CapSkip not running, or captcha not ready (manual polling)
261
+ except ApiException:
262
+ pass # API returned an error code
263
+ except TimeoutException:
264
+ pass # polling timeout exceeded
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Development
270
+
271
+ ```bash
272
+ git clone https://github.com/capskip/capskip-python.git
273
+ cd capskip-python
274
+ python -m venv .venv
275
+
276
+ # Windows
277
+ .venv\Scripts\activate
278
+
279
+ # Linux / macOS
280
+ source .venv/bin/activate
281
+
282
+ pip install -e ".[dev]"
283
+ pytest
284
+ ```
285
+
286
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow.
287
+
288
+ ---
289
+
290
+ ## Links
291
+
292
+ - [CapSkip website](https://capskip.com)
293
+ - [CapSkip API docs](https://capskip.com/api-docs/)
294
+ - [Report an issue](https://github.com/capskip/capskip-python/issues)
295
+ - [PyPI package](https://pypi.org/project/capskip/) *(when published)*
296
+
297
+ ---
298
+
299
+ ## License
300
+
301
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ capskip/__init__.py,sha256=oOVghk5aU1WoSbXXEyk7GiETRYsO3Y26inPec5BG-Ac,597
2
+ capskip/_api_params.py,sha256=zO3q-EEDxSPAYiKB0JkJSXRY1P2RRTdUYEYTSvmpvBA,3809
3
+ capskip/api.py,sha256=tvCvTmFyz8U0PayJ1Et_Z_cI0JqKb1PiswZZyptFATc,1807
4
+ capskip/async_api.py,sha256=7LgxNXPXlx0UdZ811KSNeC0Ua1qa7fiKYi_qyoX5DF0,2280
5
+ capskip/async_solver.py,sha256=bbAeO7aw67CVI3BTs7f46ip-rcSe1Adakiw-NfbjBR8,5087
6
+ capskip/exceptions.py,sha256=_dFHGjm_wc3DW1mpMtvuMiwDhpZyCL-U3gxRQDsE9d8,465
7
+ capskip/solver.py,sha256=WGeQ8pU3SxLfXhYfOP8hbIJLpfd2caE3GZmtcwGeMTY,6138
8
+ capskip-1.0.0.dist-info/licenses/LICENSE,sha256=hFVVci2vCCXeeP1G15tVusVHw_j2jiHM3WS9zEKjR1k,1064
9
+ capskip-1.0.0.dist-info/METADATA,sha256=dB-zDZXQhkYCNYMbRO6fFnAr730LcbD_xgivnOqxyMc,8251
10
+ capskip-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ capskip-1.0.0.dist-info/top_level.txt,sha256=_AHMAJcrez4g0s5jG_EON5kMwn9urg3mW-RgZxSvbas,8
12
+ capskip-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CapSkip
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ capskip