Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor websockets dependencies: move the optional dependency to extras and unite imports #51

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7","3.8", "3.9", "3.10.8", "3.11"]
mberdyshev marked this conversation as resolved.
Show resolved Hide resolved
python-version: ["3.9", "3.10.8", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Method return values are sent back as RPC responses, which the other side can wa
- As seen at <a href="https://www.youtube.com/watch?v=KP7tPeKhT3o" target="_blank">PyCon IL 2021</a> and <a href="https://www.youtube.com/watch?v=IuMZVWEUvGs" target="_blank">EuroPython 2021</a>


Supports and tested on Python >= 3.7
Supports and tested on Python >= 3.9
## Installation 🛠️
```
pip install fastapi_websocket_rpc
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 21 additions & 17 deletions fastapi_websocket_rpc/websocket_rpc_client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
"""
Expand All @@ -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:
orweis marked this conversation as resolved.
Show resolved Hide resolved
raise RuntimeError("Proxy handler requires websocket-client library")
self._websocket = None

"""
Expand Down Expand Up @@ -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")
orweis marked this conversation as resolved.
Show resolved Hide resolved
self._websocket = None

"""
Expand All @@ -114,17 +120,17 @@ async def connect(self, uri: str, **connect_kwargs):
except ConnectionRefusedError:
orweis marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down Expand Up @@ -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))
orweis marked this conversation as resolved.
Show resolved Hide resolved


class WebSocketRpcClient:
Expand All @@ -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 = {
Expand Down
11 changes: 5 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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={
mberdyshev marked this conversation as resolved.
Show resolved Hide resolved
"websocket-client": ["websocket-client>=1.1.0"],
},
)
10 changes: 5 additions & 5 deletions tests/fast_api_depends_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import sys

from websockets.exceptions import InvalidStatusCode
from websockets.exceptions import InvalidStatus
orweis marked this conversation as resolved.
Show resolved Hide resolved

from multiprocessing import Process

Expand Down Expand Up @@ -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
Expand All @@ -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
Loading