bypass-vuotlink-sdk 0.1.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.
- bypass_vuotlink_sdk-0.1.0/.gitignore +7 -0
- bypass_vuotlink_sdk-0.1.0/PKG-INFO +6 -0
- bypass_vuotlink_sdk-0.1.0/bypass_vuotlink_sdk/__init__.py +15 -0
- bypass_vuotlink_sdk-0.1.0/bypass_vuotlink_sdk/client.py +158 -0
- bypass_vuotlink_sdk-0.1.0/bypass_vuotlink_sdk/config.py +36 -0
- bypass_vuotlink_sdk-0.1.0/bypass_vuotlink_sdk/exceptions.py +19 -0
- bypass_vuotlink_sdk-0.1.0/bypass_vuotlink_sdk/models.py +10 -0
- bypass_vuotlink_sdk-0.1.0/llms.txt +194 -0
- bypass_vuotlink_sdk-0.1.0/pyproject.toml +15 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .client import AsyncBypassVuotLink, BypassVuotLink
|
|
2
|
+
from .config import ClientConfig
|
|
3
|
+
from .exceptions import ApiError, BrowserExecutionError, BypassVuotlinkError, UnsafeUrlError
|
|
4
|
+
from .models import ResolveResult
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BypassVuotLink",
|
|
8
|
+
"AsyncBypassVuotLink",
|
|
9
|
+
"ClientConfig",
|
|
10
|
+
"ResolveResult",
|
|
11
|
+
"BypassVuotlinkError",
|
|
12
|
+
"UnsafeUrlError",
|
|
13
|
+
"BrowserExecutionError",
|
|
14
|
+
"ApiError",
|
|
15
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .config import ClientConfig
|
|
8
|
+
from .exceptions import ApiError, BrowserExecutionError, UnsafeUrlError
|
|
9
|
+
from .models import ResolveResult
|
|
10
|
+
|
|
11
|
+
_ENDPOINT = "/api/v1/browser"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_response(response: httpx.Response) -> ResolveResult:
|
|
15
|
+
if response.status_code == 200:
|
|
16
|
+
return ResolveResult(**response.json())
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
detail: str = response.json().get("detail", response.text)
|
|
20
|
+
except Exception:
|
|
21
|
+
detail = response.text
|
|
22
|
+
|
|
23
|
+
if response.status_code == 400:
|
|
24
|
+
raise UnsafeUrlError(detail)
|
|
25
|
+
if response.status_code == 502:
|
|
26
|
+
raise BrowserExecutionError(detail)
|
|
27
|
+
raise ApiError(response.status_code, detail)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BypassVuotLink:
|
|
31
|
+
"""Sync client for the bypass-vuotlink API.
|
|
32
|
+
|
|
33
|
+
Usage::
|
|
34
|
+
|
|
35
|
+
client = BypassVuotLink(base_url="http://localhost:8000")
|
|
36
|
+
result = client.resolve("https://vuotnhanh.com/...")
|
|
37
|
+
print(result.final_url)
|
|
38
|
+
client.close()
|
|
39
|
+
|
|
40
|
+
# or as a context manager:
|
|
41
|
+
with BypassVuotLink(base_url="http://localhost:8000") as client:
|
|
42
|
+
result = client.resolve("https://vuotnhanh.com/...")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
base_url: str,
|
|
48
|
+
*,
|
|
49
|
+
timeout: float = ClientConfig.default_timeout,
|
|
50
|
+
api_key: str | None = None,
|
|
51
|
+
extra_headers: dict[str, str] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
config = ClientConfig(
|
|
54
|
+
base_url=base_url,
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
api_key=api_key,
|
|
57
|
+
extra_headers=extra_headers or {},
|
|
58
|
+
)
|
|
59
|
+
self._client = httpx.Client(
|
|
60
|
+
base_url=config.base_url,
|
|
61
|
+
timeout=config.timeout,
|
|
62
|
+
headers=config.build_headers(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_config(cls, config: ClientConfig) -> BypassVuotLink:
|
|
67
|
+
instance = cls.__new__(cls)
|
|
68
|
+
instance._client = httpx.Client(
|
|
69
|
+
base_url=config.base_url,
|
|
70
|
+
timeout=config.timeout,
|
|
71
|
+
headers=config.build_headers(),
|
|
72
|
+
)
|
|
73
|
+
return instance
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_env(cls) -> BypassVuotLink:
|
|
77
|
+
return cls.from_config(ClientConfig.from_env())
|
|
78
|
+
|
|
79
|
+
def resolve(self, url: str) -> ResolveResult:
|
|
80
|
+
response = self._client.post(_ENDPOINT, json={"url": url})
|
|
81
|
+
return _parse_response(response)
|
|
82
|
+
|
|
83
|
+
def close(self) -> None:
|
|
84
|
+
self._client.close()
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> BypassVuotLink:
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(
|
|
90
|
+
self,
|
|
91
|
+
exc_type: type[BaseException] | None,
|
|
92
|
+
exc_val: BaseException | None,
|
|
93
|
+
exc_tb: TracebackType | None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AsyncBypassVuotLink:
|
|
99
|
+
"""Async client for the bypass-vuotlink API.
|
|
100
|
+
|
|
101
|
+
Usage::
|
|
102
|
+
|
|
103
|
+
async with AsyncBypassVuotLink(base_url="http://localhost:8000") as client:
|
|
104
|
+
result = await client.resolve("https://vuotnhanh.com/...")
|
|
105
|
+
print(result.final_url)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
base_url: str,
|
|
111
|
+
*,
|
|
112
|
+
timeout: float = ClientConfig.default_timeout,
|
|
113
|
+
api_key: str | None = None,
|
|
114
|
+
extra_headers: dict[str, str] | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
config = ClientConfig(
|
|
117
|
+
base_url=base_url,
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
api_key=api_key,
|
|
120
|
+
extra_headers=extra_headers or {},
|
|
121
|
+
)
|
|
122
|
+
self._client = httpx.AsyncClient(
|
|
123
|
+
base_url=config.base_url,
|
|
124
|
+
timeout=config.timeout,
|
|
125
|
+
headers=config.build_headers(),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_config(cls, config: ClientConfig) -> AsyncBypassVuotLink:
|
|
130
|
+
instance = cls.__new__(cls)
|
|
131
|
+
instance._client = httpx.AsyncClient(
|
|
132
|
+
base_url=config.base_url,
|
|
133
|
+
timeout=config.timeout,
|
|
134
|
+
headers=config.build_headers(),
|
|
135
|
+
)
|
|
136
|
+
return instance
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_env(cls) -> AsyncBypassVuotLink:
|
|
140
|
+
return cls.from_config(ClientConfig.from_env())
|
|
141
|
+
|
|
142
|
+
async def resolve(self, url: str) -> ResolveResult:
|
|
143
|
+
response = await self._client.post(_ENDPOINT, json={"url": url})
|
|
144
|
+
return _parse_response(response)
|
|
145
|
+
|
|
146
|
+
async def close(self) -> None:
|
|
147
|
+
await self._client.aclose()
|
|
148
|
+
|
|
149
|
+
async def __aenter__(self) -> AsyncBypassVuotLink:
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
async def __aexit__(
|
|
153
|
+
self,
|
|
154
|
+
exc_type: type[BaseException] | None,
|
|
155
|
+
exc_val: BaseException | None,
|
|
156
|
+
exc_tb: TracebackType | None,
|
|
157
|
+
) -> None:
|
|
158
|
+
await self.close()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ClientConfig:
|
|
10
|
+
"""Configuration for BypassVuotLink / AsyncBypassVuotLink.
|
|
11
|
+
|
|
12
|
+
Can be constructed directly or loaded from environment variables::
|
|
13
|
+
|
|
14
|
+
config = ClientConfig.from_env()
|
|
15
|
+
# reads BYPASS_BASE_URL, BYPASS_TIMEOUT, BYPASS_API_KEY
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT: ClassVar[float] = 60.0
|
|
19
|
+
|
|
20
|
+
base_url: str
|
|
21
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
22
|
+
api_key: str | None = None
|
|
23
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_env(cls) -> ClientConfig:
|
|
27
|
+
base_url = os.environ["BYPASS_BASE_URL"]
|
|
28
|
+
timeout = float(os.environ.get("BYPASS_TIMEOUT", cls.DEFAULT_TIMEOUT))
|
|
29
|
+
api_key = os.environ.get("BYPASS_API_KEY")
|
|
30
|
+
return cls(base_url=base_url, timeout=timeout, api_key=api_key)
|
|
31
|
+
|
|
32
|
+
def build_headers(self) -> dict[str, str]:
|
|
33
|
+
headers = dict(self.extra_headers)
|
|
34
|
+
if self.api_key:
|
|
35
|
+
headers["X-API-Key"] = self.api_key
|
|
36
|
+
return headers
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class BypassVuotlinkError(Exception):
|
|
2
|
+
"""Base exception for all SDK errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UnsafeUrlError(BypassVuotlinkError):
|
|
6
|
+
"""The URL was rejected as unsafe or private (HTTP 400)."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserExecutionError(BypassVuotlinkError):
|
|
10
|
+
"""The browser workflow failed to execute (HTTP 502)."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(BypassVuotlinkError):
|
|
14
|
+
"""Unexpected HTTP error from the server."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
17
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.detail = detail
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# bypass-vuotlink-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the bypass-vuotlink API — a service that resolves shortlink/safelink URLs
|
|
4
|
+
(e.g. vuotnhanh.com) to their final destination by running a real browser workflow.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install bypass-vuotlink-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from bypass_vuotlink_sdk import BypassVuotLink
|
|
16
|
+
|
|
17
|
+
# Sync
|
|
18
|
+
with BypassVuotLink(base_url="http://localhost:8000") as client:
|
|
19
|
+
result = client.resolve("https://vuotnhanh.com/abc123")
|
|
20
|
+
print(result.final_url)
|
|
21
|
+
|
|
22
|
+
# Async
|
|
23
|
+
from bypass_vuotlink_sdk import AsyncBypassVuotLink
|
|
24
|
+
|
|
25
|
+
async with AsyncBypassVuotLink(base_url="http://localhost:8000") as client:
|
|
26
|
+
result = await client.resolve("https://vuotnhanh.com/abc123")
|
|
27
|
+
print(result.final_url)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Classes
|
|
31
|
+
|
|
32
|
+
### BypassVuotLink (sync)
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
class BypassVuotLink:
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
base_url: str,
|
|
39
|
+
*,
|
|
40
|
+
timeout: float = 60.0, # seconds
|
|
41
|
+
api_key: str | None = None, # sent as X-API-Key header
|
|
42
|
+
extra_headers: dict[str, str] | None = None,
|
|
43
|
+
) -> None: ...
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_config(cls, config: ClientConfig) -> BypassVuotLink: ...
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_env(cls) -> BypassVuotLink:
|
|
50
|
+
# reads: BYPASS_BASE_URL, BYPASS_TIMEOUT, BYPASS_API_KEY
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def resolve(self, url: str) -> ResolveResult: ...
|
|
54
|
+
def close(self) -> None: ...
|
|
55
|
+
|
|
56
|
+
# context manager
|
|
57
|
+
def __enter__(self) -> BypassVuotLink: ...
|
|
58
|
+
def __exit__(self, ...) -> None: ...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### AsyncBypassVuotLink (async)
|
|
62
|
+
|
|
63
|
+
Same interface as `BypassVuotLink` but all methods are async:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
class AsyncBypassVuotLink:
|
|
67
|
+
def __init__(self, base_url: str, *, timeout: float = 60.0,
|
|
68
|
+
api_key: str | None = None,
|
|
69
|
+
extra_headers: dict[str, str] | None = None) -> None: ...
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_config(cls, config: ClientConfig) -> AsyncBypassVuotLink: ...
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_env(cls) -> AsyncBypassVuotLink: ...
|
|
76
|
+
|
|
77
|
+
async def resolve(self, url: str) -> ResolveResult: ...
|
|
78
|
+
async def close(self) -> None: ...
|
|
79
|
+
|
|
80
|
+
# async context manager
|
|
81
|
+
async def __aenter__(self) -> AsyncBypassVuotLink: ...
|
|
82
|
+
async def __aexit__(self, ...) -> None: ...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### ClientConfig
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from dataclasses import dataclass
|
|
89
|
+
from typing import ClassVar
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class ClientConfig:
|
|
93
|
+
DEFAULT_TIMEOUT: ClassVar[float] = 60.0
|
|
94
|
+
|
|
95
|
+
base_url: str
|
|
96
|
+
timeout: float = 60.0
|
|
97
|
+
api_key: str | None = None
|
|
98
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_env(cls) -> ClientConfig:
|
|
102
|
+
# BYPASS_BASE_URL (required)
|
|
103
|
+
# BYPASS_TIMEOUT (optional, default 60.0)
|
|
104
|
+
# BYPASS_API_KEY (optional)
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def build_headers(self) -> dict[str, str]: ...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### ResolveResult
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class ResolveResult:
|
|
115
|
+
requested_url: str # the URL you passed in
|
|
116
|
+
final_url: str # the resolved destination URL
|
|
117
|
+
status_code: int | None
|
|
118
|
+
title: str # page title at final_url
|
|
119
|
+
workflow: str # which workflow handled it (e.g. "vuotnhanh")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Exceptions
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
BypassVuotlinkError # base — catch this to handle all SDK errors
|
|
126
|
+
├── UnsafeUrlError # HTTP 400 — URL is private/unsafe
|
|
127
|
+
├── BrowserExecutionError # HTTP 502 — browser workflow crashed
|
|
128
|
+
└── ApiError # any other unexpected HTTP status
|
|
129
|
+
.status_code: int
|
|
130
|
+
.detail: str
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from bypass_vuotlink_sdk import (
|
|
135
|
+
BypassVuotLink,
|
|
136
|
+
UnsafeUrlError,
|
|
137
|
+
BrowserExecutionError,
|
|
138
|
+
BypassVuotlinkError,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
result = client.resolve(url)
|
|
143
|
+
except UnsafeUrlError:
|
|
144
|
+
# URL points to a private/reserved address
|
|
145
|
+
...
|
|
146
|
+
except BrowserExecutionError:
|
|
147
|
+
# the browser failed to process the page
|
|
148
|
+
...
|
|
149
|
+
except BypassVuotlinkError:
|
|
150
|
+
# catch-all for any other SDK error
|
|
151
|
+
...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Environment variable config
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export BYPASS_BASE_URL="http://bypass-api:8000"
|
|
158
|
+
export BYPASS_TIMEOUT="90"
|
|
159
|
+
export BYPASS_API_KEY="secret"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
client = BypassVuotLink.from_env()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## API reference (server)
|
|
167
|
+
|
|
168
|
+
The SDK wraps a single endpoint:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
POST /api/v1/browser
|
|
172
|
+
Content-Type: application/json
|
|
173
|
+
|
|
174
|
+
{ "url": "https://vuotnhanh.com/abc123" }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Response:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"requested_url": "https://vuotnhanh.com/abc123",
|
|
182
|
+
"final_url": "https://example.com/destination",
|
|
183
|
+
"status_code": 200,
|
|
184
|
+
"title": "Example Domain",
|
|
185
|
+
"workflow": "vuotnhanh"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Errors:
|
|
190
|
+
|
|
191
|
+
| Status | Meaning |
|
|
192
|
+
|--------|---------|
|
|
193
|
+
| 400 | URL is unsafe or private |
|
|
194
|
+
| 502 | Browser workflow execution failed |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bypass-vuotlink-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for the bypass-vuotlink API"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"httpx>=0.24",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["hatchling"]
|
|
12
|
+
build-backend = "hatchling.build"
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["bypass_vuotlink_sdk"]
|