From 65c72b63bef0eada685f0f8e48dc726c13f87a10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:14:55 +0000 Subject: [PATCH 01/14] Initial plan From e6c46fc2ea4c9275fa520314947c79c68c55c1ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:19:56 +0000 Subject: [PATCH 02/14] Merge upstream datadvance/DjangoChannelsGraphqlWs (1ade107) into silverlogic fork - Brings in graphql-transport-ws WebSocket sub-protocol support - Dependency upgrades and build fixes (July 2024) - Removal of deprecated Pydocstyle - Poetry/Pyenv compatibility improvements - Various test fixes and code improvements Fork-specific changes preserved: - Package name: baseapp-django-channels-graphql-ws - Version: v1.0.0rc9 - Python requirement: ^3.10.0 - Django 5 support: ^4 || ^5" Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- .python-version | 2 + .vscode/settings.json | 103 +++ CHANGELOG.md | 22 +- README.md | 52 +- channels_graphql_ws/client.py | 116 ++- channels_graphql_ws/dict_as_object.py | 124 +-- channels_graphql_ws/graphql_ws_consumer.py | 578 +++++++++----- channels_graphql_ws/testing.py | 85 +- channels_graphql_ws/transport.py | 33 +- example/example.py | 137 +--- example/graphiql.html | 91 ++- example/settings.py | 1 + poetry.lock | 863 +++++++++++---------- poetry.toml | 6 + pyproject.toml | 41 +- tests/conftest.py | 16 +- tests/test_async.py | 91 +-- tests/test_basic.py | 196 ++--- tests/test_concurrent.py | 314 ++++---- tests/test_confirm_subscriptions.py | 83 +- tests/test_context.py | 30 +- tests/test_error.py | 471 ++++++----- tests/test_linters.py | 11 - tests/test_middleware.py | 42 +- tests/test_notification_queue_limit.py | 30 +- tests/test_publish_skip.py | 41 +- tests/test_serialization.py | 42 +- 27 files changed, 1962 insertions(+), 1659 deletions(-) create mode 100755 .vscode/settings.json diff --git a/.python-version b/.python-version index 09dcc78..aec2df3 100644 --- a/.python-version +++ b/.python-version @@ -1 +1,3 @@ 3.10.11 +3.9.16 +3.8.16 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..302b1a1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,103 @@ +// Copyright (C) DATADVANCE, 2010-2023 +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// VS Code configuration. +// https://code.visualstudio.com/docs/getstarted/settings + +{ + "cSpell.enabled": true, + "cSpell.allowCompoundWords": true, + "cSpell.words": [ + "addfinalizer", + "aget", + "aiohttp", + "asdict", + "asgi", + "asgiref", + "asyncio", + "blazingly", + "choco", + "dedent", + "deserialization", + "edouardtheron", + "envlist", + "GQLWS", + "graphiql", + "graphql", + "inmemory", + "ipython", + "iscoroutinefunction", + "isfunction", + "isort", + "itertools", + "kwds", + "mutex", + "mymutation", + "mypy", + "myquery", + "mysubscription", + "nodeid", + "packb", + "posargs", + "prokhorov", + "pydocstyle", + "pyenchant", + "pyenv", + "pylint", + "pylintrc", + "pyproject", + "pytest", + "pyutils", + "rabbitmq", + "rcfile", + "repos", + "sessionid", + "skipif", + "testenv", + "Tomáš", + "unpackb", + "unsubscriptable", + "unsubscription", + "venv", + "virtualenv", + "wakeup", + "xdist", + "xenial", + "xfail", + "zen" + ], + "editor.rulers": [72, 88], + "editor.tabSize": 4, + "git.ignoreLimitWarning": true, + "isort.args": ["--virtual-env", "${workspaceFolder}/.venv"], + "python.linting.banditEnabled": false, + "python.linting.mypyEnabled": true, + "python.linting.pydocstyleEnabled": true, + "python.linting.pylintEnabled": true, + "python.linting.pylintUseMinimalCheckers": false, + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestArgs": ["-n8"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.pythonPath": ".venv/bin/python3", + "files.insertFinalNewline": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 31fd2b0..f8891d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,31 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Changelog +## [1.0.0rc9] - 2024-04-11 + +- Package renamed to `baseapp-django-channels-graphql-ws` for PyPI distribution. +- Python requirement bumped to `^3.10.0`. +- Added Django 5 support: `Django = "^4 || ^5"`. + +## [1.0.0rc8] - 2024-07-18 + +- Merged upstream changes from datadvance/DjangoChannelsGraphqlWs. +- Dependency upgrades and build fixes. +- Removed deprecated Pydocstyle. +- Made Poetry use active Python for compatibility with Pyenv. + +## [1.0.0rc7] - 2023-08-17 + +- Supported the most recent WebSocket sub-protocol + `graphql-transport-ws` used by Apollo. See the specification: + https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md. + + ## [1.0.0rc6] - 2023-05-10 - GraphQL parsing and message serialization now perform concurrently by `sync_to_async(...,thread_sensitive=False)`. - ## [1.0.0rc5] - 2023-05-05 WARNING: Release contains backward incompatible changes! @@ -38,7 +57,6 @@ WARNING: Release contains backward incompatible changes! `SKIP` object which is no longer the case.. - Python 3.8 compatibility brought back. Tests pass OK. - ## [1.0.0rc4] - 2023-05-03 - `GraphqlWsConsumer.warn_resolver_timeout` removed to avoid mess with diff --git a/README.md b/README.md index 169e12c..3b3afc3 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,8 @@ class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer): """Channels WebSocket consumer which provides GraphQL API.""" schema = graphql_schema - # Uncomment to send keepalive message every 42 seconds. - # send_keepalive_every = 42 + # Uncomment to send ping message every 42 seconds. + # send_ping_every = 42 # Uncomment to process requests sequentially (useful for tests). # strict_ordering = True @@ -261,25 +261,18 @@ You can start with the following GraphQL requests: # Check there are no messages. query read { history(chatroom: "kittens") { chatroom text sender }} -# Send a message as Anonymous. +# Send a message from your session. mutation send { sendChatMessage(chatroom: "kittens", text: "Hi all!"){ ok }} -# Check there is a message from `Anonymous`. +# Check there is a message. query read { history(chatroom: "kittens") { text sender } } -# Login as `user`. -mutation send { login(username: "user", password: "pass") { ok } } - -# Send a message as a `user`. -mutation send { sendChatMessage(chatroom: "kittens", text: "It is me!"){ ok }} - -# Check there is a message from both `Anonymous` and from `user`. -query read { history(chatroom: "kittens") { text sender } } - -# Subscribe, do this from a separate browser tab, it waits for events. +# Open another browser or a new incognito window (to have another +# session cookie) subscribe to make it wait for events. subscription s { onNewChatMessage(chatroom: "kittens") { text sender }} -# Send something again to check subscription triggers. +# Send another message from the original window and see how subscription +# triggers in the other one. mutation send { sendChatMessage(chatroom: "kittens", text: "Something ;-)!"){ ok }} ``` @@ -310,12 +303,19 @@ recommended to have a look the documentation of these great projects: - [Graphene](http://graphene-python.org/) The implemented WebSocket-based protocol was taken from the library -[subscription-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) +[graphql-ws](https://github.com/enisdenjo/graphql-ws) which is used by the [Apollo GraphQL](https://github.com/apollographql). Check the -[protocol description](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) +[protocol description](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) for details. +NOTE: Prior to 1.0.0rc7 the library used another protocol: +[subscription-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) +(see [the protocol description](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md)). +In fact [Apollo GraphQL](https://github.com/apollographql) has been +based on this protocol for years, but eventually has switched to a new +one, so we did this as well. + ### Automatic Django model serialization @@ -481,7 +481,7 @@ tracker. To solve this problem, there is the `GraphqlWsConsumer` setting `confirm_subscriptions` which when set to `True` will make the consumer -issue an additional `data` message which confirms the subscription +issue an additional `next` message which confirms the subscription activation. Please note, you have to modify the client's code to make it consume this message, otherwise it will be mistakenly considered as the first subscription notification. @@ -577,18 +577,16 @@ _A reminder of how to setup an environment for the development._ leave the project's directory. Inside the directory `pyenv which python` should show you a python installed in pyenv, outside the dir it should be the system python. -3. Install Poetry to the system Python. +4. Install Poetry (https://python-poetry.org/docs/#installation). ```shell - $ curl -sSL https://install.python-poetry.org | python3 - + $ pipx install poetry ``` - It is important to install Poetry into the system Python, NOT in your - virtual environment. For details see Poetry docs: https://python-poetry.org/docs/#installation -4. Create local virtualenv in `.venv`, install all project dependencies +5. Create local virtualenv in `.venv`, install all project dependencies (from `pyproject.toml`) except the project itself. ```shell $ poetry install --no-root ``` -5. Activate virtualenv +6. Activate virtualenv There are options: - With Poetry: ```shell @@ -601,7 +599,7 @@ _A reminder of how to setup an environment for the development._ - With VS Code: Choose `.venv` with "Python: Select interpreter" and reopen the terminal. -6. Upgrade Pip: +7. Upgrade Pip: ```shell $ pip install --upgrade pip ``` @@ -632,8 +630,8 @@ the `Subscription`. To better dive in it is useful to understand in general terms how regular request are handled. When server receives JSON from the client, the `GraphqlWsConsumer.receive_json` method is called by Channels -routines. Then the request passes to the `_on_gql_start` method which -handles GraphQL message "START". Most magic happens there. +routines. Then the request passes to the `_on_gql_subscribe` method +which handles GraphQL message "SUBSCRIBE". Most magic happens there. ### Running tests diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 9875787..505935e 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -42,25 +42,41 @@ class GraphqlWsClient: This class implements only the protocol itself. The implementation of the message delivery extracted into the separate interface `GraphqlWsTransport`. So it is possible to use this client with - different network frameworks (e.g. Tornado, AIOHTTP). + different network frameworks, e.g. Tornado or AIOHTTP. NOTE: The `receive` method retrieves the first response received by backend, when used with subscriptions it may either return subscription data or some query result. The response type must be checked outside the client manually. + This client support two subprotocols: + - graphql-transport-ws: A recent one which Apollo supports. See + https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md. + - graphql-ws: The previous one, which is left for compatibility. + Args: transport: The `GraphqlWsTransport` instance used to send and receive messages over the WebSocket connection. + subprotocol: WebSocket subprotocol to use by the client. Either + "graphql-transport-ws" (default) or "graphql-ws". """ - def __init__(self, transport: _transport.GraphqlWsTransport): + def __init__( + self, + transport: _transport.GraphqlWsTransport, + subprotocol="graphql-transport-ws", + ): """Constructor.""" assert isinstance( transport, _transport.GraphqlWsTransport ), "Given transport does not implement the 'GraphqlWsTransport' interface!" self._transport = transport self._is_connected = False + assert subprotocol in ( + "graphql-transport-ws", + "graphql-ws", + ), "Client supports only graphql-transport-ws and graphql-ws subprotocols!" + self._subprotocol = subprotocol @property def transport(self) -> _transport.GraphqlWsTransport: @@ -72,44 +88,43 @@ def connected(self) -> bool: """Indicate whether client is connected.""" return self._is_connected - async def connect_and_init(self, connect_only: bool = False, payload="") -> None: + async def connect_and_init(self) -> None: """Establish and initialize WebSocket GraphQL connection. 1. Establish WebSocket connection. - 2. Initialize GraphQL connection. Skipped if connect_only=True. + 2. Initialize GraphQL connection. """ await self._transport.connect() - if not connect_only: - await self._transport.send({"type": "connection_init", "payload": payload}) - resp = await self._transport.receive() - assert resp["type"] == "connection_ack", f"Unexpected response `{resp}`!" + await self._transport.send({"type": "connection_init", "payload": ""}) + resp = await self._transport.receive() + assert resp["type"] == "connection_ack", f"Unexpected response `{resp}`!" self._is_connected = True # Default value for `id`, because `None` is also a valid value. AUTO = object() - async def send(self, *, msg_id=AUTO, msg_type=None, payload=None): - """Send GraphQL message. - - If any argument is `None` it is excluded from the message. + async def receive_next(self, op_id): + """Receive GraphQL message "next" ("data" for "graphql-ws"). Args: - msg_id: The message identifier. Automatically generated by - default. - msg_type: The message type. - payload: The payload dict. + op_id: Subscribe message ID, in response to which the + next/data message should be sent. Returns: - The message identifier. + The `payload` field of the message received. """ - if msg_id is self.AUTO: - msg_id = str(uuid.uuid4().hex) - message = {} - message.update({"id": msg_id} if msg_id is not None else {}) - message.update({"type": msg_type} if msg_type is not None else {}) - message.update({"payload": payload} if payload is not None else {}) - await self._transport.send(message) - return msg_id + msg_type = "next" if self._subprotocol == "graphql-transport-ws" else "data" + return await self.receive(assert_id=op_id, assert_type=msg_type) + + async def receive_complete(self, op_id): + """Receive GraphQL complete message. + + Args: + op_id: Subscribe message ID, in response + to which the complete message should be sent. + + """ + return await self.receive(assert_id=op_id, assert_type="complete") async def receive( self, *, wait_id=None, assert_id=None, assert_type=None, raw_response=False @@ -131,7 +146,7 @@ async def receive( """ while True: response = await self._transport.receive() - if self._is_keep_alive_response(response): + if self._is_ping_pong_message(response): continue if wait_id is None or response["id"] == wait_id: break @@ -145,7 +160,7 @@ async def receive( assert response["id"] == assert_id, "Response id != expected id!" payload = response.get("payload", None) - if payload is not None and "errors" in payload: + if payload is not None and "errors" in payload or response["type"] == "error": raise GraphqlWsResponseError(response) if raw_response: @@ -190,22 +205,47 @@ async def subscribe(self, query, *, variables=None, wait_confirmation=True): await self.receive(wait_id=msg_id) return msg_id - async def start(self, query, *, variables=None): + async def start(self, query, *, variables=None, operation_name=None, msg_id=AUTO): """Start GraphQL request. Responses must be checked explicitly. Args: query: A GraphQL string query. We `dedent` it, so you do not have to. variables: Dict of variables (optional). + operation_name: The name of the operation, usually should + describe what the operation does, useful for identifying + the operation and logging. Optional. + msg_id: The message identifier. Automatically generated by + default. Returns: The message identifier. """ - return await self.send( - msg_type="start", - payload={"query": textwrap.dedent(query), "variables": variables or {}}, + msg_type = ( + "subscribe" if self._subprotocol == "graphql-transport-ws" else "start" ) + payload = { + "query": textwrap.dedent(query), + "variables": variables or {}, + "operationName": operation_name, + } + if msg_id is self.AUTO: + msg_id = str(uuid.uuid4().hex) + message = {"type": msg_type, "payload": payload, "id": msg_id} + await self._transport.send(message) + return msg_id + + async def complete(self, op_id): + """Complete GraphQL request. + + Args: + op_id: Operation id that should be completed. + + """ + msg_type = "complete" if self._subprotocol == "graphql-transport-ws" else "stop" + message = {"type": msg_type, "id": op_id} + await self._transport.send(message) async def finalize(self): """Disconnect and wait the transport to finish gracefully.""" @@ -260,10 +300,16 @@ async def wait_disconnect(self, timeout=None): await self._transport.wait_disconnect(timeout) self._is_connected = False - @staticmethod - def _is_keep_alive_response(response): - """Check if received GraphQL response is keep-alive message.""" - return response.get("type") == "ka" + def _is_ping_pong_message(self, response): + """Check if GQL response is "ping" or "pong" ("keepalive" for + "graphql-ws" subprotocol).""" + msg_type = response.get("type") + return ( + msg_type in ("ping", "pong") + and self._subprotocol == "graphql-transport-ws" + or msg_type == "ka" + and self._subprotocol == "graphql-ws" + ) class GraphqlWsResponseError(Exception): diff --git a/channels_graphql_ws/dict_as_object.py b/channels_graphql_ws/dict_as_object.py index 6104c66..59619ac 100644 --- a/channels_graphql_ws/dict_as_object.py +++ b/channels_graphql_ws/dict_as_object.py @@ -20,12 +20,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Dict wrapper to access keys as attributes.""" -from urllib.parse import urljoin, urlsplit -from django.utils.encoding import escape_uri_path, iri_to_uri -from django.utils.functional import cached_property -from django.conf import settings -from django.core.exceptions import DisallowedHost -from django.http.request import split_domain_port, validate_host class DictAsObject: @@ -47,10 +41,7 @@ def __getattr__(self, name): try: return self._scope[name] except KeyError as ex: - try: - return self._scope["channels_scope"][name] - except KeyError: - raise AttributeError() from ex + raise AttributeError() from ex def __setattr__(self, name, value): """Route attributes to the scope object.""" @@ -82,116 +73,3 @@ def __str__(self): def __repr__(self): """Wrap dict method.""" return self._scope.__repr__() - - # ---------------------------- build_absolute_uri - # copy from django.http.request.HttpRequest so we can have build_absolute_uri on channels scope - - def build_meta(self): - """Build META dict from headers.""" - META = {} - for key, value in self.channels_scope.get('headers', []): - META[key.decode("utf-8").replace("-", "_").upper()] = value.decode("utf-8") - META['QUERY_STRING'] = self.channels_scope.get('query_string', b'').decode("utf-8") - self.META = META - - def build_absolute_uri(self, location=None): - """ - Build an absolute URI from the location and the variables available in - this request. If no ``location`` is specified, build the absolute URI - using request.get_full_path(). If the location is absolute, convert it - to an RFC 3987 compliant URI and return it. If location is relative or - is scheme-relative (i.e., ``//example.com/``), urljoin() it to a base - URL constructed from the request variables. - """ - if location is None: - # Make it an absolute url (but schemeless and domainless) for the - # edge case that the path starts with '//'. - location = "//%s" % self.get_full_path() - else: - # Coerce lazy locations. - location = str(location) - bits = urlsplit(location) - if not (bits.scheme and bits.netloc): - # Handle the simple, most common case. If the location is absolute - # and a scheme or host (netloc) isn't provided, skip an expensive - # urljoin() as long as no path segments are '.' or '..'. - if ( - bits.path.startswith("/") - and not bits.scheme - and not bits.netloc - and "/./" not in bits.path - and "/../" not in bits.path - ): - # If location starts with '//' but has no netloc, reuse the - # schema and netloc from the current request. Strip the double - # slashes and continue as if it wasn't specified. - location = self._current_scheme_host + location.removeprefix("//") - else: - # Join the constructed URL with the provided location, which - # allows the provided location to apply query strings to the - # base path. - location = urljoin(self._current_scheme_host + self.path, location) - return iri_to_uri(location) - - def get_full_path(self, force_append_slash=False): - return self._get_full_path(self.path, force_append_slash) - - def _get_full_path(self, path, force_append_slash): - # RFC 3986 requires query string arguments to be in the ASCII range. - # Rather than crash if this doesn't happen, we encode defensively. - return "%s%s%s" % ( - escape_uri_path(path), - "/" if force_append_slash and not path.endswith("/") else "", - ("?" + iri_to_uri(self.META.get("QUERY_STRING", ""))) - if self.META.get("QUERY_STRING", "") - else "", - ) - - def is_secure(self): - return not settings.DEBUG - - @cached_property - def _current_scheme_host(self): - return "{}://{}".format("https" if self.is_secure() else "http", self.get_host()) - - def get_host(self): - """Return the HTTP host using the environment or request headers.""" - host = self._get_raw_host() - - # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True. - allowed_hosts = settings.ALLOWED_HOSTS - if settings.DEBUG and not allowed_hosts: - allowed_hosts = [".localhost", "127.0.0.1", "[::1]"] - - domain, port = split_domain_port(host) - if domain and validate_host(domain, allowed_hosts): - return host - else: - msg = "Invalid HTTP_HOST header: %r." % host - if domain: - msg += " You may need to add %r to ALLOWED_HOSTS." % domain - else: - msg += ( - " The domain name provided is not valid according to RFC 1034/1035." - ) - raise DisallowedHost(msg) - - def _get_raw_host(self): - """ - Return the HTTP host using the environment or request headers. Skip - allowed hosts protection, so may return an insecure host. - """ - - # We try three options, in order of decreasing preference. - if settings.USE_X_FORWARDED_HOST and ("X_FORWARDED_HOST" in self.META): - host = self.META["X_FORWARDED_HOST"] - elif "HOST" in self.META: - host = self.META["HOST"] - else: - host = "localhost" - # # Reconstruct the host using the algorithm from PEP 333. - # host = self.META["SERVER_NAME"] - # server_port = self.get_port() - # if server_port != ("443" if self.is_secure() else "80"): - # host = "%s:%s" % (host, server_port) - return host diff --git a/channels_graphql_ws/graphql_ws_consumer.py b/channels_graphql_ws/graphql_ws_consumer.py index aa4211b..b9251f0 100644 --- a/channels_graphql_ws/graphql_ws_consumer.py +++ b/channels_graphql_ws/graphql_ws_consumer.py @@ -29,12 +29,19 @@ NOTE: Links based on which this functionality is implemented: - Protocol description: - https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md - https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/message-types.ts + graphql-transport-ws (the recent one): + https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md + graphql-ws (the legacy one): + https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md + https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/message-types.ts - ASGI specification for WebSockets: https://github.com/django/asgiref/blob/master/specs/www.rst#websocket - GitHubGist with the root of inspiration: https://gist.github.com/tricoder42/af3d0337c1b33d82c1b32d12bd0265ec + +NOTE: In the comments, the types of messages of the +`graphql-transport-ws` protocol are used, an analogue of this message in +the `graphql-ws` protocol is written through a slash. """ import asyncio @@ -76,9 +83,6 @@ # Module logger. LOG = logging.getLogger(__name__) -# WebSocket subprotocol used for the GraphQL. -GRAPHQL_WS_SUBPROTOCOL = "graphql-ws" -TRANSPORT_WS_SUBPROTOCOL = "graphql-transport-ws" class GraphqlWsConsumer(ch_websocket.AsyncJsonWebsocketConsumer): """Channels consumer for the WebSocket GraphQL backend. @@ -87,7 +91,10 @@ class GraphqlWsConsumer(ch_websocket.AsyncJsonWebsocketConsumer): connection to a single client. This class implements the WebSocket-based GraphQL protocol used by - `subscriptions-transport-ws` library (used by Apollo): + `graphql-ws` library (used by Apollo): + https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md + We also support the previously used but not recommended by Apollo + `graphql-transport-ws` subprotocol: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md """ @@ -97,8 +104,19 @@ class GraphqlWsConsumer(ch_websocket.AsyncJsonWebsocketConsumer): # processes GraphQL queries. schema: graphene.Schema - # The interval to send keepalive messages to the clients (seconds). - send_keepalive_every: Optional[float] = None + # The interval to send ping/keepalive messages + # to the clients (seconds). + send_ping_every: Optional[float] = None + + # Applies to `graphql-transport-ws` protocol only. + # The amount of time for which the server will wait for + # `ConnectionInit` message in seconds. + # Set the value to 0 to skip waiting. + # If the wait timeout has passed and the client has not sent the + # `ConnectionInit` message, the server will terminate the socket by + # dispatching a close event with code 4408. + # By default set to 0. + connection_init_wait_timeout: int = 0 # Set to `True` to process requests (i.e. GraphQL documents) from # a client in order of arrival, which is the same as sending order, @@ -110,8 +128,8 @@ class GraphqlWsConsumer(ch_websocket.AsyncJsonWebsocketConsumer): # are still processed asynchronously. Useful for tests. strict_ordering: bool = False - # When set to `True` the server will send an empty data message in - # response to the subscription. This is needed to let client know + # When set to `True` the server will send an empty next/data message + # in response to the subscription. This is needed to let client know # when the subscription activates, so he can be sure he doesn't miss # any notifications. Disabled by default, cause this is an extension # to the original protocol and the client must be tuned accordingly. @@ -188,7 +206,7 @@ class _SubInf: sid: int # Subscription groups the subscription belongs to. groups: List[str] - # A function which triggets subscription. + # A function which triggers subscription. enqueue_notification: Callable[[Any], None] # The callback to invoke when client unsubscribes. unsubscribed_callback: Callable[..., Awaitable[None]] @@ -201,28 +219,52 @@ def __init__(self, *args, **kwargs): "the schema which processes GraphQL subscription queries." ) + # WebSocket subprotocol used for the GraphQL. + # Determined on connect, based on the subprotocol used by the + # client. Can have a value of "graphql-transport-ws" or + # "graphql-ws" after successful connection. + self._graphql_ws_subprotocol = None + # Registry of active (subscribed) subscriptions. - self._subscriptions: Dict[ - int, GraphqlWsConsumer._SubInf - ] = {} # {'': '', ...} + self._subscriptions: Dict[int, GraphqlWsConsumer._SubInf] = ( + {} + ) # {'': '', ...} self._sids_by_group = {} # {'': ['', '', ...], ...} # Tasks which send notifications to clients indexed by an # operation/subscription id. self._notifier_tasks: Dict[int, asyncio.Task] = {} - # Task that sends keepalive messages periodically. - self._keepalive_task = None + # Task that sends ping/keepalive messages periodically. + self._ping_task = None + + # `True` if connection was initialized (client sent the + # `ConnectionInit` message), `False` otherwise. + # If this property was not set to `True` after + # `connection_init_wait_timeout` (if no equals 0) seconds after + # WebSocket accepted, WebSocket connection will be closed. + self._connection_initialized = False + + # `True` if connection was acknowledged (server sent the + # `ConnectionAck` message), `False` otherwise. + # If the connection is not acknowledged, it isn't allowed to + # execute operations. + self._connection_acknowledged = False + + # Task that checks if connection was initialized after + # `connection_init_wait_timeout` seconds. + self._connection_init_check_task = None # Background tasks to clean it up when a client disconnects. # We use weak collection so finished task will be autoremoved. self._background_tasks: weakref.WeakSet = weakref.WeakSet() # Crafty weak collection with per-operation locks. It holds a - # mapping from the operaion id (protocol message id) to the - # `asyncio.Lock` used to serialize processing of start & stop - # requests. Since the collection is weak, it automatically - # throws away items when locks are garbage collected. + # mapping from the operation id (protocol message id) to the + # `asyncio.Lock` used to serialize processing of subscribe/start + # & complete/stop requests. Since the collection is weak, it + # automatically throws away items when locks are garbage + # collected. self._operation_locks: weakref.WeakValueDictionary = ( weakref.WeakValueDictionary() ) @@ -242,26 +284,60 @@ def __init__(self, *args, **kwargs): async def connect(self): """Handle new WebSocket connection.""" - found_protocol = None + # Check the subprotocol told by the client. # # NOTE: In Python 3.6 `scope["subprotocols"]` was a string, but # starting with Python 3.7 it is a bytes. This can be a proper # change or just a bug in the Channels to be fixed. So let's # accept both variants until it becomes clear. - for protocol in [GRAPHQL_WS_SUBPROTOCOL, TRANSPORT_WS_SUBPROTOCOL]: - if protocol in self.scope["subprotocols"]: - found_protocol = protocol - break + client_subprotocols = [ + (sp.decode() if isinstance(sp, bytes) else sp) + for sp in self.scope["subprotocols"] + ] + + if "graphql-transport-ws" in client_subprotocols: + self._graphql_ws_subprotocol = "graphql-transport-ws" + elif "graphql-ws" in client_subprotocols: + self._graphql_ws_subprotocol = "graphql-ws" + else: + raise AssertionError( + "WebSocket Client does not request for the graphql-ws " + "or graphql-transport-ws subprotocols!" + ) # Accept connection with the GraphQL-specific subprotocol. - await self.accept(subprotocol=found_protocol) + await self.accept(subprotocol=self._graphql_ws_subprotocol) + + async def check_connection_initialization(): + """Check if connection was initialized. + + If connection was not initialized after + `connection_init_wait_timeout` seconds - close WebSocket + connection. + """ + await asyncio.sleep(self.connection_init_wait_timeout) + if not self._connection_initialized: + LOG.warning( + "The WebSocket connection will be closed due to: " + "Connection initialization timeout!" + ) + # According to `graphql-transport-ws` protocol + # description we must close connection immediately. + await self.close(4408) + + if ( + self.connection_init_wait_timeout != 0 + ) and self._graphql_ws_subprotocol == "graphql-transport-ws": + self._connection_init_check_task = asyncio.create_task( + check_connection_initialization() + ) async def disconnect(self, code): """Handle WebSocket disconnect. Remove itself from the Channels groups, clear triggers and stop - sending keepalive messages. + sending ping/keepalive messages. """ # Print debug or warning message depending on the value of the @@ -276,10 +352,10 @@ async def disconnect(self, code): LOG.warning("WebSocket connection closed with code: %s!", code) # The list of awaitables to simultaneously wait at the end. - waitlist: List[asyncio.Task] = [] + wait_list: List[asyncio.Task] = [] # Unsubscribe from the Channels groups. - waitlist += [ + wait_list += [ asyncio.create_task( self._channel_layer.group_discard(group, self.channel_name) ) @@ -289,24 +365,29 @@ async def disconnect(self, code): # Cancel all currently running background tasks. for bg_task in self._background_tasks: bg_task.cancel() - waitlist += list(self._background_tasks) + wait_list += list(self._background_tasks) + + # Stop sending ping/keepalive messages (if enabled). + if self._ping_task is not None: + self._ping_task.cancel() + wait_list += [self._ping_task] - # Stop sending keepalive messages (if enabled). - if self._keepalive_task is not None: - self._keepalive_task.cancel() - waitlist += [self._keepalive_task] + # Stop waiting for connection initialization (if enabled). + if self._connection_init_check_task is not None: + self._connection_init_check_task.cancel() + wait_list += [self._connection_init_check_task] # Stop tasks which listen to GraphQL lib and send notifications. for notifier_task in self._notifier_tasks.values(): notifier_task.cancel() - waitlist += [notifier_task] + wait_list += [notifier_task] # Wait for tasks to stop. - if waitlist: - await asyncio.wait(waitlist) + if wait_list: + await asyncio.wait(wait_list) self._background_tasks.clear() - self._keepalive_task = None + self._ping_task = None self._notifier_tasks.clear() self._operation_locks.clear() self._sids_by_group.clear() @@ -315,31 +396,54 @@ async def disconnect(self, code): async def receive_json(self, content): # pylint: disable=arguments-differ """Process WebSocket message received from the client. - NOTE: We force 'STOP' message processing to wait until 'START' - with the same operation id finishes (if it is running). This - protects us from race conditions which may happen when a client - stops operation immediately after starting it. An illustrative - example is a subscribe-unsubscribe pair. If we spawn processing - of both messages concurrently we can deliver subscription - confirmation after unsubscription confirmation. + NOTE: We force 'COMPLETE'/'STOP' message processing to wait + until 'SUBSCRIBE'/'START' with the same operation id finishes + (if it is running). This protects us from race conditions which + may happen when a client stops operation immediately after + starting it. An illustrative example is a subscribe-unsubscribe + pair. If we spawn processing of both messages concurrently we + can deliver subscription confirmation after unsubscription + confirmation. """ # Extract message type based on which we select how to proceed. msg_type = content["type"].upper() if msg_type == "CONNECTION_INIT": - task = self._on_gql_connection_init(payload=content["payload"]) + task = self._on_gql_connection_init(payload=content.get("payload")) elif msg_type == "CONNECTION_TERMINATE": task = self._on_gql_connection_terminate() - elif msg_type == "SUBSCRIBE": + elif ( + msg_type == "SUBSCRIBE" + and self._graphql_ws_subprotocol == "graphql-transport-ws" + or msg_type == "START" + and self._graphql_ws_subprotocol == "graphql-ws" + ): + # According to `graphql-transport-ws` protocol description, + # if `subscribe` message received before the server has + # acknowledged the connection through the `connection_ack` + # message, we must close connection immediately + # with code 4401. + if ( + not self._connection_acknowledged + and self._graphql_ws_subprotocol == "graphql-transport-ws" + ): + LOG.warning( + "The WebSocket connection will be closed due to: " + "`subscribe` message received before connection was acknowledged!" + ) + await self.close(code=4401) + return + op_id = content["id"] # Create and lock a mutex for this particular operation id, - # so STOP processing for the same operation id will wait - # until START processing finishes. Locks are stored in a - # weak collection so we do not have to manually clean it up. + # so COMPLETE/STOP processing for the same operation id will + # wait until SUBSCRIBE/START processing finishes. Locks are + # stored in a weak collection so we do not have to manually + # clean it up. if op_id in self._operation_locks: raise graphql.error.GraphQLError( f"Operation with msg_id={op_id} is already running!" @@ -348,33 +452,66 @@ async def receive_json(self, content): # pylint: disable=arguments-differ self._operation_locks[op_id] = op_lock await op_lock.acquire() - async def on_start(): + async def on_subscribe(): try: # User hook which raises to cancel processing. await self.on_operation(op_id, payload=content["payload"]) - # START message processing. - await self._on_gql_start(op_id, payload=content["payload"]) + # SUBSCRIBE/START message processing. + await self._on_gql_subscribe(op_id, payload=content["payload"]) except Exception as ex: # pylint: disable=broad-except - await self._send_gql_error(op_id, ex) + await self._send_gql_error(op_id, [ex]) finally: op_lock.release() - task = on_start() + task = on_subscribe() - elif msg_type == "COMPLETE": + elif ( + msg_type == "COMPLETE" + and self._graphql_ws_subprotocol == "graphql-transport-ws" + or msg_type == "STOP" + and self._graphql_ws_subprotocol == "graphql-ws" + ): op_id = content["id"] - async def on_stop(): - # Wait until START message processing finishes, if any. + async def on_complete(): + # Wait until SUBSCRIBE/START message processing + # finishes, if any. async with self._operation_locks.setdefault(op_id, asyncio.Lock()): - await self._on_gql_stop(op_id) - - task = on_stop() + await self._on_gql_complete(op_id) + + task = on_complete() + + elif ( + msg_type == "PING" + and self._graphql_ws_subprotocol == "graphql-transport-ws" + ): + task = self._on_gql_ping() + + elif ( + msg_type == "PONG" + and self._graphql_ws_subprotocol == "graphql-transport-ws" + ): + # Do nothing if PONG message received. According to the + # protocol description, the PONG message is a response to + # the PING message and does not require any action. + return else: + if self._graphql_ws_subprotocol == "graphql-transport-ws": + LOG.warning( + "The WebSocket connection will be closed due to: " + "Wrong message type '%s'!", + msg_type, + ) + # According to `graphql-transport-ws` protocol + # description if message of wrong type received we must + # close connection immediately with code 4400. + await self.close(code=4400) + return + task = self._send_gql_error( content["id"] if "id" in content else None, - Exception(f"Wrong message type '{msg_type}'!"), + [Exception(f"Wrong message type '{msg_type}'!")], ) # If strict ordering is required then simply wait until the @@ -425,8 +562,8 @@ async def _process_broadcast(self, message): # belonging to the subscription group. Drop the oldest payloads # if the `notification_queue` is full. for sid in self._sids_by_group[group]: - subinf = self._subscriptions[sid] - subinf.enqueue_notification(payload) + sub_inf = self._subscriptions[sid] + sub_inf.enqueue_notification(payload) async def unsubscribe(self, message): """The unsubscribe message handler. @@ -446,12 +583,19 @@ async def unsubscribe(self, message): # Send messages which look like user unsubscribes from all # subscriptions in the subscription group. This saves us from - # thinking about raise condition between subscription and + # thinking about race condition between subscription and # unsubscription. if self._sids_by_group[group]: + message_type = ( + "complete" + if self._graphql_ws_subprotocol == "graphql-transport-ws" + else "stop" + ) await asyncio.wait( [ - asyncio.create_task(self.receive_json({"type": "stop", "id": sid})) + asyncio.create_task( + self.receive_json({"type": message_type, "id": sid}) + ) for sid in self._sids_by_group[group] ] ) @@ -461,45 +605,80 @@ async def unsubscribe(self, message): async def _on_gql_connection_init(self, payload): """Process the CONNECTION_INIT message. - Start sending keepalive messages if `send_keepalive_every` set. - Respond with either CONNECTION_ACK or CONNECTION_ERROR message. + Start sending ping/keepalive messages if `send_ping_every` set. + Respond with either CONNECTION_ACK or CONNECTION_ERROR message + or close connection. NOTE: Depending on the value of the `strict_ordering` setting this method is either awaited directly or offloaded to an async task. See the `receive_json` handler. """ + # According to `graphql-transport-ws` protocol description if + # server receives more than one `connection_init` message at any + # given time we must close connection immediately + # with code 4429. + if ( + self._connection_initialized + and self._graphql_ws_subprotocol == "graphql-transport-ws" + ): + LOG.warning( + "The WebSocket connection will be closed due to: " + "Too many initialization requests!" + ) + await self.close(code=4429) + return try: # Notify subclass a new client is connected. await self.on_connect(payload) except Exception as ex: # pylint: disable=broad-except - await self._send_gql_connection_error(ex) - # Close the connection. NOTE: We use the 4000 code because - # there are two reasons: A) We can not use codes greater - # than 1000 and less than 3000 because Daphne and Autobahn - # do not allow this (see `sendClose` from - # `autobahn/websocket/protocol.py` and - # `daphne/ws_protocol.py`). B) - # https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent - # Mozilla offers codes 4000–4999 available for all apps. - await self.close(code=4000) + LOG.warning("GraphQL connection error: %s!", ex, exc_info=ex) + if self._graphql_ws_subprotocol == "graphql-transport-ws": + LOG.warning( + "The WebSocket connection will be closed due to: Forbidden!" + ) + # According to `graphql-transport-ws` protocol + # description if error while connection initialization + # occurred we must close connection immediately with + # code 4403. + await self.close(code=4403) + else: + await self._send_gql_connection_error(ex) + # Close the connection. NOTE: We use the 4000 code + # because there are two reasons: A) We can not use codes + # greater than 1000 and less than 3000 because Daphne + # and Autobahn do not allow this (see `sendClose` from + # `autobahn/websocket/protocol.py` and + # `daphne/ws_protocol.py`). B) + # https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + # Mozilla offers codes 4000–4999 available for all apps. + await self.close(code=4000) else: + # Mark connection as initialized. + self._connection_initialized = True + # Stop waiting for connection initialization (if enabled). + if self._connection_init_check_task is not None: + self._connection_init_check_task.cancel() + await asyncio.wait([self._connection_init_check_task]) # Send CONNECTION_ACK message. await self._send_gql_connection_ack() - # If keepalive enabled then send one message immediately and - # schedule periodic messages. - if self.send_keepalive_every is not None: - send_keepalive_every = self.send_keepalive_every - - async def keepalive_sender(): - """Send keepalive messages periodically.""" + # Mark connection as acknowledged. + self._connection_acknowledged = True + # If ping enabled then schedule periodic messages. + if self.send_ping_every is not None: + send_ping_every = self.send_ping_every + + async def ping_sender(): + """Send ping/keepalive messages periodically.""" while True: - await asyncio.sleep(send_keepalive_every) - await self._send_gql_connection_keep_alive() + await asyncio.sleep(send_ping_every) + await self._send_gql_ping() + + self._ping_task = asyncio.create_task(ping_sender()) - self._keepalive_task = asyncio.create_task(keepalive_sender()) - # Immediately send keepalive message cause it is - # required by the protocol description. - await self._send_gql_connection_keep_alive() + if self._graphql_ws_subprotocol == "graphql-ws": + # Immediately send keepalive message cause it is + # required by the `graphql-ws` protocol description. + await self._send_gql_ping() async def _on_gql_connection_terminate(self): """Process the CONNECTION_TERMINATE message. @@ -508,12 +687,18 @@ async def _on_gql_connection_terminate(self): this method is either awaited directly or offloaded to an async task. See the `receive_json` handler. """ - # Close the connection. await self.close(code=1000) - async def _on_gql_start(self, op_id, payload): - """Process the START message. + async def _on_gql_ping(self): + """Process the PING message. + + Send PONG message as response. + """ + await self._send_gql_pong() + + async def _on_gql_subscribe(self, op_id, payload): + """Process the SUBSCRIBE message. Handle the message with query, mutation or subscription request. @@ -523,6 +708,19 @@ async def _on_gql_start(self, op_id, payload): """ try: if op_id in self._subscriptions: + # According to `graphql-transport-ws` protocol + # description if subscription with received id already + # exists we must close connection immediately with + # code 4409. + if self._graphql_ws_subprotocol == "graphql-transport-ws": + LOG.warning( + "The WebSocket connection will be closed due to: " + "Subscriber for %s already exists!", + op_id, + ) + await self.close(code=4409) + return + message = f"Subscription with msg_id={op_id} already exists!" raise graphql.error.GraphQLError(message) @@ -532,17 +730,22 @@ async def _on_gql_start(self, op_id, payload): variables = payload.get("variables", {}) # Prepare a context object. - context = self.get_context() + context = DictAsObject({}) + context.channels_scope = self.scope + context.channel_name = self.channel_name context.graphql_operation_name = op_name context.graphql_operation_id = op_id # Process the request with Graphene and GraphQL-core. - doc_ast, op_ast, errors = await self._on_gql_start__parse_query( + doc_ast, op_ast, errors = await self._on_gql_subscribe__parse_query( op_name, query ) if errors: - await self._send_gql_data(op_id, None, errors) - await self._send_gql_complete(op_id) + if self._graphql_ws_subprotocol == "graphql-transport-ws": + await self._send_gql_error(op_id, errors) + else: + await self._send_gql_next(op_id, None, errors) + await self._send_gql_complete(op_id) return # Assert values are not None to suppress MyPy complains. assert doc_ast is not None @@ -558,30 +761,32 @@ async def _on_gql_start(self, op_id, payload): # This returns asynchronous generator or ExecutionResult # instance in case of error. - subscr_result = await self._on_gql_start__subscribe( + subscription_result = await self._on_gql_subscribe__create_subscription( doc_ast, operation_name=op_name, root_value=None, variable_values=variables, context_value=context, subscribe_field_resolver=functools.partial( - self._on_gql_start__initialize_subscription_stream, + self._on_gql_subscribe__initialize_subscription_stream, op_id, op_name, ), middleware=self._middleware, ) - # When subscr_result is an AsyncGenerator, consume + # When subscription_result is an AsyncGenerator, consume # stream of notifications and send them to clients. - if not isinstance(subscr_result, graphql.ExecutionResult): - stream = cast(AsyncIterator[graphql.ExecutionResult], subscr_result) + if not isinstance(subscription_result, graphql.ExecutionResult): + stream = cast( + AsyncIterator[graphql.ExecutionResult], subscription_result + ) # Send subscription activation message (if enabled) # NOTE: We do it before reading the the stream # stream to guarantee that no notifications are sent # before the subscription confirmation message. if self.confirm_subscriptions: - await self._send_gql_data( + await self._send_gql_next( op_id, data=self.subscription_confirmation_message["data"], errors=self.subscription_confirmation_message["errors"], @@ -598,7 +803,7 @@ async def consume_stream(): # when we have something to send. if item.data or item.errors: try: - await self._send_gql_data( + await self._send_gql_next( op_id, item.data, item.errors ) except asyncio.CancelledError: @@ -611,7 +816,10 @@ async def consume_stream(): op_id, exc_info=ex, ) - await self._send_gql_data(op_id, None, [ex]) + if self._graphql_ws_subprotocol == "graphql-transport-ws": + await self._send_gql_error(op_id, [ex]) + else: + await self._send_gql_next(op_id, None, [ex]) # We need to end this task when client drops # connection or unsubscribes, so lets store it. @@ -631,7 +839,7 @@ async def consume_stream(): # Else (when gql_subscribe returns ExecutionResult # containing error) fallback to standard handling below. - operation_result = cast(graphql.ExecutionResult, subscr_result) + operation_result = cast(graphql.ExecutionResult, subscription_result) # If the operation is query or mutation. else: @@ -685,26 +893,27 @@ async def consume_stream(): variables, ) # Respond to a query or mutation immediately. - await self._send_gql_data( + await self._send_gql_next( op_id, operation_result.data, operation_result.errors ) await self._send_gql_complete(op_id) except Exception as ex: # pylint: disable=broad-except - if isinstance(ex, graphql.error.GraphQLError): + if ( + isinstance(ex, graphql.error.GraphQLError) + and self._graphql_ws_subprotocol == "graphql-ws" + ): # Respond with details of GraphQL execution error. LOG.warning( "GraphQL error! Operation %s(%s).", op_name, op_id, exc_info=True ) - await self._send_gql_data(op_id, None, [ex]) + await self._send_gql_next(op_id, None, [ex]) await self._send_gql_complete(op_id) else: - # Respond with general error responce. - await self._send_gql_error(op_id, ex) + # Respond with general error response. + await self._send_gql_error(op_id, [ex]) - async def _on_gql_start__parse_query( - self, op_name: str, query: str - ) -> Tuple[ + async def _on_gql_subscribe__parse_query(self, op_name: str, query: str) -> Tuple[ Optional[graphql.DocumentNode], Optional[graphql.OperationDefinitionNode], Optional[Iterable[graphql.GraphQLError]], @@ -719,8 +928,9 @@ async def _on_gql_start__parse_query( document parsing and validation take a while and depends approx. linearly on the size of the selection set. - This is a part of START message processing routine so the name - prefixed with `_on_gql_start__` to make this explicit. + This is a part of SUBSCRIBE/START message processing routine + so the name prefixed with `_on_gql_subscribe__` to make this + explicit. Returns: Tuple with three optional fields: @@ -730,7 +940,7 @@ async def _on_gql_start__parse_query( """ res = await channels.db.database_sync_to_async( - self._on_gql_start__parse_query_sync_cached, thread_sensitive=False + self._on_gql_subscribe__parse_query_sync_cached, thread_sensitive=False )(op_name, query) doc_ast: Optional[graphql.DocumentNode] = res[0] @@ -740,7 +950,7 @@ async def _on_gql_start__parse_query( return (doc_ast, op_ast, errors) @functools.lru_cache(maxsize=128) - def _on_gql_start__parse_query_sync_cached( + def _on_gql_subscribe__parse_query_sync_cached( self, op_name: str, query: str ) -> Tuple[ Optional[graphql.DocumentNode], @@ -749,8 +959,9 @@ def _on_gql_start__parse_query_sync_cached( ]: """Parse and validate GraphQL query. Cached sync implementation. - This is a part of START message processing routine so the name - prefixed with `_on_gql_start__` to make this explicit. + This is a part of SUBSCRIBE/START message processing routine so + the name prefixed with `_on_gql_subscribe__` to make this + explicit. """ # Parsing. @@ -770,7 +981,7 @@ def _on_gql_start__parse_query_sync_cached( return doc_ast, op_ast, None - async def _on_gql_start__subscribe( + async def _on_gql_subscribe__create_subscription( self, document: graphql.DocumentNode, root_value: Any = None, @@ -788,8 +999,9 @@ async def _on_gql_start__subscribe( the GraphQL-core library v3.2.3 improved to support middlewares and user defined execution_context_class. - This is a part of START message processing routine so the name - prefixed with `_on_gql_start__` to make this explicit. + This is a part of SUBSCRIBE/START message processing routine so + the name prefixed with `_on_gql_subscribe__` to make this + explicit. """ result_or_stream = await graphql.create_source_event_stream( @@ -839,7 +1051,7 @@ async def map_source_to_response(payload: Any) -> graphql.ExecutionResult: # Map every source value to a ExecutionResult value. return graphql.MapAsyncIterator(result_or_stream, map_source_to_response) - async def _on_gql_start__initialize_subscription_stream( + async def _on_gql_subscribe__initialize_subscription_stream( self, operation_id: int, operation_name: str, @@ -850,11 +1062,12 @@ async def _on_gql_start__initialize_subscription_stream( ): """Create asynchronous generator with subscription events. - Called inside `_on_gql_start__subscribe` function by - graphql-core as `subscribe_field_resolver` argument. + Called inside `_on_gql_subscribe__create_subscription` function + by graphql-core as `subscribe_field_resolver` argument. - This is a part of START message processing routine so the name - prefixed with `_on_gql_start__` to make this explicit. + This is a part of SUBSCRIBE/START message processing routine so + the name prefixed with `_on_gql_subscribe__` to make this + explicit. """ # Graphene stores original subscription class in `graphene_type` # field of `return_type` object. Since subscriptions are build @@ -972,10 +1185,10 @@ def enqueue_notification(payload): # have capacity to process item. pass - waitlist = [] + wait_list = [] for group in groups: self._sids_by_group.setdefault(group, []).append(operation_id) - waitlist.append( + wait_list.append( asyncio.create_task( self._channel_layer.group_add(group, self.channel_name) ) @@ -986,16 +1199,17 @@ def enqueue_notification(payload): unsubscribed_callback=unsubscribed_callback, enqueue_notification=enqueue_notification, ) - if waitlist: - await asyncio.wait(waitlist) + if wait_list: + await asyncio.wait(wait_list) _deserialize = channels.db.database_sync_to_async( Serializer.deserialize, thread_sensitive=False ) # For each notification (event) yielded from this function the - # `_on_gql_start__subscribe` function will call subscription - # resolver (`publish`) via `graphql.execute` method. + # `_on_gql_subscribe__create_subscription` function will call + # subscription resolver (`publish`) via `graphql.execute` + # method. while True: with notification_queue_lock: payload = await notification_queue.get() @@ -1004,8 +1218,8 @@ def enqueue_notification(payload): with notification_queue_lock: notification_queue.task_done() - async def _on_gql_stop(self, op_id): - """Process the STOP message. + async def _on_gql_complete(self, op_id): + """Process the COMPLETE/STOP message. Handle an unsubscribe request. @@ -1015,26 +1229,26 @@ async def _on_gql_stop(self, op_id): """ LOG.debug("Stop handling or unsubscribe operation %s.", op_id) - # Currently only subscriptions can be stopped. But we see but - # some clients (e.g. GraphiQL) send the stop message even for - # queries and mutations. We also see that the Apollo server - # ignores such messages, so we ignore them as well. + # Currently only subscriptions can be stopped. But we see that + # some clients (e.g. GraphiQL) send the complete/stop message + # even for queries and mutations. We also see that the Apollo + # server ignores such messages, so we ignore them as well. if op_id not in self._subscriptions: return - waitlist: List[asyncio.Task] = [] + wait_list: List[asyncio.Task] = [] # Remove the subscription from the registry. - subinf = self._subscriptions.pop(op_id) + sub_inf = self._subscriptions.pop(op_id) # Cancel the task which watches the notification queue. consumer_task = self._notifier_tasks.pop(op_id, None) if consumer_task: consumer_task.cancel() - waitlist.append(consumer_task) + wait_list.append(consumer_task) # Stop listening for corresponding groups. - for group in subinf.groups: + for group in sub_inf.groups: # Remove the subscription from groups it belongs to. Remove # the group itself from the `_sids_by_group` if there are no # subscriptions left in it. @@ -1046,16 +1260,16 @@ async def _on_gql_stop(self, op_id): self._sids_by_group[group].remove(op_id) if not self._sids_by_group[group]: del self._sids_by_group[group] - waitlist.append( + wait_list.append( asyncio.create_task( self._channel_layer.group_discard(group, self.channel_name) ) ) - if waitlist: - await asyncio.wait(waitlist) + if wait_list: + await asyncio.wait(wait_list) - await subinf.unsubscribed_callback() + await sub_inf.unsubscribed_callback() # Send the unsubscription confirmation message. await self._send_gql_complete(op_id) @@ -1066,17 +1280,32 @@ async def _send_gql_connection_ack(self): """Sent in reply to the `connection_init` request.""" await self.send_json({"type": "connection_ack"}) + async def _send_gql_ping(self): + """Send the `ping`/`ka` message to the client.""" + await self.send_json( + { + "type": ( + "ping" + if self._graphql_ws_subprotocol == "graphql-transport-ws" + else "ka" + ) + } + ) + + async def _send_gql_pong(self): + """Sent in reply to the `ping` request.""" + await self.send_json({"type": "pong"}) + async def _send_gql_connection_error(self, error: Exception): """Connection error sent in reply to the `connection_init`.""" - LOG.warning("GraphQL connection error: %s!", error, exc_info=error) await self.send_json( {"type": "connection_error", "payload": self._format_error(error)} ) - async def _send_gql_data( + async def _send_gql_next( self, op_id, data: Optional[dict], errors: Optional[Iterable[Exception]] ): - """Send GraphQL `data` message to the client. + """Send GraphQL `next`/`data` message with data to the client. Args: data: Dict with GraphQL query response. @@ -1097,7 +1326,11 @@ async def _send_gql_data( await self.send_json( { - "type": "next", + "type": ( + "next" + if self._graphql_ws_subprotocol == "graphql-transport-ws" + else "data" + ), "id": op_id, "payload": { "data": data, @@ -1114,27 +1347,38 @@ async def _send_gql_data( } ) - async def _send_gql_error(self, op_id, error: Exception): + async def _send_gql_error(self, op_id, errors: Iterable[Exception]): """Tell client there is a query processing error. Server sends this message upon a failing operation. It can be an unexpected or unexplained GraphQL execution error or a bug in the code. It is unlikely that this is GraphQL - validation errors (such errors are part of data message and - must be sent by the `_send_gql_data` method). + validation errors in case of using `graphql-ws` subprotocol + (such errors are part of data message and must be sent by the + `_send_gql_next` method). Args: op_id: Id of the operation that failed on the server. error: String with the information about the error. """ - LOG.warning("Operation %s processing error: %s!", op_id, error, exc_info=error) - formatted_error = self._format_error(error) + formatted_errors = [] + for error in errors: + LOG.warning( + "Operation %s processing error: %s!", op_id, error, exc_info=error + ) + formatted_error = self._format_error(error) + formatted_errors.append(formatted_error) + payload = ( + formatted_errors + if self._graphql_ws_subprotocol == "graphql-transport-ws" + else {"errors": formatted_errors} + ) await self.send_json( { "type": "error", "id": op_id, - "payload": {"errors": [formatted_error]}, + "payload": payload, } ) @@ -1147,10 +1391,6 @@ async def _send_gql_complete(self, op_id): """ await self.send_json({"type": "complete", "id": op_id}) - async def _send_gql_connection_keep_alive(self): - """Send the keepalive (ping) message.""" - await self.send_json({"type": "ka"}) - # ---------------------------------------------------------------------- AUXILIARIES @staticmethod @@ -1165,13 +1405,13 @@ def _format_error(error: Exception) -> graphql.GraphQLFormattedError: for a client like this: { "id": "NNN", - "type": "data", + "type": "next", "payload": { "data": {...}, "errors": [{ "message": "Test error", "locations": [{"line": NNN, "column": NNN}], - "path": ["somepath"], + "path": ["some_path"], "extensions": {"code": "Exception"} }] } @@ -1225,14 +1465,6 @@ def _spawn_background_task(self, awaitable): self._background_tasks.add(background_task) return background_task - def get_context(self): - """Prepare the context object to use during GraphQL execution.""" - context = DictAsObject({}) - context.channels_scope = self.scope - context.channel_name = self.channel_name - context.build_meta() - return context - @property def _channel_layer(self): """Channel layer.""" diff --git a/channels_graphql_ws/testing.py b/channels_graphql_ws/testing.py index 2bc5383..82fda5d 100644 --- a/channels_graphql_ws/testing.py +++ b/channels_graphql_ws/testing.py @@ -58,7 +58,7 @@ async def assert_no_messages( except asyncio.TimeoutError: continue else: - if self._is_keep_alive_response(received): + if self._is_ping_pong_message(received): continue assert False, ( f"{error_message}\n{received}" @@ -66,6 +66,72 @@ async def assert_no_messages( else f"Message received when nothing expected!\n{received}" ) + async def connect_and_init(self, connect_only: bool = False) -> None: + """Establish and initialize WebSocket GraphQL connection. + + 1. Establish WebSocket connection. + 2. Initialize GraphQL connection. Skipped if connect_only=True. + """ + if connect_only: + await self._transport.connect() + self._is_connected = True + else: + await super().connect_and_init() + + async def send_raw_message(self, message): + """Send a raw message. + + This can be useful for testing, for example, to check that the + server responds appropriately to malformed messages. + """ + await self._transport.send(message) + + async def wait_disconnect(self, timeout=None, assert_code=None): + """Wait server to close the connection. + + Used in tests to check that WebSocket closed with expected code. + + Args: + timeout: Seconds to wait the connection to close. + assert_code: The code with which the connection should + be closed. + + Raises: + `asyncio.TimeoutError` when timeout is reached. + + """ + message = await self._transport.wait_disconnect(timeout) + if assert_code is not None: + assert message["code"] == assert_code, ( + "The connection was closed with the wrong code!" + f" Expected '{assert_code}' received '{message['code']}'!" + ) + self._is_connected = False + + async def receive_error(self, msg_id): + """Receive GraphQL `error` or `next` message with errors. + + Args: + msg_id: Subscribe message ID, in response + to which the `next` or `error` message should be sent. + Returns: + `errors` and `data` values from response. + + """ + if self._subprotocol == "graphql-transport-ws": + try: + await self.receive(wait_id=msg_id, assert_type="error") + except channels_graphql_ws.GraphqlWsResponseError as ex: + return ex.response["payload"], None + else: + try: + await self.receive_next(msg_id) + except channels_graphql_ws.GraphqlWsResponseError as ex: + return ( + ex.response["payload"]["errors"], + ex.response["payload"]["data"], + ) + class GraphqlWsTransport(channels_graphql_ws.transport.GraphqlWsTransport): """Testing client transport to work without WebSocket connection. @@ -76,12 +142,18 @@ class GraphqlWsTransport(channels_graphql_ws.transport.GraphqlWsTransport): # Slightly reduce timeout in testing. TIMEOUT: float = 30 - def __init__(self, application, path, communicator_kwds=None): + def __init__( + self, + application, + path, + communicator_kwds=None, + subprotocol="graphql-transport-ws", + ): """Constructor.""" self._comm = channels.testing.WebsocketCommunicator( application=application, path=path, - subprotocols=["graphql-ws"], + subprotocols=[subprotocol], **(communicator_kwds or {}), ) @@ -105,15 +177,12 @@ async def disconnect(self, timeout: Optional[float] = None) -> None: """Disconnect from the server.""" await self._comm.disconnect(timeout=timeout or self.TIMEOUT) - async def wait_disconnect(self, timeout: Optional[float] = None) -> None: + async def wait_disconnect(self, timeout: Optional[float] = None) -> dict: """Wait server to close the connection.""" message = await self._comm.receive_output(timeout or self.TIMEOUT) assert message["type"] == "websocket.close", ( "Message with a wrong type received while waiting server to close the" f" connection! Expected 'websocket.close' received '{message['type']}'!" ) - assert message["code"] == 4000, ( - "Message with a wrong code received while waiting server to close the" - f" connection! Expected '4000' received '{message['code']}'!" - ) await self._comm.disconnect(timeout=timeout or self.TIMEOUT) + return dict(message) diff --git a/channels_graphql_ws/transport.py b/channels_graphql_ws/transport.py index 746a3e7..e7b801b 100644 --- a/channels_graphql_ws/transport.py +++ b/channels_graphql_ws/transport.py @@ -28,8 +28,6 @@ import aiohttp -from . import graphql_ws_consumer - class GraphqlWsTransport: """Transport interface for the `GraphqlWsClient`.""" @@ -53,7 +51,7 @@ async def disconnect(self, timeout: Optional[float] = None) -> None: """Disconnect from the server.""" raise NotImplementedError() - async def wait_disconnect(self, timeout: Optional[float] = None) -> None: + async def wait_disconnect(self, timeout: Optional[float] = None) -> dict: """Wait server to close the connection.""" raise NotImplementedError() @@ -83,17 +81,25 @@ def __init__(self, url, cookies=None, headers=None): # A queue for incoming messages. self._incoming_messages: asyncio.Queue = asyncio.Queue() - async def connect(self, timeout: Optional[float] = None) -> None: + async def connect( + self, timeout: Optional[float] = None, subprotocol="graphql-transport-ws" + ) -> None: """Establish a connection with the WebSocket server. - Returns: - `(True, )` if connection accepted. - `(False, None)` if connection rejected. + Args: + timeout: Connection timeout in seconds. + subprotocol: WebSocket subprotocol to use by the Transport. + Can have a value of "graphql-transport-ws" or + "graphql-ws". By default set to "graphql-transport-ws". """ + assert subprotocol in ( + "graphql-transport-ws", + "graphql-ws", + ), "Transport only supports graphql-transport-ws and graphql-ws subprotocols!" connected = asyncio.Event() self._message_processor = asyncio.create_task( - self._process_messages(connected, timeout or self.TIMEOUT) + self._process_messages(connected, timeout or self.TIMEOUT, subprotocol) ) await asyncio.wait( [connected.wait(), self._message_processor], @@ -153,11 +159,11 @@ async def disconnect(self, timeout: Optional[float] = None) -> None: except asyncio.CancelledError: pass - async def wait_disconnect(self, timeout: Optional[float] = None) -> None: + async def wait_disconnect(self, timeout: Optional[float] = None) -> dict: """Wait server to close the connection.""" raise NotImplementedError() - async def _process_messages(self, connected, timeout): + async def _process_messages(self, connected, timeout, subprotocol): """Process messages coming from the connection. Args: @@ -169,14 +175,11 @@ async def _process_messages(self, connected, timeout): async with session as session: connection = session.ws_connect( self._url, - protocols=[graphql_ws_consumer.GRAPHQL_WS_SUBPROTOCOL, graphql_ws_consumer.TRANSPORT_WS_SUBPROTOCOL], + protocols=[subprotocol], timeout=timeout, ) async with connection as self._connection: - if ( - self._connection.protocol - not in [graphql_ws_consumer.GRAPHQL_WS_SUBPROTOCOL, graphql_ws_consumer.TRANSPORT_WS_SUBPROTOCOL] - ): + if self._connection.protocol != subprotocol: raise RuntimeError( f"Server uses wrong subprotocol: {self._connection.protocol}!" ) diff --git a/example/example.py b/example/example.py index cc98463..1228572 100644 --- a/example/example.py +++ b/example/example.py @@ -33,8 +33,9 @@ import django.contrib.admin import django.contrib.auth import django.core.asgi +import django.urls import graphene -import graphene_django.types +import graphene_django.views import graphql import channels_graphql_ws @@ -60,76 +61,18 @@ class Message( # type: ignore sender = graphene.String() -class User(graphene_django.types.DjangoObjectType): - """Show logged in user details via GraphQL. - - Example of working with 'info.context.channels_scope["user"]'. - """ - - class Meta: - """Wrap Django user model.""" - - model = django.contrib.auth.get_user_model() - - class Query(graphene.ObjectType): """Root GraphQL query.""" history = graphene.List(Message, chatroom=graphene.String()) - user = graphene.Field(User) def resolve_history(self, info, chatroom): """Return chat history.""" del info return chats[chatroom] if chatroom in chats else [] - def resolve_user(self, info): - """Provide currently logged in user.""" - if info.context.channels_scope["user"].is_authenticated: - return info.context.channels_scope["user"] - return None - # ---------------------------------------------------------------------------- MUTATIONS - - -class Login(graphene.Mutation, name="LoginPayload"): # type: ignore - """Login mutation. - - Login implementation, following the Channels guide: - https://channels.readthedocs.io/en/latest/topics/authentication.html - """ - - ok = graphene.Boolean(required=True) - - class Arguments: - """Login request arguments.""" - - username = graphene.String(required=True) - password = graphene.String(required=True) - - async def mutate(self, info, username, password): - """Login request.""" - - # Ask Django to authenticate user. - user = await channels.db.database_sync_to_async( - django.contrib.auth.authenticate - )(username=username, password=password) - if user is None: - return Login(ok=False) - - # Use Channels to login, in other words to put proper data to - # the session stored in the scope. The - # `info.context.channels_scope` is a reference to Channel - # `self.scope` member. - await channels.auth.login(info.context.channels_scope, user) - # Save the session, `channels.auth.login` does not do this. - session = info.context.channels_scope["session"] - await channels.db.database_sync_to_async(session.save)() - - return Login(ok=True) - - class SendChatMessage(graphene.Mutation, name="SendChatMessagePayload"): # type: ignore """Send chat message.""" @@ -141,19 +84,20 @@ class Arguments: chatroom = graphene.String() text = graphene.String() - async def mutate(self, info, chatroom, text): + def mutate(self, info, chatroom, text): """Mutation "resolver" - store and broadcast a message.""" - # Use the username from the connection scope if authorized. - user = info.context.channels_scope["user"] - username = user.username if user.is_authenticated else "Anonymous" + # Here we identify the sender by `sessionid` cookie. + user_session_key = info.context.session.session_key # Store a message. - chats[chatroom].append({"chatroom": chatroom, "text": text, "sender": username}) + chats[chatroom].append( + {"chatroom": chatroom, "text": text, "sender": user_session_key} + ) # Notify subscribers. - await OnNewChatMessage.new_chat_message( - chatroom=chatroom, text=text, sender=username + OnNewChatMessage.new_chat_message( + chatroom=chatroom, text=text, sender=user_session_key ) return SendChatMessage(ok=True) @@ -163,7 +107,6 @@ class Mutation(graphene.ObjectType): """Root GraphQL mutation.""" send_chat_message = SendChatMessage.Field() - login = Login.Field() # ------------------------------------------------------------------------ SUBSCRIPTIONS @@ -202,8 +145,9 @@ def publish(self, info, chatroom=None): assert chatroom is None or chatroom == new_msg_chatroom # Avoid self-notifications. - user = info.context.channels_scope["user"] - if user.is_authenticated and new_msg_sender == user.username: + # The sender is identified by a `sessionid` cookie. + user_session_key = info.context.channels_scope["session"].session_key + if user_session_key == new_msg_sender: return None return OnNewChatMessage( @@ -211,7 +155,7 @@ def publish(self, info, chatroom=None): ) @classmethod - async def new_chat_message(cls, chatroom, text, sender): + def new_chat_message(cls, chatroom, text, sender): """Auxiliary function to send subscription notifications. It is generally a good idea to encapsulate broadcast invocation @@ -219,7 +163,7 @@ async def new_chat_message(cls, chatroom, text, sender): That allows to consider a structure of the `payload` as an implementation details. """ - await cls.broadcast( + cls.broadcast( group=chatroom, payload={"chatroom": chatroom, "text": text, "sender": sender}, ) @@ -240,14 +184,9 @@ async def demo_middleware(next_middleware, root, info, *args, **kwds): For more information read: https://docs.graphene-python.org/en/latest/execution/middleware/#middleware """ - # Skip Graphiql introspection requests, there are a lot. - if ( - info.operation.name is not None - and info.operation.name.value != "IntrospectionQuery" - ): - print("Demo middleware report") - print(" operation :", info.operation.operation) - print(" name :", info.operation.name.value) + print("Demo middleware report") + print(" operation :", info.operation.operation) + print(" name :", info.operation.name.value) # Invoke next middleware. result = next_middleware(root, info, *args, **kwds) @@ -259,23 +198,7 @@ async def demo_middleware(next_middleware, root, info, *args, **kwds): class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer): """Channels WebSocket consumer which provides GraphQL API.""" - send_keepalive_every = 1 - - async def on_connect(self, payload): - """Handle WebSocket connection event.""" - - # Use auxiliary Channels function `get_user` to replace an - # instance of `channels.auth.UserLazyObject` with a native - # Django user object (user model instance or `AnonymousUser`) - # It is not necessary, but it helps to keep resolver code - # simpler. Cause in both HTTP/WebSocket requests they can use - # `info.context.channels_scope["user"]`, but not a wrapper. For - # example objects of type Graphene Django type - # `DjangoObjectType` does not accept - # `channels.auth.UserLazyObject` instances. - # https://github.com/datadvance/DjangoChannelsGraphqlWs/issues/23 - self.scope["user"] = await channels.auth.get_user(self.scope) - + send_ping_every = 1 schema = graphene.Schema(query=Query, mutation=Mutation, subscription=Subscription) middleware = [demo_middleware] @@ -290,7 +213,7 @@ async def on_connect(self, payload): "http": django.core.asgi.get_asgi_application(), "websocket": channels.auth.AuthMiddlewareStack( channels.routing.URLRouter( - [django.urls.path("graphql/", MyGraphqlWsConsumer.as_asgi())] + [django.urls.path("graphql-ws/", MyGraphqlWsConsumer.as_asgi())] ) ), } @@ -300,15 +223,31 @@ async def on_connect(self, payload): # -------------------------------------------------------------------- URL CONFIGURATION def graphiql(request): """Trivial view to serve the `graphiql.html` file.""" + # It is important to create a session if it hasn't already been + # created, because `sessionid` cookie is used to identify the + # sender. + if not request.session.session_key: + request.session.create() del request graphiql_filepath = pathlib.Path(__file__).absolute().parent / "graphiql.html" - # It is better to specify an encoding when opening documents. Using the - # system default implicitly can create problems on other operating systems. + # It is better to specify an encoding when opening documents. + # Using the system default implicitly can create problems on other + # operating systems. with open(graphiql_filepath, encoding="utf-8") as f: return django.http.response.HttpResponse(f.read()) urlpatterns = [ django.urls.path("", graphiql), + # `GraphQLView` used to handle mutations and queries requests + # sended by http. + django.urls.path( + "graphql/", + graphene_django.views.GraphQLView.as_view( + schema=graphene.Schema( + query=Query, mutation=Mutation, subscription=Subscription + ) + ), + ), django.urls.path("admin", django.contrib.admin.site.urls), ] diff --git a/example/graphiql.html b/example/graphiql.html index f61bc31..d17f7d5 100644 --- a/example/graphiql.html +++ b/example/graphiql.html @@ -1,4 +1,3 @@ - - - - - - - - - - + + + + + GraphiQL + - // Render into the body. - ReactDOM.render( - React.createElement(GraphiQL, { - fetcher: subFetcher, //graphQLFetcher + + + + + + +
Loading...
+ + - + defaultEditorToolsVisibility: true, + }), + document.getElementById('graphiql'), + ); + + + diff --git a/example/settings.py b/example/settings.py index 9164170..6c460a3 100644 --- a/example/settings.py +++ b/example/settings.py @@ -91,6 +91,7 @@ "django.contrib.staticfiles", "django.contrib.admin", "channels", + "graphene_django", ] ALLOWED_HOSTS = ["*"] STATIC_URL = "/static/" diff --git a/poetry.lock b/poetry.lock index aecf995..466b6e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,88 +1,88 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" -version = "3.9.3" +version = "3.9.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, ] [package.dependencies] @@ -126,13 +126,13 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] name = "asgiref" -version = "3.7.2" +version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.dependencies] @@ -143,13 +143,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" -version = "3.1.0" +version = "3.2.3" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, + {file = "astroid-3.2.3-py3-none-any.whl", hash = "sha256:3eae9ea67c11c858cdd2c91337d2e816bd019ac897ca07d7b346ac10105fceb3"}, + {file = "astroid-3.2.3.tar.gz", hash = "sha256:7099b5a60985529d8d46858befa103b82d0d05a5a5e8b816b5303ed96075e1d9"}, ] [package.dependencies] @@ -187,12 +187,12 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "autobahn" -version = "23.6.2" +version = "23.1.2" description = "WebSocket client & server library, WAMP real-time framework" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" files = [ - {file = "autobahn-23.6.2.tar.gz", hash = "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181"}, + {file = "autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a"}, ] [package.dependencies] @@ -202,7 +202,7 @@ setuptools = "*" txaio = ">=21.2.1" [package.extras] -all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] +all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi @ git+https://github.com/ethereum/eth-abi.git@v4.0.0-beta.2", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3[ipfs] @ git+https://github.com/ethereum/web3.py.git@v6.0.0-beta.9", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] compress = ["python-snappy (>=0.6.0)"] dev = ["backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610)", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service_identity (>=18.1.0)"] @@ -211,7 +211,7 @@ scram = ["argon2_cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] serialization = ["cbor2 (>=5.2.0)", "flatbuffers (>=22.12.6)", "msgpack (>=1.0.2)", "py-ubjson (>=0.16.1)", "ujson (>=4.0.2)"] twisted = ["attrs (>=20.3.0)", "twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] ui = ["PyGObject (>=3.40.0)"] -xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] +xbr = ["base58 (>=2.1.0)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi @ git+https://github.com/ethereum/eth-abi.git@v4.0.0-beta.2", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3[ipfs] @ git+https://github.com/ethereum/web3.py.git@v6.0.0-beta.9", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] [[package]] name = "automat" @@ -231,35 +231,63 @@ six = "*" [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + [[package]] name = "black" -version = "24.2.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -279,13 +307,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "5.3.3" +version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [[package]] @@ -354,19 +382,19 @@ pycparser = "*" [[package]] name = "channels" -version = "4.0.0" +version = "4.1.0" description = "Brings async, event-driven capabilities to Django 3.2 and up." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "channels-4.0.0-py3-none-any.whl", hash = "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4"}, - {file = "channels-4.0.0.tar.gz", hash = "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420"}, + {file = "channels-4.1.0-py3-none-any.whl", hash = "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48"}, + {file = "channels-4.1.0.tar.gz", hash = "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"}, ] [package.dependencies] -asgiref = ">=3.5.0,<4" +asgiref = ">=3.6.0,<4" daphne = {version = ">=4.0.0", optional = true, markers = "extra == \"daphne\""} -Django = ">=3.2" +Django = ">=4.2" [package.extras] daphne = ["daphne (>=4.0.0)"] @@ -442,63 +470,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.3" +version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.dependencies] @@ -509,43 +537,43 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.5" +version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, - {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, - {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, - {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, - {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, - {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, - {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, ] [package.dependencies] @@ -563,13 +591,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "daphne" -version = "4.1.0" +version = "4.1.2" description = "Django ASGI (HTTP/WebSocket) server" optional = false python-versions = ">=3.8" files = [ - {file = "daphne-4.1.0-py3-none-any.whl", hash = "sha256:7228cd6a3ca5a9b11c9a1c1c0414dab1bfb4ddc55ff234b545db8d71f6c24938"}, - {file = "daphne-4.1.0.tar.gz", hash = "sha256:882fab39d0b90c6b2709b38116c95f660b6cf236600115dd7c13161fb98b3448"}, + {file = "daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a"}, + {file = "daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"}, ] [package.dependencies] @@ -608,17 +636,18 @@ files = [ [[package]] name = "django" -version = "5.0.3" +version = "4.2.14" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.10" +python-versions = ">=3.8" files = [ - {file = "Django-5.0.3-py3-none-any.whl", hash = "sha256:5c7d748ad113a81b2d44750ccc41edc14e933f56581683db548c9257e078cc83"}, - {file = "Django-5.0.3.tar.gz", hash = "sha256:5fb37580dcf4a262f9258c1f4373819aacca906431f505e4688e37f3a99195df"}, + {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, + {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, ] [package.dependencies] -asgiref = ">=3.7.0,<4" +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -628,13 +657,13 @@ bcrypt = ["bcrypt"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -642,13 +671,13 @@ test = ["pytest (>=6)"] [[package]] name = "execnet" -version = "2.0.2" +version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, - {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, ] [package.extras] @@ -656,18 +685,18 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.13.1" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -778,13 +807,13 @@ test = ["coveralls (>=3.3,<4)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>= [[package]] name = "graphene-django" -version = "3.2.0" +version = "3.2.2" description = "Graphene Django integration" optional = false python-versions = "*" files = [ - {file = "graphene-django-3.2.0.tar.gz", hash = "sha256:9aca4a862f12912c2e611624bdbcf6b0f9bc7a41d240110a41bf95575a7bacab"}, - {file = "graphene_django-3.2.0-py2.py3-none-any.whl", hash = "sha256:b553ecdc1cd7fd5b2d71de1a729c03ae117321763a90ed48a7fb4fdbf7f0d43f"}, + {file = "graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556"}, + {file = "graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c"}, ] [package.dependencies] @@ -841,13 +870,13 @@ idna = ">=2.5" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -963,7 +992,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -1068,38 +1096,38 @@ files = [ [[package]] name = "mypy" -version = "1.8.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -1126,13 +1154,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1148,28 +1176,29 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -1178,13 +1207,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "plumbum" -version = "1.8.2" +version = "1.8.3" description = "Plumbum: shell combinators library" optional = false python-versions = ">=3.6" files = [ - {file = "plumbum-1.8.2-py3-none-any.whl", hash = "sha256:3ad9e5f56c6ec98f6f7988f7ea8b52159662ea9e915868d369dbccbfca0e367e"}, - {file = "plumbum-1.8.2.tar.gz", hash = "sha256:9e6dc032f4af952665f32f3206567bc23b7858b1413611afe603a3f8ad9bfd75"}, + {file = "plumbum-1.8.3-py3-none-any.whl", hash = "sha256:8595d36dae2472587d6f59789c8d7b26250f45f6f6ed75ccb378de59ee7b9cf9"}, + {file = "plumbum-1.8.3.tar.gz", hash = "sha256:6092c85ab970b7a7a9d5d85c75200bc93be82b33c9bdf640ffa87d2d7c8709f0"}, ] [package.dependencies] @@ -1213,70 +1242,53 @@ test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", [[package]] name = "pyasn1" -version = "0.5.1" +version = "0.6.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, - {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, ] [[package]] name = "pyasn1-modules" -version = "0.3.0" +version = "0.4.0" description = "A collection of ASN.1-based protocols modules" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, + {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, + {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" +pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - [[package]] name = "pylint" -version = "3.1.0" +version = "3.2.5" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, + {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, + {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, ] [package.dependencies] -astroid = ">=3.1.0,<=3.2.0-dev0" +astroid = ">=3.2.2,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -1288,6 +1300,7 @@ mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -1341,13 +1354,13 @@ pylint = ">=2.8.0" [[package]] name = "pyopenssl" -version = "24.0.0" +version = "24.1.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" files = [ - {file = "pyOpenSSL-24.0.0-py3-none-any.whl", hash = "sha256:ba07553fb6fd6a7a2259adb9b84e12302a9a8a75c44046e8bb5d3e5ee887e3c3"}, - {file = "pyOpenSSL-24.0.0.tar.gz", hash = "sha256:6aa33039a93fffa4563e655b61d11364d01264be8ccb49906101e02a334530bf"}, + {file = "pyOpenSSL-24.1.0-py3-none-any.whl", hash = "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad"}, + {file = "pyOpenSSL-24.1.0.tar.gz", hash = "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f"}, ] [package.dependencies] @@ -1355,36 +1368,36 @@ cryptography = ">=41.0.5,<43" [package.extras] docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] -test = ["flaky", "pretend", "pytest (>=3.0.1)"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyproject-api" -version = "1.6.1" +version = "1.7.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" files = [ - {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, - {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, ] [package.dependencies] -packaging = ">=23.1" +packaging = ">=24.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] [[package]] name = "pytest" -version = "8.0.2" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1392,21 +1405,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.5" +version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, - {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, ] [package.dependencies] @@ -1418,13 +1431,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -1432,7 +1445,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-django" @@ -1467,18 +1480,18 @@ pytest = ">=2.5.2" [[package]] name = "pytest-xdist" -version = "3.5.0" +version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, - {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, ] [package.dependencies] -execnet = ">=1.1" -pytest = ">=6.2.0" +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] @@ -1510,17 +1523,17 @@ files = [ [[package]] name = "redis" -version = "5.0.2" +version = "5.0.7" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.2-py3-none-any.whl", hash = "sha256:4caa8e1fcb6f3c0ef28dba99535101d80934b7d4cd541bbb47f4a3826ee472d1"}, - {file = "redis-5.0.2.tar.gz", hash = "sha256:3f82cc80d350e93042c8e6e7a5d0596e4dd68715babffba79492733e1f367037"}, + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, ] [package.dependencies] -async-timeout = ">=4.0.3" +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -1552,19 +1565,19 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" -version = "69.1.1" +version = "71.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, + {file = "setuptools-71.0.3-py3-none-any.whl", hash = "sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207"}, + {file = "setuptools-71.0.3.tar.gz", hash = "sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (<7.4)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1577,32 +1590,20 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - [[package]] name = "sqlparse" -version = "0.4.4" +version = "0.5.1" description = "A non-validating SQL parser." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, - {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, + {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, + {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, ] [package.extras] -dev = ["build", "flake8"] +dev = ["build", "hatch"] doc = ["sphinx"] -test = ["pytest", "pytest-cov"] [[package]] name = "text-unidecode" @@ -1628,41 +1629,41 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.13.0" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, ] [[package]] name = "tox" -version = "4.14.1" +version = "4.16.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.14.1-py3-none-any.whl", hash = "sha256:b03754b6ee6dadc70f2611da82b4ed8f625fcafd247e15d1d0cb056f90a06d3b"}, - {file = "tox-4.14.1.tar.gz", hash = "sha256:f0ad758c3bbf7e237059c929d3595479363c3cdd5a06ac3e49d1dd020ffbee45"}, + {file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"}, + {file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"}, ] [package.dependencies] -cachetools = ">=5.3.2" +cachetools = ">=5.3.3" chardet = ">=5.2" colorama = ">=0.4.6" -filelock = ">=3.13.1" -packaging = ">=23.2" -platformdirs = ">=4.1" -pluggy = ">=1.3" -pyproject-api = ">=1.6.1" +filelock = ">=3.15.4" +packaging = ">=24.1" +platformdirs = ">=4.2.2" +pluggy = ">=1.5" +pyproject-api = ">=1.7.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} -virtualenv = ">=20.25" +virtualenv = ">=20.26.3" [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] -testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"] +docs = ["furo (>=2024.5.6)", "sphinx (>=7.3.7)", "sphinx-argparse-cli (>=1.16)", "sphinx-autodoc-typehints (>=2.2.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] +testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.2)", "time-machine (>=2.14.2)", "wheel (>=0.43)"] [[package]] name = "twisted" @@ -1749,13 +1750,13 @@ twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1771,13 +1772,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.25.1" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -1786,7 +1787,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] @@ -1894,58 +1895,58 @@ multidict = ">=4.0" [[package]] name = "zope-interface" -version = "6.2" +version = "6.4.post2" description = "Interfaces for Python" optional = false python-versions = ">=3.7" files = [ - {file = "zope.interface-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab"}, - {file = "zope.interface-6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328"}, - {file = "zope.interface-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac"}, - {file = "zope.interface-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b"}, - {file = "zope.interface-6.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe"}, - {file = "zope.interface-6.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b"}, - {file = "zope.interface-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3"}, - {file = "zope.interface-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a"}, - {file = "zope.interface-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1"}, - {file = "zope.interface-6.2.tar.gz", hash = "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565"}, + {file = "zope.interface-6.4.post2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c"}, + {file = "zope.interface-6.4.post2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530"}, + {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef8356f16b1a83609f7a992a6e33d792bb5eff2370712c9eaae0d02e1924341"}, + {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b"}, + {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d22fce0b0f5715cdac082e35a9e735a1752dc8585f005d045abb1a7c20e197f9"}, + {file = "zope.interface-6.4.post2-cp310-cp310-win_amd64.whl", hash = "sha256:97e615eab34bd8477c3f34197a17ce08c648d38467489359cb9eb7394f1083f7"}, + {file = "zope.interface-6.4.post2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:599f3b07bde2627e163ce484d5497a54a0a8437779362395c6b25e68c6590ede"}, + {file = "zope.interface-6.4.post2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:136cacdde1a2c5e5bc3d0b2a1beed733f97e2dad8c2ad3c2e17116f6590a3827"}, + {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47937cf2e7ed4e0e37f7851c76edeb8543ec9b0eae149b36ecd26176ff1ca874"}, + {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0a6be264afb094975b5ef55c911379d6989caa87c4e558814ec4f5125cfa2e"}, + {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47654177e675bafdf4e4738ce58cdc5c6d6ee2157ac0a78a3fa460942b9d64a8"}, + {file = "zope.interface-6.4.post2-cp311-cp311-win_amd64.whl", hash = "sha256:e2fb8e8158306567a3a9a41670c1ff99d0567d7fc96fa93b7abf8b519a46b250"}, + {file = "zope.interface-6.4.post2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b912750b13d76af8aac45ddf4679535def304b2a48a07989ec736508d0bbfbde"}, + {file = "zope.interface-6.4.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ac46298e0143d91e4644a27a769d1388d5d89e82ee0cf37bf2b0b001b9712a4"}, + {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86a94af4a88110ed4bb8961f5ac72edf782958e665d5bfceaab6bf388420a78b"}, + {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73f9752cf3596771c7726f7eea5b9e634ad47c6d863043589a1c3bb31325c7eb"}, + {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854"}, + {file = "zope.interface-6.4.post2-cp312-cp312-win_amd64.whl", hash = "sha256:551db2fe892fcbefb38f6f81ffa62de11090c8119fd4e66a60f3adff70751ec7"}, + {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96ac6b3169940a8cd57b4f2b8edcad8f5213b60efcd197d59fbe52f0accd66e"}, + {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cebff2fe5dc82cb22122e4e1225e00a4a506b1a16fafa911142ee124febf2c9e"}, + {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ee982237cffaf946db365c3a6ebaa37855d8e3ca5800f6f48890209c1cfefc"}, + {file = "zope.interface-6.4.post2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:fbf649bc77510ef2521cf797700b96167bb77838c40780da7ea3edd8b78044d1"}, + {file = "zope.interface-6.4.post2-cp37-cp37m-win_amd64.whl", hash = "sha256:4c0b208a5d6c81434bdfa0f06d9b667e5de15af84d8cae5723c3a33ba6611b82"}, + {file = "zope.interface-6.4.post2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3fe667935e9562407c2511570dca14604a654988a13d8725667e95161d92e9b"}, + {file = "zope.interface-6.4.post2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a96e6d4074db29b152222c34d7eec2e2db2f92638d2b2b2c704f9e8db3ae0edc"}, + {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:866a0f583be79f0def667a5d2c60b7b4cc68f0c0a470f227e1122691b443c934"}, + {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fe919027f29b12f7a2562ba0daf3e045cb388f844e022552a5674fcdf5d21f1"}, + {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e0343a6e06d94f6b6ac52fbc75269b41dd3c57066541a6c76517f69fe67cb43"}, + {file = "zope.interface-6.4.post2-cp38-cp38-win_amd64.whl", hash = "sha256:dabb70a6e3d9c22df50e08dc55b14ca2a99da95a2d941954255ac76fd6982bc5"}, + {file = "zope.interface-6.4.post2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:706efc19f9679a1b425d6fa2b4bc770d976d0984335eaea0869bd32f627591d2"}, + {file = "zope.interface-6.4.post2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d136e5b8821073e1a09dde3eb076ea9988e7010c54ffe4d39701adf0c303438"}, + {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1730c93a38b5a18d24549bc81613223962a19d457cfda9bdc66e542f475a36f4"}, + {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc2676312cc3468a25aac001ec727168994ea3b69b48914944a44c6a0b251e79"}, + {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a62fd6cd518693568e23e02f41816adedfca637f26716837681c90b36af3671"}, + {file = "zope.interface-6.4.post2-cp39-cp39-win_amd64.whl", hash = "sha256:d3f7e001328bd6466b3414215f66dde3c7c13d8025a9c160a75d7b2687090d15"}, + {file = "zope.interface-6.4.post2.tar.gz", hash = "sha256:1c207e6f6dfd5749a26f5a5fd966602d6b824ec00d2df84a7e9a924e8933654e"}, ] [package.dependencies] setuptools = "*" [package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx_rtd_theme"] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" -python-versions = "^3.10.0" -content-hash = "d40cbe75570cd3ee5238d336b388ed74c6bf3b60e91ff27b1541af526ec331e6" +python-versions = "^3.8.0" +content-hash = "2ef03e02068f25a2bd6b57098db5d15db1a2fae18b98eed263247896b6fd920a" diff --git a/poetry.toml b/poetry.toml index e544637..250281a 100644 --- a/poetry.toml +++ b/poetry.toml @@ -26,4 +26,10 @@ # Make Poetry create virtualenvs in the project directory, so all other # tools can easily find it. In particular VS Code will easily find and # use this virtualenv. +# https://python-poetry.org/docs/configuration/#virtualenvsin-project in-project = true + +# Use currently activated Python version to create a new virtual +# environment. +# https://python-poetry.org/docs/configuration/#virtualenvsprefer-active-python-experimental +prefer-active-python = true diff --git a/pyproject.toml b/pyproject.toml index 90ac117..25c4263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,7 @@ authors = ["Alexander A. Prokhorov "] homepage = "https://github.com/datadvance/DjangoChannelsGraphqlWs" repository = "https://github.com/datadvance/DjangoChannelsGraphqlWs" license = "MIT" -packages = [ - { include = "channels_graphql_ws/" }, -] +packages = [{ include = "channels_graphql_ws/" }] # Production dependencies. # Version syntax: https://python-poetry.org/docs/dependency-specification/ @@ -60,14 +58,13 @@ msgpack = "^1" # Version syntax: https://python-poetry.org/docs/dependency-specification/ [tool.poetry.group.dev.dependencies] black = { version = "*" } -channels = {version = "^4", extras = ["daphne"]} +channels = { version = "^4", extras = ["daphne"] } channels-redis = "*" coverage = "*" graphene-django = "*" -isort = {version = "*", extras = ["pyproject"]} +isort = { version = "*", extras = ["pyproject"] } mypy = "*" plumbum = "*" -pydocstyle = "*" pylint = "*" pylint-django = "*" pylint-quotes = "*" @@ -89,7 +86,7 @@ target-version = ['py38', 'py39', 'py310'] # Isort configuration. # Docs: https://github.com/timothycrosley/isort/wiki/isort-Settings [tool.isort] -profile="black" +profile = "black" # --------------------------------------------------------------- PYLINT # Pylint code checker configuration. @@ -103,7 +100,10 @@ profile="black" # https://github.com/PyCQA/pylint-django/issues/276 #jobs = "0" jobs = "1" -load-plugins = "pylint_quotes,pylint_django" +# Disable Pylint-quotes until its issue is fixed: +# https://github.com/edaniszewski/pylint-quotes/issues/29. +## load-plugins = "pylint_quotes,pylint_django" +load-plugins = "pylint_django" # Do not store results of previous runs. persistent = "no" # Allow to load C extension modules. @@ -128,8 +128,8 @@ method-rgx = "[a-z_][a-z0-9_]{1,64}$" no-docstring-rgx = "^$" variable-rgx = "[a-z_][a-z0-9_]{1,64}$" [tool.pylint.FORMAT] -max-line-length="88" -max-module-lines="9000" # Right, over nine thousand! +max-line-length = "88" +max-module-lines = "9000" # Right, over nine thousand! [tool.pylint.STRING_QUOTES] docstring-quote = "double" string-quote = "double-avoid-escape" @@ -193,7 +193,7 @@ warn_unused_configs = true # pytest-pythonpath: # Set up paths to import Python modules from. -python_paths = ". example" +pythonpath = ". example" # pytest-django: # NOTE: This influence to both Pytest and to `pylint_django`, cause # in fact, it simply sets `DJANGO_SETTINGS_MODULE` environment variable. @@ -204,22 +204,3 @@ python_files = "**/test_*.py" # - Enable coverage measurement and set config file. # - Force verbose output: output list of tests by default. addopts = "--cov --verbose" - -# ----------------------------------------------------------- PYDOCSTYLE -# Pydocstyle configuration reference: -# http://www.pydocstyle.org/en/latest/usage.html#available-options -[tool.pydocstyle] -# Redefine match rules, cause by default it skips `test_*` files. -match=".*\\.py" -# Disable some checks: -# - D202: No blank lines allowed after function docstring. We cannot -# follow this, cause Black inject blank line before function -# declaration and if it is a nested function when this violates this -# rule. -# - D401: First line should be in imperative mood; try rephrasing. Often -# it is not possible. Especially for callback functions such as "A new -# message handler." -# - D403: First word of the first line should be properly capitalized. -# There is nothing bad in using words like "WebSocket" or -# "MessagePack" even in the beginning of the docstring. -add_ignore="D202,D401,D403" diff --git a/tests/conftest.py b/tests/conftest.py index 6cf9717..13f79e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -96,7 +96,8 @@ def gql(db, request): mutation=None, subscription=None, consumer_attrs=None, - communicator_kwds=None + communicator_kwds=None, + subprotocol="graphql-transport-ws", ): Args: @@ -106,6 +107,9 @@ def gql(db, request): consumer_attrs: `GraphqlWsConsumer` attributes dict. Optional. communicator_kwds: Extra keyword arguments for the Channels `channels.testing.WebsocketCommunicator`. Optional. + subprotocol: WebSocket subprotocol to use by the Client. Can + have a value of "graphql-transport-ws" or "graphql-ws". + By default set to "graphql-transport-ws". Returns: An instance of the `GraphqlWsClient` class which has many @@ -123,7 +127,9 @@ def test_something(gql): # `GraphqlWsConsumer` settings. consumer_attrs={"strict_ordering": True}, # `channels.testing.WebsocketCommunicator` settings. - communicator_kwds={"headers": [...]} + communicator_kwds={"headers": [...]}, + # Subprotocol to test. + subprotocol="graphql-ws" ) ... ``` @@ -143,6 +149,7 @@ def client_constructor( subscription=None, consumer_attrs=None, communicator_kwds=None, + subprotocol="graphql-transport-ws", ): """Setup GraphQL consumer and the communicator for tests.""" # Graphene will throw a exception from the `graphene.Schema` @@ -185,9 +192,12 @@ class ChannelsConsumer(channels_graphql_ws.GraphqlWsConsumer): application=application, path="graphql/", communicator_kwds=communicator_kwds, + subprotocol=subprotocol, ) - client = channels_graphql_ws.testing.GraphqlWsClient(transport) + client = channels_graphql_ws.testing.GraphqlWsClient( + transport, subprotocol=subprotocol + ) issued_clients.append(client) return client diff --git a/tests/test_async.py b/tests/test_async.py index e591ae8..cca6f89 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -23,7 +23,6 @@ # NOTE: The GraphQL schema is defined at the end of the file. -import textwrap import time import uuid from datetime import datetime @@ -35,7 +34,8 @@ @pytest.mark.asyncio -async def test_broadcast(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_broadcast(gql, subprotocol): """Test that the asynchronous 'broadcast()' call works correctly. Because we cannot use sync 'broadcasts()' method in the thread @@ -53,88 +53,78 @@ async def test_broadcast(gql): # notifications must be send in the order they were broadcasted. settings = {"strict_ordering": False} client_sender = gql( - mutation=Mutation, subscription=Subscription, consumer_attrs=settings + mutation=Mutation, + subscription=Subscription, + consumer_attrs=settings, + subprotocol=subprotocol, ) client_recipient = gql( - mutation=Mutation, subscription=Subscription, consumer_attrs=settings + mutation=Mutation, + subscription=Subscription, + consumer_attrs=settings, + subprotocol=subprotocol, ) await client_sender.connect_and_init() await client_recipient.connect_and_init() print("Subscribe to GraphQL subscription.") - sub_id = await client_recipient.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client_recipient.start( + query=""" subscription on_message_sent { on_message_sent { message } } - """ - ), - "variables": {}, - "operationName": "on_message_sent", - }, + """, + operation_name="on_message_sent", ) await client_recipient.assert_no_messages() print("Trigger the subscription by mutation to receive notification.") message = f"Hi! {str(uuid.uuid4().hex)}" - msg_id = await client_sender.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + msg_id = await client_sender.start( + query=""" mutation send_message($message: String!) { send_message(message: $message) { success } } - """ - ), - "variables": {"message": message}, - "operationName": "send_message", - }, + """, + variables={"message": message}, + operation_name="send_message", ) # Mutation response. - resp = await client_sender.receive(assert_id=msg_id, assert_type="data") + resp = await client_sender.receive_next(msg_id) assert resp["data"] == {"send_message": {"success": True}} - await client_sender.receive(assert_id=msg_id, assert_type="complete") + await client_sender.receive_complete(msg_id) # Subscription notification. - resp = await client_recipient.receive(assert_id=sub_id, assert_type="data") + resp = await client_recipient.receive_next(sub_id) data = resp["data"]["on_message_sent"] assert data["message"] == message, "Subscription notification contains wrong data!" print("Trigger sequence of timestamps with delayed publish.") count = 10 - msg_id = await client_sender.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + msg_id = await client_sender.start( + query=""" mutation send_timestamps($count: Int!) { send_timestamps(count: $count) { success } } - """ - ), - "variables": {"count": count}, - "operationName": "send_timestamps", - }, + """, + variables={"count": count}, + operation_name="send_timestamps", ) # Mutation response. - resp = await client_sender.receive(assert_id=msg_id, assert_type="data") + resp = await client_sender.receive_next(msg_id) assert resp["data"] == {"send_timestamps": {"success": True}} - await client_sender.receive(assert_id=msg_id, assert_type="complete") + await client_sender.receive_complete(msg_id) timestamps = [] for _ in range(count): - resp = await client_recipient.receive(assert_id=sub_id, assert_type="data") + resp = await client_recipient.receive_next(sub_id) data = resp["data"]["on_message_sent"] timestamps.append(data["message"]) assert timestamps == sorted( @@ -152,8 +142,9 @@ async def test_broadcast(gql): await client_recipient.finalize() +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) @pytest.mark.asyncio -async def test_subscribe_unsubscribe(gql): +async def test_subscribe_unsubscribe(gql, subprotocol): """Test that the asynchronous `unsubscribe()` works. Test the server able to handle client unsubscribe before connection @@ -168,25 +159,19 @@ async def test_subscribe_unsubscribe(gql): # Test subscription notifications order, even with disabled ordering # notifications must be send in the order they were broadcasted. - client = gql(mutation=Mutation, subscription=Subscription) + client = gql(mutation=Mutation, subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() print("Subscribe to GraphQL subscription.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription on_message_sent { on_message_sent { message } } - """ - ), - "variables": {}, - "operationName": "on_message_sent", - }, + """, + operation_name="on_message_sent", ) - await client.send(msg_id=sub_id, msg_type="stop") + await client.complete(sub_id) # If server was not able to handle unsubscribe command, then test # will hang here. await client.receive(assert_type="complete") diff --git a/tests/test_basic.py b/tests/test_basic.py index 602119b..706e43e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -25,7 +25,6 @@ # NOTE: In this file we use `strict_ordering=True` to simplify testing. import json -import textwrap import uuid import graphene @@ -35,7 +34,8 @@ @pytest.mark.asyncio -async def test_main_usecase(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_main_usecase(gql, subprotocol): """Test main use-case with the GraphQL over WebSocket.""" print("Establish & initialize WebSocket GraphQL connection.") @@ -44,67 +44,52 @@ async def test_main_usecase(gql): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Make simple GraphQL query and check the response.") - msg_id = await client.send( - msg_type="start", - payload={ - "query": "query op_name { value }", - "variables": {}, - "operationName": "op_name", - }, + msg_id = await client.start( + query="query op_name { value }", operation_name="op_name" ) - resp = await client.receive(assert_id=msg_id, assert_type="data") + resp = await client.receive_next(msg_id) assert resp["data"]["value"] == Query.VALUE - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_complete(msg_id) print("Subscribe to GraphQL subscription.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription op_name { on_chat_message_sent(user_id: ALICE) { event } } - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) await client.assert_no_messages() print("Trigger the subscription by mutation to receive notification.") message = f"Hi! {str(uuid.uuid4().hex)}" - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + msg_id = await client.start( + query=""" mutation op_name($message: String!) { send_chat_message(message: $message) { message } } - """ - ), - "variables": {"message": message}, - "operationName": "op_name", - }, + """, + variables={"message": message}, + operation_name="op_name", ) # Mutation response. - resp = await client.receive(assert_id=msg_id, assert_type="data") + resp = await client.receive_next(msg_id) assert resp["data"] == {"send_chat_message": {"message": message}} - resp = await client.receive(assert_id=msg_id, assert_type="complete") + resp = await client.receive_complete(msg_id) print("Check that subscription notification were sent.") # Subscription notification. - resp = await client.receive(assert_id=sub_id, assert_type="data") + resp = await client.receive_next(sub_id) event = resp["data"]["on_chat_message_sent"]["event"] assert json.loads(event) == { # pylint: disable=no-member @@ -117,11 +102,12 @@ async def test_main_usecase(gql): @pytest.mark.asyncio -async def test_subscribe_unsubscribe(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_subscribe_unsubscribe(gql, subprotocol): """Test subscribe-unsubscribe behavior with the GraphQL over WebSocket. 0. Subscribe to GraphQL subscription: messages for Alice. - 1. Send STOP message and unsubscribe. + 1. Send COMPLETE/STOP message and unsubscribe. 2. Subscribe to GraphQL subscription: messages for Tom. 3. Call unsubscribe method of the Subscription instance (via `kick_out_user` mutation). @@ -135,77 +121,56 @@ async def test_subscribe_unsubscribe(gql): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe to GraphQL subscription.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription op_name { on_chat_message_sent(user_id: ALICE) { event } } - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) print("Stop subscription by id.") - await client.send(msg_id=sub_id, msg_type="stop") - await client.receive(assert_id=sub_id, assert_type="complete") + await client.complete(sub_id) + await client.receive_complete(sub_id) print("Subscribe to GraphQL subscription.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription op_name { on_chat_message_sent(user_id: TOM) { event } } - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) print("Stop all subscriptions for TOM.") - msg_id = await client.send( - msg_type="start", - payload={ - "query": "mutation op_name { kick_out_user(user_id: TOM) { success } }", - "variables": {}, - "operationName": "op_name", - }, + msg_id = await client.start( + query="mutation op_name { kick_out_user(user_id: TOM) { success } }", + operation_name="op_name", ) # Mutation & unsubscription responses. - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") - await client.receive(assert_id=sub_id, assert_type="complete") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) + await client.receive_complete(sub_id) print("Trigger the subscription by mutation to receive notification.") - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + msg_id = await client.start( + query=""" mutation op_name { send_chat_message(message: "Is there anybody here?") { message } } - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) # Mutation response. - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) # Check notifications: there are no notifications! Previously, # we have unsubscribed from all subscriptions. @@ -218,7 +183,8 @@ async def test_subscribe_unsubscribe(gql): @pytest.mark.asyncio -async def test_subscription_groups(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_subscription_groups(gql, subprotocol): """Test notifications behavior with different subscription group. Test notifications and subscriptions behavior depending on the @@ -252,26 +218,22 @@ async def create_and_subscribe(user_id): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "confirm_subscriptions": True}, + subprotocol=subprotocol, ) await client.connect_and_init() - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription op_name($user_id: UserId) { on_chat_message_sent(user_id: $user_id) { event } } - """ - ), - "variables": {"user_id": user_id}, - "operationName": "op_name", - }, + """, + variables={"user_id": user_id}, + operation_name="op_name", ) # Receive the subscription confirmation message. - resp = await client.receive(assert_id=sub_id, assert_type="data") + resp = await client.receive_next(sub_id) assert resp == {"data": None} return sub_id, client @@ -285,25 +247,20 @@ async def trigger_subscription(client, user_id, message): message: Any string message. """ - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + msg_id = await client.start( + query=""" mutation op_name($message: String!, $user_id: UserId) { send_chat_message(message: $message, user_id: $user_id) { message } } - """ - ), - "variables": {"message": message, "user_id": user_id}, - "operationName": "op_name", - }, + """, + variables={"message": message, "user_id": user_id}, + operation_name="op_name", ) # Mutation response. - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) def check_resp(resp, user_id, message): """Check the response from `on_chat_message_sent` subscription. @@ -331,7 +288,7 @@ def check_resp(resp, user_id, message): message = "Hi, Tom!" await trigger_subscription(comm_alice, tom_id, message) # Check Tom's notifications. - resp = await comm_tom.receive(assert_id=uid_tom, assert_type="data") + resp = await comm_tom.receive_next(uid_tom) check_resp(resp, UserId[tom_id].value, message) # type: ignore[misc] # Any other did not receive any notifications. await comm_alice.assert_no_messages() @@ -339,7 +296,7 @@ def check_resp(resp, user_id, message): message = "Hi, Alice!" await trigger_subscription(comm_tom, alice_id, message) # Check Alice's notifications. - resp = await comm_alice.receive(assert_id=uid_alice, assert_type="data") + resp = await comm_alice.receive_next(uid_alice) check_resp(resp, UserId[alice_id].value, message) # type: ignore[misc] # Any other did not receive any notifications. await comm_tom.assert_no_messages() @@ -349,9 +306,9 @@ def check_resp(resp, user_id, message): await trigger_subscription(comm_tom, None, message) print("Check Tom's and Alice's notifications.") - resp = await comm_tom.receive(assert_id=uid_tom, assert_type="data") + resp = await comm_tom.receive_next(uid_tom) check_resp(resp, UserId[tom_id].value, message) # type: ignore[misc] - resp = await comm_alice.receive(assert_id=uid_alice, assert_type="data") + resp = await comm_alice.receive_next(uid_alice) check_resp(resp, UserId[alice_id].value, message) # type: ignore[misc] print("Disconnect and wait the application to finish gracefully.") @@ -360,29 +317,32 @@ def check_resp(resp, user_id, message): @pytest.mark.asyncio -async def test_keepalive(gql): - """Test that server sends keepalive messages.""" +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_ping(gql, subprotocol): + """Test that server sends ping(keepalive) messages.""" print("Establish & initialize WebSocket GraphQL connection.") client = gql( query=Query, mutation=Mutation, subscription=Subscription, - consumer_attrs={"strict_ordering": True, "send_keepalive_every": 0.05}, + consumer_attrs={"strict_ordering": True, "send_ping_every": 0.05}, + subprotocol=subprotocol, ) await client.connect_and_init() - async def receive_keep_alive(): + async def receive_ping(): response = await client.transport.receive() - assert response["type"] == "ka", "Non keep alive response received!" + assert response["type"] in ("ping", "ka"), "Non ping(ka) response received!" - await receive_keep_alive() - print("Receive several keepalive messages.") + await receive_ping() + print("Receive several ping messages.") for _ in range(3): - await receive_keep_alive() + await receive_ping() - print("Send connection termination message.") - await client.send(msg_id=None, msg_type="connection_terminate") + if subprotocol == "graphql-ws": + print("Send connection termination message.") + await client.send_raw_message({"type": "connection_terminate"}) print("Disconnect and wait the application to finish gracefully.") await client.finalize() diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py index f57c41a..0f32f91 100644 --- a/tests/test_concurrent.py +++ b/tests/test_concurrent.py @@ -39,50 +39,43 @@ @pytest.mark.asyncio -async def test_concurrent_queries(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_concurrent_queries(gql, subprotocol): """Check a single hanging operation does not block other ones.""" global WAKEUP # pylint: disable=global-statement WAKEUP = asyncio.Event() print("Establish & initialize WebSocket GraphQL connection.") - client = gql(query=Query, mutation=Mutation) + client = gql(query=Query, mutation=Mutation, subprotocol=subprotocol) await client.connect_and_init() print("Invoke a long operation which waits for the wakeup even.") # Since tests and server are launched using same eventloop we should # not await for response here. - long_op_id = await client.send( - msg_type="start", - payload={ - "query": "mutation op_name { long_op { is_ok } }", - "variables": {}, - "operationName": "op_name", - }, + long_op_id = await client.start( + query="mutation op_name { long_op { is_ok } }", + operation_name="op_name", ) await client.assert_no_messages() print("Make several fast operations to check they are not blocked by the long one.") for _ in range(3): - fast_op_id = await client.send( - msg_type="start", - payload={ - "query": "query op_name { fast_op_sync }", - "variables": {}, - "operationName": "op_name", - }, + fast_op_id = await client.start( + query="query op_name { fast_op_sync }", + operation_name="op_name", ) - resp = await client.receive(assert_id=fast_op_id, assert_type="data") + resp = await client.receive_next(fast_op_id) assert resp["data"] == {"fast_op_sync": True} - await client.receive(assert_id=fast_op_id, assert_type="complete") + await client.receive_complete(fast_op_id) print("Trigger the wakeup event to let long operation finish.") WAKEUP.set() - resp = await client.receive(assert_id=long_op_id, assert_type="data") + resp = await client.receive_next(long_op_id) assert "errors" not in resp assert resp["data"] == {"long_op": {"is_ok": True}} - await client.receive(assert_id=long_op_id, assert_type="complete") + await client.receive_complete(long_op_id) print("Disconnect and wait the application to finish gracefully.") await client.assert_no_messages( @@ -95,7 +88,8 @@ async def test_concurrent_queries(gql): @pytest.mark.asyncio @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) @pytest.mark.parametrize("requests_number", [1, 10, 100, 1000]) -async def test_heavy_load(gql, sync_resolvers, requests_number): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_heavy_load(gql, sync_resolvers, requests_number, subprotocol): """Test that server correctly processes many simultaneous requests. Send many requests simultaneously and make sure all of them have @@ -103,13 +97,14 @@ async def test_heavy_load(gql, sync_resolvers, requests_number): """ # Name of GraphQL Query used in this test. + query = "" if sync_resolvers == "sync": query = "fast_op_sync" elif sync_resolvers == "async": query = "fast_op_async" print("Establish & initialize WebSocket GraphQL connection.") - client = gql(query=Query) + client = gql(query=Query, subprotocol=subprotocol) await client.connect_and_init() print(f"Send {requests_number} requests and check {requests_number*2} responses.") @@ -119,18 +114,16 @@ async def test_heavy_load(gql, sync_resolvers, requests_number): for _ in range(requests_number): op_id = uuid.uuid4().hex send_waitlist += [ - client.send( + client.start( msg_id=op_id, - msg_type="start", - payload={ - "query": f"query op_name {{ {query} }}", - "variables": {}, - "operationName": "op_name", - }, + query=f"query op_name {{ {query} }}", + operation_name="op_name", ) ] # Expect two messages for each one we have sent. - expected_responses.add((op_id, "data")) + expected_responses.add( + (op_id, "next" if subprotocol == "graphql-transport-ws" else "data") + ) expected_responses.add((op_id, "complete")) receive_waitlist += [client.transport.receive(), client.transport.receive()] @@ -145,7 +138,9 @@ async def test_heavy_load(gql, sync_resolvers, requests_number): for response in (r.result() for r in responses): expected_responses.remove((response["id"], response["type"])) - if response["type"] == "data": + if response["type"] == ( + "next" if subprotocol == "graphql-transport-ws" else "data" + ): assert "errors" not in response["payload"] assert not expected_responses, "Not all expected responses received!" @@ -158,18 +153,22 @@ async def test_heavy_load(gql, sync_resolvers, requests_number): @pytest.mark.asyncio @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) -async def test_unsubscribe_one_of_many_subscriptions(gql, sync_resolvers): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_unsubscribe_one_of_many_subscriptions(gql, sync_resolvers, subprotocol): """Check that single unsubscribe does not kill other subscriptions. 0. Subscribe to the subscription twice. 1. Subscribe to the same subscription from another communicator. - 2. Send STOP message for the first subscription to unsubscribe. + 2. Send COMPLETE/STOP message for the first subscription + to unsubscribe. 3. Execute some mutation. 4. Check subscription notifications: there are notifications from the second and the third subscription. """ # Names of GraphQL mutation and subscription used in this test. + subscription = "" + mutation = "" if sync_resolvers == "sync": mutation = "send_chat_message_sync" subscription = "on_chat_message_sent_sync" @@ -183,85 +182,64 @@ async def test_unsubscribe_one_of_many_subscriptions(gql, sync_resolvers): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) client_new = gql( query=Query, mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() await client_new.connect_and_init() print("Subscribe to GraphQL subscription with the same subscription group.") - sub_id_1 = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id_1 = await client.start( + query=f""" subscription op_name {{ {subscription}(user_id: ALICE) {{ event }} }} - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) - sub_id_2 = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id_2 = await client.start( + query=f""" subscription op_name {{ {subscription}(user_id: ALICE) {{ event }} }} - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) - sub_id_new = await client_new.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id_new = await client_new.start( + query=f""" subscription op_name {{ {subscription}(user_id: ALICE) {{ event }} }} - """ - ), - "variables": {}, - "operationName": "op_name", - }, + """, + operation_name="op_name", ) print("Stop the first subscription by id.") - await client.send(msg_id=sub_id_1, msg_type="stop") - await client.receive(assert_id=sub_id_1, assert_type="complete") + await client.complete(sub_id_1) + await client.receive_complete(sub_id_1) print("Trigger the subscription by mutation to receive notifications.") message = "HELLO WORLD" - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + msg_id = await client.start( + query=f""" mutation op_name($message: String!, $user_id: UserId) {{ {mutation}(message: $message, user_id: $user_id) {{ message }} }} - """ - ), - "variables": {"message": message, "user_id": "ALICE"}, - "operationName": "op_name", - }, + """, + variables={"message": message, "user_id": "ALICE"}, + operation_name="op_name", ) # Mutation response. - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) # Check responses from subscriptions. - res = await client.receive(assert_id=sub_id_2, assert_type="data") + res = await client.receive_next(sub_id_2) assert ( message in res["data"][subscription]["event"] ), "Wrong response for second subscriber!" - res = await client_new.receive(assert_id=sub_id_new, assert_type="data") + res = await client_new.receive_next(sub_id_new) assert ( message in res["data"][subscription]["event"] ), "Wrong response for third subscriber!" @@ -280,8 +258,9 @@ async def test_unsubscribe_one_of_many_subscriptions(gql, sync_resolvers): @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) @pytest.mark.parametrize("confirm_subscriptions", [False, True]) @pytest.mark.parametrize("strict_ordering", [False, True]) +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) async def test_subscribe_and_many_unsubscribes( - gql, confirm_subscriptions, strict_ordering, sync_resolvers + gql, confirm_subscriptions, strict_ordering, sync_resolvers, subprotocol ): """Check single subscribe and many unsubscribes run in parallel. @@ -297,6 +276,8 @@ async def test_subscribe_and_many_unsubscribes( """ # Names of GraphQL mutation and subscription used in this test. + subscription = "" + mutation = "" if sync_resolvers == "sync": mutation = "send_chat_message_sync" subscription = "on_chat_message_sent_sync" @@ -313,6 +294,7 @@ async def test_subscribe_and_many_unsubscribes( "confirm_subscriptions": confirm_subscriptions, "strict_ordering": strict_ordering, }, + subprotocol=subprotocol, ) await client.connect_and_init() @@ -321,28 +303,23 @@ async def test_subscribe_and_many_unsubscribes( flag = asyncio.Event() async def subscribe_unsubscribe(client, user_id, op_id: str): - """Subscribe and spam with 'stop' until stop-flag is set.""" + """Subscribe and spam with 'complete'/'stop' until stop-flag is set.""" - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id = await client.start( + query=f""" subscription op_name($user_id: UserId) {{ {subscription}(user_id: $user_id) {{ event }} }} - """ - ), - "variables": {"user_id": user_id}, - "operationName": "op_name", - }, + """, + variables={"user_id": user_id}, + operation_name="op_name", msg_id=op_id, ) assert sub_id == op_id - # Multiple stop messages. + # Multiple complete/stop messages. while True: - await client.send(msg_id=op_id, msg_type="stop") + await client.complete(op_id) await asyncio.sleep(0.01) if flag.is_set(): break @@ -359,7 +336,11 @@ async def receiver(op_ids): if resp["type"] == "complete": op_ids.remove(op_id) else: - assert resp["type"] == "data" and resp["payload"]["data"] is None, ( + assert ( + resp["type"] == "next" + if subprotocol == "graphql-transport-ws" + else "data" and resp["payload"]["data"] is None + ), ( "This should be a successful subscription message, not '%s'", resp, ) @@ -414,25 +395,20 @@ async def receiver(op_ids): print("Trigger the subscription by mutation.") message = "HELLO WORLD" - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + msg_id = await client.start( + query=f""" mutation op_name($message: String!, $user_id: UserId) {{ {mutation}(message: $message, user_id: $user_id) {{ message }} }} - """ - ), - "variables": {"message": message, "user_id": "ALICE"}, - "operationName": "op_name", - }, + """, + variables={"message": message, "user_id": "ALICE"}, + operation_name="op_name", ) # Mutation response. - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) # Check notifications: there are no notifications. We unsubscribed # from all subscriptions and received all messages. @@ -445,8 +421,13 @@ async def receiver(op_ids): @pytest.mark.asyncio @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) @pytest.mark.parametrize("strict_ordering", [False, True]) +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) async def test_message_order_in_subscribe_unsubscribe_loop( - gql, strict_ordering, sync_resolvers, confirm_subscriptions=True + gql, + strict_ordering, + sync_resolvers, + subprotocol, + confirm_subscriptions=True, ): """Check an order of messages in the subscribe-unsubscribe loop. @@ -456,18 +437,20 @@ async def test_message_order_in_subscribe_unsubscribe_loop( before the message about the successful unsubscribe. So test: - 1) Send subscribe message and many unsubscribe 'stop' messages. + 1) Send subscribe message and many unsubscribe + 'complete'/'stop' messages. 2) Check the order of the confirmation message and the - 'complete' message. + 'complete' message. """ - NUMBER_OF_STOP_MESSAGES = 42 # pylint: disable=invalid-name + NUMBER_OF_COMPLETE_MESSAGES = 42 # pylint: disable=invalid-name # Delay in seconds. - DELAY_BETWEEN_STOP_MESSAGES = 0.001 # pylint: disable=invalid-name + DELAY_BETWEEN_COMPLETE_MESSAGES = 0.001 # pylint: disable=invalid-name # Gradually stop the test if time is up. TIME_LIMIT_SECS = 16 # pylint: disable=invalid-name # Names of GraphQL mutation and subscription used in this test. + subscription = "" if sync_resolvers == "sync": subscription = "on_chat_message_sent_sync" elif sync_resolvers == "async": @@ -482,36 +465,34 @@ async def test_message_order_in_subscribe_unsubscribe_loop( "confirm_subscriptions": confirm_subscriptions, "strict_ordering": strict_ordering, }, + subprotocol=subprotocol, ) await client.connect_and_init() async def subscribe_unsubscribe(user_id="TOM"): - """Subscribe and spam with 'stop'.""" + """Subscribe and spam with 'complete'/'stop'.""" - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id = await client.start( + query=f""" subscription op_name($user_id: UserId) {{ {subscription}(user_id: $user_id) {{ event }} }} - """ - ), - "variables": {"user_id": user_id}, - "operationName": "op_name", - }, + """, + variables={"user_id": user_id}, + operation_name="op_name", ) - # Spam with stop messages. - for _ in range(NUMBER_OF_STOP_MESSAGES): - await client.send(msg_id=sub_id, msg_type="stop") - await asyncio.sleep(DELAY_BETWEEN_STOP_MESSAGES) + # Spam with complete/stop messages. + for _ in range(NUMBER_OF_COMPLETE_MESSAGES): + await client.complete(sub_id) + await asyncio.sleep(DELAY_BETWEEN_COMPLETE_MESSAGES) resp = await client.receive(raw_response=True) assert sub_id == resp["id"] assert ( - resp["type"] == "data" and resp["payload"]["data"] is None + resp["type"] == "next" + if subprotocol == "graphql-transport-ws" + else "data" and resp["payload"]["data"] is None ), "First we expect to get a confirmation message!" resp = await client.receive(raw_response=True) @@ -545,8 +526,9 @@ async def subscribe_unsubscribe(user_id="TOM"): @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) @pytest.mark.parametrize("confirm_subscriptions", [False, True]) @pytest.mark.parametrize("strict_ordering", [False, True]) +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) async def test_message_order_in_broadcast_unsubscribe_loop( - gql, confirm_subscriptions, strict_ordering, sync_resolvers + gql, confirm_subscriptions, strict_ordering, sync_resolvers, subprotocol ): """Check an order of messages in the broadcast-unsubscribe cycle. @@ -564,15 +546,17 @@ async def test_message_order_in_broadcast_unsubscribe_loop( # Count of spam messages per connection. NUMBER_OF_MUTATION_MESSAGES = 50 # pylint: disable=invalid-name - # When 40 spam messages are sent, we will send the 'stop' + # When 40 spam messages are sent, we will send the 'complete'/'stop' # subscription message. - MUTATION_INDEX_TO_SEND_STOP = 40 # pylint: disable=invalid-name + MUTATION_INDEX_TO_SEND_COMPLETE = 40 # pylint: disable=invalid-name # Gradually stop the test if time is up. TIME_BORDER = 20 # pylint: disable=invalid-name # The timeout after which we suppose that all messages are consumed. NOTHING_RECEIVED_TIMEOUT = 1 # pylint: disable=invalid-name # Names of GraphQL mutation and subscription used in this test. + subscription = "" + mutation = "" if sync_resolvers == "sync": mutation = "send_chat_message_sync" subscription = "on_chat_message_sent_sync" @@ -589,6 +573,7 @@ async def test_message_order_in_broadcast_unsubscribe_loop( "confirm_subscriptions": confirm_subscriptions, "strict_ordering": strict_ordering, }, + subprotocol=subprotocol, ) await client.connect_and_init() @@ -598,26 +583,21 @@ async def test_message_order_in_broadcast_unsubscribe_loop( "confirm_subscriptions": confirm_subscriptions, "strict_ordering": strict_ordering, }, + subprotocol=subprotocol, ) await client_spamer.connect_and_init() async def subscribe_unsubscribe(iteration: int): """Subscribe and spam notifications from 2 different clients.""" - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id = await client.start( + query=f""" subscription op_name($user_id: UserId) {{ {subscription}(user_id: $user_id) {{ event }} }} - """ - ), - "variables": {"user_id": "ALICE"}, - "operationName": "op_name", - }, - msg_id=f"sub_{iteration} {uuid.uuid4().hex}", + """, + variables={"user_id": "ALICE"}, + operation_name="op_name", ) spam_payload = { @@ -634,21 +614,22 @@ async def subscribe_unsubscribe(iteration: int): "message": "__SPAM_SPAM_SPAM_SPAM_SPAM_SPAM__", "user_id": "ALICE", }, - "operationName": "op_name", } # Spam with broadcast messages. for index in range(NUMBER_OF_MUTATION_MESSAGES): - if index == MUTATION_INDEX_TO_SEND_STOP: - await client.send(msg_id=sub_id, msg_type="stop") - await client_spamer.send( - msg_type="start", - payload=spam_payload, + if index == MUTATION_INDEX_TO_SEND_COMPLETE: + await client.complete(sub_id) + await client_spamer.start( + query=spam_payload["query"], + variables=spam_payload["variables"], + operation_name="op_name", msg_id=f"mut_spammer_{iteration}_{index}_{uuid.uuid4().hex}", ) - await client.send( - msg_type="start", - payload=spam_payload, + await client.start( + query=spam_payload["query"], + variables=spam_payload["variables"], + operation_name="op_name", msg_id=f"mut_{iteration}_{index}_{uuid.uuid4().hex}", ) @@ -682,11 +663,12 @@ async def subscribe_unsubscribe(iteration: int): print("Subscribe-unsubscribe iterations done.") # We have unsubscribed from all the subscriptions and received all - # 'data' messages. + # 'next'/'data' messages. while True: try: resp = await asyncio.wait_for( - client.receive(raw_response=True), timeout=NOTHING_RECEIVED_TIMEOUT + client.receive(raw_response=True), + timeout=NOTHING_RECEIVED_TIMEOUT, ) except asyncio.TimeoutError: # Ok, there are no messages. @@ -710,8 +692,9 @@ async def subscribe_unsubscribe(iteration: int): @pytest.mark.asyncio @pytest.mark.parametrize("sync_resolvers", ["sync", "async"]) @pytest.mark.parametrize("strict_ordering", [False, True]) +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) async def test_message_order_in_subscribe_unsubscribe_all_loop( - gql, strict_ordering, sync_resolvers, confirm_subscriptions=True + gql, strict_ordering, sync_resolvers, subprotocol, confirm_subscriptions=True ): """Check an order of messages in the subscribe-unsubscribe all loop. @@ -734,6 +717,7 @@ async def test_message_order_in_subscribe_unsubscribe_all_loop( TIME_BORDER = 20 # pylint: disable=invalid-name # Name of GraphQL subscription used in this test. + subscription = "" if sync_resolvers == "sync": subscription = "on_chat_message_sent_sync" elif sync_resolvers == "async": @@ -748,6 +732,7 @@ async def test_message_order_in_subscribe_unsubscribe_all_loop( "confirm_subscriptions": confirm_subscriptions, "strict_ordering": strict_ordering, }, + subprotocol=subprotocol, ) await client.connect_and_init() @@ -755,25 +740,20 @@ async def test_message_order_in_subscribe_unsubscribe_all_loop( pool = concurrent.futures.ThreadPoolExecutor() async def subscribe_unsubscribe(user_id="TOM"): - """Subscribe and spam with 'stop' by the sync 'unsubscribe'.""" + """Subscribe and spam with 'complete'/'stop' by the sync 'unsubscribe'.""" # Just subscribe. - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - f""" + sub_id = await client.start( + query=f""" subscription op_name($user_id: UserId) {{ {subscription}(user_id: $user_id) {{ event }} }} - """ - ), - "variables": {"user_id": user_id}, - "operationName": "op_name", - }, + """, + variables={"user_id": user_id}, + operation_name="op_name", ) - # Spam with stop messages (unsubscribe all behavior). + # Spam with complete/stop messages (unsubscribe all behavior). if sync_resolvers == "sync": def unsubscribe_all(): @@ -791,7 +771,9 @@ def unsubscribe_all(): resp = await client.receive(raw_response=True) assert sub_id == resp["id"] assert ( - resp["type"] == "data" and resp["payload"]["data"] is None + resp["type"] == "next" + if subprotocol == "graphql-transport-ws" + else "data" and resp["payload"]["data"] is None ), "First we expect to get a confirmation message!" resp = await client.receive(raw_response=True) diff --git a/tests/test_confirm_subscriptions.py b/tests/test_confirm_subscriptions.py index a7f7b42..d979b9d 100644 --- a/tests/test_confirm_subscriptions.py +++ b/tests/test_confirm_subscriptions.py @@ -32,7 +32,8 @@ @pytest.mark.asyncio -async def test_confirmation_enabled(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_confirmation_enabled(gql, subprotocol): """Test subscription confirmation message received when enabled.""" print("Establish WebSocket GraphQL connections with subscription confirmation.") @@ -41,37 +42,30 @@ async def test_confirmation_enabled(gql): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "confirm_subscriptions": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe & check there is a subscription confirmation message.") - sub_op_id = await client.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_trigger { is_ok } }", - "operationName": "op_name", - }, + sub_op_id = await client.start( + query="subscription op_name { on_trigger { is_ok } }", operation_name="op_name" ) - resp = await client.receive(assert_id=sub_op_id, assert_type="data") + resp = await client.receive_next(sub_op_id) assert resp == {"data": None} print("Trigger the subscription.") - mut_op_id = await client.send( - msg_type="start", - payload={ - "query": """mutation op_name { trigger { is_ok } }""", - "operationName": "op_name", - }, + mut_op_id = await client.start( + query="mutation op_name { trigger { is_ok } }", operation_name="op_name" ) - await client.receive(assert_id=mut_op_id, assert_type="data") - await client.receive(assert_id=mut_op_id, assert_type="complete") + await client.receive_next(mut_op_id) + await client.receive_complete(mut_op_id) print("Check that subscription notification received.") - resp = await client.receive(assert_id=sub_op_id, assert_type="data") + resp = await client.receive_next(sub_op_id) assert resp["data"]["on_trigger"]["is_ok"] is True await client.assert_no_messages( @@ -81,7 +75,8 @@ async def test_confirmation_enabled(gql): @pytest.mark.asyncio -async def test_confirmation_disabled(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_confirmation_disabled(gql, subprotocol): """Test subscription confirmation message absent when disabled.""" print("Establish WebSocket GraphQL connections w/o a subscription confirmation.") @@ -90,36 +85,30 @@ async def test_confirmation_disabled(gql): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "confirm_subscriptions": False}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe & check there is no subscription confirmation message.") - sub_op_id = await client.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_trigger { is_ok } }", - "operationName": "op_name", - }, + sub_op_id = await client.start( + query="subscription op_name { on_trigger { is_ok } }", + operation_name="op_name", ) await client.assert_no_messages("Subscribe responded with a message!") print("Trigger the subscription.") - mut_op_id = await client.send( - msg_type="start", - payload={ - "query": """mutation op_name { trigger { is_ok } }""", - "operationName": "op_name", - }, + mut_op_id = await client.start( + query="mutation op_name { trigger { is_ok } }", operation_name="op_name" ) - await client.receive(assert_id=mut_op_id, assert_type="data") - await client.receive(assert_id=mut_op_id, assert_type="complete") + await client.receive_next(mut_op_id) + await client.receive_complete(mut_op_id) print("Check that subscription notification received.") - resp = await client.receive(assert_id=sub_op_id, assert_type="data") + resp = await client.receive_next(sub_op_id) assert resp == {"data": {"on_trigger": {"is_ok": True}}} await client.assert_no_messages( @@ -129,7 +118,8 @@ async def test_confirmation_disabled(gql): @pytest.mark.asyncio -async def test_custom_confirmation_message(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_custom_confirmation_message(gql, subprotocol): """Test custom confirmation message.""" print("Establish WebSocket GraphQL connections with a custom confirmation message.") @@ -148,21 +138,18 @@ async def test_custom_confirmation_message(gql): "errors": [expected_error], }, }, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe & check there is a subscription confirmation message.") - sub_op_id = await client.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_trigger { is_ok } }", - "operationName": "op_name", - }, + sub_op_id = await client.start( + query="subscription op_name { on_trigger { is_ok } }", operation_name="op_name" ) with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as ex: - await client.receive(assert_id=sub_op_id, assert_type="data") + await client.receive_next(sub_op_id) expected_errors = [ { "message": f"{type(expected_error).__name__}: {expected_error}", @@ -177,19 +164,15 @@ async def test_custom_confirmation_message(gql): print("Trigger the subscription.") - mut_op_id = await client.send( - msg_type="start", - payload={ - "query": """mutation op_name { trigger { is_ok } }""", - "operationName": "op_name", - }, + mut_op_id = await client.start( + query="mutation op_name { trigger { is_ok } }", operation_name="op_name" ) - await client.receive(assert_id=mut_op_id, assert_type="data") - await client.receive(assert_id=mut_op_id, assert_type="complete") + await client.receive_next(mut_op_id) + await client.receive_complete(mut_op_id) print("Check that subscription notification received.") - resp = await client.receive(assert_id=sub_op_id, assert_type="data") + resp = await client.receive_next(sub_op_id) assert resp["data"]["on_trigger"]["is_ok"] is True await client.assert_no_messages( diff --git a/tests/test_context.py b/tests/test_context.py index cc31722..390f828 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -67,7 +67,8 @@ def test_dict__as_object(): @pytest.mark.asyncio -async def test_context_lifetime(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_context_lifetime(gql, subprotocol): """Check `info.context` does hold data between requests.""" # Store ids of `info.context` to check them later. @@ -92,12 +93,16 @@ def resolve_ok(self, info): for _ in range(2): print("Make connection,perform query, and close connection.") - client = gql(query=Query, consumer_attrs={"strict_ordering": True}) + client = gql( + query=Query, + consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, + ) await client.connect_and_init() for _ in range(2): - await client.send(msg_type="start", payload={"query": "{ ok }"}) - await client.receive(assert_type="data") - await client.receive(assert_type="complete") + op_id = await client.start(query="{ ok }") + await client.receive_next(op_id) + await client.receive_complete(op_id) await client.finalize() # Expected run log: [False, False, False, False]. @@ -105,7 +110,8 @@ def resolve_ok(self, info): @pytest.mark.asyncio -async def test_context_channels_scope_lifetime(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_context_channels_scope_lifetime(gql, subprotocol): """Check `info.context.channels_scope` holds data in connection.""" # Store ids of `info.context.channels_scope` to check them later. @@ -132,12 +138,16 @@ def resolve_ok(self, info): for _ in range(2): print("Make connection,perform query, and close connection.") - client = gql(query=Query, consumer_attrs={"strict_ordering": True}) + client = gql( + query=Query, + consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, + ) await client.connect_and_init() for _ in range(2): - await client.send(msg_type="start", payload={"query": "{ ok }"}) - await client.receive(assert_type="data") - await client.receive(assert_type="complete") + op_id = await client.start(query="{ ok }") + await client.receive_next(op_id) + await client.receive_complete(op_id) await client.finalize() # Expected run log: [False, True, False, True]. diff --git a/tests/test_error.py b/tests/test_error.py index 966868d..282e96d 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -23,7 +23,6 @@ # NOTE: In this file we use `strict_ordering=True` to simplify testing. -import textwrap import uuid import graphene @@ -33,91 +32,74 @@ @pytest.mark.asyncio -async def test_error_cases(gql): - """Test that server responds correctly when errors happen. - - Check that server responds with message of type `data` when there - is a syntax error in the request or the exception in a resolver - was raised. Check that server responds with message of type `error` - when there was an exceptional situation, for example, field `query` - of `payload` is missing or field `type` has a wrong value. - """ - - print("Setup test GraphQL backend.") - - class Query(graphene.ObjectType): - """Root GraphQL query.""" - - VALUE = str(uuid.uuid4().hex) - value = graphene.String( - args={"issue_error": graphene.Boolean(default_value=False)} - ) - - def resolve_value(self, info, issue_error): - """Resolver to return predefined value which can be tested.""" - del info - assert self is None, "Root `self` expected to be `None`!" - if issue_error: - raise RuntimeError(Query.VALUE) - return Query.VALUE +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_syntax_error(gql, subprotocol): + """Test that server responds correctly on syntax error(s).""" print("Establish & initialize WebSocket GraphQL connection.") - client = gql(query=Query) + client = gql(query=Query, subprotocol=subprotocol) await client.connect_and_init() - print("Check that query syntax error leads to the `error` response.") - msg_id = await client.send( - msg_type="wrong_type__(ツ)_/¯", payload={"variables": {}, "operationName": ""} - ) - with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as exc_info: - await client.receive(assert_id=msg_id, assert_type="error") - payload = exc_info.value.response["payload"] - assert len(payload["errors"]) == 1, "Multiple errors received instead of one!" - assert isinstance(payload["errors"][0], dict), "Error must be of dict type!" - assert isinstance( - payload["errors"][0]["message"], str - ), "Error's message must be of str type!" - assert ( - payload["errors"][0]["extensions"]["code"] == "Exception" - ), "Error must have 'code' field." - print( "Check that query syntax error leads to the `data` response " "with `errors` array." ) - msg_id = await client.send( - msg_type="start", - payload={ - "query": "This produces a syntax error!", - "variables": {}, - "operationName": "", - }, - ) - with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as exc_info: - await client.receive(assert_id=msg_id, assert_type="data") + msg_id = await client.start(query="This produces a syntax error!") + errors, data = await client.receive_error(msg_id) - payload = exc_info.value.response["payload"] - assert "data" in payload - assert payload["data"] is None - assert len(payload["errors"]) == 1, "Single error expected!" + assert data is None + assert len(errors) == 1, "Single error expected!" assert ( - "message" in payload["errors"][0] and "locations" in payload["errors"][0] + "message" in errors[0] and "locations" in errors[0] ), "Response missing mandatory fields!" - assert payload["errors"][0]["locations"] == [{"line": 1, "column": 1}] - await client.receive(assert_id=msg_id, assert_type="complete") - - print("Check that syntax error leads to the `data` response with `errors` array.") - msg_id = await client.send( - msg_type="start", - payload={ - "query": "query op_name { value(issue_error: true) }", - "variables": {}, - "operationName": "op_name", - }, + assert errors[0]["locations"] == [{"line": 1, "column": 1}] + if subprotocol == "graphql-ws": + await client.receive_complete(msg_id) + + print("Check multiple errors in the `data` message.") + msg_id = await client.start( + query=""" + query { projects { path wrong_field } } + query a { projects } + { wrong_name } + """ + ) + errors, data = await client.receive_error(msg_id) + assert data is None + assert len(errors) == 5, f"Five errors expected, but {len(errors)} errors received!" + assert errors[0]["message"] == errors[3]["message"] + assert "locations" in errors[2], "The `locations` field expected" + assert "locations" in errors[4], "The `locations` field expected" + if subprotocol == "graphql-ws": + await client.receive_complete(msg_id) + + print("Disconnect and wait the application to finish gracefully.") + await client.finalize() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_resolver_error(gql, subprotocol): + """Test that server responds correctly when error in resolver. + + Check that server responds with message of type `next`/`data` with + `errors` array , when the exception in a resolver was raised. + """ + + print("Establish & initialize WebSocket GraphQL connection.") + client = gql(query=Query, subprotocol=subprotocol) + await client.connect_and_init() + + print( + "Check that syntax error leads to the `next`/`data`" + "response with `errors` array." + ) + msg_id = await client.start( + query="query op_name { value(issue_error: true) }", operation_name="op_name" ) with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as exc_info: - await client.receive(assert_id=msg_id, assert_type="data") + await client.receive_next(msg_id) payload = exc_info.value.response["payload"] assert payload["data"]["value"] is None assert len(payload["errors"]) == 1, "Single error expected!" @@ -126,65 +108,97 @@ def resolve_value(self, info, issue_error): assert ( "extensions" not in payload["errors"][0] ), "For syntax error there should be no 'extensions'." - await client.receive(assert_id=msg_id, assert_type="complete") + await client.receive_complete(msg_id) - print("Check multiple errors in the `data` message.") - msg_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ - query { projects { path wrong_field } } - query a { projects } - { wrong_name } - """ - ), - "variables": {}, - "operationName": "", - }, + print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() + await client.finalize() + + +@pytest.mark.asyncio +async def test_connection_init_timeout_error_graphql_transport_ws(gql): + """Test how server works with `connection_init_wait_timeout`. + + Server must close WebSocket connection with code 4408 if connection + was not initialized (client don't send `connection_init` message) + after `connection_init_wait_timeout` seconds. + """ + + print("Establish WebSocket GraphQL connection.") + client = gql( + consumer_attrs={"strict_ordering": True, "connection_init_wait_timeout": 3}, ) - with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as exc_info: - await client.receive(assert_id=msg_id, assert_type="data") - payload = exc_info.value.response["payload"] - assert payload["data"] is None - assert ( - len(payload["errors"]) == 5 - ), f"Five errors expected, but {len(payload['errors'])} errors received!" - assert payload["errors"][0]["message"] == payload["errors"][3]["message"] - assert "locations" in payload["errors"][2], "The `locations` field expected" - assert "locations" in payload["errors"][4], "The `locations` field expected" - await client.receive(assert_id=msg_id, assert_type="complete") + await client.connect_and_init(connect_only=True) + + print( + "Wait until server close connection because client don't send" + "`connection_init` message within 3 seconds." + ) + await client.wait_disconnect(assert_code=4408) print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() await client.finalize() @pytest.mark.asyncio -async def test_connection_error(gql): - """Test that server disconnects user when `on_connect` raises error. +async def test_connection_unauthorized_error_graphql_transport_ws(gql): + """Test how server handles `subscribe` before `connection_ack`. - When GraphqlWsConsumer method `on_connect` raises server must: - 1. Sends proper error message - with type `connection_error`. - 2. Disconnect user + Server must close WebSocket connection with code 4401 if client + trying to subscribe before connection acknowledgment (before server + send `connection_ack` message). """ print("Establish WebSocket GraphQL connection.") + client = gql( + query=Query, + consumer_attrs={"strict_ordering": True}, + ) + await client.connect_and_init(connect_only=True) - def on_connect(self, payload): - del self, payload - raise RuntimeError("Connection rejected!") + print("Send `subscribe` message.") + await client.start(query="query op_name { value }", operation_name="op_name") + await client.wait_disconnect(assert_code=4401) - client = gql(consumer_attrs={"strict_ordering": True, "on_connect": on_connect}) + print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() + await client.finalize() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_wrong_message_type_error(gql, subprotocol): + """Test how server handles request with wrong message type. + + With the `graphql-transport-ws` subprotocol, server must close + WebSocket connection with code 4400 when client send request with + the wrong message type. With the `graphql-ws` subprotocol server + must send `error` message. + """ + + print("Establish WebSocket GraphQL connection.") + client = gql(consumer_attrs={"strict_ordering": True}, subprotocol=subprotocol) await client.connect_and_init(connect_only=True) - print("Try to initialize the connection.") - await client.send(msg_type="connection_init", payload="") - resp = await client.receive(assert_type="connection_error") - assert resp["message"] == "RuntimeError: Connection rejected!" - assert ( - resp["extensions"]["code"] == "RuntimeError" - ), "Error should have 'extensions' with 'code'." - await client.wait_disconnect() + print("Send message with wrong type.") + msg_id = await client.send_raw_message( + {"type": "wrong_type__(ツ)_/¯", "variables": {}, "operationName": ""} + ) + if subprotocol == "graphql-transport-ws": + await client.wait_disconnect(assert_code=4400) + else: + with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as exc_info: + await client.receive(assert_id=msg_id, assert_type="error") + payload = exc_info.value.response["payload"] + assert len(payload["errors"]) == 1, "Multiple errors received instead of one!" + assert isinstance(payload["errors"][0], dict), "Error must be of dict type!" + assert isinstance( + payload["errors"][0]["message"], str + ), "Error's message must be of str type!" + assert ( + payload["errors"][0]["extensions"]["code"] == "Exception" + ), "Error must have 'code' field." print("Disconnect and wait the application to finish gracefully.") await client.assert_no_messages() @@ -192,100 +206,205 @@ def on_connect(self, payload): @pytest.mark.asyncio -async def test_subscribe_return_value(gql): - """Assure the return value of the `subscribe` method is checked. +async def test_many_init_requests_error_graphql_transport_ws(gql): + """Test how server handles more than 1 `connection_init` messages. - - Check there is no error when `subscribe` returns nothing, a list - or a tuple. - - Check there is an `AssertionError` when `subscribe` returns - a dict, a string or an empty string. + Server must close WebSocket connection with code 4429 if client send + more than 1 `connection_init` messages. """ - class TestSubscription(channels_graphql_ws.Subscription): - """Test subscription with a "special" `subscribe` method. + print("Establish WebSocket GraphQL connection.") + client = gql( + consumer_attrs={"strict_ordering": True}, + ) + await client.connect_and_init() + + print("Send second `connection_init` message.") + await client.send_raw_message({"type": "connection_init", "payload": ""}) + await client.wait_disconnect(assert_code=4429) + + print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() + await client.finalize() + - The `subscribe` method returns values of different types - depending on the subscription parameter `switch`. - """ +@pytest.mark.asyncio +async def test_subscriber_already_exists_error_graphql_transport_ws(gql): + """Test how server handles subscription with same id. - # Returning different values (even `None`) from the `subscribe` - # method is the main idea of this test. Make Pylint ignore this. - # pylint: disable=inconsistent-return-statements + Server must close WebSocket connection with code 4409 if client sent + `subscribe` message with id of subscription that already exists. + """ - ok = graphene.Boolean() + print("Establish WebSocket GraphQL connection.") + client = gql( + subscription=Subscription, + consumer_attrs={"strict_ordering": True}, + ) + await client.connect_and_init() - class Arguments: - """Argument which controls `subscribe` result value.""" + print("Send two `subscribe` messages with same id.") + for _ in range(2): + await client.start( + query=""" + subscription { test_subscription (switch: "NONE\") { ok } } + """, + msg_id="NOT_UNIQUE_ID", + ) + await client.wait_disconnect(assert_code=4409) - switch = graphene.String() + print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() + await client.finalize() - @staticmethod - def subscribe(root, info, switch): - """This returns nothing which must be OK.""" - del root, info - if switch == "NONE": - return None - if switch == "LIST": - return ["group"] - if switch == "TUPLE": - return ("group",) - if switch == "STR": - return "group" - if switch == "DICT": - return "group" - if switch == "EMPTYSTR": - return "" - @staticmethod - def publish(payload, info): - """We will never get here in this test.""" - del payload, info - assert False # raises AssertionError exception +@pytest.mark.asyncio +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_connection_error(gql, subprotocol): + """Test that server disconnects user when `on_connect` raises error. - class Subscription(graphene.ObjectType): - """Root subscription.""" + With the `graphql-transport-ws` subprotocol, server must close the + connection with code 4403 when `on_connect` raises `RuntimeError`. + With the `graphql-ws` subprotocol server must send a proper error + message (with type `connection_error`) and then disconnect. + """ - test_subscription = TestSubscription.Field() + print("Establish WebSocket GraphQL connection.") + + def on_connect(self, payload): + del self, payload + raise RuntimeError("Connection rejected!") + + client = gql( + consumer_attrs={"strict_ordering": True, "on_connect": on_connect}, + subprotocol=subprotocol, + ) + await client.connect_and_init(connect_only=True) + + print("Try to initialize the connection.") + await client.send_raw_message({"type": "connection_init"}) + if subprotocol == "graphql-ws": + resp = await client.receive(assert_type="connection_error") + assert resp["message"] == "RuntimeError: Connection rejected!" + assert ( + resp["extensions"]["code"] == "RuntimeError" + ), "Error should have 'extensions' with 'code'." + await client.wait_disconnect() + else: + await client.wait_disconnect(assert_code=4403) + + print("Disconnect and wait the application to finish gracefully.") + await client.assert_no_messages() + await client.finalize() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_subscribe_return_value(gql, subprotocol): + """Assure the return value of the `subscribe` method is checked. + + - Check there is no error when `subscribe` returns nothing, a list + or a tuple. + - Check there is an `AssertionError` when `subscribe` returns + a dict, a string or an empty string. + """ print("Check there is no error when `subscribe` returns nothing, list, or tuple.") for result_type in ["NONE", "LIST", "TUPLE"]: - client = gql(subscription=Subscription) + client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - await client.send( - msg_type="start", - payload={ - "query": f""" + await client.start( + query=f""" subscription {{ test_subscription (switch: "{result_type}\") {{ ok }} }} """ - }, ) await client.assert_no_messages("Subscribe responded with a message!") await client.finalize() - print("Check there is a error when `subscribe` returns string or dict.") + print("Check there is an error when `subscribe` returns string or dict.") for result_type in ["STR", "DICT", "EMPTYSTR"]: - client = gql(subscription=Subscription) + client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - msg_id = await client.send( - msg_type="start", - payload={ - "query": f""" + msg_id = await client.start( + query=f""" subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} """ - }, ) - with pytest.raises(channels_graphql_ws.GraphqlWsResponseError) as ex: - await client.receive(assert_id=msg_id, assert_type="data") - - payload = ex.value.response["payload"] - assert "AssertionError" in payload["errors"][0]["message"], ( + errors, _ = await client.receive_error(msg_id) + assert "AssertionError" in errors[0]["message"], ( "There is no error in response" " to the wrong type of the `subscribe` result!" ) assert ( - payload["errors"][0]["extensions"]["code"] == "AssertionError" + errors[0]["extensions"]["code"] == "AssertionError" ), "Error should have 'extensions' with 'code'." await client.finalize() + + +# ---------------------------------------------------------------------- GRAPHQL BACKEND + + +class Query(graphene.ObjectType): + """Root GraphQL query.""" + + VALUE = str(uuid.uuid4().hex) + value = graphene.String(args={"issue_error": graphene.Boolean(default_value=False)}) + + def resolve_value(self, info, issue_error): + """Resolver to return predefined value which can be tested.""" + del info + assert self is None, "Root `self` expected to be `None`!" + if issue_error: + raise RuntimeError(Query.VALUE) + return Query.VALUE + + +class TestSubscription(channels_graphql_ws.Subscription): + """Test subscription with a "special" `subscribe` method. + + The `subscribe` method returns values of different types + depending on the subscription parameter `switch`. + """ + + # Returning different values (even `None`) from the `subscribe` + # method is the main idea of this test. Make Pylint ignore this. + # pylint: disable=inconsistent-return-statements + + ok = graphene.Boolean() + + class Arguments: + """Argument which controls `subscribe` result value.""" + + switch = graphene.String() + + @staticmethod + def subscribe(root, info, switch): + """This returns nothing which must be OK.""" + del root, info + if switch == "NONE": + return None + if switch == "LIST": + return ["group"] + if switch == "TUPLE": + return ("group",) + if switch == "STR": + return "group" + if switch == "DICT": + return "group" + if switch == "EMPTYSTR": + return "" + + @staticmethod + def publish(payload, info): + """We will never get here in this test.""" + del payload, info + assert False # raises AssertionError exception + + +class Subscription(graphene.ObjectType): + """Root subscription.""" + + test_subscription = TestSubscription.Field() diff --git a/tests/test_linters.py b/tests/test_linters.py index 9beddea..2ed268a 100644 --- a/tests/test_linters.py +++ b/tests/test_linters.py @@ -73,14 +73,3 @@ def test_mypy(src_dir): result = mypy(src_dir) if result: print("\nMyPy:", result) - - -@pytest.mark.parametrize("src_dir", SOURCE_DIRS) -def test_pydocstyle(src_dir): - """Run Pydocstyle.""" - - pydocstyle = plumbum.local["pydocstyle"] - with plumbum.local.cwd(PROJECT_ROOT_DIR): - result = pydocstyle(src_dir) - if result: - print("\nPydocstyle:", result) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index bfc09d2..8055b9d 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -29,7 +29,8 @@ @pytest.mark.asyncio -async def test_middleware_called_in_query(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_middleware_called_in_query(gql, subprotocol): """Check that middleware called during query request.""" middleware_called = False @@ -45,13 +46,14 @@ def middleware(next_middleware, root, info, *args, **kwds): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "middleware": [middleware]}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Make simple query and assert that middleware function called.") - msg_id = await client.send(msg_type="start", payload={"query": "query { ok }"}) - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + msg_id = await client.start(query="query { ok }") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) assert middleware_called, "Middleware is not called!" @@ -60,7 +62,8 @@ def middleware(next_middleware, root, info, *args, **kwds): @pytest.mark.asyncio -async def test_middleware_called_in_mutation(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_middleware_called_in_mutation(gql, subprotocol): """Check that middleware called during mutation request.""" middleware_called = False @@ -76,15 +79,14 @@ def middleware(next_middleware, root, info, *args, **kwds): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "middleware": [middleware]}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Make simple mutation and assert that middleware function called.") - msg_id = await client.send( - msg_type="start", payload={"query": "mutation { noop { ok } }"} - ) - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + msg_id = await client.start(query="mutation { noop { ok } }") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) assert middleware_called, "Middleware is not called!" @@ -93,7 +95,8 @@ def middleware(next_middleware, root, info, *args, **kwds): @pytest.mark.asyncio -async def test_middleware_called_in_subscription(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_middleware_called_in_subscription(gql, subprotocol): """Check that middleware called during subscription processing.""" middleware_call_counter = 0 @@ -112,13 +115,12 @@ async def middleware(next_middleware, root, info, *args, **kwds): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True, "middleware": [middleware]}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe to GraphQL subscription.") - sub_id = await client.send( - msg_type="start", payload={"query": "subscription { on_trigger{ ok } }"} - ) + sub_id = await client.start(query="subscription { on_trigger{ ok } }") await client.assert_no_messages() # Middleware is not called during subscription initialization. @@ -131,7 +133,7 @@ async def middleware(next_middleware, root, info, *args, **kwds): # Receive subscription notification to guarantee that the # subscription processing has finished. - await client.receive(assert_id=sub_id, assert_type="data") + await client.receive_next(sub_id) # Middleware must be called two times: # - to resolve "on_trigger"; @@ -145,7 +147,8 @@ async def middleware(next_middleware, root, info, *args, **kwds): @pytest.mark.asyncio -async def test_middleware_invocation_order(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_middleware_invocation_order(gql, subprotocol): """Check that several middleware called in a proper order.""" middleware_invocation_log = [] @@ -167,13 +170,14 @@ def middleware2(next_middleware, root, info, *args, **kwds): "strict_ordering": True, "middleware": [middleware2, middleware1], }, + subprotocol=subprotocol, ) await client.connect_and_init() print("Make simple query and assert that middleware function called.") - msg_id = await client.send(msg_type="start", payload={"query": "query { ok }"}) - await client.receive(assert_id=msg_id, assert_type="data") - await client.receive(assert_id=msg_id, assert_type="complete") + msg_id = await client.start(query="query { ok }") + await client.receive_next(msg_id) + await client.receive_complete(msg_id) assert middleware_invocation_log == [1, 2], "Middleware invocation order is wrong!" diff --git a/tests/test_notification_queue_limit.py b/tests/test_notification_queue_limit.py index 78c0258..e1544d3 100644 --- a/tests/test_notification_queue_limit.py +++ b/tests/test_notification_queue_limit.py @@ -31,7 +31,8 @@ @pytest.mark.asyncio -async def test_notification_queue_limit(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_notification_queue_limit(gql, subprotocol): """Test it is possible to skip intermediate notifications. Here we start subscription which send 10 messages and server took @@ -94,36 +95,29 @@ class Mutation(graphene.ObjectType): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await comm.connect_and_init() print("Trigger notifications.") - await comm.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_new_message { message } }", - "variables": {}, - "operationName": "op_name", - }, + await comm.start( + query="subscription op_name { on_new_message { message } }", + operation_name="op_name", ) - mut_op_id = await comm.send( - msg_type="start", - payload={ - "query": """mutation op_name { send_messages { is_ok } }""", - "variables": {}, - "operationName": "op_name", - }, + mut_op_id = await comm.start( + query="mutation op_name { send_messages { is_ok } }", + operation_name="op_name", ) - await comm.receive(assert_id=mut_op_id, assert_type="data") - await comm.receive(assert_id=mut_op_id, assert_type="complete") + await comm.receive_next(mut_op_id) + await comm.receive_complete(mut_op_id) # Here we store ids of processed notifications. received_ids: List[int] = [] while True: - msg = await comm.receive(raw_response=False) + msg = await comm.receive() print("Received message:", msg) received_ids.append(msg["data"]["on_new_message"]["message"]) if msg["data"]["on_new_message"]["message"] == msgs_count - 1: diff --git a/tests/test_publish_skip.py b/tests/test_publish_skip.py index 2bdc10b..7061ad7 100644 --- a/tests/test_publish_skip.py +++ b/tests/test_publish_skip.py @@ -31,7 +31,8 @@ @pytest.mark.asyncio -async def test_publish_skip(gql): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_publish_skip(gql, subprotocol): """Test it is possible to skip the broadcast from the `publish`. Here we send the message to the fake chat server and make sure that @@ -114,6 +115,7 @@ class Mutation(graphene.ObjectType): communicator_kwds={ "headers": [(b"cookie", b"sessionid=%s" % uuid.uuid4().hex.encode())] }, + subprotocol=subprotocol, ) await comm1.connect_and_init() @@ -124,47 +126,36 @@ class Mutation(graphene.ObjectType): communicator_kwds={ "headers": [(b"cookie", b"sessionid=%s" % uuid.uuid4().hex.encode())] }, + subprotocol=subprotocol, ) await comm2.connect_and_init() print("Subscribe to receive a new message notifications.") - await comm1.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_new_message { message } }", - "variables": {}, - "operationName": "op_name", - }, + await comm1.start( + query="subscription op_name { on_new_message { message } }", + operation_name="op_name", ) await comm1.assert_no_messages("Subscribe responded with a message!") - sub_op_id = await comm2.send( - msg_type="start", - payload={ - "query": "subscription op_name { on_new_message { message } }", - "variables": {}, - "operationName": "op_name", - }, + sub_op_id = await comm2.start( + query="subscription op_name { on_new_message { message } }", + operation_name="op_name", ) await comm2.assert_no_messages("Subscribe responded with a message!") print("Send a new message to check we have not received notification about it.") - mut_op_id = await comm1.send( - msg_type="start", - payload={ - "query": """mutation op_name { send_message(message: "Hi!") { is_ok } }""", - "variables": {}, - "operationName": "op_name", - }, + mut_op_id = await comm1.start( + query="""mutation op_name { send_message(message: "Hi!") { is_ok } }""", + operation_name="op_name", ) - await comm1.receive(assert_id=mut_op_id, assert_type="data") - await comm1.receive(assert_id=mut_op_id, assert_type="complete") + await comm1.receive_next(mut_op_id) + await comm1.receive_complete(mut_op_id) await comm1.assert_no_messages("Self-notification happened!") - resp = await comm2.receive(assert_id=sub_op_id, assert_type="data") + resp = await comm2.receive_next(sub_op_id) assert resp["data"]["on_new_message"]["message"] == "Hi!" await comm1.assert_no_messages( diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 5b36d91..f11c606 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -24,7 +24,6 @@ import datetime import random import sys -import textwrap import uuid import channels @@ -44,7 +43,10 @@ @pytest.mark.asyncio -async def test_models_serialization_with_nested_db_query(gql, transactional_db): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_models_serialization_with_nested_db_query( + gql, transactional_db, subprotocol +): """Test serialization with resolver that executes DB queries.""" del transactional_db @@ -119,24 +121,20 @@ class Mutation(graphene.ObjectType): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe to receive subscription notifications.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription { on_models_received { user { id, username } } } """ - ) - }, ) print("Invoke mutation which sends Django model to the subscription.") @@ -154,7 +152,8 @@ class Mutation(graphene.ObjectType): @pytest.mark.asyncio -async def test_models_serialization(gql, transactional_db): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_models_serialization(gql, transactional_db, subprotocol): """Test serialization of the Django model inside the `payload`.""" del transactional_db @@ -228,16 +227,14 @@ class Mutation(graphene.ObjectType): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe to receive subscription notifications.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription { on_models_received { user1_id @@ -249,8 +246,6 @@ class Mutation(graphene.ObjectType): } } """ - ) - }, ) print("Invoke mutation which sends Django model to the subscription.") @@ -272,7 +267,8 @@ class Mutation(graphene.ObjectType): @pytest.mark.asyncio -async def test_timestamps_serialization(gql, transactional_db): +@pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) +async def test_timestamps_serialization(gql, transactional_db, subprotocol): """Test serialization of timestamps inside the `payload`. Check that instances of `datetime.date`, `datetime.time`, and @@ -345,16 +341,14 @@ class Mutation(graphene.ObjectType): mutation=Mutation, subscription=Subscription, consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, ) await client.connect_and_init() print("Subscribe to receive subscription notifications.") - sub_id = await client.send( - msg_type="start", - payload={ - "query": textwrap.dedent( - """ + sub_id = await client.start( + query=""" subscription { on_timestamps_received { now_date @@ -366,8 +360,6 @@ class Mutation(graphene.ObjectType): } } """ - ) - }, ) print("Invoke mutation which sends timestamps to the subscription.") From ecfa6083e4e216c73103534dfa3b05a6a09665ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:23:10 +0000 Subject: [PATCH 03/14] Fix vulnerable aiohttp and django in poetry.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aiohttp 3.9.5 → 3.13.3 (fixes zip bomb via auto_decompress, CVE affecting ≤ 3.13.2) - django 4.2.14 → 5.2.12 (fixes SQL injection + DoS vulnerabilities; patched at 4.2.26/5.1.14/5.2.8) Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- poetry.lock | 752 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 521 insertions(+), 231 deletions(-) diff --git a/poetry.lock b/poetry.lock index 466b6e1..8fea1c5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,114 +1,175 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] [[package]] name = "aiohttp" -version = "3.9.5" +version = "3.13.3" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, - {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, - {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, - {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, - {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, - {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, - {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, - {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, - {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, - {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, + {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, + {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, + {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, + {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, + {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, + {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, + {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, + {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, + {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, + {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, + {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, + {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, + {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, ] [package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] [package.dependencies] frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "aniso8601" @@ -116,6 +177,7 @@ version = "9.0.1" description = "A library for parsing ISO 8601 strings." optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, @@ -130,6 +192,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -147,6 +210,7 @@ version = "3.2.3" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "astroid-3.2.3-py3-none-any.whl", hash = "sha256:3eae9ea67c11c858cdd2c91337d2e816bd019ac897ca07d7b346ac10105fceb3"}, {file = "astroid-3.2.3.tar.gz", hash = "sha256:7099b5a60985529d8d46858befa103b82d0d05a5a5e8b816b5303ed96075e1d9"}, @@ -161,10 +225,12 @@ version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""} [[package]] name = "attrs" @@ -172,6 +238,7 @@ version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, @@ -182,8 +249,8 @@ cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "autobahn" @@ -191,6 +258,7 @@ version = "23.1.2" description = "WebSocket client & server library, WAMP real-time framework" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a"}, ] @@ -204,7 +272,7 @@ txaio = ">=21.2.1" [package.extras] all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi @ git+https://github.com/ethereum/eth-abi.git@v4.0.0-beta.2", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3[ipfs] @ git+https://github.com/ethereum/web3.py.git@v6.0.0-beta.9", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] compress = ["python-snappy (>=0.6.0)"] -dev = ["backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610)", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] +dev = ["backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610) ; python_version >= \"3.4\" and platform_python_implementation != \"PyPy\"", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service_identity (>=18.1.0)"] nvx = ["cffi (>=1.14.5)"] scram = ["argon2_cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] @@ -219,6 +287,7 @@ version = "22.10.0" description = "Self-service finite-state machines for the programmer on the go." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "Automat-22.10.0-py2.py3-none-any.whl", hash = "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180"}, {file = "Automat-22.10.0.tar.gz", hash = "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e"}, @@ -231,40 +300,13 @@ six = "*" [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "black" version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, @@ -301,7 +343,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -311,6 +353,7 @@ version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, @@ -322,6 +365,8 @@ version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, @@ -386,6 +431,7 @@ version = "4.1.0" description = "Brings async, event-driven capabilities to Django 3.2 and up." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "channels-4.1.0-py3-none-any.whl", hash = "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48"}, {file = "channels-4.1.0.tar.gz", hash = "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"}, @@ -406,6 +452,7 @@ version = "4.2.0" description = "Redis-backed ASGI channel layer implementation" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "channels_redis-4.2.0-py3-none-any.whl", hash = "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4"}, {file = "channels_redis-4.2.0.tar.gz", hash = "sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd"}, @@ -427,6 +474,7 @@ version = "5.2.0" description = "Universal encoding detector for Python 3" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, @@ -438,6 +486,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -452,6 +501,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -463,6 +513,7 @@ version = "23.10.4" description = "Symbolic constants in Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, @@ -474,6 +525,7 @@ version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, @@ -533,7 +585,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -541,6 +593,7 @@ version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, @@ -595,6 +648,7 @@ version = "4.1.2" description = "Django ASGI (HTTP/WebSocket) server" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a"}, {file = "daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"}, @@ -614,6 +668,7 @@ version = "0.3.8" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, @@ -629,6 +684,7 @@ version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, @@ -636,18 +692,18 @@ files = [ [[package]] name = "django" -version = "4.2.14" +version = "5.2.12" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["main", "dev"] files = [ - {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, - {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, + {file = "django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7"}, + {file = "django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb"}, ] [package.dependencies] -asgiref = ">=3.6.0,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +asgiref = ">=3.8.1" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -661,6 +717,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -675,6 +733,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -689,6 +748,7 @@ version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, @@ -697,7 +757,7 @@ files = [ [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] [[package]] name = "frozenlist" @@ -705,6 +765,7 @@ version = "1.4.1" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, @@ -791,6 +852,7 @@ version = "3.3" description = "GraphQL Framework for Python" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "graphene-3.3-py2.py3-none-any.whl", hash = "sha256:bb3810be33b54cb3e6969506671eb72319e8d7ba0d5ca9c8066472f75bf35a38"}, {file = "graphene-3.3.tar.gz", hash = "sha256:529bf40c2a698954217d3713c6041d69d3f719ad0080857d7ee31327112446b0"}, @@ -811,6 +873,7 @@ version = "3.2.2" description = "Graphene Django integration" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556"}, {file = "graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c"}, @@ -835,6 +898,7 @@ version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = ">=3.6,<4" +groups = ["main", "dev"] files = [ {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, @@ -846,6 +910,7 @@ version = "3.2.0" description = "Relay library for graphql-core" optional = false python-versions = ">=3.6,<4" +groups = ["main", "dev"] files = [ {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, @@ -860,6 +925,7 @@ version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, @@ -874,6 +940,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main", "dev"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -885,6 +952,7 @@ version = "22.10.0" description = "\"A small library that versions your Python projects.\"" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, @@ -900,6 +968,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -911,6 +980,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -925,6 +995,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -936,6 +1007,7 @@ version = "1.0.8" description = "MessagePack serializer" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, @@ -1001,6 +1073,7 @@ version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, @@ -1100,6 +1173,7 @@ version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, @@ -1147,6 +1221,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -1158,6 +1233,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -1169,6 +1245,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1180,6 +1257,7 @@ version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, @@ -1196,6 +1274,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1211,6 +1290,7 @@ version = "1.8.3" description = "Plumbum: shell combinators library" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "plumbum-1.8.3-py3-none-any.whl", hash = "sha256:8595d36dae2472587d6f59789c8d7b26250f45f6f6ed75ccb378de59ee7b9cf9"}, {file = "plumbum-1.8.3.tar.gz", hash = "sha256:6092c85ab970b7a7a9d5d85c75200bc93be82b33c9bdf640ffa87d2d7c8709f0"}, @@ -1230,6 +1310,7 @@ version = "2.3" description = "Promises/A+ implementation for Python" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, ] @@ -1240,12 +1321,145 @@ six = "*" [package.extras] test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + [[package]] name = "pyasn1" version = "0.6.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, @@ -1257,6 +1471,7 @@ version = "0.4.0" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, @@ -1271,6 +1486,8 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1282,25 +1499,25 @@ version = "3.2.5" description = "python code static checker" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +astroid = ">=3.2.2,<=3.3.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -1312,6 +1529,7 @@ version = "2.5.5" description = "A Pylint plugin to help Pylint understand the Django web framework" optional = false python-versions = ">=3.7,<4.0" +groups = ["dev"] files = [ {file = "pylint_django-2.5.5-py3-none-any.whl", hash = "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7"}, {file = "pylint_django-2.5.5.tar.gz", hash = "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b"}, @@ -1330,6 +1548,7 @@ version = "0.8.2" description = "Utilities and helpers for writing Pylint plugins" optional = false python-versions = ">=3.7,<4.0" +groups = ["dev"] files = [ {file = "pylint_plugin_utils-0.8.2-py3-none-any.whl", hash = "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507"}, {file = "pylint_plugin_utils-0.8.2.tar.gz", hash = "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4"}, @@ -1344,6 +1563,7 @@ version = "0.2.3" description = "Quote consistency checker for PyLint.." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pylint-quotes-0.2.3.tar.gz", hash = "sha256:2d6bb3fa8a1a85af3af8a0ca875a719ac5bcdb735c45756284699d809c109c95"}, {file = "pylint_quotes-0.2.3-py2.py3-none-any.whl", hash = "sha256:89decd985d3c019314da630f5e3fe0e0df951c2310525fbd6e710bca329c810e"}, @@ -1358,6 +1578,7 @@ version = "24.1.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyOpenSSL-24.1.0-py3-none-any.whl", hash = "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad"}, {file = "pyOpenSSL-24.1.0.tar.gz", hash = "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f"}, @@ -1376,6 +1597,7 @@ version = "1.7.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, @@ -1395,6 +1617,7 @@ version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, @@ -1417,6 +1640,7 @@ version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, @@ -1435,6 +1659,7 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -1453,6 +1678,7 @@ version = "4.8.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, @@ -1471,6 +1697,7 @@ version = "0.7.3" description = "pytest plugin for adding to the PYTHONPATH from command line or configs." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pytest-pythonpath-0.7.3.tar.gz", hash = "sha256:63fc546ace7d2c845c1ee289e8f7a6362c2b6bae497d10c716e58e253e801d62"}, ] @@ -1484,6 +1711,7 @@ version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -1504,6 +1732,8 @@ version = "306" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["dev"] +markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\"" files = [ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, @@ -1527,6 +1757,7 @@ version = "5.0.7" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, @@ -1545,6 +1776,7 @@ version = "24.1.0" description = "Service identity verification for pyOpenSSL & cryptography." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"}, {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"}, @@ -1569,6 +1801,7 @@ version = "71.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "setuptools-71.0.3-py3-none-any.whl", hash = "sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207"}, {file = "setuptools-71.0.3.tar.gz", hash = "sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d"}, @@ -1577,7 +1810,7 @@ files = [ [package.extras] core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (<7.4)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff (<0.4) ; platform_system == \"Windows\"", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "pytest-ruff (>=0.3.2) ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1585,6 +1818,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1596,6 +1830,7 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -1611,6 +1846,7 @@ version = "1.3" description = "The most basic Text::Unidecode port" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, @@ -1622,6 +1858,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1633,6 +1871,7 @@ version = "0.13.0" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, @@ -1644,6 +1883,7 @@ version = "4.16.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"}, {file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"}, @@ -1663,7 +1903,7 @@ virtualenv = ">=20.26.3" [package.extras] docs = ["furo (>=2024.5.6)", "sphinx (>=7.3.7)", "sphinx-argparse-cli (>=1.16)", "sphinx-autodoc-typehints (>=2.2.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] -testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.2)", "time-machine (>=2.14.2)", "wheel (>=0.43)"] +testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.2)", "time-machine (>=2.14.2) ; implementation_name != \"pypy\"", "wheel (>=0.43)"] [[package]] name = "twisted" @@ -1671,6 +1911,7 @@ version = "24.3.0" description = "An asynchronous networking framework written in Python" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "twisted-24.3.0-py3-none-any.whl", hash = "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63"}, {file = "twisted-24.3.0.tar.gz", hash = "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae"}, @@ -1699,7 +1940,7 @@ http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] macos-platform = ["pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "twisted[all-non-platform]", "twisted[all-non-platform]"] mypy = ["mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"] osx-platform = ["twisted[macos-platform]", "twisted[macos-platform]"] -serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] +serial = ["pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\""] test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platform]", "twisted[all-non-platform]"] @@ -1710,6 +1951,8 @@ version = "1.0.4" description = "An extension for use in the twisted I/O Completion Ports reactor." optional = false python-versions = "*" +groups = ["dev"] +markers = "platform_system == \"Windows\"" files = [ {file = "twisted-iocpsupport-1.0.4.tar.gz", hash = "sha256:858096c0d15e33f15ac157f455d8f86f2f2cdd223963e58c0f682a3af8362d89"}, {file = "twisted_iocpsupport-1.0.4-cp310-cp310-win32.whl", hash = "sha256:afa2b630797f9ed2f27f3d9f55e3f72b4244911e45a8c82756f44babbf0b243e"}, @@ -1738,6 +1981,7 @@ version = "23.1.1" description = "Compatibility API between asyncio/Twisted/Trollius" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, @@ -1754,10 +1998,12 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +markers = {main = "python_version < \"3.13\""} [[package]] name = "tzdata" @@ -1765,6 +2011,8 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, @@ -1776,6 +2024,7 @@ version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, @@ -1788,110 +2037,150 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "yarl" -version = "1.9.4" +version = "1.23.0" description = "Yet another URL library" optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6"}, + {file = "yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d"}, + {file = "yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb"}, + {file = "yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2"}, + {file = "yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5"}, + {file = "yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46"}, + {file = "yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34"}, + {file = "yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d"}, + {file = "yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e"}, + {file = "yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543"}, + {file = "yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957"}, + {file = "yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3"}, + {file = "yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5"}, + {file = "yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595"}, + {file = "yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090"}, + {file = "yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe"}, + {file = "yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169"}, + {file = "yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70"}, + {file = "yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4"}, + {file = "yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4"}, + {file = "yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2"}, + {file = "yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25"}, + {file = "yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f"}, + {file = "yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +propcache = ">=0.2.1" [[package]] name = "zope-interface" @@ -1899,6 +2188,7 @@ version = "6.4.post2" description = "Interfaces for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "zope.interface-6.4.post2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c"}, {file = "zope.interface-6.4.post2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530"}, @@ -1947,6 +2237,6 @@ test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] -lock-version = "2.0" -python-versions = "^3.8.0" -content-hash = "2ef03e02068f25a2bd6b57098db5d15db1a2fae18b98eed263247896b6fd920a" +lock-version = "2.1" +python-versions = "^3.10.0" +content-hash = "b98175400ac170979d795f0a522b8629e2f4c0089edb3ecc99901cfa132faca6" From bdd08e75662267911d13e9fac8747e119bec7538 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:28:05 +0000 Subject: [PATCH 04/14] Fix GitHub Actions CI failures: update Python versions and action versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade actions/checkout@v2 → @v4 - Upgrade actions/setup-python@v1 → @v5 - Change Python matrix from [3.8.16, 3.9.16, 3.10.11] to ["3.10", "3.11", "3.12"] (specific patch versions no longer available on runners; aligns with pyproject.toml python = "^3.10.0") - Update black target-version and tox envlist to py310/py311/py312 Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- .github/workflows/tests.yml | 12 ++++++------ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e39fd2..c62dc76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,11 +34,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8.16, 3.9.16, 3.10.11] + python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry @@ -55,11 +55,11 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: [3.8.16, 3.9.16, 3.10.11] + python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry diff --git a/pyproject.toml b/pyproject.toml index 25c4263..9554252 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ tox = "*" # Black - the uncompromising code formatter. # https://black.readthedocs.io/en/stable/index.html [tool.black] -target-version = ['py38', 'py39', 'py310'] +target-version = ['py310', 'py311', 'py312'] # ---------------------------------------------------------------- ISORT # Isort configuration. @@ -166,7 +166,7 @@ omit = ["**/test_*.py"] legacy_tox_ini = """ [tox] isolated_build = true -envlist = py38,py39,py310 +envlist = py310,py311,py312 [testenv] allowlist_externals = poetry commands_pre = poetry install -v From 0d0173adbbfc064af10bc3b1527ff871b7ffac99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:36:13 +0000 Subject: [PATCH 05/14] Restore fork-specific fixes removed during upstream rebase - Restore build_absolute_uri fix in dict_as_object.py (commit 137cdebed4): channels_scope fallback in __getattr__, build_meta(), build_absolute_uri(), and supporting Django HTTP utility methods - Restore get_context() overrideable hook in graphql_ws_consumer.py (commit 64b6ec6b99) and use it instead of inline context setup so build_meta() is always called - Restore connect_and_init(connect_only, payload) parameters in client.py (commit 4f60020643) Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/client.py | 11 +- channels_graphql_ws/dict_as_object.py | 126 ++++++++++++++++++++- channels_graphql_ws/graphql_ws_consumer.py | 12 +- 3 files changed, 140 insertions(+), 9 deletions(-) diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 505935e..a0ab7cf 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -88,16 +88,17 @@ def connected(self) -> bool: """Indicate whether client is connected.""" return self._is_connected - async def connect_and_init(self) -> None: + async def connect_and_init(self, connect_only: bool = False, payload="") -> None: """Establish and initialize WebSocket GraphQL connection. 1. Establish WebSocket connection. - 2. Initialize GraphQL connection. + 2. Initialize GraphQL connection. Skipped if connect_only=True. """ await self._transport.connect() - await self._transport.send({"type": "connection_init", "payload": ""}) - resp = await self._transport.receive() - assert resp["type"] == "connection_ack", f"Unexpected response `{resp}`!" + if not connect_only: + await self._transport.send({"type": "connection_init", "payload": payload}) + resp = await self._transport.receive() + assert resp["type"] == "connection_ack", f"Unexpected response `{resp}`!" self._is_connected = True # Default value for `id`, because `None` is also a valid value. diff --git a/channels_graphql_ws/dict_as_object.py b/channels_graphql_ws/dict_as_object.py index 59619ac..a7b2cc3 100644 --- a/channels_graphql_ws/dict_as_object.py +++ b/channels_graphql_ws/dict_as_object.py @@ -20,6 +20,12 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Dict wrapper to access keys as attributes.""" +from urllib.parse import urljoin, urlsplit +from django.utils.encoding import escape_uri_path, iri_to_uri +from django.utils.functional import cached_property +from django.conf import settings +from django.core.exceptions import DisallowedHost +from django.http.request import split_domain_port, validate_host class DictAsObject: @@ -41,7 +47,10 @@ def __getattr__(self, name): try: return self._scope[name] except KeyError as ex: - raise AttributeError() from ex + try: + return self._scope["channels_scope"][name] + except KeyError: + raise AttributeError() from ex def __setattr__(self, name, value): """Route attributes to the scope object.""" @@ -73,3 +82,118 @@ def __str__(self): def __repr__(self): """Wrap dict method.""" return self._scope.__repr__() + + # ---------------------------- build_absolute_uri + # copy from django.http.request.HttpRequest so we can have build_absolute_uri on channels scope + + def build_meta(self): + """Build META dict from headers.""" + META = {} + for key, value in self.channels_scope.get('headers', []): + META[key.decode("utf-8").replace("-", "_").upper()] = value.decode("utf-8") + META['QUERY_STRING'] = self.channels_scope.get('query_string', b'').decode("utf-8") + self.META = META + + def build_absolute_uri(self, location=None): + """ + Build an absolute URI from the location and the variables available in + this request. If no ``location`` is specified, build the absolute URI + using request.get_full_path(). If the location is absolute, convert it + to an RFC 3987 compliant URI and return it. If location is relative or + is scheme-relative (i.e., ``//example.com/``), urljoin() it to a base + URL constructed from the request variables. + """ + if location is None: + # Make it an absolute url (but schemeless and domainless) for the + # edge case that the path starts with '//'. + location = "//%s" % self.get_full_path() + else: + # Coerce lazy locations. + location = str(location) + bits = urlsplit(location) + if not (bits.scheme and bits.netloc): + # Handle the simple, most common case. If the location is absolute + # and a scheme or host (netloc) isn't provided, skip an expensive + # urljoin() as long as no path segments are '.' or '..'. + if ( + bits.path.startswith("/") + and not bits.scheme + and not bits.netloc + and "/./" not in bits.path + and "/../" not in bits.path + ): + # If location starts with '//' but has no netloc, reuse the + # schema and netloc from the current request. Strip the double + # slashes and continue as if it wasn't specified. + location = self._current_scheme_host + location.removeprefix("//") + else: + # Join the constructed URL with the provided location, which + # allows the provided location to apply query strings to the + # base path. + location = urljoin(self._current_scheme_host + self.path, location) + return iri_to_uri(location) + + def get_full_path(self, force_append_slash=False): + """Return the full path including query string.""" + return self._get_full_path(self.path, force_append_slash) + + def _get_full_path(self, path, force_append_slash): + # RFC 3986 requires query string arguments to be in the ASCII range. + # Rather than crash if this doesn't happen, we encode defensively. + return "%s%s%s" % ( + escape_uri_path(path), + "/" if force_append_slash and not path.endswith("/") else "", + ("?" + iri_to_uri(self.META.get("QUERY_STRING", ""))) + if self.META.get("QUERY_STRING", "") + else "", + ) + + def is_secure(self): + """Return True if the request is secure (HTTPS).""" + return not settings.DEBUG + + @cached_property + def _current_scheme_host(self): + return "{}://{}".format("https" if self.is_secure() else "http", self.get_host()) + + def get_host(self): + """Return the HTTP host using the environment or request headers.""" + host = self._get_raw_host() + + # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True. + allowed_hosts = settings.ALLOWED_HOSTS + if settings.DEBUG and not allowed_hosts: + allowed_hosts = [".localhost", "127.0.0.1", "[::1]"] + + domain, port = split_domain_port(host) + if domain and validate_host(domain, allowed_hosts): + return host + else: + msg = "Invalid HTTP_HOST header: %r." % host + if domain: + msg += " You may need to add %r to ALLOWED_HOSTS." % domain + else: + msg += ( + " The domain name provided is not valid according to RFC 1034/1035." + ) + raise DisallowedHost(msg) + + def _get_raw_host(self): + """ + Return the HTTP host using the environment or request headers. Skip + allowed hosts protection, so may return an insecure host. + """ + + # We try three options, in order of decreasing preference. + if settings.USE_X_FORWARDED_HOST and ("X_FORWARDED_HOST" in self.META): + host = self.META["X_FORWARDED_HOST"] + elif "HOST" in self.META: + host = self.META["HOST"] + else: + host = "localhost" + # # Reconstruct the host using the algorithm from PEP 333. + # host = self.META["SERVER_NAME"] + # server_port = self.get_port() + # if server_port != ("443" if self.is_secure() else "80"): + # host = "%s:%s" % (host, server_port) + return host diff --git a/channels_graphql_ws/graphql_ws_consumer.py b/channels_graphql_ws/graphql_ws_consumer.py index b9251f0..0510c6d 100644 --- a/channels_graphql_ws/graphql_ws_consumer.py +++ b/channels_graphql_ws/graphql_ws_consumer.py @@ -730,9 +730,7 @@ async def _on_gql_subscribe(self, op_id, payload): variables = payload.get("variables", {}) # Prepare a context object. - context = DictAsObject({}) - context.channels_scope = self.scope - context.channel_name = self.channel_name + context = self.get_context() context.graphql_operation_name = op_name context.graphql_operation_id = op_id @@ -1465,6 +1463,14 @@ def _spawn_background_task(self, awaitable): self._background_tasks.add(background_task) return background_task + def get_context(self): + """Prepare the context object to use during GraphQL execution.""" + context = DictAsObject({}) + context.channels_scope = self.scope + context.channel_name = self.channel_name + context.build_meta() + return context + @property def _channel_layer(self): """Channel layer.""" From 6075cebc12c3965bd932a9bd4b2213097ac46c7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:04:58 +0000 Subject: [PATCH 06/14] Fix event loop closed errors and asyncio Python 3.10+ incompatibilities - Fix dict_as_object.py: black/isort formatting (linter failures) - Fix transport.py: wrap connected.wait() coroutine in create_task() for asyncio.wait() (Python 3.10+ TypeError + mypy error) - Fix testing.py: add payload='' to connect_and_init() to match parent signature (mypy [override] error) - Fix test_concurrent.py: wrap coroutines in create_task() for asyncio.wait() calls in test_heavy_load and test_subscribe_and_many_unsubscribes (Python 3.10+ 'Passing coroutines is forbidden, use tasks explicitly') - Fix conftest.py: replace deprecated event_loop fixture with event_loop_policy (pytest-asyncio 0.23+ API, eliminates 'will lead to errors in the future' DeprecationWarning which was causing RuntimeError: Event loop is closed) Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/dict_as_object.py | 23 +++++++++++++++-------- channels_graphql_ws/testing.py | 4 ++-- channels_graphql_ws/transport.py | 12 ++++++++---- tests/conftest.py | 24 +++++++++--------------- tests/test_concurrent.py | 21 ++++++++++++++------- 5 files changed, 48 insertions(+), 36 deletions(-) diff --git a/channels_graphql_ws/dict_as_object.py b/channels_graphql_ws/dict_as_object.py index a7b2cc3..7b3f3c0 100644 --- a/channels_graphql_ws/dict_as_object.py +++ b/channels_graphql_ws/dict_as_object.py @@ -21,11 +21,12 @@ """Dict wrapper to access keys as attributes.""" from urllib.parse import urljoin, urlsplit -from django.utils.encoding import escape_uri_path, iri_to_uri -from django.utils.functional import cached_property + from django.conf import settings from django.core.exceptions import DisallowedHost from django.http.request import split_domain_port, validate_host +from django.utils.encoding import escape_uri_path, iri_to_uri +from django.utils.functional import cached_property class DictAsObject: @@ -89,9 +90,11 @@ def __repr__(self): def build_meta(self): """Build META dict from headers.""" META = {} - for key, value in self.channels_scope.get('headers', []): + for key, value in self.channels_scope.get("headers", []): META[key.decode("utf-8").replace("-", "_").upper()] = value.decode("utf-8") - META['QUERY_STRING'] = self.channels_scope.get('query_string', b'').decode("utf-8") + META["QUERY_STRING"] = self.channels_scope.get("query_string", b"").decode( + "utf-8" + ) self.META = META def build_absolute_uri(self, location=None): @@ -143,9 +146,11 @@ def _get_full_path(self, path, force_append_slash): return "%s%s%s" % ( escape_uri_path(path), "/" if force_append_slash and not path.endswith("/") else "", - ("?" + iri_to_uri(self.META.get("QUERY_STRING", ""))) - if self.META.get("QUERY_STRING", "") - else "", + ( + ("?" + iri_to_uri(self.META.get("QUERY_STRING", ""))) + if self.META.get("QUERY_STRING", "") + else "" + ), ) def is_secure(self): @@ -154,7 +159,9 @@ def is_secure(self): @cached_property def _current_scheme_host(self): - return "{}://{}".format("https" if self.is_secure() else "http", self.get_host()) + return "{}://{}".format( + "https" if self.is_secure() else "http", self.get_host() + ) def get_host(self): """Return the HTTP host using the environment or request headers.""" diff --git a/channels_graphql_ws/testing.py b/channels_graphql_ws/testing.py index 82fda5d..87420ab 100644 --- a/channels_graphql_ws/testing.py +++ b/channels_graphql_ws/testing.py @@ -66,7 +66,7 @@ async def assert_no_messages( else f"Message received when nothing expected!\n{received}" ) - async def connect_and_init(self, connect_only: bool = False) -> None: + async def connect_and_init(self, connect_only: bool = False, payload="") -> None: """Establish and initialize WebSocket GraphQL connection. 1. Establish WebSocket connection. @@ -76,7 +76,7 @@ async def connect_and_init(self, connect_only: bool = False) -> None: await self._transport.connect() self._is_connected = True else: - await super().connect_and_init() + await super().connect_and_init(payload=payload) async def send_raw_message(self, message): """Send a raw message. diff --git a/channels_graphql_ws/transport.py b/channels_graphql_ws/transport.py index e7b801b..08fb53e 100644 --- a/channels_graphql_ws/transport.py +++ b/channels_graphql_ws/transport.py @@ -101,10 +101,14 @@ async def connect( self._message_processor = asyncio.create_task( self._process_messages(connected, timeout or self.TIMEOUT, subprotocol) ) - await asyncio.wait( - [connected.wait(), self._message_processor], - return_when=asyncio.FIRST_COMPLETED, - ) + connected_task = asyncio.create_task(connected.wait()) + try: + await asyncio.wait( + [connected_task, self._message_processor], + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + connected_task.cancel() if self._message_processor.done(): # Make sure to raise an exception from the task. self._message_processor.result() diff --git a/tests/conftest.py b/tests/conftest.py index 13f79e3..c1ff6e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,27 +35,21 @@ import channels_graphql_ws.testing -@pytest.fixture -def event_loop(request): - """Overwrite `pytest_asyncio` eventloop to fix Windows issue. +@pytest.fixture(scope="session") +def event_loop_policy(): + """Return the asyncio event loop policy for tests. - Default implementation causes `NotImplementedError` on Windows with - Python 3.8, because they changed default eventloop in 3.8. + On Windows with Python 3.8+, use WindowsSelectorEventLoopPolicy to avoid + NotImplementedError: the default ProactorEventLoop is incompatible with + some libraries. On other platforms return the default policy. NOTE: We do the same thing in the `example/settings.py` because it imports (and fails) before we have a chance to invoke this fixture. - So, we could avoid adding this fixture, but I feel it is better to - keep the proper solution here as well. """ - del request - if sys.platform == "win32" and sys.version_info.minor >= 8: - asyncio.set_event_loop_policy( - asyncio.WindowsSelectorEventLoopPolicy() # pylint: disable=no-member - ) - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() + if sys.platform == "win32": + return asyncio.WindowsSelectorEventLoopPolicy() # pylint: disable=no-member + return asyncio.DefaultEventLoopPolicy() class DummyQuery(graphene.ObjectType): diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py index 0f32f91..c817f9d 100644 --- a/tests/test_concurrent.py +++ b/tests/test_concurrent.py @@ -114,10 +114,12 @@ async def test_heavy_load(gql, sync_resolvers, requests_number, subprotocol): for _ in range(requests_number): op_id = uuid.uuid4().hex send_waitlist += [ - client.start( - msg_id=op_id, - query=f"query op_name {{ {query} }}", - operation_name="op_name", + asyncio.create_task( + client.start( + msg_id=op_id, + query=f"query op_name {{ {query} }}", + operation_name="op_name", + ) ) ] # Expect two messages for each one we have sent. @@ -125,7 +127,10 @@ async def test_heavy_load(gql, sync_resolvers, requests_number, subprotocol): (op_id, "next" if subprotocol == "graphql-transport-ws" else "data") ) expected_responses.add((op_id, "complete")) - receive_waitlist += [client.transport.receive(), client.transport.receive()] + receive_waitlist += [ + asyncio.create_task(client.transport.receive()), + asyncio.create_task(client.transport.receive()), + ] start_ts = time.monotonic() await asyncio.wait(send_waitlist) @@ -362,13 +367,15 @@ async def receiver(op_ids): op_ids: Set[str] = set() # List to collect tasks. We immediately add a handler to receive # successful messages. - awaitables = [receiver(op_ids)] + awaitables = [asyncio.create_task(receiver(op_ids))] op_id = 0 for user_id in itertools.cycle(["ALICE", "TOM", None]): op_id += 1 op_ids.add(str(op_id)) - awaitables.append(subscribe_unsubscribe(client, user_id, str(op_id))) + awaitables.append( + asyncio.create_task(subscribe_unsubscribe(client, user_id, str(op_id))) + ) if number_of_tasks == op_id: print("Tasks with the following ids prepared:", op_ids) break From 34264592fc610861e696e02b5747868854591ffa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:35:08 +0000 Subject: [PATCH 07/14] Fix pylint CI failures in dict_as_object.py and pyproject.toml - Remove [tool.pylint.STRING_QUOTES] from pyproject.toml (pylint-quotes plugin is disabled, causing E0015 unrecognized-option error) - Add META to pylint good-names (Django-conventional uppercase attr) - Init self.META = {} in __init__ to fix W0201 (attr outside __init__) - Rename local META to meta in build_meta() to fix C0103 (invalid-name) - Break overlong comment to fix C0301 (line-too-long) - Convert % and .format() strings to f-strings (C0209) - Add missing docstrings to _get_full_path and _current_scheme_host (C0116) - Remove unnecessary else after return in get_host() (R1705) - Replace unused port variable with _ in get_host() (W0612) Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/dict_as_object.py | 55 +++++++++++++-------------- pyproject.toml | 6 +-- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/channels_graphql_ws/dict_as_object.py b/channels_graphql_ws/dict_as_object.py index 7b3f3c0..4d3736f 100644 --- a/channels_graphql_ws/dict_as_object.py +++ b/channels_graphql_ws/dict_as_object.py @@ -35,6 +35,7 @@ class DictAsObject: def __init__(self, scope): """Remember given `scope`.""" self._scope = scope + self.META = {} def _asdict(self): """Provide inner Channels scope object.""" @@ -85,17 +86,18 @@ def __repr__(self): return self._scope.__repr__() # ---------------------------- build_absolute_uri - # copy from django.http.request.HttpRequest so we can have build_absolute_uri on channels scope + # Copy from django.http.request.HttpRequest so we can have + # build_absolute_uri on channels scope. def build_meta(self): """Build META dict from headers.""" - META = {} + meta = {} for key, value in self.channels_scope.get("headers", []): - META[key.decode("utf-8").replace("-", "_").upper()] = value.decode("utf-8") - META["QUERY_STRING"] = self.channels_scope.get("query_string", b"").decode( + meta[key.decode("utf-8").replace("-", "_").upper()] = value.decode("utf-8") + meta["QUERY_STRING"] = self.channels_scope.get("query_string", b"").decode( "utf-8" ) - self.META = META + self.META = meta def build_absolute_uri(self, location=None): """ @@ -109,7 +111,7 @@ def build_absolute_uri(self, location=None): if location is None: # Make it an absolute url (but schemeless and domainless) for the # edge case that the path starts with '//'. - location = "//%s" % self.get_full_path() + location = f"//{self.get_full_path()}" else: # Coerce lazy locations. location = str(location) @@ -141,17 +143,15 @@ def get_full_path(self, force_append_slash=False): return self._get_full_path(self.path, force_append_slash) def _get_full_path(self, path, force_append_slash): - # RFC 3986 requires query string arguments to be in the ASCII range. - # Rather than crash if this doesn't happen, we encode defensively. - return "%s%s%s" % ( - escape_uri_path(path), - "/" if force_append_slash and not path.endswith("/") else "", - ( - ("?" + iri_to_uri(self.META.get("QUERY_STRING", ""))) - if self.META.get("QUERY_STRING", "") - else "" - ), - ) + """Build the full path including optional slash and query string. + + RFC 3986 requires query string arguments to be in the ASCII range. + Rather than crash if this doesn't happen, we encode defensively. + """ + query_string = self.META.get("QUERY_STRING", "") + slash = "/" if force_append_slash and not path.endswith("/") else "" + qs = f"?{iri_to_uri(query_string)}" if query_string else "" + return f"{escape_uri_path(path)}{slash}{qs}" def is_secure(self): """Return True if the request is secure (HTTPS).""" @@ -159,9 +159,9 @@ def is_secure(self): @cached_property def _current_scheme_host(self): - return "{}://{}".format( - "https" if self.is_secure() else "http", self.get_host() - ) + """Return the current scheme and host as a string.""" + scheme = "https" if self.is_secure() else "http" + return f"{scheme}://{self.get_host()}" def get_host(self): """Return the HTTP host using the environment or request headers.""" @@ -172,18 +172,15 @@ def get_host(self): if settings.DEBUG and not allowed_hosts: allowed_hosts = [".localhost", "127.0.0.1", "[::1]"] - domain, port = split_domain_port(host) + domain, _ = split_domain_port(host) if domain and validate_host(domain, allowed_hosts): return host + msg = f"Invalid HTTP_HOST header: {host!r}." + if domain: + msg += f" You may need to add {domain!r} to ALLOWED_HOSTS." else: - msg = "Invalid HTTP_HOST header: %r." % host - if domain: - msg += " You may need to add %r to ALLOWED_HOSTS." % domain - else: - msg += ( - " The domain name provided is not valid according to RFC 1034/1035." - ) - raise DisallowedHost(msg) + msg += " The domain name provided is not valid according to RFC 1034/1035." + raise DisallowedHost(msg) def _get_raw_host(self): """ diff --git a/pyproject.toml b/pyproject.toml index 9554252..22e7136 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,7 @@ attr-rgx = "[a-z_][a-z0-9_]{1,64}$" bad-names = "foo,bar,baz,toto,tutu,tata,responce,pathes" class-attribute-rgx = "([A-Za-z_][A-Za-z0-9_]{1,64}|(__.*__))$" function-rgx = "[a-z_][a-z0-9_]{1,64}$" -good-names = "i,j,k,ex,Run,_" +good-names = "i,j,k,ex,Run,_,META" method-rgx = "[a-z_][a-z0-9_]{1,64}$" # Docstrings are needed everywhere. no-docstring-rgx = "^$" @@ -130,10 +130,6 @@ variable-rgx = "[a-z_][a-z0-9_]{1,64}$" [tool.pylint.FORMAT] max-line-length = "88" max-module-lines = "9000" # Right, over nine thousand! -[tool.pylint.STRING_QUOTES] -docstring-quote = "double" -string-quote = "double-avoid-escape" -triple-quote = "double" [tool.pylint.PYLINT_DJANGO] # NOTE: When Pytest runs Pylint this setting is overwritten by the # environment variable `DJANGO_SETTINGS_MODULE` set by `pytest-django`. From e3b5bee93c874084259370ee7f364f9bd0a02377 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:19:37 +0000 Subject: [PATCH 08/14] Apply Copilot review suggestions: fix bugs, improve robustness, fix typos Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- .python-version | 2 -- .vscode/settings.json | 2 +- channels_graphql_ws/client.py | 7 +++++-- channels_graphql_ws/graphql_ws_consumer.py | 22 ++++++++++++---------- channels_graphql_ws/testing.py | 7 ++++--- channels_graphql_ws/transport.py | 11 +++++++++-- example/example.py | 2 +- tests/test_concurrent.py | 18 +++++++++--------- tests/test_error.py | 4 ++-- 9 files changed, 43 insertions(+), 32 deletions(-) diff --git a/.python-version b/.python-version index aec2df3..09dcc78 100644 --- a/.python-version +++ b/.python-version @@ -1,3 +1 @@ 3.10.11 -3.9.16 -3.8.16 diff --git a/.vscode/settings.json b/.vscode/settings.json index 302b1a1..b5d35b4 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -90,7 +90,7 @@ "isort.args": ["--virtual-env", "${workspaceFolder}/.venv"], "python.linting.banditEnabled": false, "python.linting.mypyEnabled": true, - "python.linting.pydocstyleEnabled": true, + "python.linting.pydocstyleEnabled": false, "python.linting.pylintEnabled": true, "python.linting.pylintUseMinimalCheckers": false, "python.testing.autoTestDiscoverOnSaveEnabled": true, diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index a0ab7cf..93ad0bc 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -72,10 +72,13 @@ def __init__( ), "Given transport does not implement the 'GraphqlWsTransport' interface!" self._transport = transport self._is_connected = False - assert subprotocol in ( + if subprotocol not in ( "graphql-transport-ws", "graphql-ws", - ), "Client supports only graphql-transport-ws and graphql-ws subprotocols!" + ): + raise ValueError( + "Client supports only graphql-transport-ws and graphql-ws subprotocols!" + ) self._subprotocol = subprotocol @property diff --git a/channels_graphql_ws/graphql_ws_consumer.py b/channels_graphql_ws/graphql_ws_consumer.py index 0510c6d..e98151f 100644 --- a/channels_graphql_ws/graphql_ws_consumer.py +++ b/channels_graphql_ws/graphql_ws_consumer.py @@ -91,11 +91,11 @@ class GraphqlWsConsumer(ch_websocket.AsyncJsonWebsocketConsumer): connection to a single client. This class implements the WebSocket-based GraphQL protocol used by - `graphql-ws` library (used by Apollo): - https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md - We also support the previously used but not recommended by Apollo - `graphql-transport-ws` subprotocol: - https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md + `graphql-transport-ws` (the modern one, documented at + https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md). + We also support the older `graphql-ws` subprotocol (Apollo's legacy + `subscriptions-transport-ws` protocol, documented at + https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md). """ # ----------------------------------------------------------------- PUBLIC INTERFACE @@ -287,13 +287,15 @@ async def connect(self): # Check the subprotocol told by the client. # - # NOTE: In Python 3.6 `scope["subprotocols"]` was a string, but - # starting with Python 3.7 it is a bytes. This can be a proper - # change or just a bug in the Channels to be fixed. So let's - # accept both variants until it becomes clear. + # NOTE: In some environments `scope["subprotocols"]` may be a + # plain `str` or `bytes` instead of a list. Normalize to a list + # so the list-comprehension below always works correctly. + raw_subprotocols = self.scope["subprotocols"] + if isinstance(raw_subprotocols, (bytes, str)): + raw_subprotocols = [raw_subprotocols] client_subprotocols = [ (sp.decode() if isinstance(sp, bytes) else sp) - for sp in self.scope["subprotocols"] + for sp in raw_subprotocols ] if "graphql-transport-ws" in client_subprotocols: diff --git a/channels_graphql_ws/testing.py b/channels_graphql_ws/testing.py index 87420ab..9cf4b3b 100644 --- a/channels_graphql_ws/testing.py +++ b/channels_graphql_ws/testing.py @@ -73,18 +73,19 @@ async def connect_and_init(self, connect_only: bool = False, payload="") -> None 2. Initialize GraphQL connection. Skipped if connect_only=True. """ if connect_only: - await self._transport.connect() + await self._transport.connect(subprotocol=self._subprotocol) self._is_connected = True else: await super().connect_and_init(payload=payload) - async def send_raw_message(self, message): + async def send_raw_message(self, message) -> Optional[str]: """Send a raw message. This can be useful for testing, for example, to check that the server responds appropriately to malformed messages. """ await self._transport.send(message) + return message.get("id") if isinstance(message, dict) else None async def wait_disconnect(self, timeout=None, assert_code=None): """Wait server to close the connection. @@ -157,7 +158,7 @@ def __init__( **(communicator_kwds or {}), ) - async def connect(self, timeout: Optional[float] = None) -> None: + async def connect(self, timeout: Optional[float] = None, subprotocol=None) -> None: """Connect to the server.""" ok, code = await self._comm.connect(timeout or self.TIMEOUT) if not ok: diff --git a/channels_graphql_ws/transport.py b/channels_graphql_ws/transport.py index 08fb53e..deebe41 100644 --- a/channels_graphql_ws/transport.py +++ b/channels_graphql_ws/transport.py @@ -93,10 +93,13 @@ async def connect( "graphql-ws". By default set to "graphql-transport-ws". """ - assert subprotocol in ( + if subprotocol not in ( "graphql-transport-ws", "graphql-ws", - ), "Transport only supports graphql-transport-ws and graphql-ws subprotocols!" + ): + raise ValueError( + "Transport only supports graphql-transport-ws and graphql-ws subprotocols!" + ) connected = asyncio.Event() self._message_processor = asyncio.create_task( self._process_messages(connected, timeout or self.TIMEOUT, subprotocol) @@ -109,6 +112,10 @@ async def connect( ) finally: connected_task.cancel() + try: + await connected_task + except asyncio.CancelledError: + pass if self._message_processor.done(): # Make sure to raise an exception from the task. self._message_processor.result() diff --git a/example/example.py b/example/example.py index 1228572..f0f0bf5 100644 --- a/example/example.py +++ b/example/example.py @@ -240,7 +240,7 @@ def graphiql(request): urlpatterns = [ django.urls.path("", graphiql), # `GraphQLView` used to handle mutations and queries requests - # sended by http. + # sent over HTTP. django.urls.path( "graphql/", graphene_django.views.GraphQLView.as_view( diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py index c817f9d..0379759 100644 --- a/tests/test_concurrent.py +++ b/tests/test_concurrent.py @@ -341,10 +341,12 @@ async def receiver(op_ids): if resp["type"] == "complete": op_ids.remove(op_id) else: + expected_type = ( + "next" if subprotocol == "graphql-transport-ws" else "data" + ) assert ( - resp["type"] == "next" - if subprotocol == "graphql-transport-ws" - else "data" and resp["payload"]["data"] is None + resp["type"] == expected_type + and resp["payload"]["data"] is None ), ( "This should be a successful subscription message, not '%s'", resp, @@ -496,10 +498,9 @@ async def subscribe_unsubscribe(user_id="TOM"): resp = await client.receive(raw_response=True) assert sub_id == resp["id"] + expected_type = "next" if subprotocol == "graphql-transport-ws" else "data" assert ( - resp["type"] == "next" - if subprotocol == "graphql-transport-ws" - else "data" and resp["payload"]["data"] is None + resp["type"] == expected_type and resp["payload"]["data"] is None ), "First we expect to get a confirmation message!" resp = await client.receive(raw_response=True) @@ -777,10 +778,9 @@ def unsubscribe_all(): resp = await client.receive(raw_response=True) assert sub_id == resp["id"] + expected_type = "next" if subprotocol == "graphql-transport-ws" else "data" assert ( - resp["type"] == "next" - if subprotocol == "graphql-transport-ws" - else "data" and resp["payload"]["data"] is None + resp["type"] == expected_type and resp["payload"]["data"] is None ), "First we expect to get a confirmation message!" resp = await client.receive(raw_response=True) diff --git a/tests/test_error.py b/tests/test_error.py index 282e96d..3f75527 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -247,7 +247,7 @@ async def test_subscriber_already_exists_error_graphql_transport_ws(gql): for _ in range(2): await client.start( query=""" - subscription { test_subscription (switch: "NONE\") { ok } } + subscription { test_subscription (switch: "NONE") { ok } } """, msg_id="NOT_UNIQUE_ID", ) @@ -316,7 +316,7 @@ async def test_subscribe_return_value(gql, subprotocol): await client.connect_and_init() await client.start( query=f""" - subscription {{ test_subscription (switch: "{result_type}\") {{ ok }} }} + subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} """ ) await client.assert_no_messages("Subscribe responded with a message!") From 587955b275751a7d002632b17841d236f83b30cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:51:28 +0000 Subject: [PATCH 09/14] Fix CI failures: mypy, pylint line-length, pylint unused-arg, black formatting Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/client.py | 1 - channels_graphql_ws/dict_as_object.py | 1 + channels_graphql_ws/graphql_ws_consumer.py | 3 +-- channels_graphql_ws/testing.py | 2 ++ channels_graphql_ws/transport.py | 7 +++++-- tests/test_concurrent.py | 9 +++------ tests/test_error.py | 18 ++++++------------ tests/test_serialization.py | 18 ++++++------------ 8 files changed, 24 insertions(+), 35 deletions(-) diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 93ad0bc..2992966 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -21,7 +21,6 @@ """GraphQL client.""" - import asyncio import textwrap import time diff --git a/channels_graphql_ws/dict_as_object.py b/channels_graphql_ws/dict_as_object.py index 4d3736f..b2a6e25 100644 --- a/channels_graphql_ws/dict_as_object.py +++ b/channels_graphql_ws/dict_as_object.py @@ -20,6 +20,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Dict wrapper to access keys as attributes.""" + from urllib.parse import urljoin, urlsplit from django.conf import settings diff --git a/channels_graphql_ws/graphql_ws_consumer.py b/channels_graphql_ws/graphql_ws_consumer.py index e98151f..3bc2a80 100644 --- a/channels_graphql_ws/graphql_ws_consumer.py +++ b/channels_graphql_ws/graphql_ws_consumer.py @@ -294,8 +294,7 @@ async def connect(self): if isinstance(raw_subprotocols, (bytes, str)): raw_subprotocols = [raw_subprotocols] client_subprotocols = [ - (sp.decode() if isinstance(sp, bytes) else sp) - for sp in raw_subprotocols + (sp.decode() if isinstance(sp, bytes) else sp) for sp in raw_subprotocols ] if "graphql-transport-ws" in client_subprotocols: diff --git a/channels_graphql_ws/testing.py b/channels_graphql_ws/testing.py index 9cf4b3b..72637df 100644 --- a/channels_graphql_ws/testing.py +++ b/channels_graphql_ws/testing.py @@ -160,6 +160,8 @@ def __init__( async def connect(self, timeout: Optional[float] = None, subprotocol=None) -> None: """Connect to the server.""" + # The subprotocol is already baked into the communicator at construction. + # pylint: disable=unused-argument ok, code = await self._comm.connect(timeout or self.TIMEOUT) if not ok: raise RuntimeError( diff --git a/channels_graphql_ws/transport.py b/channels_graphql_ws/transport.py index deebe41..75d7a87 100644 --- a/channels_graphql_ws/transport.py +++ b/channels_graphql_ws/transport.py @@ -35,7 +35,9 @@ class GraphqlWsTransport: # Default timeout for the WebSocket messages. TIMEOUT: float = 60.0 - async def connect(self, timeout: Optional[float] = None) -> None: + async def connect( + self, timeout: Optional[float] = None, subprotocol: Optional[str] = None + ) -> None: """Connect to the server.""" raise NotImplementedError() @@ -98,7 +100,8 @@ async def connect( "graphql-ws", ): raise ValueError( - "Transport only supports graphql-transport-ws and graphql-ws subprotocols!" + "Transport only supports graphql-transport-ws" + " and graphql-ws subprotocols!" ) connected = asyncio.Event() self._message_processor = asyncio.create_task( diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py index 0379759..b13beba 100644 --- a/tests/test_concurrent.py +++ b/tests/test_concurrent.py @@ -345,8 +345,7 @@ async def receiver(op_ids): "next" if subprotocol == "graphql-transport-ws" else "data" ) assert ( - resp["type"] == expected_type - and resp["payload"]["data"] is None + resp["type"] == expected_type and resp["payload"]["data"] is None ), ( "This should be a successful subscription message, not '%s'", resp, @@ -609,15 +608,13 @@ async def subscribe_unsubscribe(iteration: int): ) spam_payload = { - "query": textwrap.dedent( - f""" + "query": textwrap.dedent(f""" mutation op_name($message: String!, $user_id: UserId) {{ {mutation}(message: $message, user_id: $user_id) {{ message }} }} - """ - ), + """), "variables": { "message": "__SPAM_SPAM_SPAM_SPAM_SPAM_SPAM__", "user_id": "ALICE", diff --git a/tests/test_error.py b/tests/test_error.py index 3f75527..414c9a6 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -58,13 +58,11 @@ async def test_syntax_error(gql, subprotocol): await client.receive_complete(msg_id) print("Check multiple errors in the `data` message.") - msg_id = await client.start( - query=""" + msg_id = await client.start(query=""" query { projects { path wrong_field } } query a { projects } { wrong_name } - """ - ) + """) errors, data = await client.receive_error(msg_id) assert data is None assert len(errors) == 5, f"Five errors expected, but {len(errors)} errors received!" @@ -314,11 +312,9 @@ async def test_subscribe_return_value(gql, subprotocol): for result_type in ["NONE", "LIST", "TUPLE"]: client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - await client.start( - query=f""" + await client.start(query=f""" subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} - """ - ) + """) await client.assert_no_messages("Subscribe responded with a message!") await client.finalize() @@ -327,11 +323,9 @@ async def test_subscribe_return_value(gql, subprotocol): for result_type in ["STR", "DICT", "EMPTYSTR"]: client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - msg_id = await client.start( - query=f""" + msg_id = await client.start(query=f""" subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} - """ - ) + """) errors, _ = await client.receive_error(msg_id) assert "AssertionError" in errors[0]["message"], ( "There is no error in response" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index f11c606..ed247e8 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -127,15 +127,13 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start( - query=""" + sub_id = await client.start(query=""" subscription { on_models_received { user { id, username } } } - """ - ) + """) print("Invoke mutation which sends Django model to the subscription.") @@ -233,8 +231,7 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start( - query=""" + sub_id = await client.start(query=""" subscription { on_models_received { user1_id @@ -245,8 +242,7 @@ class Mutation(graphene.ObjectType): user2_typename } } - """ - ) + """) print("Invoke mutation which sends Django model to the subscription.") @@ -347,8 +343,7 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start( - query=""" + sub_id = await client.start(query=""" subscription { on_timestamps_received { now_date @@ -359,8 +354,7 @@ class Mutation(graphene.ObjectType): now_time_typename } } - """ - ) + """) print("Invoke mutation which sends timestamps to the subscription.") From a32d20456557da9cbee31e85fc05fc03ee16a9c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:16:19 +0000 Subject: [PATCH 10/14] Fix black formatting using pinned version 24.4.2 (as in poetry.lock) Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- tests/test_concurrent.py | 6 ++++-- tests/test_error.py | 18 ++++++++++++------ tests/test_serialization.py | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py index b13beba..8f72739 100644 --- a/tests/test_concurrent.py +++ b/tests/test_concurrent.py @@ -608,13 +608,15 @@ async def subscribe_unsubscribe(iteration: int): ) spam_payload = { - "query": textwrap.dedent(f""" + "query": textwrap.dedent( + f""" mutation op_name($message: String!, $user_id: UserId) {{ {mutation}(message: $message, user_id: $user_id) {{ message }} }} - """), + """ + ), "variables": { "message": "__SPAM_SPAM_SPAM_SPAM_SPAM_SPAM__", "user_id": "ALICE", diff --git a/tests/test_error.py b/tests/test_error.py index 414c9a6..3f75527 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -58,11 +58,13 @@ async def test_syntax_error(gql, subprotocol): await client.receive_complete(msg_id) print("Check multiple errors in the `data` message.") - msg_id = await client.start(query=""" + msg_id = await client.start( + query=""" query { projects { path wrong_field } } query a { projects } { wrong_name } - """) + """ + ) errors, data = await client.receive_error(msg_id) assert data is None assert len(errors) == 5, f"Five errors expected, but {len(errors)} errors received!" @@ -312,9 +314,11 @@ async def test_subscribe_return_value(gql, subprotocol): for result_type in ["NONE", "LIST", "TUPLE"]: client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - await client.start(query=f""" + await client.start( + query=f""" subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} - """) + """ + ) await client.assert_no_messages("Subscribe responded with a message!") await client.finalize() @@ -323,9 +327,11 @@ async def test_subscribe_return_value(gql, subprotocol): for result_type in ["STR", "DICT", "EMPTYSTR"]: client = gql(subscription=Subscription, subprotocol=subprotocol) await client.connect_and_init() - msg_id = await client.start(query=f""" + msg_id = await client.start( + query=f""" subscription {{ test_subscription (switch: "{result_type}") {{ ok }} }} - """) + """ + ) errors, _ = await client.receive_error(msg_id) assert "AssertionError" in errors[0]["message"], ( "There is no error in response" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index ed247e8..f11c606 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -127,13 +127,15 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start(query=""" + sub_id = await client.start( + query=""" subscription { on_models_received { user { id, username } } } - """) + """ + ) print("Invoke mutation which sends Django model to the subscription.") @@ -231,7 +233,8 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start(query=""" + sub_id = await client.start( + query=""" subscription { on_models_received { user1_id @@ -242,7 +245,8 @@ class Mutation(graphene.ObjectType): user2_typename } } - """) + """ + ) print("Invoke mutation which sends Django model to the subscription.") @@ -343,7 +347,8 @@ class Mutation(graphene.ObjectType): print("Subscribe to receive subscription notifications.") - sub_id = await client.start(query=""" + sub_id = await client.start( + query=""" subscription { on_timestamps_received { now_date @@ -354,7 +359,8 @@ class Mutation(graphene.ObjectType): now_time_typename } } - """) + """ + ) print("Invoke mutation which sends timestamps to the subscription.") From 16f77f8105bcbb28a12020d42a984573247c2579 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:29:41 +0000 Subject: [PATCH 11/14] docs: update CHANGELOG and PR description to highlight upstream rebase Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8891d1..ac3011f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,17 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Changelog +## [Unreleased] + +- Rebased with upstream `datadvance/DjangoChannelsGraphqlWs` (commit `1ade107`). +- Updated GitHub Actions CI: Python matrix bumped to 3.10 / 3.11 / 3.12; + `actions/checkout` and `actions/setup-python` upgraded from v2 to v4; + macOS runner added to test matrix. +- Fixed Python 3.10+ asyncio incompatibilities (event loop handling, + `asyncio.wait` coroutine usage). +- Fixed vulnerable `aiohttp` and `django` versions in `poetry.lock`. +- Post-rebase linting/formatting fixes (mypy, pylint, black 24.4.2). + ## [1.0.0rc9] - 2024-04-11 - Package renamed to `baseapp-django-channels-graphql-ws` for PyPI distribution. From ca3b096b62696b6581466385623bd98fb2e6c866 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:44:39 +0000 Subject: [PATCH 12/14] fix: restore GraphqlWsClient.send() for backward-compat; fix asyncio.get_event_loop() deprecation in subscription.py Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/client.py | 29 +++++++++++++++++++++++++++++ channels_graphql_ws/subscription.py | 14 ++++++-------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 2992966..40466ea 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -239,6 +239,35 @@ async def start(self, query, *, variables=None, operation_name=None, msg_id=AUTO await self._transport.send(message) return msg_id + async def send(self, *, msg_type, payload, op_id=AUTO): + """Send a raw GraphQL operation message (subscribe/start/etc.). + + Backward-compatible helper that lets callers build and send an + operation message directly without going through the higher-level + `start()` / `subscribe()` helpers. The message is always sent + with an ``"id"`` field because this method is intended for + operation messages (``subscribe``, ``start``, ...) which require + one; it is not suitable for connection-level messages such as + ``connection_init``. + + Args: + msg_type: The ``type`` field of the GraphQL WS message, + e.g. ``"subscribe"`` (graphql-transport-ws) or + ``"start"`` (graphql-ws). + payload: The ``payload`` field of the message. + op_id: The operation identifier. Auto-generated when not + supplied. + + Returns: + The operation identifier that was used. + + """ + if op_id is self.AUTO: + op_id = str(uuid.uuid4().hex) + message = {"type": msg_type, "payload": payload, "id": op_id} + await self._transport.send(message) + return op_id + async def complete(self, op_id): """Complete GraphQL request. diff --git a/channels_graphql_ws/subscription.py b/channels_graphql_ws/subscription.py index 62f03d9..47baa13 100644 --- a/channels_graphql_ws/subscription.py +++ b/channels_graphql_ws/subscription.py @@ -180,14 +180,13 @@ def broadcast(cls, *, group=None, payload=None): """ try: - event_loop = asyncio.get_event_loop() + asyncio.get_running_loop() except RuntimeError: pass else: - if event_loop.is_running(): - return event_loop.create_task( - cls.broadcast_async(group=group, payload=payload) - ) + return asyncio.create_task( + cls.broadcast_async(group=group, payload=payload) + ) return cls.broadcast_sync(group=group, payload=payload) @@ -248,12 +247,11 @@ def unsubscribe(cls, *, group=None): subscription will be unsubscribed. """ try: - event_loop = asyncio.get_event_loop() + asyncio.get_running_loop() except RuntimeError: pass else: - if event_loop.is_running(): - return asyncio.create_task(cls.unsubscribe_async(group=group)) + return asyncio.create_task(cls.unsubscribe_async(group=group)) return cls.unsubscribe_sync(group=group) From 69550414243b4731767c2511d82e6e1ee30449ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:53:07 +0000 Subject: [PATCH 13/14] Add send() method to GraphqlWsClient for direct message sending Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/client.py | 23 ++++++++++++ tests/test_basic.py | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 40466ea..141bb59 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -208,6 +208,29 @@ async def subscribe(self, query, *, variables=None, wait_confirmation=True): await self.receive(wait_id=msg_id) return msg_id + async def send(self, msg_type, payload, msg_id=AUTO): + """Send a GraphQL message with the given type and payload. + + This is a low-level method that sends a message directly without + any subprotocol-specific type mapping. Useful when you need full + control over the message type, e.g. in tests or custom clients. + + Args: + msg_type: The type of the message (e.g. "subscribe", "start"). + payload: The payload dict of the message. + msg_id: The message identifier. Automatically generated by + default. + + Returns: + The message identifier. + + """ + if msg_id is self.AUTO: + msg_id = str(uuid.uuid4().hex) + message = {"type": msg_type, "payload": payload, "id": msg_id} + await self._transport.send(message) + return msg_id + async def start(self, query, *, variables=None, operation_name=None, msg_id=AUTO): """Start GraphQL request. Responses must be checked explicitly. diff --git a/tests/test_basic.py b/tests/test_basic.py index 706e43e..4305348 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -316,6 +316,74 @@ def check_resp(resp, user_id, message): await comm_alice.finalize() +@pytest.mark.asyncio +async def test_send_method(gql): + """Test the low-level ``send`` method on the GraphQL client. + + Verify that ``client.send(msg_type, payload)`` works as an + alternative to ``client.start`` - it should send a message with + the given type and payload and return the generated message id. + """ + subprotocol = "graphql-transport-ws" + + print("Establish & initialize WebSocket GraphQL connection.") + client = gql( + query=Query, + mutation=Mutation, + subscription=Subscription, + consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, + ) + await client.connect_and_init() + + print("Subscribe using the low-level send method.") + sub_id = await client.send( + msg_type="subscribe", + payload={ + "query": """ + subscription op_name { + on_chat_message_sent(user_id: ALICE) { event } + } + """, + "operationName": "op_name", + }, + ) + assert sub_id is not None, "send() should return a message id" + + await client.assert_no_messages() + + print("Trigger the subscription by mutation.") + message = f"Hi via send! {str(uuid.uuid4().hex)}" + msg_id = await client.start( + query=""" + mutation op_name($message: String!) { + send_chat_message(message: $message) { + message + } + } + """, + variables={"message": message}, + operation_name="op_name", + ) + + # Mutation response. + resp = await client.receive_next(msg_id) + assert resp["data"] == {"send_chat_message": {"message": message}} + await client.receive_complete(msg_id) + + print("Check subscription notification was received using receive with assert_id.") + resp = await client.receive(assert_id=sub_id, assert_type="next") + event = resp["data"]["on_chat_message_sent"]["event"] + assert json.loads(event) == { + # pylint: disable=no-member + "user_id": UserId.ALICE.value, # type: ignore[attr-defined] + "payload": message, + }, "Subscription notification contains wrong data!" + + print("Disconnect and wait the application to finish gracefully.") + await client.finalize() + + @pytest.mark.asyncio @pytest.mark.parametrize("subprotocol", ["graphql-transport-ws", "graphql-ws"]) async def test_ping(gql, subprotocol): From fac7ff94971ed8de25ab26a3711074351a6def7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:28:42 +0000 Subject: [PATCH 14/14] Fix send() to auto-connect when client is not yet connected Co-authored-by: nossila <32983+nossila@users.noreply.github.com> --- channels_graphql_ws/client.py | 32 +++++---------- tests/test_basic.py | 74 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/channels_graphql_ws/client.py b/channels_graphql_ws/client.py index 141bb59..01f4690 100644 --- a/channels_graphql_ws/client.py +++ b/channels_graphql_ws/client.py @@ -208,29 +208,6 @@ async def subscribe(self, query, *, variables=None, wait_confirmation=True): await self.receive(wait_id=msg_id) return msg_id - async def send(self, msg_type, payload, msg_id=AUTO): - """Send a GraphQL message with the given type and payload. - - This is a low-level method that sends a message directly without - any subprotocol-specific type mapping. Useful when you need full - control over the message type, e.g. in tests or custom clients. - - Args: - msg_type: The type of the message (e.g. "subscribe", "start"). - payload: The payload dict of the message. - msg_id: The message identifier. Automatically generated by - default. - - Returns: - The message identifier. - - """ - if msg_id is self.AUTO: - msg_id = str(uuid.uuid4().hex) - message = {"type": msg_type, "payload": payload, "id": msg_id} - await self._transport.send(message) - return msg_id - async def start(self, query, *, variables=None, operation_name=None, msg_id=AUTO): """Start GraphQL request. Responses must be checked explicitly. @@ -273,6 +250,13 @@ async def send(self, *, msg_type, payload, op_id=AUTO): one; it is not suitable for connection-level messages such as ``connection_init``. + If the client is not yet connected, ``connect_and_init()`` is + called automatically so that the WebSocket handshake and the + GraphQL ``connection_init``/``connection_ack`` exchange complete + before the operation message is sent. This allows callers to + skip an explicit ``connect_and_init()`` call when it is not + needed (e.g. anonymous subscription tests). + Args: msg_type: The ``type`` field of the GraphQL WS message, e.g. ``"subscribe"`` (graphql-transport-ws) or @@ -285,6 +269,8 @@ async def send(self, *, msg_type, payload, op_id=AUTO): The operation identifier that was used. """ + if not self._is_connected: + await self.connect_and_init() if op_id is self.AUTO: op_id = str(uuid.uuid4().hex) message = {"type": msg_type, "payload": payload, "id": op_id} diff --git a/tests/test_basic.py b/tests/test_basic.py index 4305348..ac1331c 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -316,6 +316,80 @@ def check_resp(resp, user_id, message): await comm_alice.finalize() +@pytest.mark.asyncio +async def test_send_method_auto_connects(gql): + """Test that ``send`` auto-connects when the client is not yet connected. + + When a test (or caller) uses ``client.send(...)`` without calling + ``connect_and_init()`` first, the connection must be established + and initialized transparently so that the server's subprotocol + detection runs before the first application-level message arrives. + + This reproduces the pattern used by baseapp-backend's anonymous + subscription tests which create a client via the ``graphql_websocket`` + fixture and call ``send()`` directly. + """ + subprotocol = "graphql-transport-ws" + + print("Create client but DO NOT call connect_and_init.") + client = gql( + query=Query, + mutation=Mutation, + subscription=Subscription, + consumer_attrs={"strict_ordering": True}, + subprotocol=subprotocol, + ) + assert not client.connected, "Client should not be connected yet" + + print("send() should auto-connect and then subscribe.") + sub_id = await client.send( + msg_type="subscribe", + payload={ + "query": """ + subscription op_name { + on_chat_message_sent(user_id: ALICE) { event } + } + """, + "operationName": "op_name", + }, + ) + assert sub_id is not None, "send() should return a message id" + assert client.connected, "Client should be connected after send()" + + await client.assert_no_messages() + + print("Trigger the subscription by mutation.") + message = f"Hi auto-connect! {str(uuid.uuid4().hex)}" + msg_id = await client.start( + query=""" + mutation op_name($message: String!) { + send_chat_message(message: $message) { + message + } + } + """, + variables={"message": message}, + operation_name="op_name", + ) + + # Mutation response. + resp = await client.receive_next(msg_id) + assert resp["data"] == {"send_chat_message": {"message": message}} + await client.receive_complete(msg_id) + + print("Check subscription notification was received.") + resp = await client.receive(assert_id=sub_id, assert_type="next") + event = resp["data"]["on_chat_message_sent"]["event"] + assert json.loads(event) == { + # pylint: disable=no-member + "user_id": UserId.ALICE.value, # type: ignore[attr-defined] + "payload": message, + }, "Subscription notification contains wrong data!" + + print("Disconnect and wait the application to finish gracefully.") + await client.finalize() + + @pytest.mark.asyncio async def test_send_method(gql): """Test the low-level ``send`` method on the GraphQL client.