diff --git a/README.md b/README.md index 50ecb627..30164660 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架 | [Quart](https://quart.palletsprojects.com/en/latest/)(异步 Flask) | 服务端 | | [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 | | [httpx](https://www.python-httpx.org/) | 客户端 | + | [httpx2](https://github.com/pydantic/httpx2) | 客户端 | | [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 | 更多:[概览](https://nonebot.dev/docs/) diff --git a/assets/drivers.json5 b/assets/drivers.json5 index 61ee459b..902e10cf 100644 --- a/assets/drivers.json5 +++ b/assets/drivers.json5 @@ -39,6 +39,16 @@ "tags": [], "is_official": true }, + { + "module_name": "~httpx2", + "project_link": "nonebot2[httpx2]", + "name": "HTTPX2", + "desc": "HTTPX2 驱动器", + "author_id": 42488585, + "homepage": "/docs/advanced/driver", + "tags": [], + "is_official": true + }, { "module_name": "~websockets", "project_link": "nonebot2[websockets]", diff --git a/nonebot/drivers/httpx2.py b/nonebot/drivers/httpx2.py new file mode 100644 index 00000000..a190d49b --- /dev/null +++ b/nonebot/drivers/httpx2.py @@ -0,0 +1,262 @@ +"""[HTTPX2](https://github.com/pydantic/httpx2) 驱动适配 + +```bash +nb driver install httpx2 +# 或者 +pip install nonebot2[httpx2] +``` + +:::tip[提示] +本驱动仅支持客户端 HTTP 连接 +::: + +FrontMatter: + mdx: + format: md + sidebar_position: 7 + description: nonebot.drivers.httpx2 模块 +""" + +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING +from typing_extensions import override + +from multidict import CIMultiDict + +from nonebot.drivers import ( + URL, + HTTPClientMixin, + HTTPClientSession, + HTTPVersion, + Request, + Response, + combine_driver, +) +from nonebot.drivers.none import Driver as NoneDriver +from nonebot.internal.driver import ( + DEFAULT_TIMEOUT, + Cookies, + CookieTypes, + HeaderTypes, + QueryTypes, + Timeout, + TimeoutTypes, +) +from nonebot.utils import UNSET, UnsetType, exclude_unset + +try: + import httpx2 +except ModuleNotFoundError as e: # pragma: no cover + raise ImportError( + "Please install httpx2 first to use this driver. " + "Install with pip: `pip install nonebot2[httpx2]`" + ) from e + + +class Session(HTTPClientSession): + @override + def __init__( + self, + params: QueryTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + version: str | HTTPVersion = HTTPVersion.H11, + timeout: TimeoutTypes | UnsetType = UNSET, + proxy: str | None = None, + ): + self._client: httpx2.AsyncClient | None = None + + self._params = ( + tuple(URL.build(query=params).query.items()) if params is not None else None + ) + self._headers = ( + tuple(CIMultiDict(headers).items()) if headers is not None else None + ) + self._cookies = Cookies(cookies) + self._version = HTTPVersion(version) + + _timeout = None + if isinstance(timeout, Timeout): + avg_timeout = timeout.total and timeout.total / 4 + timeout_kwargs: dict[str, float | None] = exclude_unset( + { + "timeout": avg_timeout, + "connect": timeout.connect, + "read": timeout.read, + } + ) + if timeout_kwargs: + _timeout = httpx2.Timeout(**timeout_kwargs) + elif timeout is not UNSET: + _timeout = httpx2.Timeout(timeout) + + if _timeout is None: + avg_timeout = DEFAULT_TIMEOUT.total and DEFAULT_TIMEOUT.total / 4 + _timeout = httpx2.Timeout( + **exclude_unset( + { + "timeout": avg_timeout, + "connect": DEFAULT_TIMEOUT.connect, + "read": DEFAULT_TIMEOUT.read, + } + ) + ) + + self._timeout = _timeout + self._proxy = proxy + + @property + def client(self) -> httpx2.AsyncClient: + if self._client is None: + raise RuntimeError("Session is not initialized") + return self._client + + def _get_timeout(self, timeout: TimeoutTypes | UnsetType) -> httpx2.Timeout: + _timeout = None + if isinstance(timeout, Timeout): + avg_timeout = timeout.total and timeout.total / 4 + timeout_kwargs: dict[str, float | None] = exclude_unset( + { + "timeout": avg_timeout, + "connect": timeout.connect, + "read": timeout.read, + } + ) + if timeout_kwargs: + _timeout = httpx2.Timeout(**timeout_kwargs) + elif timeout is not UNSET: + _timeout = httpx2.Timeout(timeout) + + if _timeout is None: + return self._timeout + return _timeout + + @override + async def request(self, setup: Request) -> Response: + response = await self.client.request( + setup.method, + str(setup.url), + content=setup.content, + data=setup.data, + files=setup.files, + json=setup.json, + # ensure the params priority + params=setup.url.raw_query_string, + headers=tuple(setup.headers.items()), + cookies=setup.cookies.jar, + timeout=self._get_timeout(setup.timeout), + ) + return Response( + response.status_code, + headers=response.headers.multi_items(), + content=response.content, + request=setup, + ) + + @override + async def stream_request( + self, + setup: Request, + *, + chunk_size: int = 1024, + ) -> AsyncGenerator[Response, None]: + async with self.client.stream( + setup.method, + str(setup.url), + content=setup.content, + data=setup.data, + files=setup.files, + json=setup.json, + # ensure the params priority + params=setup.url.raw_query_string, + headers=tuple(setup.headers.items()), + cookies=setup.cookies.jar, + timeout=self._get_timeout(setup.timeout), + ) as response: + response_headers = response.headers.multi_items() + async for chunk in response.aiter_bytes(chunk_size=chunk_size): + yield Response( + response.status_code, + headers=response_headers, + content=chunk, + request=setup, + ) + + @override + async def setup(self) -> None: + if self._client is not None: + raise RuntimeError("Session has already been initialized") + self._client = httpx2.AsyncClient( + params=self._params, + headers=self._headers, + cookies=self._cookies.jar, + http2=self._version == HTTPVersion.H2, + proxy=self._proxy, + follow_redirects=True, + ) + await self._client.__aenter__() + + @override + async def close(self) -> None: + try: + if self._client is not None: + await self._client.aclose() + finally: + self._client = None + + +class Mixin(HTTPClientMixin): + """HTTPX2 Mixin""" + + @property + @override + def type(self) -> str: + return "httpx2" + + @override + async def request(self, setup: Request) -> Response: + async with self.get_session( + version=setup.version, proxy=setup.proxy + ) as session: + return await session.request(setup) + + @override + async def stream_request( + self, + setup: Request, + *, + chunk_size: int = 1024, + ) -> AsyncGenerator[Response, None]: + async with self.get_session( + version=setup.version, proxy=setup.proxy + ) as session: + async for response in session.stream_request(setup, chunk_size=chunk_size): + yield response + + @override + def get_session( + self, + params: QueryTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + version: str | HTTPVersion = HTTPVersion.H11, + timeout: TimeoutTypes = None, + proxy: str | None = None, + ) -> Session: + return Session( + params=params, + headers=headers, + cookies=cookies, + version=version, + timeout=timeout, + proxy=proxy, + ) + + +if TYPE_CHECKING: + + class Driver(Mixin, NoneDriver): ... + +else: + Driver = combine_driver(NoneDriver, Mixin) + """HTTPX2 Driver""" diff --git a/pyproject.toml b/pyproject.toml index 57d96b76..32fea384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.optional-dependencies] websockets = ["websockets >=15.0"] httpx = ["httpx[http2] >=0.26.0, <1.0.0"] +httpx2 = ["httpx2[http2] >=2.0.0, <3.0.0"] aiohttp = ["aiohttp[speedups] >=3.11.0, <4.0.0"] quart = ["Quart >=0.18.0, <1.0.0", "uvicorn[standard] >=0.20.0, <1.0.0"] fastapi = ["fastapi >=0.93.0, <1.0.0", "uvicorn[standard] >=0.20.0, <1.0.0"] @@ -36,6 +37,7 @@ all = [ "websockets >=15.0", "fastapi >=0.93.0, <1.0.0", "httpx[http2] >=0.26.0, <1.0.0", + "httpx2[http2] >=2.0.0, <3.0.0", "aiohttp[speedups] >=3.11.0, <4.0.0", "uvicorn[standard] >=0.20.0, <1.0.0", ] diff --git a/tests/test_adapters/test_adapter.py b/tests/test_adapters/test_adapter.py index 33ab379e..86baac10 100644 --- a/tests/test_adapters/test_adapter.py +++ b/tests/test_adapters/test_adapter.py @@ -56,6 +56,13 @@ async def test_adapter_connect(app: App, driver: Driver): reason="not a server", raises=TypeError, strict=True ), ), + pytest.param( + "nonebot.drivers.httpx2:Driver", + id="httpx2", + marks=pytest.mark.xfail( + reason="not a server", raises=TypeError, strict=True + ), + ), pytest.param( "nonebot.drivers.websockets:Driver", id="websockets", @@ -129,6 +136,7 @@ def test_adapter_server(driver: Driver): ), ), pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), pytest.param( "nonebot.drivers.websockets:Driver", id="websockets", @@ -182,6 +190,13 @@ async def test_adapter_http_client(driver: Driver): reason="not a websocket client", raises=TypeError, strict=True ), ), + pytest.param( + "nonebot.drivers.httpx2:Driver", + id="httpx2", + marks=pytest.mark.xfail( + reason="not a websocket client", raises=TypeError, strict=True + ), + ), pytest.param("nonebot.drivers.websockets:Driver", id="websockets"), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], diff --git a/tests/test_driver.py b/tests/test_driver.py index 2bd2897c..57930a4c 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -225,6 +225,7 @@ async def test_cross_context(app: App, driver: Driver): "driver", [ pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, @@ -392,6 +393,7 @@ async def test_http_client(driver: Driver, server_url: URL): "driver", [ pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, @@ -733,6 +735,7 @@ def test_timeout_unset_vs_none(): "driver", [ pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, diff --git a/uv.lock b/uv.lock index e7f1e76b..373b6baf 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,8 @@ requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] conflicts = [[ { package = "nonebot2", group = "pydantic-v1" }, @@ -845,7 +846,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ { name = "annotated-doc", marker = "extra == 'group-8-nonebot2-pydantic-v1'" }, @@ -865,7 +867,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ { name = "annotated-doc", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, @@ -1070,6 +1073,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "truststore", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httptools" version = "0.8.0" @@ -1140,6 +1156,37 @@ http2 = [ { name = "h2" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "httpx2-jsfetch", marker = "(python_full_version >= '3.12' and sys_platform == 'emscripten') or (python_full_version < '3.12' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2') or (sys_platform != 'emscripten' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "hypercorn" version = "0.18.0" @@ -1500,6 +1547,7 @@ all = [ { name = "fastapi", version = "0.125.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-8-nonebot2-pydantic-v1'" }, { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, { name = "httpx", extra = ["http2"] }, + { name = "httpx2", extra = ["http2"] }, { name = "uvicorn", extra = ["standard"] }, { name = "websockets" }, ] @@ -1511,6 +1559,9 @@ fastapi = [ httpx = [ { name = "httpx", extra = ["http2"] }, ] +httpx2 = [ + { name = "httpx2", extra = ["http2"] }, +] quart = [ { name = "quart" }, { name = "uvicorn", extra = ["standard"] }, @@ -1562,6 +1613,8 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.93.0,<1.0.0" }, { name = "httpx", extras = ["http2"], marker = "extra == 'all'", specifier = ">=0.26.0,<1.0.0" }, { name = "httpx", extras = ["http2"], marker = "extra == 'httpx'", specifier = ">=0.26.0,<1.0.0" }, + { name = "httpx2", extras = ["http2"], marker = "extra == 'all'", specifier = ">=2.0.0,<3.0.0" }, + { name = "httpx2", extras = ["http2"], marker = "extra == 'httpx2'", specifier = ">=2.0.0,<3.0.0" }, { name = "loguru", specifier = ">=0.6.0,<1.0.0" }, { name = "pydantic", specifier = ">=1.10.0,!=2.5.0,!=2.5.1,!=2.10.0,!=2.10.1,<3.0.0" }, { name = "pygtrie", specifier = ">=2.4.1,<3.0.0" }, @@ -1576,7 +1629,7 @@ requires-dist = [ { name = "websockets", marker = "extra == 'websockets'", specifier = ">=15.0" }, { name = "yarl", specifier = ">=1.7.2,<2.0.0" }, ] -provides-extras = ["websockets", "httpx", "aiohttp", "quart", "fastapi", "all"] +provides-extras = ["websockets", "httpx", "httpx2", "aiohttp", "quart", "fastapi", "all"] [package.metadata.requires-dev] dev = [ @@ -1947,7 +2000,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ { name = "typing-extensions", marker = "extra == 'group-8-nonebot2-pydantic-v1'" }, @@ -1989,13 +2043,14 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -2007,7 +2062,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -2352,7 +2407,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ { name = "anyio", marker = "extra == 'group-8-nonebot2-pydantic-v1'" }, @@ -2370,7 +2426,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] dependencies = [ { name = "anyio", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, @@ -2386,8 +2443,8 @@ name = "taskgroup" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.13' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "exceptiongroup", marker = "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten') or (python_full_version >= '3.12' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2') or (sys_platform != 'emscripten' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten') or (python_full_version >= '3.12' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2') or (sys_platform != 'emscripten' and extra == 'group-8-nonebot2-pydantic-v1' and extra == 'group-8-nonebot2-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } wheels = [ @@ -2466,6 +2523,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/93/dab25dc87ac48da0fe0f6419e07d0bfd98799bed4e05e7b9e0f85a1a4b4b/trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", size = 510294, upload-time = "2026-02-14T18:40:53.313Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -2480,7 +2546,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "extra == 'group-8-nonebot2-pydantic-v2' or extra != 'group-8-nonebot2-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ diff --git a/website/docs/advanced/driver.md b/website/docs/advanced/driver.md index 15f8f0ff..391498ba 100644 --- a/website/docs/advanced/driver.md +++ b/website/docs/advanced/driver.md @@ -262,6 +262,20 @@ nonebot.run(app="bot:app") DRIVER=~httpx ``` +### HTTPX2 + +**类型:**HTTP 客户端驱动器 + +:::warning[注意] +本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 +::: + +> [HTTPX2](https://github.com/pydantic/httpx2) is a fully featured HTTP client library for Python. It includes an integrated command line client, has support for both HTTP/1.1 and HTTP/2, and provides both sync and async APIs. + +```env +DRIVER=~httpx2 +``` + ### websockets **类型:**WebSocket 客户端驱动器