nslsolver 1.0.0__tar.gz

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,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: nslsolver
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the NSLSolver captcha solving API
5
+ Author-email: NSLSolver <support@nslsolver.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://nslsolver.com
8
+ Project-URL: Documentation, https://docs.nslsolver.com
9
+ Project-URL: Repository, https://github.com/NSLSolver/NSLSolver-SDK-Python
10
+ Project-URL: Issues, https://github.com/NSLSolver/NSLSolver-SDK-Python/issues
11
+ Keywords: captcha,turnstile,cloudflare,solver,nslsolver
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: requests>=2.25.0
29
+ Provides-Extra: async
30
+ Requires-Dist: aiohttp>=3.8.0; extra == "async"
31
+ Provides-Extra: dev
32
+ Requires-Dist: aiohttp>=3.8.0; extra == "dev"
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
35
+ Requires-Dist: mypy>=1.0; extra == "dev"
36
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
37
+
38
+ # NSLSolver Python SDK
39
+
40
+ Python SDK for the [NSLSolver](https://nslsolver.com) captcha solving API.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install nslsolver
46
+
47
+ # async support
48
+ pip install nslsolver[async]
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from nslsolver import NSLSolver
55
+
56
+ solver = NSLSolver("your-api-key")
57
+
58
+ # Turnstile
59
+ result = solver.solve_turnstile(
60
+ site_key="0x4AAAAAAAB...",
61
+ url="https://example.com",
62
+ )
63
+ print(result.token)
64
+
65
+ # Cloudflare challenge (proxy required)
66
+ result = solver.solve_challenge(
67
+ url="https://example.com/protected",
68
+ proxy="http://user:pass@host:port",
69
+ )
70
+ print(result.cookies, result.user_agent)
71
+
72
+ # Balance
73
+ balance = solver.get_balance()
74
+ print(balance.balance, balance.max_threads, balance.allowed_types)
75
+ ```
76
+
77
+ ## Async
78
+
79
+ ```python
80
+ import asyncio
81
+ from nslsolver import AsyncNSLSolver
82
+
83
+ async def main():
84
+ async with AsyncNSLSolver("your-api-key") as solver:
85
+ result = await solver.solve_turnstile(
86
+ site_key="0x4AAAAAAAB...",
87
+ url="https://example.com",
88
+ )
89
+ print(result.token)
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## Error Handling
95
+
96
+ ```python
97
+ from nslsolver import (
98
+ NSLSolver,
99
+ AuthenticationError,
100
+ InsufficientBalanceError,
101
+ RateLimitError,
102
+ SolveError,
103
+ NSLSolverError,
104
+ )
105
+
106
+ solver = NSLSolver("your-api-key")
107
+
108
+ try:
109
+ result = solver.solve_turnstile(
110
+ site_key="0x4AAAAAAAB...",
111
+ url="https://example.com",
112
+ )
113
+ except AuthenticationError:
114
+ print("Bad API key.")
115
+ except InsufficientBalanceError:
116
+ print("Top up your balance.")
117
+ except RateLimitError:
118
+ print("Rate limited after all retries.")
119
+ except SolveError as e:
120
+ print(f"Solve failed: {e.message}")
121
+ except NSLSolverError as e:
122
+ print(f"API error (HTTP {e.status_code}): {e.message}")
123
+ ```
124
+
125
+ Rate-limit (429) and backend (503) errors are retried automatically with exponential backoff before raising.
126
+
127
+ ## Configuration
128
+
129
+ ```python
130
+ solver = NSLSolver(
131
+ api_key="your-api-key",
132
+ base_url="https://api.nslsolver.com", # default
133
+ timeout=120, # seconds (default: 120)
134
+ max_retries=3, # retries for 429/503 (default: 3)
135
+ )
136
+ ```
137
+
138
+ Both clients support context managers (`with` / `async with`) for session cleanup.
139
+
140
+ ## Requirements
141
+
142
+ - Python 3.8+
143
+ - `requests` (sync client)
144
+ - `aiohttp` (async client, optional)
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,111 @@
1
+ # NSLSolver Python SDK
2
+
3
+ Python SDK for the [NSLSolver](https://nslsolver.com) captcha solving API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install nslsolver
9
+
10
+ # async support
11
+ pip install nslsolver[async]
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```python
17
+ from nslsolver import NSLSolver
18
+
19
+ solver = NSLSolver("your-api-key")
20
+
21
+ # Turnstile
22
+ result = solver.solve_turnstile(
23
+ site_key="0x4AAAAAAAB...",
24
+ url="https://example.com",
25
+ )
26
+ print(result.token)
27
+
28
+ # Cloudflare challenge (proxy required)
29
+ result = solver.solve_challenge(
30
+ url="https://example.com/protected",
31
+ proxy="http://user:pass@host:port",
32
+ )
33
+ print(result.cookies, result.user_agent)
34
+
35
+ # Balance
36
+ balance = solver.get_balance()
37
+ print(balance.balance, balance.max_threads, balance.allowed_types)
38
+ ```
39
+
40
+ ## Async
41
+
42
+ ```python
43
+ import asyncio
44
+ from nslsolver import AsyncNSLSolver
45
+
46
+ async def main():
47
+ async with AsyncNSLSolver("your-api-key") as solver:
48
+ result = await solver.solve_turnstile(
49
+ site_key="0x4AAAAAAAB...",
50
+ url="https://example.com",
51
+ )
52
+ print(result.token)
53
+
54
+ asyncio.run(main())
55
+ ```
56
+
57
+ ## Error Handling
58
+
59
+ ```python
60
+ from nslsolver import (
61
+ NSLSolver,
62
+ AuthenticationError,
63
+ InsufficientBalanceError,
64
+ RateLimitError,
65
+ SolveError,
66
+ NSLSolverError,
67
+ )
68
+
69
+ solver = NSLSolver("your-api-key")
70
+
71
+ try:
72
+ result = solver.solve_turnstile(
73
+ site_key="0x4AAAAAAAB...",
74
+ url="https://example.com",
75
+ )
76
+ except AuthenticationError:
77
+ print("Bad API key.")
78
+ except InsufficientBalanceError:
79
+ print("Top up your balance.")
80
+ except RateLimitError:
81
+ print("Rate limited after all retries.")
82
+ except SolveError as e:
83
+ print(f"Solve failed: {e.message}")
84
+ except NSLSolverError as e:
85
+ print(f"API error (HTTP {e.status_code}): {e.message}")
86
+ ```
87
+
88
+ Rate-limit (429) and backend (503) errors are retried automatically with exponential backoff before raising.
89
+
90
+ ## Configuration
91
+
92
+ ```python
93
+ solver = NSLSolver(
94
+ api_key="your-api-key",
95
+ base_url="https://api.nslsolver.com", # default
96
+ timeout=120, # seconds (default: 120)
97
+ max_retries=3, # retries for 429/503 (default: 3)
98
+ )
99
+ ```
100
+
101
+ Both clients support context managers (`with` / `async with`) for session cleanup.
102
+
103
+ ## Requirements
104
+
105
+ - Python 3.8+
106
+ - `requests` (sync client)
107
+ - `aiohttp` (async client, optional)
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,40 @@
1
+ """NSLSolver Python SDK."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .client import NSLSolver
6
+ from .exceptions import (
7
+ AuthenticationError,
8
+ BackendError,
9
+ InsufficientBalanceError,
10
+ NSLSolverError,
11
+ RateLimitError,
12
+ SolveError,
13
+ TypeNotAllowedError,
14
+ )
15
+ from .types import BalanceResult, ChallengeResult, TurnstileResult
16
+
17
+
18
+ def __getattr__(name: str) -> object:
19
+ if name == "AsyncNSLSolver":
20
+ from .async_client import AsyncNSLSolver
21
+
22
+ return AsyncNSLSolver
23
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
24
+
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "NSLSolver",
29
+ "AsyncNSLSolver",
30
+ "TurnstileResult",
31
+ "ChallengeResult",
32
+ "BalanceResult",
33
+ "NSLSolverError",
34
+ "AuthenticationError",
35
+ "InsufficientBalanceError",
36
+ "TypeNotAllowedError",
37
+ "RateLimitError",
38
+ "BackendError",
39
+ "SolveError",
40
+ ]
@@ -0,0 +1,235 @@
1
+ """Async NSLSolver client (requires aiohttp)."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Dict, Optional
6
+
7
+ try:
8
+ import aiohttp
9
+ except ImportError:
10
+ raise ImportError(
11
+ "The 'aiohttp' package is required for the async client. "
12
+ "Install it with: pip install nslsolver[async]"
13
+ )
14
+
15
+ from .exceptions import (
16
+ AuthenticationError,
17
+ BackendError,
18
+ InsufficientBalanceError,
19
+ NSLSolverError,
20
+ RateLimitError,
21
+ SolveError,
22
+ TypeNotAllowedError,
23
+ )
24
+ from .types import BalanceResult, ChallengeResult, TurnstileResult
25
+
26
+ logger = logging.getLogger("nslsolver")
27
+
28
+ _DEFAULT_BASE_URL = "https://api.nslsolver.com"
29
+ _DEFAULT_TIMEOUT = 120
30
+ _DEFAULT_MAX_RETRIES = 3
31
+ _INITIAL_BACKOFF = 1.0
32
+ _BACKOFF_MULTIPLIER = 2.0
33
+ _MAX_BACKOFF = 30.0
34
+
35
+ _RETRYABLE_EXCEPTIONS = (RateLimitError, BackendError)
36
+
37
+
38
+ class AsyncNSLSolver:
39
+ """Async client for the NSLSolver API."""
40
+
41
+ def __init__(
42
+ self,
43
+ api_key: str,
44
+ base_url: str = _DEFAULT_BASE_URL,
45
+ timeout: int = _DEFAULT_TIMEOUT,
46
+ max_retries: int = _DEFAULT_MAX_RETRIES,
47
+ ) -> None:
48
+ if not api_key:
49
+ raise ValueError("api_key must be a non-empty string.")
50
+
51
+ self._api_key = api_key
52
+ self._base_url = base_url.rstrip("/")
53
+ self._timeout = aiohttp.ClientTimeout(total=timeout)
54
+ self._max_retries = max_retries
55
+ self._session: Optional[aiohttp.ClientSession] = None
56
+ self._headers = {
57
+ "X-API-Key": self._api_key,
58
+ "Content-Type": "application/json",
59
+ "User-Agent": "nslsolver-python/1.0.0 (async)",
60
+ }
61
+
62
+ async def _get_session(self) -> aiohttp.ClientSession:
63
+ if self._session is None or self._session.closed:
64
+ self._session = aiohttp.ClientSession(
65
+ headers=self._headers,
66
+ timeout=self._timeout,
67
+ )
68
+ return self._session
69
+
70
+ # -- Public API ----------------------------------------------------------
71
+
72
+ async def solve_turnstile(
73
+ self,
74
+ site_key: str,
75
+ url: str,
76
+ action: Optional[str] = None,
77
+ cdata: Optional[str] = None,
78
+ proxy: Optional[str] = None,
79
+ user_agent: Optional[str] = None,
80
+ ) -> TurnstileResult:
81
+ """Solve a Cloudflare Turnstile captcha."""
82
+ payload: Dict[str, Any] = {
83
+ "type": "turnstile",
84
+ "site_key": site_key,
85
+ "url": url,
86
+ }
87
+ if action is not None:
88
+ payload["action"] = action
89
+ if cdata is not None:
90
+ payload["cdata"] = cdata
91
+ if proxy is not None:
92
+ payload["proxy"] = proxy
93
+ if user_agent is not None:
94
+ payload["user_agent"] = user_agent
95
+
96
+ data = await self._request("POST", "/solve", json_body=payload)
97
+
98
+ return TurnstileResult(
99
+ token=data["token"],
100
+ type=data.get("type", "turnstile"),
101
+ )
102
+
103
+ async def solve_challenge(
104
+ self,
105
+ url: str,
106
+ proxy: str,
107
+ user_agent: Optional[str] = None,
108
+ ) -> ChallengeResult:
109
+ """Solve a Cloudflare challenge page."""
110
+ payload: Dict[str, Any] = {
111
+ "type": "challenge",
112
+ "url": url,
113
+ "proxy": proxy,
114
+ }
115
+ if user_agent is not None:
116
+ payload["user_agent"] = user_agent
117
+
118
+ data = await self._request("POST", "/solve", json_body=payload)
119
+
120
+ return ChallengeResult(
121
+ cookies=data.get("cookies", {}),
122
+ user_agent=data.get("user_agent", ""),
123
+ type=data.get("type", "challenge"),
124
+ )
125
+
126
+ async def get_balance(self) -> BalanceResult:
127
+ """Retrieve current account balance."""
128
+ data = await self._request("GET", "/balance")
129
+
130
+ known_keys = {"balance", "max_threads", "allowed_types"}
131
+ extra = {k: v for k, v in data.items() if k not in known_keys}
132
+
133
+ return BalanceResult(
134
+ balance=float(data["balance"]),
135
+ max_threads=int(data["max_threads"]),
136
+ allowed_types=list(data.get("allowed_types", [])),
137
+ extra=extra,
138
+ )
139
+
140
+ # -- Internal ------------------------------------------------------------
141
+
142
+ async def _request(
143
+ self,
144
+ method: str,
145
+ path: str,
146
+ json_body: Optional[Dict[str, Any]] = None,
147
+ ) -> Dict[str, Any]:
148
+ """Execute an HTTP request with retries on transient errors."""
149
+ url = f"{self._base_url}{path}"
150
+ last_exc: Optional[Exception] = None
151
+ backoff = _INITIAL_BACKOFF
152
+ session = await self._get_session()
153
+
154
+ for attempt in range(self._max_retries + 1):
155
+ try:
156
+ async with session.request(
157
+ method=method,
158
+ url=url,
159
+ json=json_body,
160
+ ) as response:
161
+ if response.status == 200:
162
+ return await response.json() # type: ignore[no-any-return]
163
+
164
+ response_text = await response.text()
165
+ self._handle_error_response(response.status, response_text)
166
+
167
+ except _RETRYABLE_EXCEPTIONS as exc:
168
+ last_exc = exc
169
+ except asyncio.TimeoutError:
170
+ last_exc = NSLSolverError("Request timed out.")
171
+ except aiohttp.ClientConnectionError:
172
+ last_exc = NSLSolverError("Connection error.")
173
+ else:
174
+ break # pragma: no cover
175
+
176
+ if attempt < self._max_retries:
177
+ sleep_time = min(backoff, _MAX_BACKOFF)
178
+ logger.warning(
179
+ "%s on attempt %d/%d, retrying in %.1fs",
180
+ last_exc,
181
+ attempt + 1,
182
+ self._max_retries + 1,
183
+ sleep_time,
184
+ )
185
+ await asyncio.sleep(sleep_time)
186
+ backoff *= _BACKOFF_MULTIPLIER
187
+ else:
188
+ raise last_exc # type: ignore[misc]
189
+
190
+ if last_exc is not None:
191
+ raise last_exc
192
+ raise NSLSolverError("Unexpected error: no response received.")
193
+
194
+ @staticmethod
195
+ def _handle_error_response(status: int, response_text: str) -> None:
196
+ """Map an HTTP error response to the appropriate exception."""
197
+ import json
198
+
199
+ try:
200
+ body = json.loads(response_text)
201
+ except (ValueError, KeyError):
202
+ body = {}
203
+
204
+ message = body.get("error", body.get("message", response_text))
205
+
206
+ exc_map = {
207
+ 400: SolveError,
208
+ 401: AuthenticationError,
209
+ 402: InsufficientBalanceError,
210
+ 403: TypeNotAllowedError,
211
+ 429: RateLimitError,
212
+ 503: BackendError,
213
+ }
214
+ cls = exc_map.get(status)
215
+ if cls:
216
+ raise cls(message=str(message), status_code=status, response_body=body)
217
+ raise NSLSolverError(
218
+ message=f"Unexpected API error (HTTP {status}): {message}",
219
+ status_code=status,
220
+ response_body=body,
221
+ )
222
+
223
+ async def close(self) -> None:
224
+ """Close the underlying aiohttp session."""
225
+ if self._session and not self._session.closed:
226
+ await self._session.close()
227
+
228
+ async def __aenter__(self) -> "AsyncNSLSolver":
229
+ return self
230
+
231
+ async def __aexit__(self, *args: Any) -> None:
232
+ await self.close()
233
+
234
+ def __repr__(self) -> str:
235
+ return f"AsyncNSLSolver(base_url={self._base_url!r})"
@@ -0,0 +1,227 @@
1
+ """Synchronous NSLSolver client."""
2
+
3
+ import time
4
+ import logging
5
+ from typing import Any, Dict, Optional
6
+
7
+ import requests
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ BackendError,
12
+ InsufficientBalanceError,
13
+ NSLSolverError,
14
+ RateLimitError,
15
+ SolveError,
16
+ TypeNotAllowedError,
17
+ )
18
+ from .types import BalanceResult, ChallengeResult, TurnstileResult
19
+
20
+ logger = logging.getLogger("nslsolver")
21
+
22
+ _DEFAULT_BASE_URL = "https://api.nslsolver.com"
23
+ _DEFAULT_TIMEOUT = 120
24
+ _DEFAULT_MAX_RETRIES = 3
25
+ _INITIAL_BACKOFF = 1.0
26
+ _BACKOFF_MULTIPLIER = 2.0
27
+ _MAX_BACKOFF = 30.0
28
+
29
+ # Errors worth retrying
30
+ _RETRYABLE_EXCEPTIONS = (RateLimitError, BackendError)
31
+
32
+
33
+ class NSLSolver:
34
+ """Synchronous client for the NSLSolver API."""
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str,
39
+ base_url: str = _DEFAULT_BASE_URL,
40
+ timeout: int = _DEFAULT_TIMEOUT,
41
+ max_retries: int = _DEFAULT_MAX_RETRIES,
42
+ ) -> None:
43
+ if not api_key:
44
+ raise ValueError("api_key must be a non-empty string.")
45
+
46
+ self._api_key = api_key
47
+ self._base_url = base_url.rstrip("/")
48
+ self._timeout = timeout
49
+ self._max_retries = max_retries
50
+ self._session = requests.Session()
51
+ self._session.headers.update(
52
+ {
53
+ "X-API-Key": self._api_key,
54
+ "Content-Type": "application/json",
55
+ "User-Agent": "nslsolver-python/1.0.0",
56
+ }
57
+ )
58
+
59
+ # -- Public API ----------------------------------------------------------
60
+
61
+ def solve_turnstile(
62
+ self,
63
+ site_key: str,
64
+ url: str,
65
+ action: Optional[str] = None,
66
+ cdata: Optional[str] = None,
67
+ proxy: Optional[str] = None,
68
+ user_agent: Optional[str] = None,
69
+ ) -> TurnstileResult:
70
+ """Solve a Cloudflare Turnstile captcha."""
71
+ payload: Dict[str, Any] = {
72
+ "type": "turnstile",
73
+ "site_key": site_key,
74
+ "url": url,
75
+ }
76
+ if action is not None:
77
+ payload["action"] = action
78
+ if cdata is not None:
79
+ payload["cdata"] = cdata
80
+ if proxy is not None:
81
+ payload["proxy"] = proxy
82
+ if user_agent is not None:
83
+ payload["user_agent"] = user_agent
84
+
85
+ data = self._request("POST", "/solve", json_body=payload)
86
+
87
+ return TurnstileResult(
88
+ token=data["token"],
89
+ type=data.get("type", "turnstile"),
90
+ )
91
+
92
+ def solve_challenge(
93
+ self,
94
+ url: str,
95
+ proxy: str,
96
+ user_agent: Optional[str] = None,
97
+ ) -> ChallengeResult:
98
+ """Solve a Cloudflare challenge page."""
99
+ payload: Dict[str, Any] = {
100
+ "type": "challenge",
101
+ "url": url,
102
+ "proxy": proxy,
103
+ }
104
+ if user_agent is not None:
105
+ payload["user_agent"] = user_agent
106
+
107
+ data = self._request("POST", "/solve", json_body=payload)
108
+
109
+ return ChallengeResult(
110
+ cookies=data.get("cookies", {}),
111
+ user_agent=data.get("user_agent", ""),
112
+ type=data.get("type", "challenge"),
113
+ )
114
+
115
+ def get_balance(self) -> BalanceResult:
116
+ """Retrieve current account balance."""
117
+ data = self._request("GET", "/balance")
118
+
119
+ known_keys = {"balance", "max_threads", "allowed_types"}
120
+ extra = {k: v for k, v in data.items() if k not in known_keys}
121
+
122
+ return BalanceResult(
123
+ balance=float(data["balance"]),
124
+ max_threads=int(data["max_threads"]),
125
+ allowed_types=list(data.get("allowed_types", [])),
126
+ extra=extra,
127
+ )
128
+
129
+ # -- Internal ------------------------------------------------------------
130
+
131
+ def _request(
132
+ self,
133
+ method: str,
134
+ path: str,
135
+ json_body: Optional[Dict[str, Any]] = None,
136
+ ) -> Dict[str, Any]:
137
+ """Execute an HTTP request with retries on transient errors."""
138
+ url = f"{self._base_url}{path}"
139
+ last_exc: Optional[Exception] = None
140
+ backoff = _INITIAL_BACKOFF
141
+
142
+ for attempt in range(self._max_retries + 1):
143
+ try:
144
+ response = self._session.request(
145
+ method=method,
146
+ url=url,
147
+ json=json_body,
148
+ timeout=self._timeout,
149
+ )
150
+
151
+ if response.status_code == 200:
152
+ return response.json() # type: ignore[no-any-return]
153
+
154
+ self._handle_error_response(response)
155
+
156
+ except _RETRYABLE_EXCEPTIONS as exc:
157
+ last_exc = exc
158
+ except requests.exceptions.Timeout:
159
+ last_exc = NSLSolverError(f"Request timed out after {self._timeout}s.")
160
+ except requests.exceptions.ConnectionError:
161
+ last_exc = NSLSolverError("Connection error.")
162
+ else:
163
+ # _handle_error_response always raises, so we only land here
164
+ # on a 200, which already returned above.
165
+ break # pragma: no cover
166
+
167
+ # Retry or give up
168
+ if attempt < self._max_retries:
169
+ sleep_time = min(backoff, _MAX_BACKOFF)
170
+ logger.warning(
171
+ "%s on attempt %d/%d, retrying in %.1fs",
172
+ last_exc,
173
+ attempt + 1,
174
+ self._max_retries + 1,
175
+ sleep_time,
176
+ )
177
+ time.sleep(sleep_time)
178
+ backoff *= _BACKOFF_MULTIPLIER
179
+ else:
180
+ raise last_exc # type: ignore[misc]
181
+
182
+ # Should not be reached, but keeps the type checker happy.
183
+ if last_exc is not None:
184
+ raise last_exc
185
+ raise NSLSolverError("Unexpected error: no response received.")
186
+
187
+ @staticmethod
188
+ def _handle_error_response(response: requests.Response) -> None:
189
+ """Map an HTTP error response to the appropriate exception."""
190
+ status = response.status_code
191
+
192
+ try:
193
+ body = response.json()
194
+ except (ValueError, KeyError):
195
+ body = {}
196
+
197
+ message = body.get("error", body.get("message", response.text))
198
+
199
+ exc_map = {
200
+ 400: SolveError,
201
+ 401: AuthenticationError,
202
+ 402: InsufficientBalanceError,
203
+ 403: TypeNotAllowedError,
204
+ 429: RateLimitError,
205
+ 503: BackendError,
206
+ }
207
+ cls = exc_map.get(status)
208
+ if cls:
209
+ raise cls(message=str(message), status_code=status, response_body=body)
210
+ raise NSLSolverError(
211
+ message=f"Unexpected API error (HTTP {status}): {message}",
212
+ status_code=status,
213
+ response_body=body,
214
+ )
215
+
216
+ def close(self) -> None:
217
+ """Close the underlying HTTP session."""
218
+ self._session.close()
219
+
220
+ def __enter__(self) -> "NSLSolver":
221
+ return self
222
+
223
+ def __exit__(self, *args: Any) -> None:
224
+ self.close()
225
+
226
+ def __repr__(self) -> str:
227
+ return f"NSLSolver(base_url={self._base_url!r})"
@@ -0,0 +1,49 @@
1
+ """NSLSolver SDK exceptions."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class NSLSolverError(Exception):
7
+ """Base exception for all NSLSolver API errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ response_body: Optional[dict] = None,
14
+ ) -> None:
15
+ self.message = message
16
+ self.status_code = status_code
17
+ self.response_body = response_body
18
+ super().__init__(message)
19
+
20
+ def __repr__(self) -> str:
21
+ return (
22
+ f"{self.__class__.__name__}("
23
+ f"message={self.message!r}, "
24
+ f"status_code={self.status_code!r})"
25
+ )
26
+
27
+
28
+ class AuthenticationError(NSLSolverError):
29
+ """Invalid or missing API key (401)."""
30
+
31
+
32
+ class InsufficientBalanceError(NSLSolverError):
33
+ """Account balance too low (402)."""
34
+
35
+
36
+ class TypeNotAllowedError(NSLSolverError):
37
+ """Solve type not allowed for this key (403)."""
38
+
39
+
40
+ class RateLimitError(NSLSolverError):
41
+ """Rate limit exceeded (429). Retried automatically."""
42
+
43
+
44
+ class BackendError(NSLSolverError):
45
+ """Backend unavailable (503). Retried automatically."""
46
+
47
+
48
+ class SolveError(NSLSolverError):
49
+ """Solve request failed (400)."""
@@ -0,0 +1,45 @@
1
+ """Response types for the NSLSolver API."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List, Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class TurnstileResult:
9
+ """Result of a Turnstile captcha solve."""
10
+
11
+ token: str
12
+ type: str = "turnstile"
13
+
14
+ def __str__(self) -> str:
15
+ return self.token
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ChallengeResult:
20
+ """Result of a Cloudflare challenge solve."""
21
+
22
+ cookies: Dict[str, str]
23
+ user_agent: str
24
+ type: str = "challenge"
25
+
26
+ @property
27
+ def cf_clearance(self) -> Optional[str]:
28
+ return self.cookies.get("cf_clearance")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class BalanceResult:
33
+ """Account balance and capability info."""
34
+
35
+ balance: float
36
+ max_threads: int
37
+ allowed_types: List[str]
38
+ extra: Dict[str, object] = field(default_factory=dict)
39
+
40
+ def __str__(self) -> str:
41
+ return (
42
+ f"Balance: {self.balance}, "
43
+ f"Max Threads: {self.max_threads}, "
44
+ f"Allowed Types: {self.allowed_types}"
45
+ )
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: nslsolver
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the NSLSolver captcha solving API
5
+ Author-email: NSLSolver <support@nslsolver.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://nslsolver.com
8
+ Project-URL: Documentation, https://docs.nslsolver.com
9
+ Project-URL: Repository, https://github.com/NSLSolver/NSLSolver-SDK-Python
10
+ Project-URL: Issues, https://github.com/NSLSolver/NSLSolver-SDK-Python/issues
11
+ Keywords: captcha,turnstile,cloudflare,solver,nslsolver
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: requests>=2.25.0
29
+ Provides-Extra: async
30
+ Requires-Dist: aiohttp>=3.8.0; extra == "async"
31
+ Provides-Extra: dev
32
+ Requires-Dist: aiohttp>=3.8.0; extra == "dev"
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
35
+ Requires-Dist: mypy>=1.0; extra == "dev"
36
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
37
+
38
+ # NSLSolver Python SDK
39
+
40
+ Python SDK for the [NSLSolver](https://nslsolver.com) captcha solving API.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install nslsolver
46
+
47
+ # async support
48
+ pip install nslsolver[async]
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from nslsolver import NSLSolver
55
+
56
+ solver = NSLSolver("your-api-key")
57
+
58
+ # Turnstile
59
+ result = solver.solve_turnstile(
60
+ site_key="0x4AAAAAAAB...",
61
+ url="https://example.com",
62
+ )
63
+ print(result.token)
64
+
65
+ # Cloudflare challenge (proxy required)
66
+ result = solver.solve_challenge(
67
+ url="https://example.com/protected",
68
+ proxy="http://user:pass@host:port",
69
+ )
70
+ print(result.cookies, result.user_agent)
71
+
72
+ # Balance
73
+ balance = solver.get_balance()
74
+ print(balance.balance, balance.max_threads, balance.allowed_types)
75
+ ```
76
+
77
+ ## Async
78
+
79
+ ```python
80
+ import asyncio
81
+ from nslsolver import AsyncNSLSolver
82
+
83
+ async def main():
84
+ async with AsyncNSLSolver("your-api-key") as solver:
85
+ result = await solver.solve_turnstile(
86
+ site_key="0x4AAAAAAAB...",
87
+ url="https://example.com",
88
+ )
89
+ print(result.token)
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## Error Handling
95
+
96
+ ```python
97
+ from nslsolver import (
98
+ NSLSolver,
99
+ AuthenticationError,
100
+ InsufficientBalanceError,
101
+ RateLimitError,
102
+ SolveError,
103
+ NSLSolverError,
104
+ )
105
+
106
+ solver = NSLSolver("your-api-key")
107
+
108
+ try:
109
+ result = solver.solve_turnstile(
110
+ site_key="0x4AAAAAAAB...",
111
+ url="https://example.com",
112
+ )
113
+ except AuthenticationError:
114
+ print("Bad API key.")
115
+ except InsufficientBalanceError:
116
+ print("Top up your balance.")
117
+ except RateLimitError:
118
+ print("Rate limited after all retries.")
119
+ except SolveError as e:
120
+ print(f"Solve failed: {e.message}")
121
+ except NSLSolverError as e:
122
+ print(f"API error (HTTP {e.status_code}): {e.message}")
123
+ ```
124
+
125
+ Rate-limit (429) and backend (503) errors are retried automatically with exponential backoff before raising.
126
+
127
+ ## Configuration
128
+
129
+ ```python
130
+ solver = NSLSolver(
131
+ api_key="your-api-key",
132
+ base_url="https://api.nslsolver.com", # default
133
+ timeout=120, # seconds (default: 120)
134
+ max_retries=3, # retries for 429/503 (default: 3)
135
+ )
136
+ ```
137
+
138
+ Both clients support context managers (`with` / `async with`) for session cleanup.
139
+
140
+ ## Requirements
141
+
142
+ - Python 3.8+
143
+ - `requests` (sync client)
144
+ - `aiohttp` (async client, optional)
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ nslsolver/__init__.py
4
+ nslsolver/async_client.py
5
+ nslsolver/client.py
6
+ nslsolver/exceptions.py
7
+ nslsolver/types.py
8
+ nslsolver.egg-info/PKG-INFO
9
+ nslsolver.egg-info/SOURCES.txt
10
+ nslsolver.egg-info/dependency_links.txt
11
+ nslsolver.egg-info/requires.txt
12
+ nslsolver.egg-info/top_level.txt
@@ -0,0 +1,11 @@
1
+ requests>=2.25.0
2
+
3
+ [async]
4
+ aiohttp>=3.8.0
5
+
6
+ [dev]
7
+ aiohttp>=3.8.0
8
+ pytest>=7.0
9
+ pytest-asyncio>=0.21
10
+ mypy>=1.0
11
+ ruff>=0.1.0
@@ -0,0 +1 @@
1
+ nslsolver
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nslsolver"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for the NSLSolver captcha solving API"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "NSLSolver", email = "support@nslsolver.com"},
14
+ ]
15
+ keywords = ["captcha", "turnstile", "cloudflare", "solver", "nslsolver"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Internet :: WWW/HTTP",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "requests>=2.25.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ async = ["aiohttp>=3.8.0"]
38
+ dev = [
39
+ "aiohttp>=3.8.0",
40
+ "pytest>=7.0",
41
+ "pytest-asyncio>=0.21",
42
+ "mypy>=1.0",
43
+ "ruff>=0.1.0",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://nslsolver.com"
48
+ Documentation = "https://docs.nslsolver.com"
49
+ Repository = "https://github.com/NSLSolver/NSLSolver-SDK-Python"
50
+ Issues = "https://github.com/NSLSolver/NSLSolver-SDK-Python/issues"
51
+
52
+ [tool.setuptools.packages.find]
53
+ include = ["nslsolver*"]
54
+
55
+ [tool.mypy]
56
+ strict = true
57
+ python_version = "3.8"
58
+
59
+ [tool.ruff]
60
+ target-version = "py38"
61
+ line-length = 100
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "N", "W", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+