tlsreq 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.
tlsreq-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiaoweigege
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.
tlsreq-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: tlsreq
3
+ Version: 0.1.0
4
+ Summary: Unified async/sync HTTP client over curl_cffi, wreq, niquests+utls, and httpx+utls
5
+ Author: xiaoweigege
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xiaoweigege/tlsreq
8
+ Project-URL: Repository, https://github.com/xiaoweigege/tlsreq
9
+ Project-URL: Issues, https://github.com/xiaoweigege/tlsreq/issues
10
+ Project-URL: utls (Chrome 152), https://github.com/xiaoweigege/utls/releases/tag/2026.9.4
11
+ Keywords: tls,http2,fingerprint,ja4,utls,curl_cffi,wreq
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: curl-cffi
22
+ Requires-Dist: curl_cffi>=0.13; extra == "curl-cffi"
23
+ Provides-Extra: wreq
24
+ Requires-Dist: wreq>=0.12; extra == "wreq"
25
+ Provides-Extra: niquests
26
+ Requires-Dist: niquests>=3.0; extra == "niquests"
27
+ Provides-Extra: httpx
28
+ Requires-Dist: httpx>=0.27; extra == "httpx"
29
+ Requires-Dist: httpcore>=1.0; extra == "httpx"
30
+ Requires-Dist: h2>=4.1; extra == "httpx"
31
+ Provides-Extra: all
32
+ Requires-Dist: tlsreq[curl_cffi]; extra == "all"
33
+ Requires-Dist: tlsreq[wreq]; extra == "all"
34
+ Requires-Dist: tlsreq[niquests]; extra == "all"
35
+ Requires-Dist: tlsreq[httpx]; extra == "all"
36
+ Dynamic: license-file
37
+
38
+ # tlsreq
39
+
40
+ 把 `curl_cffi`、`wreq`、`niquests+utls`、`httpx+utls` 收成同一套 Session API。换库只改 `backend` 和 `impersonate`。
41
+
42
+ GitHub: https://github.com/xiaoweigege/tlsreq
43
+
44
+ ## 安装
45
+
46
+ ```bash
47
+ pip install tlsreq
48
+ pip install "tlsreq[httpx]" # 或 niquests / wreq / curl_cffi / all
49
+ ```
50
+
51
+ Chrome 152 不在 PyPI 的 `utls` 里。httpx / niquests 需要再装 GitHub 2026.9.4:
52
+
53
+ https://github.com/xiaoweigege/utls/releases/tag/2026.9.4
54
+
55
+ ```bash
56
+ python -m tlsreq.install_utls
57
+ # 或 tlsreq-install-utls
58
+ ```
59
+
60
+ ## 用法
61
+
62
+ ```python
63
+ from tlsreq import AsyncSession, Session
64
+
65
+ async with AsyncSession(
66
+ backend="niquests", # curl_cffi / wreq / niquests / httpx
67
+ impersonate="chrome152",
68
+ proxy="http://user:pass@host:port",
69
+ timeout=30,
70
+ extra={"pool_maxsize": 1}, # 只传给当前库
71
+ ) as s:
72
+ r = await s.get(url, headers=headers)
73
+ print(r.status_code, r.text, r.cookies, r.http_version)
74
+ r = await s.post(url, data=body, headers=headers, header_order=["user-agent", "accept"])
75
+
76
+ s = Session(backend="wreq", impersonate="chrome149", proxy=proxy)
77
+ r = s.get(url, headers=headers)
78
+ ```
79
+
80
+ `extra=` 会合并进底层构造函数或单次请求,后写覆盖统一层映射。底层 client 在 `s.raw`。
81
+
82
+ niquests / httpx 默认带 Chrome 152 的 HTTP/2 SETTINGS、窗口和 HEADERS Priority,JA4 / peetprint / akamai_fingerprint 对齐真机。
tlsreq-0.1.0/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # tlsreq
2
+
3
+ 把 `curl_cffi`、`wreq`、`niquests+utls`、`httpx+utls` 收成同一套 Session API。换库只改 `backend` 和 `impersonate`。
4
+
5
+ GitHub: https://github.com/xiaoweigege/tlsreq
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pip install tlsreq
11
+ pip install "tlsreq[httpx]" # 或 niquests / wreq / curl_cffi / all
12
+ ```
13
+
14
+ Chrome 152 不在 PyPI 的 `utls` 里。httpx / niquests 需要再装 GitHub 2026.9.4:
15
+
16
+ https://github.com/xiaoweigege/utls/releases/tag/2026.9.4
17
+
18
+ ```bash
19
+ python -m tlsreq.install_utls
20
+ # 或 tlsreq-install-utls
21
+ ```
22
+
23
+ ## 用法
24
+
25
+ ```python
26
+ from tlsreq import AsyncSession, Session
27
+
28
+ async with AsyncSession(
29
+ backend="niquests", # curl_cffi / wreq / niquests / httpx
30
+ impersonate="chrome152",
31
+ proxy="http://user:pass@host:port",
32
+ timeout=30,
33
+ extra={"pool_maxsize": 1}, # 只传给当前库
34
+ ) as s:
35
+ r = await s.get(url, headers=headers)
36
+ print(r.status_code, r.text, r.cookies, r.http_version)
37
+ r = await s.post(url, data=body, headers=headers, header_order=["user-agent", "accept"])
38
+
39
+ s = Session(backend="wreq", impersonate="chrome149", proxy=proxy)
40
+ r = s.get(url, headers=headers)
41
+ ```
42
+
43
+ `extra=` 会合并进底层构造函数或单次请求,后写覆盖统一层映射。底层 client 在 `s.raw`。
44
+
45
+ niquests / httpx 默认带 Chrome 152 的 HTTP/2 SETTINGS、窗口和 HEADERS Priority,JA4 / peetprint / akamai_fingerprint 对齐真机。
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tlsreq"
7
+ version = "0.1.0"
8
+ description = "Unified async/sync HTTP client over curl_cffi, wreq, niquests+utls, and httpx+utls"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{name = "xiaoweigege"}]
13
+ keywords = ["tls", "http2", "fingerprint", "ja4", "utls", "curl_cffi", "wreq"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ # Chrome 152 不在 PyPI utls。装完 extras 后执行: python -m tlsreq.install_utls
25
+ curl_cffi = ["curl_cffi>=0.13"]
26
+ wreq = ["wreq>=0.12"]
27
+ niquests = ["niquests>=3.0"]
28
+ httpx = ["httpx>=0.27", "httpcore>=1.0", "h2>=4.1"]
29
+ all = [
30
+ "tlsreq[curl_cffi]",
31
+ "tlsreq[wreq]",
32
+ "tlsreq[niquests]",
33
+ "tlsreq[httpx]",
34
+ ]
35
+
36
+ [project.scripts]
37
+ tlsreq-install-utls = "tlsreq.install_utls:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/xiaoweigege/tlsreq"
41
+ Repository = "https://github.com/xiaoweigege/tlsreq"
42
+ Issues = "https://github.com/xiaoweigege/tlsreq/issues"
43
+ "utls (Chrome 152)" = "https://github.com/xiaoweigege/utls/releases/tag/2026.9.4"
44
+
45
+ [tool.setuptools]
46
+ package-dir = {"" = "src"}
47
+ license-files = ["LICENSE"]
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+ include = ["tlsreq*"]
52
+
53
+ [tool.setuptools.package-data]
54
+ tlsreq = ["py.typed"]
tlsreq-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from .errors import BackendNotInstalled, TlsReqError, UnknownBackend, UnknownFingerprint
2
+ from .response import Response
3
+ from .session import AsyncSession, Session
4
+ from .utls_release import UTLS_RELEASE, UTLS_RELEASE_PAGE
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "AsyncSession",
9
+ "Session",
10
+ "Response",
11
+ "TlsReqError",
12
+ "UnknownBackend",
13
+ "BackendNotInstalled",
14
+ "UnknownFingerprint",
15
+ "UTLS_RELEASE",
16
+ "UTLS_RELEASE_PAGE",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from ..errors import UnknownBackend
6
+ from .base import AsyncBackend, SyncBackend
7
+
8
+ _ALIASES = {
9
+ "curl": "curl_cffi",
10
+ "cffi": "curl_cffi",
11
+ "nirequest": "niquests",
12
+ "nirequests": "niquests",
13
+ "nio": "niquests",
14
+ }
15
+
16
+
17
+ def _normalize_backend(name: str) -> str:
18
+ key = name.strip().lower().replace("-", "_")
19
+ return _ALIASES.get(key, key)
20
+
21
+
22
+ def create_sync_backend(
23
+ backend: str,
24
+ impersonate: Optional[str] = None,
25
+ **kwargs: Any,
26
+ ) -> SyncBackend:
27
+ name = _normalize_backend(backend)
28
+ if name == "curl_cffi":
29
+ from .curl_cffi import CurlCffiSync
30
+ return CurlCffiSync(impersonate=impersonate, **kwargs)
31
+ if name == "wreq":
32
+ from .wreq import WreqSync
33
+ return WreqSync(impersonate=impersonate, **kwargs)
34
+ if name == "niquests":
35
+ from .niquests import NiquestsSync
36
+ return NiquestsSync(impersonate=impersonate, **kwargs)
37
+ if name == "httpx":
38
+ from .httpx import HttpxSync
39
+ return HttpxSync(impersonate=impersonate, **kwargs)
40
+ raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
41
+
42
+
43
+ def create_async_backend(
44
+ backend: str,
45
+ impersonate: Optional[str] = None,
46
+ **kwargs: Any,
47
+ ) -> AsyncBackend:
48
+ name = _normalize_backend(backend)
49
+ if name == "curl_cffi":
50
+ from .curl_cffi import CurlCffiAsync
51
+ return CurlCffiAsync(impersonate=impersonate, **kwargs)
52
+ if name == "wreq":
53
+ from .wreq import WreqAsync
54
+ return WreqAsync(impersonate=impersonate, **kwargs)
55
+ if name == "niquests":
56
+ from .niquests import NiquestsAsync
57
+ return NiquestsAsync(impersonate=impersonate, **kwargs)
58
+ if name == "httpx":
59
+ from .httpx import HttpxAsync
60
+ return HttpxAsync(impersonate=impersonate, **kwargs)
61
+ raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
@@ -0,0 +1,122 @@
1
+ """httpcore/h2:Chrome 152 SETTINGS、连接窗口、HEADERS Priority。"""
2
+ from __future__ import annotations
3
+
4
+ import collections
5
+ from typing import Any
6
+
7
+ import h2.settings
8
+
9
+ from ..chrome_h2 import (
10
+ CONNECTION_WINDOW_INCREMENT,
11
+ ENABLE_PUSH,
12
+ H2_HOP_BY_HOP,
13
+ HEADER_TABLE_SIZE,
14
+ INITIAL_WINDOW_SIZE,
15
+ MAX_HEADER_LIST_SIZE,
16
+ PRIORITY_DEPENDS_ON,
17
+ PRIORITY_EXCLUSIVE,
18
+ PRIORITY_WEIGHT,
19
+ )
20
+
21
+ _PATCHED = False
22
+
23
+
24
+ def _chrome_local_settings() -> h2.settings.Settings:
25
+ settings = h2.settings.Settings(
26
+ client=True,
27
+ initial_values={
28
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: HEADER_TABLE_SIZE,
29
+ h2.settings.SettingCodes.ENABLE_PUSH: ENABLE_PUSH,
30
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: INITIAL_WINDOW_SIZE,
31
+ h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: MAX_HEADER_LIST_SIZE,
32
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
33
+ },
34
+ )
35
+ del settings[h2.settings.SettingCodes.MAX_FRAME_SIZE]
36
+ del settings[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL]
37
+ del settings[h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS]
38
+ return settings
39
+
40
+
41
+ def _restore_local_only(settings: h2.settings.Settings) -> None:
42
+ """SETTINGS 帧发出后再补回 h2/httpcore 内部会读的项,避免再发一帧。"""
43
+ codes = h2.settings.SettingCodes
44
+ settings._settings.setdefault(codes.MAX_CONCURRENT_STREAMS, collections.deque([100]))
45
+ settings._settings.setdefault(codes.MAX_FRAME_SIZE, collections.deque([16384]))
46
+
47
+
48
+ def patch_httpcore_chrome_h2() -> None:
49
+ global _PATCHED
50
+ if _PATCHED:
51
+ return
52
+
53
+ from httpcore._async.http2 import AsyncHTTP2Connection
54
+ from httpcore._async.http2 import has_body_headers as async_has_body_headers
55
+ from httpcore._sync.http2 import HTTP2Connection
56
+ from httpcore._sync.http2 import has_body_headers as sync_has_body_headers
57
+
58
+ async def async_send_connection_init(self: Any, request: Any) -> None:
59
+ self._h2_state.local_settings = _chrome_local_settings()
60
+ self._h2_state.initiate_connection()
61
+ _restore_local_only(self._h2_state.local_settings)
62
+ self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
63
+ await self._write_outgoing_data(request)
64
+
65
+ def sync_send_connection_init(self: Any, request: Any) -> None:
66
+ self._h2_state.local_settings = _chrome_local_settings()
67
+ self._h2_state.initiate_connection()
68
+ _restore_local_only(self._h2_state.local_settings)
69
+ self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
70
+ self._write_outgoing_data(request)
71
+
72
+ async def async_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
73
+ end_stream = not async_has_body_headers(request)
74
+ authority = [value for key, value in request.headers if key.lower() == b"host"][0]
75
+ headers = [
76
+ (b":method", request.method),
77
+ (b":authority", authority),
78
+ (b":scheme", request.url.scheme),
79
+ (b":path", request.url.target),
80
+ ] + [
81
+ (key.lower(), value)
82
+ for key, value in request.headers
83
+ if key.lower() not in H2_HOP_BY_HOP
84
+ ]
85
+ self._h2_state.send_headers(
86
+ stream_id,
87
+ headers,
88
+ end_stream=end_stream,
89
+ priority_weight=PRIORITY_WEIGHT,
90
+ priority_depends_on=PRIORITY_DEPENDS_ON,
91
+ priority_exclusive=PRIORITY_EXCLUSIVE,
92
+ )
93
+ await self._write_outgoing_data(request)
94
+
95
+ def sync_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
96
+ end_stream = not sync_has_body_headers(request)
97
+ authority = [value for key, value in request.headers if key.lower() == b"host"][0]
98
+ headers = [
99
+ (b":method", request.method),
100
+ (b":authority", authority),
101
+ (b":scheme", request.url.scheme),
102
+ (b":path", request.url.target),
103
+ ] + [
104
+ (key.lower(), value)
105
+ for key, value in request.headers
106
+ if key.lower() not in H2_HOP_BY_HOP
107
+ ]
108
+ self._h2_state.send_headers(
109
+ stream_id,
110
+ headers,
111
+ end_stream=end_stream,
112
+ priority_weight=PRIORITY_WEIGHT,
113
+ priority_depends_on=PRIORITY_DEPENDS_ON,
114
+ priority_exclusive=PRIORITY_EXCLUSIVE,
115
+ )
116
+ self._write_outgoing_data(request)
117
+
118
+ AsyncHTTP2Connection._send_connection_init = async_send_connection_init
119
+ AsyncHTTP2Connection._send_request_headers = async_send_request_headers
120
+ HTTP2Connection._send_connection_init = sync_send_connection_init
121
+ HTTP2Connection._send_request_headers = sync_send_request_headers
122
+ _PATCHED = True