swpreq 0.1.0__py3-none-any.whl

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.
swpreq/__init__.py ADDED
@@ -0,0 +1,80 @@
1
+ from ._native import get_lib, get_platform_info, detect_platform
2
+ from .client import Client, Session
3
+ from .response import Response
4
+ from .exceptions import (
5
+ SwpreqError,
6
+ PlatformNotSupportedError,
7
+ CoreLoadError,
8
+ FileLockedError,
9
+ NetworkError,
10
+ TimeoutError,
11
+ InvalidUrlError,
12
+ TlsError,
13
+ ProxyError,
14
+ DecodeError,
15
+ EncodeError,
16
+ NotFoundError,
17
+ HTTPError,
18
+ )
19
+ from .sync_api import get, post, put, delete, patch, head, options, request
20
+ from .streaming import ResponseStream
21
+ from .sse import SseStream, SseEvent
22
+ from .multipart import MultipartBuilder
23
+ from .retry import RetryPolicy
24
+ from .oauth import OAuth1, OAuth1Params, OAuth2
25
+ from .websocket import WebSocket, AsyncWebSocket, WebSocketMessage
26
+
27
+ __version__ = "0.1.0"
28
+
29
+ __all__ = [
30
+ "Client",
31
+ "Session",
32
+ "Response",
33
+ "ResponseStream",
34
+ "SseStream",
35
+ "SseEvent",
36
+ "MultipartBuilder",
37
+ "RetryPolicy",
38
+ "OAuth1",
39
+ "OAuth1Params",
40
+ "OAuth2",
41
+ "WebSocket",
42
+ "AsyncWebSocket",
43
+ "WebSocketMessage",
44
+ "SwpreqError",
45
+ "PlatformNotSupportedError",
46
+ "CoreLoadError",
47
+ "FileLockedError",
48
+ "NetworkError",
49
+ "TimeoutError",
50
+ "InvalidUrlError",
51
+ "TlsError",
52
+ "ProxyError",
53
+ "DecodeError",
54
+ "EncodeError",
55
+ "NotFoundError",
56
+ "HTTPError",
57
+ "get",
58
+ "post",
59
+ "put",
60
+ "delete",
61
+ "patch",
62
+ "head",
63
+ "options",
64
+ "request",
65
+ "get_lib",
66
+ "get_platform_info",
67
+ "detect_platform",
68
+ ]
69
+
70
+
71
+ def __getattr__(name):
72
+ if name == "AsyncClient":
73
+ from .async_api import AsyncClient
74
+
75
+ return AsyncClient
76
+ if name == "AsyncSession":
77
+ from .async_api import AsyncSession
78
+
79
+ return AsyncSession
80
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
swpreq/_native.py ADDED
@@ -0,0 +1,334 @@
1
+ import ctypes
2
+ import ctypes.util
3
+ import json
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from .exceptions import (
16
+ PlatformNotSupportedError,
17
+ CoreLoadError,
18
+ FileLockedError,
19
+ )
20
+
21
+
22
+ SUPPORTED_PLATFORMS = {
23
+ ("windows", "x64"): "swpreq.dll",
24
+ ("windows", "arm64"): "swpreq.dll",
25
+ ("linux", "x64"): "libswpreq.so",
26
+ ("linux", "arm64"): "libswpreq.so",
27
+ ("linux", "armv7"): "libswpreq.so",
28
+ ("macos", "x64"): "libswpreq.dylib",
29
+ ("macos", "arm64"): "libswpreq.dylib",
30
+ ("android", "arm64"): "libswpreq.so",
31
+ ("android", "armv7"): "libswpreq.so",
32
+ ("android", "x64"): "libswpreq.so",
33
+ ("android", "x86"): "libswpreq.so",
34
+ }
35
+
36
+
37
+ def _detect_android() -> bool:
38
+ if "ANDROID_ROOT" in os.environ:
39
+ return True
40
+ if "ANDROID_DATA" in os.environ:
41
+ return True
42
+ if os.path.exists("/system/build.prop"):
43
+ return True
44
+ try:
45
+ return "android" in platform.platform().lower()
46
+ except Exception:
47
+ return False
48
+
49
+
50
+ def _detect_macos_arch() -> str:
51
+ machine = platform.machine().lower()
52
+ if machine in ("x86_64", "amd64"):
53
+ try:
54
+ result = subprocess.run(
55
+ ["sysctl", "-n", "sysctl.proc_translated"],
56
+ capture_output=True,
57
+ text=True,
58
+ timeout=1,
59
+ )
60
+ if result.returncode == 0 and result.stdout.strip() == "1":
61
+ return "arm64"
62
+ except Exception:
63
+ pass
64
+ return machine
65
+
66
+
67
+ def detect_platform() -> tuple:
68
+ system = platform.system().lower()
69
+ machine = platform.machine().lower()
70
+
71
+ if system == "windows":
72
+ os_name = "windows"
73
+ elif system == "darwin":
74
+ os_name = "macos"
75
+ machine = _detect_macos_arch()
76
+ elif system == "linux":
77
+ os_name = "android" if _detect_android() else "linux"
78
+ else:
79
+ os_name = system
80
+
81
+ if machine in ("x86_64", "amd64"):
82
+ arch = "x64"
83
+ elif machine in ("aarch64", "arm64"):
84
+ arch = "arm64"
85
+ elif machine in ("armv7l", "armv6l", "armv8l"):
86
+ arch = "armv7"
87
+ elif machine in ("i386", "i686", "x86"):
88
+ arch = "x86"
89
+ else:
90
+ arch = machine
91
+
92
+ return os_name, arch
93
+
94
+
95
+ def find_library() -> str:
96
+ os_name, arch = detect_platform()
97
+ key = (os_name, arch)
98
+
99
+ if key not in SUPPORTED_PLATFORMS:
100
+ supported = ", ".join(f"{o}-{a}" for o, a in sorted(SUPPORTED_PLATFORMS))
101
+ raise PlatformNotSupportedError(
102
+ f"Platform '{os_name}-{arch}' is not supported.\n"
103
+ f"Supported: {supported}"
104
+ )
105
+
106
+ lib_name = SUPPORTED_PLATFORMS[key]
107
+ pkg_dir = Path(__file__).parent
108
+
109
+ candidates = [
110
+ pkg_dir / "lib" / f"{os_name}-{arch}" / lib_name,
111
+ pkg_dir / lib_name,
112
+ pkg_dir.parent.parent / "target" / "release" / lib_name,
113
+ pkg_dir.parent.parent / "target" / "debug" / lib_name,
114
+ ]
115
+
116
+ for c in candidates:
117
+ if c.exists():
118
+ return str(c)
119
+
120
+ tried = "\n".join(f" - {c}" for c in candidates)
121
+ raise CoreLoadError(
122
+ f"Binary not found for {os_name}-{arch}.\n"
123
+ f"Expected: {lib_name}\n"
124
+ f"Tried:\n{tried}"
125
+ )
126
+
127
+
128
+ class SwpreqResponse(ctypes.Structure):
129
+ _fields_ = [
130
+ ("status_code", ctypes.c_uint16),
131
+ ("_pad", ctypes.c_uint16),
132
+ ("body", ctypes.POINTER(ctypes.c_uint8)),
133
+ ("body_len", ctypes.c_size_t),
134
+ ("headers_json", ctypes.c_char_p),
135
+ ("url", ctypes.c_char_p),
136
+ ("error", ctypes.c_char_p),
137
+ ("elapsed_ms", ctypes.c_uint64),
138
+ ("http_version", ctypes.c_uint32),
139
+ ]
140
+
141
+
142
+ ON_MESSAGE_CB = ctypes.CFUNCTYPE(
143
+ None,
144
+ ctypes.c_void_p,
145
+ ctypes.c_int,
146
+ ctypes.POINTER(ctypes.c_uint8),
147
+ ctypes.c_size_t,
148
+ )
149
+
150
+ ON_EVENT_CB = ctypes.CFUNCTYPE(
151
+ None,
152
+ ctypes.c_void_p,
153
+ ctypes.c_int,
154
+ ctypes.c_char_p,
155
+ )
156
+
157
+
158
+ class CoreLib:
159
+ _instance = None
160
+ _lock = threading.Lock()
161
+
162
+ def __new__(cls):
163
+ if cls._instance is None:
164
+ with cls._lock:
165
+ if cls._instance is None:
166
+ cls._instance = super().__new__(cls)
167
+ cls._instance._load()
168
+ return cls._instance
169
+
170
+ def __getattr__(self, name):
171
+ # Delegate unknown attributes to the raw CDLL
172
+ # so that client.py can call lib.swpreq_client_new() etc.
173
+ lib = self.__dict__.get("lib")
174
+ if lib is not None:
175
+ return getattr(lib, name)
176
+ raise AttributeError(f"CoreLib has no attribute '{name}'")
177
+
178
+ def _load(self) -> None:
179
+ lib_path = find_library()
180
+
181
+ try:
182
+ self.lib = ctypes.CDLL(lib_path)
183
+ except OSError:
184
+ try:
185
+ tmp_dir = Path(tempfile.gettempdir()) / "swpreq_lib"
186
+ tmp_dir.mkdir(parents=True, exist_ok=True)
187
+ tmp_path = tmp_dir / f"{os.getpid()}_{Path(lib_path).name}"
188
+ shutil.copy2(lib_path, tmp_path)
189
+ self.lib = ctypes.CDLL(str(tmp_path))
190
+ except OSError as e2:
191
+ raise FileLockedError(f"Failed to load library: {e2}")
192
+
193
+ self._setup_signatures()
194
+
195
+ def _setup_signatures(self) -> None:
196
+ lib = self.lib
197
+
198
+ # Version + memory
199
+ lib.swpreq_version.restype = ctypes.c_char_p
200
+ lib.swpreq_version.argtypes = []
201
+
202
+ lib.swpreq_string_free.restype = None
203
+ lib.swpreq_string_free.argtypes = [ctypes.c_char_p]
204
+
205
+ lib.swpreq_response_free.restype = None
206
+ lib.swpreq_response_free.argtypes = [ctypes.POINTER(SwpreqResponse)]
207
+
208
+ # Client
209
+ lib.swpreq_client_new.restype = ctypes.c_void_p
210
+ lib.swpreq_client_new.argtypes = [ctypes.c_char_p]
211
+
212
+ lib.swpreq_client_free.restype = None
213
+ lib.swpreq_client_free.argtypes = [ctypes.c_void_p]
214
+
215
+ # Headers + cookies
216
+ lib.swpreq_set_default_header.restype = ctypes.c_int
217
+ lib.swpreq_set_default_header.argtypes = [
218
+ ctypes.c_void_p,
219
+ ctypes.c_char_p,
220
+ ctypes.c_char_p,
221
+ ]
222
+
223
+ lib.swpreq_remove_default_header.restype = ctypes.c_int
224
+ lib.swpreq_remove_default_header.argtypes = [
225
+ ctypes.c_void_p,
226
+ ctypes.c_char_p,
227
+ ]
228
+
229
+ lib.swpreq_clear_default_headers.restype = ctypes.c_int
230
+ lib.swpreq_clear_default_headers.argtypes = [ctypes.c_void_p]
231
+
232
+ lib.swpreq_set_cookie.restype = ctypes.c_int
233
+ lib.swpreq_set_cookie.argtypes = [
234
+ ctypes.c_void_p,
235
+ ctypes.c_char_p,
236
+ ctypes.c_char_p,
237
+ ctypes.c_char_p,
238
+ ]
239
+
240
+ lib.swpreq_clear_cookies.restype = ctypes.c_int
241
+ lib.swpreq_clear_cookies.argtypes = [ctypes.c_void_p]
242
+
243
+ # Request
244
+ lib.swpreq_request_sync.restype = ctypes.POINTER(SwpreqResponse)
245
+ lib.swpreq_request_sync.argtypes = [
246
+ ctypes.c_void_p,
247
+ ctypes.c_char_p,
248
+ ctypes.c_char_p,
249
+ ctypes.c_char_p,
250
+ ctypes.POINTER(ctypes.c_uint8),
251
+ ctypes.c_size_t,
252
+ ctypes.c_uint64,
253
+ ctypes.c_char_p,
254
+ ctypes.c_int64,
255
+ ctypes.c_int,
256
+ ]
257
+
258
+ # Retry (optional)
259
+ if hasattr(lib, "swpreq_client_set_retry"):
260
+ lib.swpreq_client_set_retry.restype = ctypes.c_int
261
+ lib.swpreq_client_set_retry.argtypes = [
262
+ ctypes.c_void_p,
263
+ ctypes.c_uint32,
264
+ ctypes.c_uint64,
265
+ ctypes.c_int,
266
+ ctypes.c_int,
267
+ ]
268
+
269
+ # WebSocket (optional)
270
+ if hasattr(lib, "swpreq_ws_connect"):
271
+ lib.swpreq_ws_connect.restype = ctypes.c_int
272
+ lib.swpreq_ws_connect.argtypes = [
273
+ ctypes.c_char_p,
274
+ ctypes.c_char_p,
275
+ ctypes.POINTER(ctypes.c_int),
276
+ ctypes.POINTER(ctypes.c_char_p),
277
+ ]
278
+
279
+ lib.swpreq_ws_set_callbacks.restype = ctypes.c_int
280
+ lib.swpreq_ws_set_callbacks.argtypes = [
281
+ ctypes.c_int,
282
+ ctypes.c_void_p, ctypes.c_void_p,
283
+ ctypes.c_void_p, ctypes.c_void_p,
284
+ ctypes.c_void_p, ctypes.c_void_p,
285
+ ctypes.c_void_p, ctypes.c_void_p,
286
+ ]
287
+
288
+ lib.swpreq_ws_send_text.restype = ctypes.c_int
289
+ lib.swpreq_ws_send_text.argtypes = [ctypes.c_int, ctypes.c_char_p]
290
+
291
+ lib.swpreq_ws_send_binary.restype = ctypes.c_int
292
+ lib.swpreq_ws_send_binary.argtypes = [
293
+ ctypes.c_int,
294
+ ctypes.POINTER(ctypes.c_uint8),
295
+ ctypes.c_size_t,
296
+ ]
297
+
298
+ lib.swpreq_ws_send_ping.restype = ctypes.c_int
299
+ lib.swpreq_ws_send_ping.argtypes = [ctypes.c_int]
300
+
301
+ lib.swpreq_ws_close.restype = ctypes.c_int
302
+ lib.swpreq_ws_close.argtypes = [ctypes.c_int]
303
+
304
+ lib.swpreq_ws_free.restype = None
305
+ lib.swpreq_ws_free.argtypes = [ctypes.c_int]
306
+
307
+ def version(self) -> str:
308
+ v = self.lib.swpreq_version()
309
+ if not v:
310
+ return ""
311
+ return v.decode("utf-8")
312
+
313
+ def platform_info(self) -> dict:
314
+ os_name, arch = detect_platform()
315
+ return {
316
+ "os": os_name,
317
+ "arch": arch,
318
+ "supported": (os_name, arch) in SUPPORTED_PLATFORMS,
319
+ "library_name": SUPPORTED_PLATFORMS.get((os_name, arch)),
320
+ }
321
+
322
+
323
+ _core_instance: Optional[CoreLib] = None
324
+
325
+
326
+ def get_lib() -> CoreLib:
327
+ global _core_instance
328
+ if _core_instance is None:
329
+ _core_instance = CoreLib()
330
+ return _core_instance
331
+
332
+
333
+ def get_platform_info() -> dict:
334
+ return get_lib().platform_info()
swpreq/async_api.py ADDED
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from typing import Any, Optional
4
+
5
+ from .client import Client
6
+ from .response import Response
7
+ from .streaming import ResponseStream
8
+ from .sse import SseStream
9
+ from .retry import RetryPolicy
10
+ from .hooks import Hooks
11
+
12
+
13
+ class AsyncClient:
14
+ def __init__(self, max_workers: int = 32, **client_kwargs):
15
+ self._sync = Client(**client_kwargs)
16
+ self._executor = ThreadPoolExecutor(
17
+ max_workers=max_workers, thread_name_prefix="swpreq"
18
+ )
19
+
20
+ async def _run(self, fn, *args, **kwargs):
21
+ loop = asyncio.get_running_loop()
22
+ return await loop.run_in_executor(
23
+ self._executor, lambda: fn(*args, **kwargs)
24
+ )
25
+
26
+ async def get(self, url: str, **kw) -> Response:
27
+ return await self._run(self._sync.get, url, **kw)
28
+
29
+ async def post(self, url: str, **kw) -> Response:
30
+ return await self._run(self._sync.post, url, **kw)
31
+
32
+ async def put(self, url: str, **kw) -> Response:
33
+ return await self._run(self._sync.put, url, **kw)
34
+
35
+ async def delete(self, url: str, **kw) -> Response:
36
+ return await self._run(self._sync.delete, url, **kw)
37
+
38
+ async def patch(self, url: str, **kw) -> Response:
39
+ return await self._run(self._sync.patch, url, **kw)
40
+
41
+ async def head(self, url: str, **kw) -> Response:
42
+ return await self._run(self._sync.head, url, **kw)
43
+
44
+ async def options(self, url: str, **kw) -> Response:
45
+ return await self._run(self._sync.options, url, **kw)
46
+
47
+ async def request(self, method: str, url: str, **kw) -> Response:
48
+ return await self._run(self._sync.request, method, url, **kw)
49
+
50
+ async def gather(self, *coros):
51
+ return await asyncio.gather(*coros)
52
+
53
+ async def map(self, urls, method: str = "GET", **kw):
54
+ tasks = [self.request(method, u, **kw) for u in urls]
55
+ return await asyncio.gather(*tasks)
56
+
57
+ async def stream(self, method: str, url: str, **kw) -> ResponseStream:
58
+ return self._sync.stream(method, url, **kw)
59
+
60
+ async def sse(self, url: str, headers=None, timeout=None) -> SseStream:
61
+ return self._sync.sse(url, headers=headers, timeout=timeout)
62
+
63
+ async def close(self):
64
+ self._executor.shutdown(wait=True)
65
+ self._sync.close()
66
+
67
+ async def __aenter__(self):
68
+ return self
69
+
70
+ async def __aexit__(self, *args):
71
+ await self.close()
72
+
73
+
74
+ AsyncSession = AsyncClient