solvegate 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.
- solvegate-1.0.0/LICENSE +21 -0
- solvegate-1.0.0/PKG-INFO +55 -0
- solvegate-1.0.0/README.md +43 -0
- solvegate-1.0.0/pyproject.toml +20 -0
- solvegate-1.0.0/setup.cfg +4 -0
- solvegate-1.0.0/solvegate/__init__.py +148 -0
- solvegate-1.0.0/solvegate.egg-info/PKG-INFO +55 -0
- solvegate-1.0.0/solvegate.egg-info/SOURCES.txt +8 -0
- solvegate-1.0.0/solvegate.egg-info/dependency_links.txt +1 -0
- solvegate-1.0.0/solvegate.egg-info/top_level.txt +1 -0
solvegate-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SolveGate
|
|
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.
|
solvegate-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solvegate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the SolveGate API (Cloudflare Turnstile + WAF solving).
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://solvegate.io
|
|
7
|
+
Keywords: solvegate,captcha,turnstile,cloudflare,waf
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# solvegate — Python SDK
|
|
14
|
+
|
|
15
|
+
Official client for the [SolveGate API](https://solvegate.io) — clear Cloudflare
|
|
16
|
+
Turnstile + WAF challenges and get a token back. **Zero dependencies** (stdlib only);
|
|
17
|
+
wraps auth, the error envelope, `429` backoff, and async polling.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install solvegate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import os
|
|
25
|
+
from solvegate import SolveGate
|
|
26
|
+
|
|
27
|
+
sg = SolveGate(os.environ["SOLVEGATE_KEY"]) # sg_live_… or a free sg_test_… sandbox key
|
|
28
|
+
|
|
29
|
+
# Synchronous — blocks until solved (or raises on timeout):
|
|
30
|
+
res = sg.solve(gate="turnstile", sitekey="0x4AAAAAAAAA_target", url="https://app.example.com")
|
|
31
|
+
print(res.token, res.solve_ms)
|
|
32
|
+
|
|
33
|
+
# Asynchronous — return immediately, then poll:
|
|
34
|
+
pending = sg.solve(gate="turnstile", sitekey=sitekey, url=url, async_=True)
|
|
35
|
+
solved = sg.wait(pending.id)
|
|
36
|
+
if solved.status == "solved":
|
|
37
|
+
use(solved.token)
|
|
38
|
+
|
|
39
|
+
# Retrieve any solve by id:
|
|
40
|
+
s = sg.retrieve("slv_8Kd2aF9")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
| Method | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `SolveGate(api_key, base_url=…, timeout=60, max_retries=3)` | construct a client |
|
|
48
|
+
| `sg.solve(gate, sitekey, url, action=None, async_=False, proxy=None, idempotency_key=None)` | → `Solve` |
|
|
49
|
+
| `sg.retrieve(id)` | → `Solve` (never billed) |
|
|
50
|
+
| `sg.wait(id, interval=1.0, timeout=60.0)` | poll an async solve until it leaves `pending` |
|
|
51
|
+
|
|
52
|
+
`Solve` has `.id .status .gate .token .solve_ms .expires_at .billed`. Errors raise
|
|
53
|
+
`SolveGateError` with `.code` / `.status` / `.billed` (e.g. `balance_empty`,
|
|
54
|
+
`rate_limited`, `solve_timeout`). Note `async_` (trailing underscore) maps to the
|
|
55
|
+
API's `async` field. You're only billed for a successful solve.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# solvegate — Python SDK
|
|
2
|
+
|
|
3
|
+
Official client for the [SolveGate API](https://solvegate.io) — clear Cloudflare
|
|
4
|
+
Turnstile + WAF challenges and get a token back. **Zero dependencies** (stdlib only);
|
|
5
|
+
wraps auth, the error envelope, `429` backoff, and async polling.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install solvegate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import os
|
|
13
|
+
from solvegate import SolveGate
|
|
14
|
+
|
|
15
|
+
sg = SolveGate(os.environ["SOLVEGATE_KEY"]) # sg_live_… or a free sg_test_… sandbox key
|
|
16
|
+
|
|
17
|
+
# Synchronous — blocks until solved (or raises on timeout):
|
|
18
|
+
res = sg.solve(gate="turnstile", sitekey="0x4AAAAAAAAA_target", url="https://app.example.com")
|
|
19
|
+
print(res.token, res.solve_ms)
|
|
20
|
+
|
|
21
|
+
# Asynchronous — return immediately, then poll:
|
|
22
|
+
pending = sg.solve(gate="turnstile", sitekey=sitekey, url=url, async_=True)
|
|
23
|
+
solved = sg.wait(pending.id)
|
|
24
|
+
if solved.status == "solved":
|
|
25
|
+
use(solved.token)
|
|
26
|
+
|
|
27
|
+
# Retrieve any solve by id:
|
|
28
|
+
s = sg.retrieve("slv_8Kd2aF9")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
| Method | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `SolveGate(api_key, base_url=…, timeout=60, max_retries=3)` | construct a client |
|
|
36
|
+
| `sg.solve(gate, sitekey, url, action=None, async_=False, proxy=None, idempotency_key=None)` | → `Solve` |
|
|
37
|
+
| `sg.retrieve(id)` | → `Solve` (never billed) |
|
|
38
|
+
| `sg.wait(id, interval=1.0, timeout=60.0)` | poll an async solve until it leaves `pending` |
|
|
39
|
+
|
|
40
|
+
`Solve` has `.id .status .gate .token .solve_ms .expires_at .billed`. Errors raise
|
|
41
|
+
`SolveGateError` with `.code` / `.status` / `.billed` (e.g. `balance_empty`,
|
|
42
|
+
`rate_limited`, `solve_timeout`). Note `async_` (trailing underscore) maps to the
|
|
43
|
+
API's `async` field. You're only billed for a successful solve.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "solvegate"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the SolveGate API (Cloudflare Turnstile + WAF solving)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["solvegate", "captcha", "turnstile", "cloudflare", "waf"]
|
|
13
|
+
dependencies = [] # zero runtime dependencies — stdlib only
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://solvegate.io"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["."]
|
|
20
|
+
include = ["solvegate*"]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Official SolveGate Python SDK.
|
|
2
|
+
|
|
3
|
+
A tiny, zero-dependency client (stdlib only) over the public HTTPS+JSON API.
|
|
4
|
+
Wraps auth, the documented error envelope, ``429`` backoff, and async polling.
|
|
5
|
+
|
|
6
|
+
from solvegate import SolveGate
|
|
7
|
+
|
|
8
|
+
sg = SolveGate(os.environ["SOLVEGATE_KEY"]) # sg_live_… or a free sg_test_… key
|
|
9
|
+
res = sg.solve(gate="turnstile", sitekey="0x4AAAAAAAAA_target", url="https://app.example.com")
|
|
10
|
+
print(res.token)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.parse
|
|
19
|
+
import urllib.request
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
__all__ = ["SolveGate", "Solve", "SolveGateError"]
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Solve:
|
|
29
|
+
"""A solve attempt. ``token`` is populated once ``status == 'solved'``."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
status: str # "pending" | "solved" | "failed"
|
|
33
|
+
gate: str
|
|
34
|
+
token: Optional[str]
|
|
35
|
+
solve_ms: Optional[int]
|
|
36
|
+
expires_at: Optional[int]
|
|
37
|
+
billed: bool
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _from(cls, d: dict) -> "Solve":
|
|
41
|
+
return cls(
|
|
42
|
+
id=d.get("id"),
|
|
43
|
+
status=d.get("status"),
|
|
44
|
+
gate=d.get("gate"),
|
|
45
|
+
token=d.get("token"),
|
|
46
|
+
solve_ms=d.get("solve_ms"),
|
|
47
|
+
expires_at=d.get("expires_at"),
|
|
48
|
+
billed=bool(d.get("billed", False)),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SolveGateError(Exception):
|
|
53
|
+
"""Raised on any non-2xx response, carrying the documented error envelope."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, status: int, code: str, message: str, billed: bool = False):
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.status = status
|
|
58
|
+
self.code = code
|
|
59
|
+
self.billed = billed
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SolveGate:
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: str,
|
|
66
|
+
base_url: str = "https://api.solvegate.io",
|
|
67
|
+
timeout: float = 60.0,
|
|
68
|
+
max_retries: int = 3,
|
|
69
|
+
):
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise ValueError("SolveGate: an API key is required.")
|
|
72
|
+
self.api_key = api_key
|
|
73
|
+
self.base_url = base_url.rstrip("/")
|
|
74
|
+
self.timeout = timeout
|
|
75
|
+
self.max_retries = max_retries
|
|
76
|
+
|
|
77
|
+
def solve(
|
|
78
|
+
self,
|
|
79
|
+
gate: str,
|
|
80
|
+
sitekey: str,
|
|
81
|
+
url: str,
|
|
82
|
+
action: Optional[str] = None,
|
|
83
|
+
async_: bool = False,
|
|
84
|
+
proxy: Optional[str] = None,
|
|
85
|
+
idempotency_key: Optional[str] = None,
|
|
86
|
+
) -> Solve:
|
|
87
|
+
"""Solve a challenge. Blocks until solved unless ``async_=True``."""
|
|
88
|
+
body = {"gate": gate, "sitekey": sitekey, "url": url, "async": async_}
|
|
89
|
+
if action is not None:
|
|
90
|
+
body["action"] = action
|
|
91
|
+
if proxy is not None:
|
|
92
|
+
body["proxy"] = proxy
|
|
93
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
|
|
94
|
+
return self._request("POST", "/v1/solve", body=body, headers=headers)
|
|
95
|
+
|
|
96
|
+
def retrieve(self, id: str) -> Solve:
|
|
97
|
+
"""Retrieve a solve by id. Never billed."""
|
|
98
|
+
return self._request("GET", "/v1/solve/" + urllib.parse.quote(id, safe=""))
|
|
99
|
+
|
|
100
|
+
def wait(self, id: str, interval: float = 1.0, timeout: float = 60.0) -> Solve:
|
|
101
|
+
"""Poll ``retrieve(id)`` until the solve leaves ``pending`` (for async solves)."""
|
|
102
|
+
deadline = time.monotonic() + timeout
|
|
103
|
+
while True:
|
|
104
|
+
s = self.retrieve(id)
|
|
105
|
+
if s.status != "pending" or time.monotonic() >= deadline:
|
|
106
|
+
return s
|
|
107
|
+
time.sleep(interval)
|
|
108
|
+
|
|
109
|
+
# -- internals ----------------------------------------------------------
|
|
110
|
+
def _request(self, method: str, path: str, body: Optional[dict] = None, headers: Optional[dict] = None) -> Solve:
|
|
111
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
112
|
+
attempt = 0
|
|
113
|
+
while True:
|
|
114
|
+
req = urllib.request.Request(self.base_url + path, data=data, method=method)
|
|
115
|
+
req.add_header("Authorization", "Bearer " + self.api_key)
|
|
116
|
+
if data is not None:
|
|
117
|
+
req.add_header("Content-Type", "application/json")
|
|
118
|
+
for k, v in (headers or {}).items():
|
|
119
|
+
req.add_header(k, v)
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
122
|
+
return Solve._from(json.loads(resp.read().decode()))
|
|
123
|
+
except urllib.error.HTTPError as e:
|
|
124
|
+
payload = self._read_json(e)
|
|
125
|
+
if e.code == 429 and attempt < self.max_retries:
|
|
126
|
+
retry_after = e.headers.get("Retry-After")
|
|
127
|
+
time.sleep(float(retry_after) if retry_after and retry_after.isdigit() else self._backoff(attempt))
|
|
128
|
+
attempt += 1
|
|
129
|
+
continue
|
|
130
|
+
err = (payload or {}).get("error", {})
|
|
131
|
+
raise SolveGateError(e.code, err.get("code", "error"), err.get("message", str(e)), err.get("billed", False))
|
|
132
|
+
except urllib.error.URLError as e:
|
|
133
|
+
if attempt < self.max_retries:
|
|
134
|
+
time.sleep(self._backoff(attempt))
|
|
135
|
+
attempt += 1
|
|
136
|
+
continue
|
|
137
|
+
raise SolveGateError(0, "network_error", str(e.reason))
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _read_json(e: "urllib.error.HTTPError") -> Optional[dict]:
|
|
141
|
+
try:
|
|
142
|
+
return json.loads(e.read().decode())
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _backoff(attempt: int) -> float:
|
|
148
|
+
return min(8.0, 0.5 * (2 ** attempt)) # 0.5s, 1s, 2s, 4s … capped 8s
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solvegate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the SolveGate API (Cloudflare Turnstile + WAF solving).
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://solvegate.io
|
|
7
|
+
Keywords: solvegate,captcha,turnstile,cloudflare,waf
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# solvegate — Python SDK
|
|
14
|
+
|
|
15
|
+
Official client for the [SolveGate API](https://solvegate.io) — clear Cloudflare
|
|
16
|
+
Turnstile + WAF challenges and get a token back. **Zero dependencies** (stdlib only);
|
|
17
|
+
wraps auth, the error envelope, `429` backoff, and async polling.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install solvegate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import os
|
|
25
|
+
from solvegate import SolveGate
|
|
26
|
+
|
|
27
|
+
sg = SolveGate(os.environ["SOLVEGATE_KEY"]) # sg_live_… or a free sg_test_… sandbox key
|
|
28
|
+
|
|
29
|
+
# Synchronous — blocks until solved (or raises on timeout):
|
|
30
|
+
res = sg.solve(gate="turnstile", sitekey="0x4AAAAAAAAA_target", url="https://app.example.com")
|
|
31
|
+
print(res.token, res.solve_ms)
|
|
32
|
+
|
|
33
|
+
# Asynchronous — return immediately, then poll:
|
|
34
|
+
pending = sg.solve(gate="turnstile", sitekey=sitekey, url=url, async_=True)
|
|
35
|
+
solved = sg.wait(pending.id)
|
|
36
|
+
if solved.status == "solved":
|
|
37
|
+
use(solved.token)
|
|
38
|
+
|
|
39
|
+
# Retrieve any solve by id:
|
|
40
|
+
s = sg.retrieve("slv_8Kd2aF9")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
| Method | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `SolveGate(api_key, base_url=…, timeout=60, max_retries=3)` | construct a client |
|
|
48
|
+
| `sg.solve(gate, sitekey, url, action=None, async_=False, proxy=None, idempotency_key=None)` | → `Solve` |
|
|
49
|
+
| `sg.retrieve(id)` | → `Solve` (never billed) |
|
|
50
|
+
| `sg.wait(id, interval=1.0, timeout=60.0)` | poll an async solve until it leaves `pending` |
|
|
51
|
+
|
|
52
|
+
`Solve` has `.id .status .gate .token .solve_ms .expires_at .billed`. Errors raise
|
|
53
|
+
`SolveGateError` with `.code` / `.status` / `.billed` (e.g. `balance_empty`,
|
|
54
|
+
`rate_limited`, `solve_timeout`). Note `async_` (trailing underscore) maps to the
|
|
55
|
+
API's `async` field. You're only billed for a successful solve.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
solvegate
|