From 61cedb0deacd8d89ae0c187dee486a8708d8cc71 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 4 Sep 2025 13:34:33 +0100 Subject: [PATCH 1/3] chore: upgrade poetry check workflow --- .github/workflows/check.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 214f63fc..c948e424 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,13 +24,24 @@ jobs: with: submodules: 'recursive' - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Setup poetry - uses: abatilo/actions-poetry@v2.0.0 + uses: abatilo/actions-poetry@v4 + + - name: Setup a local virtual environment + run: | + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + + - uses: actions/cache@v3 + name: Define a cache for the virtual environment based on the dependencies lock file with: - poetry-version: 1.3.2 + path: ./.venv + key: venv-${{ hashFiles('poetry.lock') }} + - name: Install dependencies run: poetry install -E crypto - name: Generate rest sync code and tests From 9bfa4db3ba5177082b3a13b60114e93381443134 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 4 Sep 2025 16:50:46 +0100 Subject: [PATCH 2/3] fix: auth_url handling and add timeout to `once_async` calls in realtime tests - Replaced all `connection.once_async` calls with `asyncio.wait_for` to include a 5-second timeout. - Ensures tests fail gracefully if connection isn't established within the specified timeframe. --- .github/workflows/check.yml | 2 + ably/http/http.py | 8 +- ably/rest/auth.py | 28 ++- ably/util/helper.py | 31 ++- poetry.lock | 182 +++++++++++------- pyproject.toml | 5 +- test/ably/realtime/realtimeauth_test.py | 37 ++-- test/ably/realtime/realtimechannel_test.py | 28 +-- test/ably/realtime/realtimeconnection_test.py | 20 +- test/ably/realtime/realtimeinit_test.py | 3 +- test/ably/realtime/realtimeresume_test.py | 18 +- 11 files changed, 227 insertions(+), 135 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c948e424..77c1e42e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -30,6 +30,8 @@ jobs: - name: Setup poetry uses: abatilo/actions-poetry@v4 + with: + poetry-version: '1.8.5' - name: Setup a local virtual environment run: | diff --git a/ably/http/http.py b/ably/http/http.py index 8314da08..45367eef 100644 --- a/ably/http/http.py +++ b/ably/http/http.py @@ -11,7 +11,7 @@ from ably.http.httputils import HttpUtils from ably.transport.defaults import Defaults from ably.util.exceptions import AblyException -from ably.util.helper import is_token_error +from ably.util.helper import is_token_error, extract_url_params log = logging.getLogger(__name__) @@ -198,11 +198,13 @@ def should_stop_retrying(): self.preferred_port) url = urljoin(base_url, path) + (clean_url, url_params) = extract_url_params(url) + request = self.__client.build_request( method=method, - url=url, + url=clean_url, content=body, - params=params, + params=dict(url_params, **params), headers=all_headers, timeout=timeout, ) diff --git a/ably/rest/auth.py b/ably/rest/auth.py index ab255a3e..a48cc162 100644 --- a/ably/rest/auth.py +++ b/ably/rest/auth.py @@ -1,13 +1,16 @@ from __future__ import annotations + import base64 -from datetime import timedelta import logging import time -from typing import Optional, TYPE_CHECKING, Union import uuid +from datetime import timedelta +from typing import Optional, TYPE_CHECKING, Union + import httpx from ably.types.options import Options + if TYPE_CHECKING: from ably.rest.rest import AblyRest from ably.realtime.realtime import AblyRealtime @@ -16,6 +19,7 @@ from ably.types.tokendetails import TokenDetails from ably.types.tokenrequest import TokenRequest from ably.util.exceptions import AblyAuthException, AblyException, IncompatibleClientIdException +from ably.util.helper import extract_url_params __all__ = ["Auth"] @@ -23,7 +27,6 @@ class Auth: - class Method: BASIC = "BASIC" TOKEN = "TOKEN" @@ -271,8 +274,7 @@ async def create_token_request(self, token_params: Optional[dict | str] = None, if capability is not None: token_request['capability'] = str(Capability(capability)) - token_request["client_id"] = ( - token_params.get('client_id') or self.client_id) + token_request["client_id"] = token_params.get('client_id') or self.client_id # Note: There is no expectation that the client # specifies the nonce; this is done by the library @@ -388,17 +390,27 @@ def _random_nonce(self): async def token_request_from_auth_url(self, method: str, url: str, token_params, headers, auth_params): + # Extract URL parameters using utility function + clean_url, url_params = extract_url_params(url) + body = None params = None if method == 'GET': body = {} - params = dict(auth_params, **token_params) + # Merge URL params, auth_params, and token_params (later params override earlier ones) + # we do this because httpx version has inconsistency and some versions override query params + # that are specified in url string + params = {**url_params, **auth_params, **token_params} elif method == 'POST': if isinstance(auth_params, TokenDetails): auth_params = auth_params.to_dict() - params = {} + # For POST, URL params go in query string, auth_params and token_params go in body + params = url_params body = dict(auth_params, **token_params) + # Use clean URL for the request + url = clean_url + from ably.http.http import Response async with httpx.AsyncClient(http2=True) as client: resp = await client.request(method=method, url=url, headers=headers, params=params, data=body) @@ -420,6 +432,6 @@ async def token_request_from_auth_url(self, method: str, url: str, token_params, token_request = response.text else: msg = 'auth_url responded with unacceptable content-type ' + content_type + \ - ', should be either text/plain, application/jwt or application/json', + ', should be either text/plain, application/jwt or application/json', raise AblyAuthException(msg, 401, 40170) return token_request diff --git a/ably/util/helper.py b/ably/util/helper.py index 2a767e83..76ff9e2d 100644 --- a/ably/util/helper.py +++ b/ably/util/helper.py @@ -3,7 +3,8 @@ import string import asyncio import time -from typing import Callable +from typing import Callable, Tuple, Dict +from urllib.parse import urlparse, parse_qs def get_random_id(): @@ -25,6 +26,34 @@ def is_token_error(exception): return 40140 <= exception.code < 40150 +def extract_url_params(url: str) -> Tuple[str, Dict[str, str]]: + """ + Extract URL parameters from a URL and return a clean URL and parameters dict. + + Args: + url: The URL to parse + + Returns: + Tuple of (clean_url_without_params, url_params_dict) + """ + parsed_url = urlparse(url) + url_params = {} + + if parsed_url.query: + # Convert query parameters to a flat dictionary + query_params = parse_qs(parsed_url.query) + for key, values in query_params.items(): + # Take the last value if multiple values exist for the same key + url_params[key] = values[-1] + + # Reconstruct clean URL without query parameters + clean_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" + if parsed_url.fragment: + clean_url += f"#{parsed_url.fragment}" + + return clean_url, url_params + + class Timer: def __init__(self, timeout: float, callback: Callable): self._timeout = timeout diff --git a/poetry.lock b/poetry.lock index 99a96dae..bd912cd2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,13 +24,13 @@ trio = ["trio (<0.22)"] [[package]] name = "anyio" -version = "4.3.0" +version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, ] [package.dependencies] @@ -40,9 +40,9 @@ sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "async-case" @@ -56,13 +56,13 @@ files = [ [[package]] name = "certifi" -version = "2024.2.2" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] [[package]] @@ -150,15 +150,18 @@ toml = ["tomli"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.3.0" 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.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -207,6 +210,17 @@ files = [ [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + [[package]] name = "h2" version = "4.1.0" @@ -256,24 +270,24 @@ socks = ["socksio (==1.*)"] [[package]] name = "httpcore" -version = "1.0.4" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, - {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.25.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" @@ -300,13 +314,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -314,13 +328,13 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "hyperframe" @@ -335,15 +349,18 @@ files = [ [[package]] name = "idna" -version = "3.6" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" version = "4.13.0" @@ -559,43 +576,52 @@ files = [ [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.23.0" description = "Cryptographic library for Python" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, ] [[package]] @@ -614,13 +640,13 @@ typing-extensions = "*" [[package]] name = "pyee" -version = "11.1.0" +version = "12.1.1" description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false python-versions = ">=3.8" files = [ - {file = "pyee-11.1.0-py3-none-any.whl", hash = "sha256:5d346a7d0f861a4b2e6c47960295bd895f816725b27d656181947346be98d7c1"}, - {file = "pyee-11.1.0.tar.gz", hash = "sha256:b53af98f6990c810edd9b56b87791021a8f54fd13db4edd1142438d44ba2263f"}, + {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, + {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, ] [package.dependencies] @@ -699,13 +725,13 @@ pytest = ">=3.10" [[package]] name = "pytest-timeout" -version = "2.3.1" +version = "2.4.0" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, - {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, + {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, + {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, ] [package.dependencies] @@ -745,15 +771,29 @@ files = [ [package.dependencies] httpx = ">=0.21.0" +[[package]] +name = "respx" +version = "0.22.0" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +optional = false +python-versions = ">=3.8" +files = [ + {file = "respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0"}, + {file = "respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91"}, +] + +[package.dependencies] +httpx = ">=0.25.0" + [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -1085,4 +1125,4 @@ oldcrypto = ["pycrypto"] [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "be01764fbf3dbbd9b87f731dc298eb6a77379915e715f2364bc992b30d924e46" +content-hash = "202ad35d679177a9cdd52df65101346d14d4d16548796620991649eab7e08062" diff --git a/pyproject.toml b/pyproject.toml index 52d2c26a..32706d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,10 @@ pep8-naming = "^0.4.1" pytest-cov = "^2.4" flake8="^3.9.2" pytest-xdist = "^1.15" -respx = "^0.20.0" +respx = [ + { version = "^0.20.0", python = "~3.7" }, + { version = "^0.22.0", python = "^3.8" }, +] importlib-metadata = "^4.12" pytest-timeout = "^2.1.0" async-case = { version = "^10.1.0", python = "~3.7" } diff --git a/test/ably/realtime/realtimeauth_test.py b/test/ably/realtime/realtimeauth_test.py index 15f93835..4011e621 100644 --- a/test/ably/realtime/realtimeauth_test.py +++ b/test/ably/realtime/realtimeauth_test.py @@ -34,7 +34,7 @@ async def auth_callback_failure(options, expect_failure=False): class TestRealtimeAuth(BaseAsyncTestCase): async def test_auth_valid_api_key(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.error_reason is None response_time_ms = await ably.connection.ping() assert response_time_ms is not None @@ -53,7 +53,7 @@ async def test_auth_with_token_string(self): rest = await TestApp.get_ably_rest() token_details = await rest.auth.request_token() ably = await TestApp.get_ably_realtime(token=token_details.token) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -71,7 +71,7 @@ async def test_auth_with_token_details(self): rest = await TestApp.get_ably_rest() token_details = await rest.auth.request_token() ably = await TestApp.get_ably_realtime(token_details=token_details) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -93,7 +93,7 @@ async def callback(params): return token_details ably = await TestApp.get_ably_realtime(auth_callback=callback) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -107,7 +107,7 @@ async def callback(params): return token_details ably = await TestApp.get_ably_realtime(auth_callback=callback) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -121,7 +121,7 @@ async def callback(params): return token_details.token ably = await TestApp.get_ably_realtime(auth_callback=callback) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -144,7 +144,10 @@ async def test_auth_with_auth_url_json(self): url_path = f"{echo_url}/?type=json&body={urllib.parse.quote_plus(token_details_json)}" ably = await TestApp.get_ably_realtime(auth_url=url_path) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for( + ably.connection.once_async(ConnectionState.CONNECTED), + timeout=5, + ) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -156,7 +159,7 @@ async def test_auth_with_auth_url_text_plain(self): url_path = f"{echo_url}/?type=text&body={token_details.token}" ably = await TestApp.get_ably_realtime(auth_url=url_path) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -169,7 +172,7 @@ async def test_auth_with_auth_url_post(self): ably = await TestApp.get_ably_realtime(auth_url=url_path, auth_method='POST', auth_params=token_details) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert ably.connection.error_reason is None @@ -183,7 +186,7 @@ async def callback(params): return token_details.token ably = await TestApp.get_ably_realtime(auth_callback=callback) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.transport original_access_token = ably.connection.connection_manager.transport.params.get('accessToken') @@ -307,7 +310,7 @@ async def callback(params): "action": ProtocolMessageAction.AUTH, } - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) auth_future = asyncio.Future() def on_update(state_change): @@ -334,7 +337,7 @@ async def auth_callback(_): ably = await TestApp.get_ably_realtime(auth_callback=auth_callback) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) original_token_details = ably.auth.token_details await ably.connection.once_async(ConnectionEvent.UPDATE) assert ably.auth.token_details is not original_token_details @@ -496,7 +499,7 @@ async def callback(params): } } - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) original_token_details = ably.auth.token_details assert ably.connection.connection_manager.transport await ably.connection.connection_manager.transport.on_protocol_message(msg) @@ -511,7 +514,7 @@ async def test_renew_token_no_renew_means_provided_upon_disconnection(self): ably = await TestApp.get_ably_realtime(token_details=token_details) - state_change = await ably.connection.once_async(ConnectionState.CONNECTED) + state_change = await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) msg = { "action": ProtocolMessageAction.DISCONNECTED, "error": { @@ -544,7 +547,7 @@ async def callback(params): } } - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) connection_key = ably.connection.connection_details.connection_key await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) @@ -572,12 +575,12 @@ async def test_renew_token_no_renew_means_provided_on_resume(self): } } - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) connection_key = ably.connection.connection_details.connection_key await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) - state_change = await ably.connection.once_async(ConnectionState.CONNECTED) + state_change = await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.transport.params["resume"] == connection_key assert ably.connection.connection_manager.transport diff --git a/test/ably/realtime/realtimechannel_test.py b/test/ably/realtime/realtimechannel_test.py index fb9b274e..488f3059 100644 --- a/test/ably/realtime/realtimechannel_test.py +++ b/test/ably/realtime/realtimechannel_test.py @@ -33,7 +33,7 @@ async def test_channels_release(self): async def test_channel_attach(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') assert channel.state == ChannelState.INITIALIZED await channel.attach() @@ -42,7 +42,7 @@ async def test_channel_attach(self): async def test_channel_detach(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() await channel.detach() @@ -62,7 +62,7 @@ def listener(message): else: second_message_future.set_result(message) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() await channel.subscribe('event', listener) @@ -91,7 +91,7 @@ def listener(msg: Message): if not message_future.done(): message_future.set_result(msg) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() await channel.subscribe('event', listener) @@ -110,7 +110,7 @@ def listener(msg: Message): async def test_subscribe_coroutine(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() @@ -138,7 +138,7 @@ async def listener(msg): # RTL7a async def test_subscribe_all_events(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() @@ -165,7 +165,7 @@ def listener(msg): # RTL7c async def test_subscribe_auto_attach(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') assert channel.state == ChannelState.INITIALIZED @@ -181,7 +181,7 @@ def listener(_): # RTL8b async def test_unsubscribe(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() @@ -216,7 +216,7 @@ def listener(msg): # RTL8c async def test_unsubscribe_all(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get('my_channel') await channel.attach() @@ -250,7 +250,7 @@ def listener(msg): async def test_realtime_request_timeout_attach(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) original_send_protocol_message = ably.connection.connection_manager.send_protocol_message async def new_send_protocol_message(msg): @@ -268,7 +268,7 @@ async def new_send_protocol_message(msg): async def test_realtime_request_timeout_detach(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) original_send_protocol_message = ably.connection.connection_manager.send_protocol_message async def new_send_protocol_message(msg): @@ -287,7 +287,7 @@ async def new_send_protocol_message(msg): async def test_channel_detached_once_connection_closed(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get(random_string(5)) await channel.attach() @@ -296,7 +296,7 @@ async def test_channel_detached_once_connection_closed(self): async def test_channel_failed_once_connection_failed(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get(random_string(5)) await channel.attach() @@ -307,7 +307,7 @@ async def test_channel_failed_once_connection_failed(self): async def test_channel_suspended_once_connection_suspended(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get(random_string(5)) await channel.attach() diff --git a/test/ably/realtime/realtimeconnection_test.py b/test/ably/realtime/realtimeconnection_test.py index 9d9b58f5..126c77f0 100644 --- a/test/ably/realtime/realtimeconnection_test.py +++ b/test/ably/realtime/realtimeconnection_test.py @@ -43,7 +43,7 @@ async def test_auth_invalid_key(self): async def test_connection_ping_connected(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) response_time_ms = await ably.connection.ping() assert response_time_ms is not None assert type(response_time_ms) is float @@ -70,7 +70,7 @@ async def test_connection_ping_failed(self): async def test_connection_ping_closed(self): ably = await TestApp.get_ably_realtime() ably.connect() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) await ably.close() with pytest.raises(AblyException) as exception: await ably.connection.ping() @@ -123,7 +123,7 @@ async def test_realtime_request_timeout_connect(self): async def test_realtime_request_timeout_ping(self): ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) original_send_protocol_message = ably.connection.connection_manager.send_protocol_message @@ -162,7 +162,7 @@ async def new_connect(): await ably.connection.once_async(ConnectionState.DISCONNECTED) # Test that the library eventually connects after two failed attempts - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) await ably.close() @@ -275,7 +275,7 @@ async def test_retry_immediately_upon_unexpected_disconnection(self): ) # Wait for the client to connect - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) # Simulate random loss of connection assert ably.connection.connection_manager.transport @@ -284,7 +284,7 @@ async def test_retry_immediately_upon_unexpected_disconnection(self): assert ably.connection.state == ConnectionState.DISCONNECTED # Wait for the client to connect again - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) await ably.close() async def test_fallback_host(self): @@ -294,7 +294,7 @@ async def test_fallback_host(self): assert ably.connection.connection_manager.transport ably.connection.connection_manager.transport._emit('failed', AblyException("test exception", 502, 50200)) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.transport.host != self.test_vars["realtime_host"] assert ably.options.fallback_realtime_host != self.test_vars["realtime_host"] @@ -339,7 +339,7 @@ async def test_fallback_host_disconnected_protocol_msg(self): } })) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.transport.host != self.test_vars["realtime_host"] assert ably.options.fallback_realtime_host != self.test_vars["realtime_host"] @@ -365,7 +365,7 @@ async def test_connection_client_id_query_params(self): ably = await TestApp.get_ably_realtime(client_id=client_id) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.transport.params["client_id"] == client_id assert ably.auth.client_id == client_id @@ -394,6 +394,6 @@ async def on_protocol_message(msg): await ably.connection.once_async(ConnectionState.DISCONNECTED) # should re-establish connection after disconnected_retry_timeout - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) await ably.close() diff --git a/test/ably/realtime/realtimeinit_test.py b/test/ably/realtime/realtimeinit_test.py index 96fa540c..ef8f99b4 100644 --- a/test/ably/realtime/realtimeinit_test.py +++ b/test/ably/realtime/realtimeinit_test.py @@ -1,3 +1,4 @@ +import asyncio from ably.realtime.connection import ConnectionState import pytest from ably import Auth @@ -32,7 +33,7 @@ async def test_init_without_autoconnect(self): ably = await TestApp.get_ably_realtime(auto_connect=False) assert ably.connection.state == ConnectionState.INITIALIZED ably.connect() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.state == ConnectionState.CONNECTED await ably.close() assert ably.connection.state == ConnectionState.CLOSED diff --git a/test/ably/realtime/realtimeresume_test.py b/test/ably/realtime/realtimeresume_test.py index f37ea440..15ec73b2 100644 --- a/test/ably/realtime/realtimeresume_test.py +++ b/test/ably/realtime/realtimeresume_test.py @@ -29,13 +29,13 @@ async def asyncSetUp(self): async def test_connection_resume(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) prev_connection_id = ably.connection.connection_manager.connection_id connection_key = ably.connection.connection_details.connection_key await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) new_connection_id = ably.connection.connection_manager.connection_id assert ably.connection.connection_manager.transport.params["resume"] == connection_key assert prev_connection_id == new_connection_id @@ -46,7 +46,7 @@ async def test_connection_resume(self): async def test_fatal_resume_error(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) ably.auth.auth_options.key_name = "wrong-key" await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) @@ -60,7 +60,7 @@ async def test_fatal_resume_error(self): async def test_invalid_resume_response(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert ably.connection.connection_manager.connection_details ably.connection.connection_manager.connection_details.connection_key = 'ably-python-fake-key' @@ -69,7 +69,7 @@ async def test_invalid_resume_response(self): await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) - state_change = await ably.connection.once_async(ConnectionState.CONNECTED) + state_change = await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert state_change.reason.code == 80018 assert state_change.reason.status_code == 400 @@ -80,7 +80,7 @@ async def test_invalid_resume_response(self): async def test_attached_channel_reattaches_on_invalid_resume(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get(random_string(5)) @@ -93,7 +93,7 @@ async def test_attached_channel_reattaches_on_invalid_resume(self): await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert channel.state == ChannelState.ATTACHING @@ -104,7 +104,7 @@ async def test_attached_channel_reattaches_on_invalid_resume(self): async def test_suspended_channel_reattaches_on_invalid_resume(self): ably = await TestApp.get_ably_realtime() - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) channel = ably.channels.get(random_string(5)) channel.state = ChannelState.SUSPENDED @@ -116,7 +116,7 @@ async def test_suspended_channel_reattaches_on_invalid_resume(self): await ably.connection.connection_manager.transport.dispose() ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED) - await ably.connection.once_async(ConnectionState.CONNECTED) + await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5) assert channel.state == ChannelState.ATTACHING From 30fdc5dc55c447d010070537daf910ec91d53b74 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 5 Sep 2025 00:09:15 +0100 Subject: [PATCH 3/3] fix: set correct python env for poetr and add retries --- .github/workflows/check.yml | 69 +++++++++++++++++++++---------------- .github/workflows/lint.yml | 52 ++++++++++++++++++++-------- poetry.lock | 33 +++++++++++++++++- pyproject.toml | 4 +++ setup.cfg | 7 ++-- 5 files changed, 114 insertions(+), 51 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 77c1e42e..5bebcec8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,33 +20,42 @@ jobs: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Setup poetry - uses: abatilo/actions-poetry@v4 - with: - poetry-version: '1.8.5' - - - name: Setup a local virtual environment - run: | - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - - uses: actions/cache@v3 - name: Define a cache for the virtual environment based on the dependencies lock file - with: - path: ./.venv - key: venv-${{ hashFiles('poetry.lock') }} - - - name: Install dependencies - run: poetry install -E crypto - - name: Generate rest sync code and tests - run: poetry run unasync - - name: Test with pytest - run: poetry run pytest --verbose --tb=short + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: ${{ matrix.python-version }} + + - name: Setup poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: '2.1.4' + + - name: Setup a local virtual environment + run: | + poetry env use ${{ steps.setup-python.outputs.python-path }} + poetry run python --version + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + + - uses: actions/cache@v4 + name: Define a cache for the virtual environment based on the dependencies lock file + id: cache + with: + path: ./.venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + run: poetry run pip --version >/dev/null 2>&1 || (echo "Cache is broken, skip it" && rm -rf .venv) + + - name: Install dependencies + run: poetry install -E crypto + - name: Generate rest sync code and tests + run: poetry run unasync + - name: Test with pytest + run: poetry run pytest --verbose --tb=short --reruns 3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 45bd0b83..1b1b86b3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,18 +10,40 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Setup poetry - uses: abatilo/actions-poetry@v2.0.0 - with: - poetry-version: 1.3.2 - - name: Install dependencies - run: poetry install -E crypto - - name: Lint with flake8 - run: poetry run flake8 + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: '3.9' + + - name: Setup poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: '2.1.4' + + - name: Setup a local virtual environment + run: | + poetry env use ${{ steps.setup-python.outputs.python-path }} + poetry run python --version + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + + - uses: actions/cache@v4 + name: Define a cache for the virtual environment based on the dependencies lock file + id: cache + with: + path: ./.venv + key: venv-${{ runner.os }}-3.9-${{ hashFiles('poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + run: poetry run pip --version >/dev/null 2>&1 || (echo "Cache is broken, skip it." && rm -rf .venv) + + - name: Install dependencies + run: poetry install + - name: Lint with flake8 + run: poetry run flake8 diff --git a/poetry.lock b/poetry.lock index bd912cd2..f70afeb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -723,6 +723,37 @@ files = [ py = "*" pytest = ">=3.10" +[[package]] +name = "pytest-rerunfailures" +version = "13.0" +description = "pytest plugin to re-run tests to eliminate flaky failures" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-rerunfailures-13.0.tar.gz", hash = "sha256:e132dbe420bc476f544b96e7036edd0a69707574209b6677263c950d19b09199"}, + {file = "pytest_rerunfailures-13.0-py3-none-any.whl", hash = "sha256:34919cb3fcb1f8e5d4b940aa75ccdea9661bade925091873b7c6fa5548333069"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=1", markers = "python_version < \"3.8\""} +packaging = ">=17.1" +pytest = ">=7" + +[[package]] +name = "pytest-rerunfailures" +version = "14.0" +description = "pytest plugin to re-run tests to eliminate flaky failures" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92"}, + {file = "pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32"}, +] + +[package.dependencies] +packaging = ">=17.1" +pytest = ">=7.2" + [[package]] name = "pytest-timeout" version = "2.4.0" @@ -1125,4 +1156,4 @@ oldcrypto = ["pycrypto"] [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "202ad35d679177a9cdd52df65101346d14d4d16548796620991649eab7e08062" +content-hash = "f4eccc80c57888b82f8dfe72d821b62b6dd5bfb38fb324cd8fa494d08d80357a" diff --git a/pyproject.toml b/pyproject.toml index 32706d56..e3a9e4a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,10 @@ respx = [ ] importlib-metadata = "^4.12" pytest-timeout = "^2.1.0" +pytest-rerunfailures = [ + { version = "^13.0", python = "~3.7" }, + { version = "^14.0", python = "^3.8" }, +] async-case = { version = "^10.1.0", python = "~3.7" } tokenize_rt = "*" diff --git a/setup.cfg b/setup.cfg index 28f68fb8..cef1b15a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,11 +7,8 @@ ignore = W503, W504, N818 per-file-ignores = # imported but unused __init__.py: F401 - -exclude = - # Exclude virtual environment check - venv - +# Exclude virtual environment check +exclude = .venv,venv,env,.env,.git,__pycache__,.pytest_cache,build,dist,*.egg-info [tool:pytest] #log_level = DEBUG