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 1fb33fd..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
@@ -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..902e5db 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()
@@ -31,6 +27,9 @@ 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"],
+ },
)
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