Skip to content

Commit

Permalink
Merge pull request #51 from mberdyshev/reformat-optional-dependency
Browse files Browse the repository at this point in the history
Refactor websockets dependencies: move the optional dependency to extras and unite imports
  • Loading branch information
orweis authored Jan 13, 2025
2 parents 1b19728 + 05b3301 commit 919d791
Show file tree
Hide file tree
Showing 5 changed files with 34 additions and 31 deletions.
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"]
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:
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")
self._websocket = None

"""
Expand All @@ -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:
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))


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={
"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

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

0 comments on commit 919d791

Please sign in to comment.