Skip to content

Commit 847c7c8

Browse files
committed
Initial release v1.0.0
0 parents  commit 847c7c8

3 files changed

Lines changed: 307 additions & 0 deletions

File tree

bypasstools/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""BypassTools Python SDK"""
2+
from .client import BypassTools, BypassToolsError, BypassResult, TaskResult
3+
4+
__all__ = ["BypassTools", "BypassToolsError", "BypassResult", "TaskResult"]
5+
__version__ = "1.0.0"

bypasstools/client.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
"""BypassTools API client — sync + async implementations."""
2+
from __future__ import annotations
3+
4+
import time
5+
import json
6+
from dataclasses import dataclass, field
7+
from typing import Optional
8+
from urllib.request import Request, urlopen
9+
from urllib.error import HTTPError, URLError
10+
from urllib.parse import urlencode
11+
12+
DEFAULT_BASE_URL = "https://api.bypass.tools/api/v1"
13+
DEFAULT_TIMEOUT = 60
14+
DEFAULT_POLL_INTERVAL = 1.5
15+
DEFAULT_POLL_TIMEOUT = 90
16+
17+
18+
@dataclass
19+
class BypassResult:
20+
result_url: str
21+
cached: bool
22+
process_time: Optional[float]
23+
request_id: Optional[str]
24+
25+
26+
@dataclass
27+
class TaskResult:
28+
status: str # "pending" | "processing" | "completed" | "failed"
29+
result_url: Optional[str] = None
30+
error: Optional[str] = None
31+
32+
33+
class BypassToolsError(Exception):
34+
def __init__(self, message: str, code: str = "UNKNOWN_ERROR", status: int = 0):
35+
super().__init__(message)
36+
self.code = code
37+
self.status = status
38+
39+
def __repr__(self) -> str:
40+
return f"BypassToolsError(code={self.code!r}, status={self.status}, message={str(self)!r})"
41+
42+
43+
class BypassTools:
44+
"""
45+
Synchronous BypassTools API client.
46+
47+
Usage::
48+
49+
from bypasstools import BypassTools
50+
51+
client = BypassTools(api_key="bt_your_key_here")
52+
result = client.bypass("https://linkvertise.com/example")
53+
print(result.result_url)
54+
"""
55+
56+
def __init__(
57+
self,
58+
api_key: str,
59+
base_url: str = DEFAULT_BASE_URL,
60+
timeout: int = DEFAULT_TIMEOUT,
61+
):
62+
if not api_key:
63+
raise BypassToolsError("api_key is required", "MISSING_API_KEY")
64+
self._api_key = api_key
65+
self._base = base_url.rstrip("/")
66+
self._timeout = timeout
67+
68+
# ─── Private helpers ────────────────────────────────────────────────
69+
70+
def _request(self, method: str, path: str, body: Optional[dict] = None) -> dict:
71+
url = self._base + path
72+
data = json.dumps(body).encode() if body else None
73+
req = Request(
74+
url,
75+
data=data,
76+
method=method,
77+
headers={
78+
"x-api-key": self._api_key,
79+
"Content-Type": "application/json",
80+
},
81+
)
82+
try:
83+
with urlopen(req, timeout=self._timeout) as resp:
84+
return json.loads(resp.read().decode())
85+
except HTTPError as e:
86+
try:
87+
payload = json.loads(e.read().decode())
88+
except Exception:
89+
payload = {}
90+
raise BypassToolsError(
91+
payload.get("message", f"HTTP {e.code}"),
92+
payload.get("code", "API_ERROR"),
93+
e.code,
94+
) from None
95+
except URLError as e:
96+
raise BypassToolsError(str(e.reason), "NETWORK_ERROR") from None
97+
except TimeoutError:
98+
raise BypassToolsError("Request timed out", "TIMEOUT") from None
99+
100+
# ─── Public API ─────────────────────────────────────────────────────
101+
102+
def bypass(self, url: str, *, refresh: bool = False) -> BypassResult:
103+
"""
104+
Bypass a URL synchronously and return the result immediately.
105+
106+
:param url: The URL to bypass (e.g. a Linkvertise or Loot.link URL)
107+
:param refresh: Set True to skip cache and force a fresh bypass
108+
:raises BypassToolsError: on API or network failure
109+
"""
110+
if not url:
111+
raise BypassToolsError("url is required", "MISSING_URL")
112+
data = self._request("POST", "/bypass/direct", {"url": url, "refresh": refresh})
113+
return BypassResult(
114+
result_url = data.get("result", ""),
115+
cached = data.get("cached", False),
116+
process_time = data.get("processTime"),
117+
request_id = data.get("requestId"),
118+
)
119+
120+
def create_task(self, url: str) -> str:
121+
"""
122+
Create an async bypass task and return the taskId.
123+
124+
:param url: The URL to bypass
125+
:returns: taskId string
126+
"""
127+
if not url:
128+
raise BypassToolsError("url is required", "MISSING_URL")
129+
data = self._request("POST", "/bypass/createTask", {"url": url})
130+
return data["taskId"]
131+
132+
def get_task_result(self, task_id: str) -> TaskResult:
133+
"""
134+
Retrieve the current state of a bypass task.
135+
136+
:param task_id: The taskId returned by :meth:`create_task`
137+
"""
138+
if not task_id:
139+
raise BypassToolsError("task_id is required", "MISSING_TASK_ID")
140+
data = self._request("GET", f"/bypass/getTaskResult/{task_id}")
141+
return TaskResult(
142+
status = data.get("status", ""),
143+
result_url = data.get("result"),
144+
error = data.get("error"),
145+
)
146+
147+
def bypass_async(
148+
self,
149+
url: str,
150+
*,
151+
poll_interval: float = DEFAULT_POLL_INTERVAL,
152+
timeout: float = DEFAULT_POLL_TIMEOUT,
153+
) -> BypassResult:
154+
"""
155+
Create a task and block until it completes (or times out).
156+
157+
:param url: The URL to bypass
158+
:param poll_interval: Seconds between status checks (default 1.5)
159+
:param timeout: Max seconds to wait before raising (default 90)
160+
:raises BypassToolsError: if the task fails or times out
161+
"""
162+
task_id = self.create_task(url)
163+
deadline = time.monotonic() + timeout
164+
165+
while time.monotonic() < deadline:
166+
time.sleep(poll_interval)
167+
result = self.get_task_result(task_id)
168+
if result.status == "completed":
169+
return BypassResult(
170+
result_url = result.result_url or "",
171+
cached = False,
172+
process_time = None,
173+
request_id = task_id,
174+
)
175+
if result.status == "failed":
176+
raise BypassToolsError(result.error or "Task failed", "TASK_FAILED")
177+
178+
raise BypassToolsError("Task timed out waiting for result", "TASK_TIMEOUT")
179+
180+
181+
# ─── Async client (requires Python 3.8+ with aiohttp or httpx) ──────────────
182+
# Optional async variant — only available when aiohttp is installed.
183+
try:
184+
import asyncio
185+
186+
class AsyncBypassTools(BypassTools):
187+
"""
188+
Async variant of :class:`BypassTools`.
189+
190+
Requires Python ≥ 3.8 and the ``aiohttp`` package::
191+
192+
pip install bypasstools aiohttp
193+
194+
Usage::
195+
196+
import asyncio
197+
from bypasstools import AsyncBypassTools
198+
199+
async def main():
200+
client = AsyncBypassTools(api_key="bt_your_key_here")
201+
result = await client.bypass("https://linkvertise.com/example")
202+
print(result.result_url)
203+
204+
asyncio.run(main())
205+
"""
206+
207+
async def _arequest(self, method: str, path: str, body: Optional[dict] = None) -> dict:
208+
try:
209+
import aiohttp # noqa: PLC0415
210+
except ImportError:
211+
raise BypassToolsError(
212+
"aiohttp is required for async usage: pip install aiohttp",
213+
"MISSING_DEPENDENCY",
214+
) from None
215+
216+
url = self._base + path
217+
async with aiohttp.ClientSession() as session:
218+
kwargs = {
219+
"method": method,
220+
"url": url,
221+
"headers": {"x-api-key": self._api_key, "Content-Type": "application/json"},
222+
"timeout": aiohttp.ClientTimeout(total=self._timeout),
223+
}
224+
if body:
225+
kwargs["json"] = body
226+
try:
227+
async with session.request(**kwargs) as resp:
228+
data = await resp.json()
229+
if resp.status >= 400:
230+
raise BypassToolsError(
231+
data.get("message", f"HTTP {resp.status}"),
232+
data.get("code", "API_ERROR"),
233+
resp.status,
234+
)
235+
return data
236+
except aiohttp.ClientError as e:
237+
raise BypassToolsError(str(e), "NETWORK_ERROR") from None
238+
239+
async def bypass(self, url: str, *, refresh: bool = False) -> BypassResult: # type: ignore[override]
240+
if not url:
241+
raise BypassToolsError("url is required", "MISSING_URL")
242+
data = await self._arequest("POST", "/bypass/direct", {"url": url, "refresh": refresh})
243+
return BypassResult(
244+
result_url = data.get("result", ""),
245+
cached = data.get("cached", False),
246+
process_time = data.get("processTime"),
247+
request_id = data.get("requestId"),
248+
)
249+
250+
async def create_task(self, url: str) -> str: # type: ignore[override]
251+
if not url:
252+
raise BypassToolsError("url is required", "MISSING_URL")
253+
data = await self._arequest("POST", "/bypass/createTask", {"url": url})
254+
return data["taskId"]
255+
256+
async def get_task_result(self, task_id: str) -> TaskResult: # type: ignore[override]
257+
data = await self._arequest("GET", f"/bypass/getTaskResult/{task_id}")
258+
return TaskResult(status=data.get("status", ""), result_url=data.get("result"), error=data.get("error"))
259+
260+
async def bypass_async( # type: ignore[override]
261+
self,
262+
url: str,
263+
*,
264+
poll_interval: float = DEFAULT_POLL_INTERVAL,
265+
timeout: float = DEFAULT_POLL_TIMEOUT,
266+
) -> BypassResult:
267+
task_id = await self.create_task(url)
268+
deadline = asyncio.get_event_loop().time() + timeout
269+
270+
while asyncio.get_event_loop().time() < deadline:
271+
await asyncio.sleep(poll_interval)
272+
result = await self.get_task_result(task_id)
273+
if result.status == "completed":
274+
return BypassResult(result_url=result.result_url or "", cached=False, process_time=None, request_id=task_id)
275+
if result.status == "failed":
276+
raise BypassToolsError(result.error or "Task failed", "TASK_FAILED")
277+
278+
raise BypassToolsError("Task timed out", "TASK_TIMEOUT")
279+
280+
except Exception:
281+
pass # asyncio unavailable — async client not exposed

pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.backends.legacy:build"
4+
5+
[project]
6+
name = "bypasstools"
7+
version = "1.0.0"
8+
description = "Official Python SDK for the BypassTools API"
9+
readme = "README.md"
10+
license = { text = "MIT" }
11+
requires-python = ">=3.8"
12+
dependencies = []
13+
keywords = ["bypasstools", "bypass", "linkvertise", "api"]
14+
15+
[project.urls]
16+
Homepage = "https://bypass.tools"
17+
Repository = "https://github.com/XxEASTRxX/bypasstools-sdks-python.git"
18+
19+
[tool.setuptools.packages.find]
20+
where = ["."]
21+
include = ["bypasstools*"]

0 commit comments

Comments
 (0)