diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index ff7b3e3..3cb4ebb 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python @@ -38,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: ["ubuntu-latest", "windows-latest"] steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index b640f5b..39230cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#30](https://github.com/WSH032/fastapi-proxy-lib/pull/30) - fix(internal): use `websocket` in favor of `websocket_route`. Thanks [@WSH032](https://github.com/WSH032)! +### Removed + +- [#49](https://github.com/WSH032/fastapi-proxy-lib/pull/49) - Drop support for `Python 3.8`. + +### Fixed + +- [#49](https://github.com/WSH032/fastapi-proxy-lib/pull/49) - fix!: bump `httpx-ws >= 0.7.1` to fix frankie567/httpx-ws#29. Thanks [@WSH032](https://github.com/WSH032)! + ### Internal - [#47](https://github.com/WSH032/fastapi-proxy-lib/pull/47) - test: do not use deprecated and removed APIs of httpx. Thanks [@WSH032](https://github.com/WSH032)! diff --git a/pyproject.toml b/pyproject.toml index d663107..1b52408 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" # https://hatch.pypa.io/latest/config/metadata/ [project] name = "fastapi-proxy-lib" -requires-python = ">=3.8" +requires-python = ">=3.9" readme = "README.md" license = { file = "LICENSE" } authors = [ @@ -29,11 +29,11 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: Session", @@ -50,7 +50,7 @@ dynamic = ["version"] dependencies = [ "httpx", - "httpx-ws >= 0.4.2", + "httpx-ws >= 0.7.1", "starlette", "typing_extensions >=4.5.0", ] @@ -202,7 +202,7 @@ convention = "google" # https://microsoft.github.io/pyright/#/configuration [tool.pyright] typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" reportUnusedImport = "warning" reportUnusedFunction = "warning" reportUnusedExpression = "warning" diff --git a/scripts/pre_commit_scripts/ver_sync.py b/scripts/pre_commit_scripts/ver_sync.py index 511f026..9e4cca6 100644 --- a/scripts/pre_commit_scripts/ver_sync.py +++ b/scripts/pre_commit_scripts/ver_sync.py @@ -13,8 +13,6 @@ from pathlib import Path from typing import ( Any, - Dict, - List, Union, ) @@ -30,13 +28,13 @@ pre_commit_config_yaml_path = Path(".pre-commit-config.yaml") pyproject_toml_path = Path("pyproject.toml") -RepoType = Dict[str, Any] -HookType = Dict[str, Any] +RepoType = dict[str, Any] +HookType = dict[str, Any] if __name__ == "__main__": # NOTE: 这三个键名应该对应 # pyproject_toml["tool"]["hatch"]["envs"]["default"]["dependencies"] 里的值 - vers_in_pre_commit: Dict[str, Union[None, str]] = { + vers_in_pre_commit: dict[str, Union[None, str]] = { "ruff": None, "black": None, "codespell": None, @@ -44,10 +42,10 @@ # 找出pre-commit-config.yaml中的版本 pre_commit_yaml = yaml.load(pre_commit_config_yaml_path) - repos_lst: List[RepoType] = pre_commit_yaml["repos"] + repos_lst: list[RepoType] = pre_commit_yaml["repos"] for repo in repos_lst: - hooks_lst: List[HookType] = repo["hooks"] + hooks_lst: list[HookType] = repo["hooks"] hook = hooks_lst[0] # 特殊标记的只有一个hook hook_alias = hook.get("alias") # 只有特殊标记的才有alias if hook_alias is None: @@ -56,7 +54,7 @@ vers_in_pre_commit[hook_alias] = repo["rev"] # 检查是否正确 - new_vers: Dict[str, Version] = {} + new_vers: dict[str, Version] = {} for name, ver in vers_in_pre_commit.items(): if not isinstance(ver, str): sys.exit(f"Error: version of `{name}` not found in pre-commit-config.yaml") diff --git a/src/fastapi_proxy_lib/core/_tool.py b/src/fastapi_proxy_lib/core/_tool.py index 5b3d014..8f33b2b 100644 --- a/src/fastapi_proxy_lib/core/_tool.py +++ b/src/fastapi_proxy_lib/core/_tool.py @@ -3,12 +3,11 @@ import ipaddress import logging import warnings +from collections.abc import Iterable, Mapping from functools import lru_cache from textwrap import dedent from typing import ( Any, - Iterable, - Mapping, Optional, Protocol, TypedDict, diff --git a/src/fastapi_proxy_lib/core/http.py b/src/fastapi_proxy_lib/core/http.py index fab3316..6f73d36 100644 --- a/src/fastapi_proxy_lib/core/http.py +++ b/src/fastapi_proxy_lib/core/http.py @@ -4,7 +4,6 @@ from textwrap import dedent from typing import ( Any, - List, NamedTuple, NoReturn, Optional, @@ -185,7 +184,7 @@ def _change_server_header( Returns: The **oringinal headers**, but **had been changed**. """ - server_connection_header: List[str] = [ + server_connection_header: list[str] = [ v.strip() for v in headers.get("connection", "").lower().split(",") ] diff --git a/src/fastapi_proxy_lib/core/websocket.py b/src/fastapi_proxy_lib/core/websocket.py index 36dacd7..48c88b7 100644 --- a/src/fastapi_proxy_lib/core/websocket.py +++ b/src/fastapi_proxy_lib/core/websocket.py @@ -6,7 +6,6 @@ from typing import ( TYPE_CHECKING, Any, - List, Literal, NamedTuple, NoReturn, @@ -80,7 +79,7 @@ class _ClientServerProxyTask(NamedTuple): _change_client_header = change_necessary_client_header_for_httpx -def _get_client_request_subprotocols(ws_scope: Scope) -> Union[List[str], None]: +def _get_client_request_subprotocols(ws_scope: Scope) -> Union[list[str], None]: """Get client request subprotocols. Args: @@ -91,7 +90,7 @@ def _get_client_request_subprotocols(ws_scope: Scope) -> Union[List[str], None]: Else return `None`. """ # https://asgi.readthedocs.io/en/latest/specs/www.html#websocket-connection-scope - subprotocols: List[str] = ws_scope.get("subprotocols", []) + subprotocols: list[str] = ws_scope.get("subprotocols", []) if not subprotocols: # 即为 [] return None return subprotocols @@ -484,7 +483,7 @@ async def send_request_to_target( # pyright: ignore [reportIncompatibleMethodOv keepalive_ping_interval_seconds = self.keepalive_ping_interval_seconds keepalive_ping_timeout_seconds = self.keepalive_ping_timeout_seconds - client_request_subprotocols: Union[List[str], None] = ( + client_request_subprotocols: Union[list[str], None] = ( _get_client_request_subprotocols(websocket.scope) ) diff --git a/src/fastapi_proxy_lib/fastapi/router.py b/src/fastapi_proxy_lib/fastapi/router.py index 076f69d..3dd8eb6 100644 --- a/src/fastapi_proxy_lib/fastapi/router.py +++ b/src/fastapi_proxy_lib/fastapi/router.py @@ -5,15 +5,13 @@ import asyncio import warnings -from contextlib import asynccontextmanager +from collections.abc import AsyncIterator +from contextlib import AbstractAsyncContextManager, asynccontextmanager from typing import ( Any, - AsyncContextManager, - AsyncIterator, Callable, Literal, Optional, - Set, TypeVar, Union, ) @@ -170,13 +168,13 @@ class RouterHelper: def __init__(self) -> None: """Initialize RouterHelper.""" - self._registered_proxy: Set[Union[_HttpProxyTypes, _WebSocketProxyTypes]] = ( + self._registered_proxy: set[Union[_HttpProxyTypes, _WebSocketProxyTypes]] = ( set() ) - self._registered_router_id: Set[int] = set() + self._registered_router_id: set[int] = set() @property - def registered_proxy(self) -> Set[Union[_HttpProxyTypes, _WebSocketProxyTypes]]: + def registered_proxy(self) -> set[Union[_HttpProxyTypes, _WebSocketProxyTypes]]: """The proxy that has been registered.""" return self._registered_proxy @@ -253,7 +251,7 @@ def register_router( self._registered_proxy.add(proxy) return router - def get_lifespan(self) -> Callable[..., AsyncContextManager[None]]: + def get_lifespan(self) -> Callable[..., AbstractAsyncContextManager[None]]: """The lifespan event for closing registered proxy. Returns: diff --git a/tests/app/echo_http_app.py b/tests/app/echo_http_app.py index f3d3012..5989dd0 100644 --- a/tests/app/echo_http_app.py +++ b/tests/app/echo_http_app.py @@ -2,7 +2,8 @@ # pyright: reportUnusedFunction=false import io -from typing import Literal, Mapping, Union +from collections.abc import Mapping +from typing import Literal, Union from fastapi import FastAPI, Request, Response from fastapi.responses import StreamingResponse diff --git a/tests/app/tool.py b/tests/app/tool.py index c740478..454bc55 100644 --- a/tests/app/tool.py +++ b/tests/app/tool.py @@ -3,7 +3,7 @@ import asyncio import socket from dataclasses import dataclass -from typing import Any, Callable, List, Optional, Type, TypedDict, TypeVar, Union +from typing import Any, Callable, Optional, TypedDict, TypeVar, Union import httpx import uvicorn @@ -12,7 +12,7 @@ from starlette.websockets import WebSocket from typing_extensions import Self, override -_Decoratable_T = TypeVar("_Decoratable_T", bound=Union[Callable[..., Any], Type[Any]]) +_Decoratable_T = TypeVar("_Decoratable_T", bound=Union[Callable[..., Any], type[Any]]) ServerRecvRequestsTypes = Union[Request, WebSocket] @@ -100,7 +100,7 @@ def __init__( self.contx_exit_timeout = contx_exit_timeout @override - async def startup(self, sockets: Optional[List[socket.socket]] = None) -> None: + async def startup(self, sockets: Optional[list[socket.socket]] = None) -> None: """The same as `uvicorn.Server.startup`.""" super_return = await super().startup(sockets=sockets) self.contx_server_started_event.set() diff --git a/tests/conftest.py b/tests/conftest.py index 0527101..0a96842 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,11 @@ # https://anyio.readthedocs.io/en/stable/testing.html import typing +from collections.abc import AsyncIterator, Coroutine from contextlib import AsyncExitStack from dataclasses import dataclass from typing import ( - AsyncIterator, Callable, - Coroutine, Literal, Protocol, Union, diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index 12bea06..848084e 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -3,8 +3,8 @@ def test_forward_http_proxy() -> None: """测试 ForwardHttpProxy 中的例子.""" + from collections.abc import AsyncIterator from contextlib import asynccontextmanager - from typing import AsyncIterator from fastapi import FastAPI from fastapi_proxy_lib.core.http import ForwardHttpProxy @@ -33,8 +33,8 @@ async def _(request: Request, path: str = ""): def test_reverse_http_proxy() -> None: """测试 ReverseHttpProxy 中的例子.""" + from collections.abc import AsyncIterator from contextlib import asynccontextmanager - from typing import AsyncIterator from fastapi import FastAPI from fastapi_proxy_lib.core.http import ReverseHttpProxy @@ -71,8 +71,8 @@ async def _(request: Request, path: str = ""): def test_reverse_ws_proxy() -> None: """测试 ReverseWebSocketProxy 中的例子.""" + from collections.abc import AsyncIterator from contextlib import asynccontextmanager - from typing import AsyncIterator from fastapi import FastAPI from fastapi_proxy_lib.core.websocket import ReverseWebSocketProxy diff --git a/tests/test_ws.py b/tests/test_ws.py index ae66dbc..6f95f1a 100644 --- a/tests/test_ws.py +++ b/tests/test_ws.py @@ -4,7 +4,7 @@ import asyncio from contextlib import AsyncExitStack from multiprocessing import Process, Queue -from typing import Any, Dict, Literal, Optional +from typing import Any, Literal, Optional import httpx import httpx_ws @@ -34,7 +34,7 @@ WS_BACKENDS_NEED_BE_TESTED = ("websockets",) # https://www.python-httpx.org/advanced/transports/#no-proxy-support -NO_PROXIES: Dict[Any, Any] = {"all://": None} +NO_PROXIES: dict[Any, Any] = {"all://": None} def _subprocess_run_echo_ws_uvicorn_server(queue: "Queue[str]", **kwargs: Any): @@ -68,8 +68,8 @@ async def run(): def _subprocess_run_httpx_ws( queue: "Queue[str]", - kwargs_async_client: Optional[Dict[str, Any]] = None, - kwargs_aconnect_ws: Optional[Dict[str, Any]] = None, + kwargs_async_client: Optional[dict[str, Any]] = None, + kwargs_aconnect_ws: Optional[dict[str, Any]] = None, ): """Run aconnect_ws in subprocess.