From c2cf6671d01561e9f3990a215d088c98e48289e3 Mon Sep 17 00:00:00 2001 From: mberdyshev Date: Fri, 6 Dec 2024 10:41:25 +0500 Subject: [PATCH 1/3] Refactor websockets dependencies: move the optional dependency to extras and unite imports --- README.md | 2 +- fastapi_websocket_rpc/websocket_rpc_client.py | 38 ++++++++++--------- setup.py | 9 ++--- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1fb33fd..c746ca8 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ logging_config.set_mode(LoggingModes.UVICORN) By default, fastapi-websocket-rpc uses websockets module as websocket client handler. This does not support HTTP(S) Proxy, see https://github.com/python-websockets/websockets/issues/364 . If the ability to use a proxy is important to, another websocket client implementation can be used, e.g. websocket-client (https://websocket-client.readthedocs.io). Here is how to use it. Installation: ``` -pip install websocket-client +pip install fastapi_websocket_rpc[websocket-client] ``` Then use websocket_client_handler_cls parameter: diff --git a/fastapi_websocket_rpc/websocket_rpc_client.py b/fastapi_websocket_rpc/websocket_rpc_client.py index 6a6d0d3..f30103e 100644 --- a/fastapi_websocket_rpc/websocket_rpc_client.py +++ b/fastapi_websocket_rpc/websocket_rpc_client.py @@ -1,12 +1,9 @@ import asyncio import logging from typing import Coroutine, Dict, List, Type -from tenacity import retry, wait -import tenacity -from tenacity.retry import retry_if_exception -import websockets -from websockets.exceptions import InvalidStatusCode, WebSocketException, ConnectionClosedError, ConnectionClosedOK +from tenacity import retry, RetryCallState, wait +from tenacity.retry import retry_if_exception from .rpc_methods import PING_RESPONSE, RpcMethodsBase from .rpc_channel import RpcChannel, OnConnectCallback, OnDisconnectCallback @@ -15,11 +12,16 @@ logger = get_logger("RPC_CLIENT") +try: + import websockets +except ImportError: + websockets = None + try: import websocket except ImportError: - # Websocket-client optional module not installed. - pass + # Websocket-client optional module is not installed. + websocket = None class ProxyEnabledWebSocketClientHandler(SimpleWebSocket): """ @@ -33,6 +35,8 @@ class ProxyEnabledWebSocketClientHandler(SimpleWebSocket): Note: the connect timeout, if not specified, is the default socket connect timeout, which could be around 2min, so a bit longer than WebSocketsClientHandler. """ def __init__(self): + if websocket is None: + raise RuntimeError("Proxy handler requires websocket-client library") self._websocket = None """ @@ -101,6 +105,8 @@ class WebSocketsClientHandler(SimpleWebSocket): This implementation does not support HTTP proxy (see https://github.com/python-websockets/websockets/issues/364). """ def __init__(self): + if websockets is None: + raise RuntimeError("Default handler requires websockets library") self._websocket = None """ @@ -114,17 +120,17 @@ async def connect(self, uri: str, **connect_kwargs): except ConnectionRefusedError: logger.info("RPC connection was refused by server") raise - except ConnectionClosedError: + except websockets.ConnectionClosedError: logger.info("RPC connection lost") raise - except ConnectionClosedOK: + except websockets.ConnectionClosedOK: logger.info("RPC connection closed") raise - except InvalidStatusCode as err: + except websockets.InvalidStatusCode as err: logger.info( f"RPC Websocket failed - with invalid status code {err.status_code}") raise - except WebSocketException as err: + except websockets.WebSocketException as err: logger.info(f"RPC Websocket failed - with {err}") raise except OSError as err: @@ -156,16 +162,14 @@ async def close(self, code: int = 1000): # Case opened, we have something to close. await self._websocket.close(code) -def isNotInvalidStatusCode(value): - return not isinstance(value, InvalidStatusCode) - def isNotForbbiden(value) -> bool: """ Returns: - bool: Returns True as long as the given exception value is not InvalidStatusCode with 401 or 403 + bool: Returns True as long as the given exception value doesn't hold HTTP status codes 401 or 403 """ - return not (isinstance(value, InvalidStatusCode) and (value.status_code == 401 or value.status_code == 403)) + value = getattr(value, "response", value) # `websockets.InvalidStatus` exception contains a status code inside the `response` property + return not (hasattr(value, "status_code") and value.status_code in (401, 403)) class WebSocketRpcClient: @@ -175,7 +179,7 @@ class WebSocketRpcClient: Exposes methods that the server can call """ - def logerror(retry_state: tenacity.RetryCallState): + def logerror(retry_state: RetryCallState): logger.exception(retry_state.outcome.exception()) DEFAULT_RETRY_CONFIG = { diff --git a/setup.py b/setup.py index 31ad46b..a164dd8 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,7 @@ def get_requirements(env=""): if env: env = "-{}".format(env) with open("requirements{}.txt".format(env)) as fp: - requirements = [x.strip() for x in fp.read().split("\n") if not x.startswith("#")] - withWebsocketClient = os.environ.get("WITH_WEBSOCKET_CLIENT", "False") - if bool(withWebsocketClient): - requirements.append("websocket-client>=1.1.0") - return requirements + return [x.strip() for x in fp.read().split("\n") if not x.startswith("#")] with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() @@ -33,4 +29,7 @@ def get_requirements(env=""): ], python_requires=">=3.7", install_requires=get_requirements(), + extras_require={ + "websocket-client": ["websocket-client>=1.1.0"], + }, ) From 1be938cd873af18226557033bf7067342cb8859d Mon Sep 17 00:00:00 2001 From: mberdyshev Date: Fri, 6 Dec 2024 11:43:02 +0500 Subject: [PATCH 2/3] Fix tests Upgrade guide: https://websockets.readthedocs.io/en/stable/howto/upgrade.html#arguments-of-connect --- tests/fast_api_depends_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/fast_api_depends_test.py b/tests/fast_api_depends_test.py index 8b6868d..da2e434 100644 --- a/tests/fast_api_depends_test.py +++ b/tests/fast_api_depends_test.py @@ -1,7 +1,7 @@ import os import sys -from websockets.exceptions import InvalidStatusCode +from websockets.exceptions import InvalidStatus from multiprocessing import Process @@ -54,7 +54,7 @@ async def test_valid_token(server): """ Test basic RPC with a simple echo """ - async with WebSocketRpcClient(uri, RpcUtilityMethods(), default_response_timeout=4, extra_headers=[("X-TOKEN", SECRET_TOKEN)]) as client: + async with WebSocketRpcClient(uri, RpcUtilityMethods(), default_response_timeout=4, additional_headers=[("X-TOKEN", SECRET_TOKEN)]) as client: text = "Hello World!" response = await client.other.echo(text=text) assert response.result == text @@ -66,9 +66,9 @@ async def test_invalid_token(server): Test basic RPC with a simple echo """ try: - async with WebSocketRpcClient(uri, RpcUtilityMethods(), default_response_timeout=4, extra_headers=[("X-TOKEN", "bad-token")]) as client: + async with WebSocketRpcClient(uri, RpcUtilityMethods(), default_response_timeout=4, additional_headers=[("X-TOKEN", "bad-token")]) as client: assert client is not None # if we got here - the server didn't reject us assert False - except InvalidStatusCode as e: - assert e.status_code == 403 + except InvalidStatus as e: + assert e.response.status_code == 403 From 05b3301b79a49d274f4335bc6fe0ae15ebb16721 Mon Sep 17 00:00:00 2001 From: mberdyshev Date: Fri, 6 Dec 2024 12:38:01 +0500 Subject: [PATCH 3/3] Remove EOL Python versions. Support for Python >= 3.9 Support latest versions for tests. --- .github/workflows/tests.yml | 2 +- README.md | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8781869..43a3071 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7","3.8", "3.9", "3.10.8", "3.11"] + python-version: ["3.9", "3.10.8", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index c746ca8..3db1303 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Method return values are sent back as RPC responses, which the other side can wa - As seen at PyCon IL 2021 and EuroPython 2021 -Supports and tested on Python >= 3.7 +Supports and tested on Python >= 3.9 ## Installation 🛠️ ``` pip install fastapi_websocket_rpc diff --git a/setup.py b/setup.py index a164dd8..902e5db 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def get_requirements(env=""): "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", ], - python_requires=">=3.7", + python_requires=">=3.9", install_requires=get_requirements(), extras_require={ "websocket-client": ["websocket-client>=1.1.0"],