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.
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,8 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ solvegate/__init__.py
5
+ solvegate.egg-info/PKG-INFO
6
+ solvegate.egg-info/SOURCES.txt
7
+ solvegate.egg-info/dependency_links.txt
8
+ solvegate.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ solvegate