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.
@@ -0,0 +1,7 @@
1
+ .env
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .venv/
5
+ .browser-profiles/
6
+ __pycache__/
7
+ *.py[cod]
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: bypass-vuotlink-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the bypass-vuotlink API
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.24
@@ -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,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class ResolveResult:
6
+ requested_url: str
7
+ final_url: str
8
+ status_code: int | None
9
+ title: str
10
+ workflow: str
@@ -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"]