From e8e02370372a1235198dde78a9c824366bc406dc Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 16:39:56 +0300 Subject: [PATCH 1/6] bump --- wsrpc_aiohttp/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsrpc_aiohttp/version.py b/wsrpc_aiohttp/version.py index bc9e814..679832d 100644 --- a/wsrpc_aiohttp/version.py +++ b/wsrpc_aiohttp/version.py @@ -5,7 +5,7 @@ team_email = "me@mosquito.su" -version_info = (3, 0, 0) +version_info = (3, 0, 1) __author__ = ", ".join("{} <{}>".format(*info) for info in author_info) From 46fb04877702f97eb6a9cd2c1b20f39ae543da58 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 18:07:38 +0300 Subject: [PATCH 2/6] typehints --- .github/workflows/tests.yml | 55 ++++++ MANIFEST.in | 7 + setup.py | 3 +- tox.ini | 14 +- wsrpc_aiohttp/py.typed | 0 wsrpc_aiohttp/websocket/abc.py | 279 ++++++++++++++++++++++++++++++ wsrpc_aiohttp/websocket/client.py | 6 +- wsrpc_aiohttp/websocket/common.py | 137 ++++++++------- wsrpc_aiohttp/websocket/route.py | 14 +- wsrpc_aiohttp/websocket/tools.py | 4 +- 10 files changed, 433 insertions(+), 86 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 MANIFEST.in create mode 100644 wsrpc_aiohttp/py.typed create mode 100644 wsrpc_aiohttp/websocket/abc.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a42ac12 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + lint: + + runs-on: ubuntu-latest + + strategy: + matrix: + linter: + - lint + + steps: + - uses: actions/checkout@v2 + - name: tox ${{ matrix.linter }} + uses: docker://snakepacker/python:all + env: + TOXENV: ${{ matrix.linter }} + with: + args: tox + + build: + needs: lint + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + matrix: + toxenv: + - py35 + - py36 + - py37 + - py38 + - py39 + + steps: + - uses: actions/checkout@v2 + + - name: tox ${{ matrix.toxenv }} + uses: docker://snakepacker/python:all + env: + TOXENV: ${{ matrix.toxenv }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + args: tox diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f05f329 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +recursive-exclude tests * +recursive-exclude __pycache__ * +exclude .* + +include README.rst +include wsrpc_aiohttp/py.typed +recursive-include wsrpc_aiohttp/static diff --git a/setup.py b/setup.py index b926ffc..f04816f 100644 --- a/setup.py +++ b/setup.py @@ -35,11 +35,12 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", ], long_description=open("README.rst").read(), packages=find_packages(exclude=["tests", "doc"]), - package_data={"wsrpc_aiohttp": ["static/*"]}, + package_data={"wsrpc_aiohttp": ["static/*", "py.typed"]}, install_requires=["aiohttp<4", "yarl"], python_requires=">3.5.*, <4", extras_require={ diff --git a/tox.ini b/tox.ini index a1377b0..fddd41b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = lint,py3{5,6,7,8} +envlist = lint,checkdoc,mypy,py3{5,6,7,8,9} [testenv] passenv = COVERALLS_* AMQP_* @@ -34,15 +34,11 @@ commands = [testenv:mypy] usedevelop = true +extras = + develop + deps = mypy commands = - mypy --strict \ - --warn-return-any \ - --warn-unused-ignores \ - --warn-incomplete-stub \ - --disallow-untyped-calls \ - --disallow-untyped-defs \ - --disallow-untyped-decorators \ - -m wsrpc_aiohttp + mypy wsrpc_aiohttp diff --git a/wsrpc_aiohttp/py.typed b/wsrpc_aiohttp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/wsrpc_aiohttp/websocket/abc.py b/wsrpc_aiohttp/websocket/abc.py new file mode 100644 index 0000000..8418dee --- /dev/null +++ b/wsrpc_aiohttp/websocket/abc.py @@ -0,0 +1,279 @@ +import asyncio +from abc import ( + ABC, abstractmethod, abstractclassmethod, abstractproperty, + abstractstaticmethod +) +from enum import IntEnum +from typing import Any, Mapping, Coroutine, Union, Callable, Dict, Tuple + +from aiohttp import WSMessage +from aiohttp.web import Request +from aiohttp.web_ws import WebSocketResponse + + +class AbstractWebSocket(ABC): + @abstractmethod + def __init__(self, request: Request): + raise NotImplementedError(request) + + @abstractclassmethod + def configure(cls, keepalive_timeout: int, + client_timeout: int, + max_concurrent_requests: int) -> None: + """ Configures the handler class + + :param keepalive_timeout: sets timeout of client pong response + :param client_timeout: internal lock timeout + :param max_concurrent_requests: how many concurrent requests might + be performed by each client + """ + raise NotImplementedError(( + keepalive_timeout, client_timeout, max_concurrent_requests + )) + + @abstractmethod + def __await__(self) -> Coroutine: + raise NotImplementedError + + @abstractmethod + async def authorize(self) -> bool: + """ Special method for authorize client. + If this method return True then access allowed, + otherwise ``403 Forbidden`` will be sent. + + This method will be called before socket connection establishment. + + By default everyone has access. You have to inherit this class + and change this behaviour. + + .. note:: + You can validate some headers (self.request.headers) or + check cookies (self.reauest.cookies). + """ + raise NotImplementedError + + async def __handle_request(self) -> WebSocketResponse: + raise NotImplementedError + + @abstractclassmethod + def broadcast( + cls, func, callback=None, return_exceptions=True, + **kwargs: Mapping[str, Any] + ) -> asyncio.Task: + """ Call remote function on all connected clients + + :param func: Remote route name + :param callback: Function which receive responses + :param return_exceptions: Return exceptions of client calls + instead of raise a first one + """ + + raise NotImplementedError + + async def close(self, message: Any = None): + """ Cancel all pending tasks and stop this socket connection """ + raise NotImplementedError + + +class AbstractRoute: + def __init__(self, socket: AbstractWebSocket): + raise NotImplementedError(socket) + + @property + def socket(self) -> AbstractWebSocket: + raise NotImplementedError + + @property + def loop(self) -> asyncio.AbstractEventLoop: + raise NotImplementedError + + +class ProxyMethod: + __slots__ = "__call", "__name" + + def __init__(self, call_method, name): + self.__call = call_method + self.__name = name + + def __call__(self, **kwargs): + return self.__call(self.__name, **kwargs) + + def __getattr__(self, item: str): + return self.__class__(self.__call, ".".join((self.__name, item))) + + +class Proxy: + __slots__ = ("__call",) + + def __init__(self, call_method): + self.__call = call_method + + def __getattr__(self, item: str): + return ProxyMethod(self.__call, item) + + +EventListenerType = Callable[[Dict[str, Any]], Any] + + +class AbstactWSRPC(ABC): + @abstractmethod + def __init__(self, loop: asyncio.AbstractEventLoop = None, + timeout: Union[int, float] = None): + raise NotImplementedError((loop, timeout)) + + @abstractmethod + async def close(self, message: WSMessage = None): + """ Cancel all pending tasks """ + raise NotImplementedError + + @abstractmethod + async def handle_binary(self, message: WSMessage) -> None: + raise NotImplementedError + + @abstractmethod + async def handle_message(self, message: WSMessage): + raise NotImplementedError + + @abstractmethod + async def _on_message(self, msg: WSMessage): + raise NotImplementedError + + @abstractclassmethod + def get_routes(cls) -> Mapping[str, "RouteType"]: + raise NotImplementedError + + @classmethod + def get_clients(cls) -> Dict[str, "AbstactWSRPC"]: + raise NotImplementedError + + @abstractproperty + def routes(self) -> Dict[str, "RouteType"]: + raise NotImplementedError + + @property + def clients(self) -> Dict[str, "AbstactWSRPC"]: + """ Property which contains the socket clients """ + raise NotImplementedError + + @abstractmethod + def prepare_args(self, args) -> Tuple[Tuple[Any, ...], Dict[str, Any]]: + raise NotImplementedError + + @abstractstaticmethod + def is_route(func) -> bool: + raise NotImplementedError + + @abstractmethod + async def handle_method(self, method: str, serial: int, + args: Tuple[Tuple[Any, ...]], + kwargs: Mapping[str, Any]) -> None: + raise NotImplementedError + + @abstractmethod + async def handle_result(self, serial: int, result: Any): + raise NotImplementedError + + @abstractmethod + async def handle_error(self, serial, error): + raise NotImplementedError + + @abstractmethod + async def handle_event(self, event): + raise NotImplementedError + + @abstractmethod + def resolver(self, func_name: str) -> Callable[..., Any]: + raise NotImplementedError + + @abstractmethod + async def call(self, func: str, timeout: Union[int, float] = None, + **kwargs: Mapping[str, Any]): + """ Method for call remote function + + Remote methods allows only kwargs as arguments. + + You might use functions as route or classes + + .. code-block:: python + + async def remote_function(socket: WSRPCBase, *, foo, bar): + # call function from the client-side + await self.socket.proxy.ping() + return foo + bar + + class RemoteClass(WebSocketRoute): + + # this method executes when remote side call route name + asyc def init(self): + # call function from the client-side + await self.socket.proxy.ping() + + async def make_something(self, foo, bar): + return foo + bar + + """ + raise NotImplementedError + + async def emit(self, event: Any) -> None: + pass + + @abstractclassmethod + def add_route(cls, route: str, + handler: Union[AbstractRoute, Callable]) -> None: + """ Expose local function through RPC + + :param route: Name which function will be aliased for this function. + Remote side should call function by this name. + :param handler: Function or Route class (classes based on + :class:`wsrpc_aiohttp.WebSocketRoute`). + For route classes the public methods will + be registered automatically. + + .. note:: + + Route classes might be initialized only once for the each + socket instance. + + In case the method of class will be called first, + :func:`wsrpc_aiohttp.WebSocketRoute.init` will be called + without params before callable method. + + """ + raise NotImplementedError + + @abstractmethod + def add_event_listener(self, func: EventListenerType) -> None: + raise NotImplementedError + + def remove_event_listeners(self, func: EventListenerType) -> None: + raise NotImplementedError + + @classmethod + def remove_route(cls, route: str, fail=True): + """ Removes route by name. If `fail=True` an exception + will be raised in case the route was not found. """ + + raise NotImplementedError + + @abstractproperty + def proxy(self) -> Proxy: + """ Special property which allow run the remote functions + by `dot` notation + + .. code-block:: python + + # calls remote function with name ping + await client.proxy.ping() + + # full equivalent of + await client.call('ping') + """ + raise NotImplementedError + + +RouteType = Union[ + Callable[[AbstactWSRPC, Any], Any], + Callable[[AbstactWSRPC, Any], Coroutine[Any, None, Any]], + AbstractRoute +] +FrameMappingItemType = Mapping[IntEnum, Callable[[WSMessage], Any]] diff --git a/wsrpc_aiohttp/websocket/client.py b/wsrpc_aiohttp/websocket/client.py index 431a928..861a257 100644 --- a/wsrpc_aiohttp/websocket/client.py +++ b/wsrpc_aiohttp/websocket/client.py @@ -1,5 +1,5 @@ import logging -from typing import Union +from typing import Union, Optional import aiohttp from yarl import URL @@ -9,6 +9,7 @@ log = logging.getLogger(__name__) +SocketType = Optional[aiohttp.ClientWebSocketResponse] class WSRPCClient(WSRPCBase): @@ -29,9 +30,10 @@ def __init__( loop=self._loop, **kwargs ) - self.socket = None # type: aiohttp.ClientWebSocketResponse + self.socket = None # type: SocketType self.closed = False + # noinspection PyMethodOverriding async def close(self): """ Close the client connect connection """ diff --git a/wsrpc_aiohttp/websocket/common.py b/wsrpc_aiohttp/websocket/common.py index cf74185..d97a497 100644 --- a/wsrpc_aiohttp/websocket/common.py +++ b/wsrpc_aiohttp/websocket/common.py @@ -3,23 +3,16 @@ import logging import types from collections import defaultdict -from enum import IntEnum from functools import partial -from typing import ( - Any, - Callable, - Dict, - List, - Mapping, - NamedTuple, - Optional, - Union, -) +import typing as t import aiohttp from . import decorators -from .route import Route +from .abc import ( + Proxy, AbstactWSRPC, FrameMappingItemType, RouteType, EventListenerType +) +from .route import Route, ProxyCollectionType from .tools import Singleton, awaitable, loads @@ -45,56 +38,45 @@ def ping(_, **kwargs): log = logging.getLogger(__name__) -RouteType = Union[Callable[["WSRPCBase", Any], Any], Route] -FrameMappingItemType = Mapping[IntEnum, Callable[[aiohttp.WSMessage], Any]] - - -class _ProxyMethod: - __slots__ = "__call", "__name" - - def __init__(self, call_method, name): - self.__call = call_method - self.__name = name - - def __call__(self, **kwargs): - return self.__call(self.__name, **kwargs) - - def __getattr__(self, item: str): - return self.__class__(self.__call, ".".join((self.__name, item))) - - -class _Proxy: - __slots__ = ("__call",) - - def __init__(self, call_method): - self.__call = call_method - - def __getattr__(self, item: str): - return _ProxyMethod(self.__call, item) class Nothing(Singleton): pass -CallItem = NamedTuple( +CallItem = t.NamedTuple( "CallItem", ( - ("serial", int), - ("method", Union[Nothing, Optional[str]]), - ("error", Union[Nothing, Any]), - ("result", Union[Nothing, Any]), - ("params", Optional[Union[List, Dict]]), + ("serial", t.Optional[int]), + ("method", t.Union[Nothing, str, None]), + ("error", t.Union[Nothing, t.Any]), + ("result", t.Union[Nothing, t.Any]), + ("params", t.Optional[t.Union[t.List, t.Dict]]), ), ) -class WSRPCBase: +RouteCollectionType = t.DefaultDict[ + t.Type[AbstactWSRPC], t.Dict[str, RouteType] +] +ClientCollectionType = t.DefaultDict[ + t.Type[AbstactWSRPC], t.Dict[str, AbstactWSRPC] +] +LocksCollectionType = t.DefaultDict[int, asyncio.Lock] +FutureCollectionType = t.DefaultDict[int, asyncio.Future] +EventListenerCollectionType = t.Set[EventListenerType] + + +def _route_maker() -> t.Dict[str, RouteType]: + return {"ping": ping} # type: ignore + + +class WSRPCBase(AbstactWSRPC): """ Common WSRPC abstraction """ - _ROUTES = defaultdict(lambda: {"ping": ping}) - _CLIENTS = defaultdict(dict) - _CLEAN_LOCK_TIMEOUT = 2 + _ROUTES = defaultdict(_route_maker) # type: RouteCollectionType + _CLIENTS = defaultdict(dict) # type: ClientCollectionType + _CLEAN_LOCK_TIMEOUT = 2 # type: t.Union[int, float] __slots__ = ( "_handlers", @@ -108,15 +90,18 @@ class WSRPCBase: "_message_type_mapping", ) - def __init__(self, loop: asyncio.AbstractEventLoop = None, timeout=None): + def __init__(self, loop: asyncio.AbstractEventLoop = None, + timeout: t.Union[int, float] = None): self._loop = loop or asyncio.get_event_loop() - self._handlers = {} - self._pending_tasks = set() + self._handlers = {} # type: t.Dict[str, RouteType] + self._pending_tasks = set() # type: t.Set[asyncio.Task] self._serial = 0 self._timeout = timeout - self._locks = defaultdict(asyncio.Lock) - self._futures = defaultdict(self._loop.create_future) - self._event_listeners = set() + self._locks = defaultdict(asyncio.Lock) # type: LocksCollectionType + self._futures = defaultdict( + self._loop.create_future + ) # type: FutureCollectionType + self._event_listeners = set() # type: EventListenerCollectionType self._message_type_mapping = self._create_type_mapping() def _create_type_mapping(self) -> FrameMappingItemType: @@ -172,7 +157,7 @@ async def handle_binary(self, message: aiohttp.WSMessage): async def _call_method(self, call_item: CallItem): try: - if not isinstance(call_item.method, Nothing): + if not isinstance(call_item.method, Nothing) and call_item.serial: log.debug( "Acquiring lock for %r serial %r", self, call_item.serial ) @@ -207,12 +192,30 @@ async def _call_method(self, call_item: CallItem): @staticmethod def _parse_message(data: dict) -> CallItem: + message_id = data.get("id") # type: t.Optional[int] + + if message_id and not isinstance(message_id, int): + raise ValueError + + message_method = data.get( + "method", Nothing() + ) # type: t.Union[str, Nothing, None] + + message_result = data.get( + "result", Nothing() + ) # type: t.Union[str, Nothing, None] + + message_error = data.get( + "error", Nothing() + ) # type: t.Union[str, Nothing, None] + + message_params = data.get( + "params", Nothing() + ) # type: t.Union[t.List[t.Any], t.Dict[t.Any, t.Any], None] + return CallItem( - serial=data.get("id"), - method=data.get("method", Nothing()), - result=data.get("result", Nothing()), - error=data.get("error", Nothing()), - params=data.get("params"), + serial=message_id, method=message_method, result=message_result, + error=message_error, params=message_params, ) async def handle_message(self, message: aiohttp.WSMessage): @@ -236,20 +239,20 @@ async def unknown_method(msg: aiohttp.WSMessage): self._create_task(awaitable(handler)(msg)) @classmethod - def get_routes(cls) -> Dict[str, RouteType]: + def get_routes(cls) -> t.Dict[str, RouteType]: return cls._ROUTES[cls] @classmethod - def get_clients(cls) -> Dict[str, "WSRPCBase"]: + def get_clients(cls) -> t.Dict[str, AbstactWSRPC]: return cls._CLIENTS[cls] @property - def routes(self) -> Dict[str, RouteType]: + def routes(self) -> t.Dict[str, RouteType]: """ Property which contains the socket routes """ return self.get_routes() @property - def clients(self) -> Dict[str, "WSRPCBase"]: + def clients(self) -> t.Dict[str, AbstactWSRPC]: """ Property which contains the socket clients """ return self.get_clients() @@ -417,7 +420,7 @@ async def emit(self, event): await self._send(**event) @classmethod - def add_route(cls, route: str, handler: Union[Route, Callable]): + def add_route(cls, route: str, handler: RouteType): """ Expose local function through RPC :param route: Name which function will be aliased for this function. @@ -443,7 +446,7 @@ def add_route(cls, route: str, handler: Union[Route, Callable]): cls.get_routes()[route] = handler - def add_event_listener(self, func: Callable[[dict], Any]): + def add_event_listener(self, func: t.Callable[[dict], t.Any]): self._event_listeners.add(func) def remove_event_listeners(self, func): @@ -482,7 +485,7 @@ def proxy(self): # full equivalent of await client.call('ping') """ - return _Proxy(self.call) + return Proxy(self.call) __all__ = ("Route", "WSRPCBase", "ClientException", "WSRPCError") diff --git a/wsrpc_aiohttp/websocket/route.py b/wsrpc_aiohttp/websocket/route.py index 0c8d520..b48954c 100644 --- a/wsrpc_aiohttp/websocket/route.py +++ b/wsrpc_aiohttp/websocket/route.py @@ -2,9 +2,10 @@ import logging from abc import ABCMeta from types import MappingProxyType +from typing import Any, Callable, Mapping from . import decorators - +from .abc import AbstractRoute, AbstractWebSocket log = logging.getLogger("wsrpc") @@ -52,9 +53,12 @@ def __new__(cls, clsname, superclasses, attributedict): return instance -class RouteBase(metaclass=RouteMeta): - __proxy__ = MappingProxyType({}) - __no_proxy__ = MappingProxyType({}) +ProxyCollectionType = Mapping[str, Callable[..., Any]] + + +class RouteBase(AbstractRoute, metaclass=RouteMeta): + __proxy__ = MappingProxyType({}) # type: ProxyCollectionType + __no_proxy__ = MappingProxyType({}) # type: ProxyCollectionType def __init__(self, obj): self.__socket = obj @@ -64,7 +68,7 @@ def __init__(self, obj): self.__loop = asyncio.get_event_loop() @property - def socket(self) -> "WebSocketBase": # noqa + def socket(self) -> AbstractWebSocket: return self.__socket @property diff --git a/wsrpc_aiohttp/websocket/tools.py b/wsrpc_aiohttp/websocket/tools.py index 572732b..c2f4a24 100644 --- a/wsrpc_aiohttp/websocket/tools.py +++ b/wsrpc_aiohttp/websocket/tools.py @@ -5,9 +5,9 @@ try: - from ujson import loads + from ujson import loads # type: ignore except ImportError: - from json import loads + from json import loads # type: ignore class Lazy: From 48f98bd87e3f834115944020b25412392fba631d Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 18:08:32 +0300 Subject: [PATCH 3/6] add linters --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a42ac12..cf417dc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,8 @@ jobs: matrix: linter: - lint + - checkdoc + - mypy steps: - uses: actions/checkout@v2 From 754d01621bac54e47bcb8b7537c8ad29f85ecb99 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 18:11:12 +0300 Subject: [PATCH 4/6] useless import --- wsrpc_aiohttp/websocket/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsrpc_aiohttp/websocket/common.py b/wsrpc_aiohttp/websocket/common.py index d97a497..b81ee84 100644 --- a/wsrpc_aiohttp/websocket/common.py +++ b/wsrpc_aiohttp/websocket/common.py @@ -12,7 +12,7 @@ from .abc import ( Proxy, AbstactWSRPC, FrameMappingItemType, RouteType, EventListenerType ) -from .route import Route, ProxyCollectionType +from .route import Route from .tools import Singleton, awaitable, loads From 341866e3bf0a31da97681923be5fd8f20b088a73 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 18:25:13 +0300 Subject: [PATCH 5/6] is not registered in runner aiohttp bug --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f04816f..cfa8f5a 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ long_description=open("README.rst").read(), packages=find_packages(exclude=["tests", "doc"]), package_data={"wsrpc_aiohttp": ["static/*", "py.typed"]}, - install_requires=["aiohttp<4", "yarl"], + install_requires=["aiohttp<3.7", "yarl"], python_requires=">3.5.*, <4", extras_require={ "ujson": ["ujson"], From 9327e1374408b8e8a284448371d3d6b80f5a7ae7 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Wed, 11 Nov 2020 18:29:39 +0300 Subject: [PATCH 6/6] close client --- setup.py | 2 +- tests/test_auth.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cfa8f5a..f04816f 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ long_description=open("README.rst").read(), packages=find_packages(exclude=["tests", "doc"]), package_data={"wsrpc_aiohttp": ["static/*", "py.typed"]}, - install_requires=["aiohttp<3.7", "yarl"], + install_requires=["aiohttp<4", "yarl"], python_requires=">3.5.*, <4", extras_require={ "ujson": ["ujson"], diff --git a/tests/test_auth.py b/tests/test_auth.py index 660f264..0049e02 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -29,3 +29,4 @@ async def test_auth_fail(client: WSRPCClient, handler): async def test_auth_ok(client: WSRPCClient, handler): handler.AUTHORIZE = True await client.connect() + await client.close()