From 5f2c71756ef90e71848aaecbd4e8f6005b510acb Mon Sep 17 00:00:00 2001 From: moyosore Date: Mon, 12 Sep 2022 08:34:29 +0100 Subject: [PATCH 01/19] add basic realtime auth --- ably/__init__.py | 1 + ably/realtime/__init__.py | 0 ably/realtime/realtime.py | 34 ++++++++++++++++++++++++++++++++++ test/ably/realtimeauthtest.py | 21 +++++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 ably/realtime/__init__.py create mode 100644 ably/realtime/realtime.py create mode 100644 test/ably/realtimeauthtest.py diff --git a/ably/__init__.py b/ably/__init__.py index 578a1537..128e3d08 100644 --- a/ably/__init__.py +++ b/ably/__init__.py @@ -1,4 +1,5 @@ from ably.rest.rest import AblyRest +from ably.realtime.realtime import AblyRealtime from ably.rest.auth import Auth from ably.rest.push import Push from ably.types.capability import Capability diff --git a/ably/realtime/__init__.py b/ably/realtime/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py new file mode 100644 index 00000000..d9baff7c --- /dev/null +++ b/ably/realtime/realtime.py @@ -0,0 +1,34 @@ +import logging +from ably.rest.auth import Auth +from ably.types.options import Options + + +log = logging.getLogger(__name__) + +class AblyRealtime: + """Ably Realtime Client""" + + def __init__(self, key=None, **kwargs): + """Create an AblyRealtime instance. + + :Parameters: + **Credentials** + - `key`: a valid ably key string + """ + + if key is not None: + options = Options(key=key, **kwargs) + else: + options = Options(**kwargs) + + self.__auth = Auth(self, options) + + self.__options = options + + @property + def auth(self): + return self.__auth + + @property + def options(self): + return self.__options diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py new file mode 100644 index 00000000..2c759481 --- /dev/null +++ b/test/ably/realtimeauthtest.py @@ -0,0 +1,21 @@ +import pytest +from ably import Auth, AblyRealtime +from ably.util.exceptions import AblyAuthException +from test.ably.utils import BaseAsyncTestCase + + +class TestRealtimeAuth(BaseAsyncTestCase): + async def setUp(self): + self.invalid_key = "some key" + self.valid_key_format = "Vjhddw.owt:R97sjjbdERJdjwer" + + def test_auth_with_correct_key_format(self): + key = self.valid_key_format.split(":") + ably = AblyRealtime(self.valid_key_format) + assert Auth.Method.BASIC == ably.auth.auth_mechanism, "Unexpected Auth method mismatch" + assert ably.auth.auth_options.key_name == key[0] + assert ably.auth.auth_options.key_secret == key[1] + + def test_auth_incorrect_key_format(self): + with pytest.raises(AblyAuthException): + ably = AblyRealtime(self.invalid_key) \ No newline at end of file From bc1b8a492576825e9873196f5b3701c204fecb87 Mon Sep 17 00:00:00 2001 From: moyosore Date: Tue, 13 Sep 2022 09:28:14 +0100 Subject: [PATCH 02/19] create connection --- ably/realtime/connection.py | 33 +++++++++++++ ably/realtime/realtime.py | 11 ++++- poetry.lock | 94 ++++++++++++++++++++++++++++++------- pyproject.toml | 1 + 4 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 ably/realtime/connection.py diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py new file mode 100644 index 00000000..0fab035a --- /dev/null +++ b/ably/realtime/connection.py @@ -0,0 +1,33 @@ +import asyncio +import websockets +import json + + +class RealtimeConnection: + def __init__(self, realtime): + self.options = realtime.options + self.__ably = realtime + + async def connect(self): + self.connected_future = asyncio.Future() + asyncio.create_task(self.connect_impl()) + return await self.connected_future + + async def connect_impl(self): + async with websockets.connect(f'wss://realtime.ably.io?key={self.ably.key}') as websocket: + self.websocket = websocket + task = asyncio.create_task(self.ws_read_loop()) + await task + + async def ws_read_loop(self): + while True: + raw = await self.websocket.recv() + msg = json.loads(raw) + action = msg['action'] + if (action == 4): # CONNECTED + self.connected_future.set_result(msg) + return msg + + @property + def ably(self): + return self.__ably diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index d9baff7c..475a728e 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -1,4 +1,5 @@ import logging +from ably.realtime.connection import RealtimeConnection from ably.rest.auth import Auth from ably.types.options import Options @@ -8,7 +9,7 @@ class AblyRealtime: """Ably Realtime Client""" - def __init__(self, key=None, **kwargs): + def __init__(self, key=None, token=None, token_details=None, **kwargs): """Create an AblyRealtime instance. :Parameters: @@ -22,8 +23,9 @@ def __init__(self, key=None, **kwargs): options = Options(**kwargs) self.__auth = Auth(self, options) - self.__options = options + self.key = key + self.__connection = RealtimeConnection(self) @property def auth(self): @@ -32,3 +34,8 @@ def auth(self): @property def options(self): return self.__options + + @property + def connection(self): + """Returns the channels container object""" + return self.__connection diff --git a/poetry.lock b/poetry.lock index 18243444..27c6cbb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,8 +12,8 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] [[package]] @@ -33,10 +33,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "certifi" @@ -161,8 +161,8 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10.0.0,<11.0.0)"] http2 = ["h2 (>=3,<5)"] [[package]] @@ -194,9 +194,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -235,7 +235,7 @@ pbr = ">=0.11" six = ">=1.7" [package.extras] -docs = ["sphinx", "jinja2 (<2.7)", "Pygments (<2)", "sphinx (<1.3)"] +docs = ["Pygments (<2)", "jinja2 (<2.7)", "sphinx", "sphinx (<1.3)"] test = ["unittest2 (>=1.1.0)"] [[package]] @@ -285,8 +285,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" @@ -337,7 +337,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -374,7 +374,7 @@ pytest = ">=4.6" toml = "*" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-flake8" @@ -482,6 +482,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "websockets" +version = "10.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "zipp" version = "3.8.1" @@ -491,8 +499,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] crypto = ["pycryptodome"] @@ -501,7 +509,7 @@ oldcrypto = ["pycrypto"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "0f5fa1c07bd116047635d4d34692f7f9ca1bb194988445ef61854469c2ce214d" +content-hash = "e7cc9e61014182ddade6f85e62d97616a52fb4a60a9a471e0b963eb1e82630aa" [metadata.files] anyio = [ @@ -805,6 +813,56 @@ typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] +websockets = [ + {file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"}, + {file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"}, + {file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"}, + {file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"}, + {file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"}, + {file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"}, + {file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"}, + {file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"}, + {file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"}, + {file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"}, + {file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"}, + {file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"}, + {file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"}, + {file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"}, + {file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"}, + {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, + {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, +] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, diff --git a/pyproject.toml b/pyproject.toml index 355ed464..51cb1353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ h2 = "^4.0.0" # Optional dependencies pycrypto = { version = "^2.6.1", optional = true } pycryptodome = { version = "*", optional = true } +websockets = "^10.3" [tool.poetry.extras] oldcrypto = ["pycrypto"] From 4b321368bd3b421aa92ede128e7e4b9c41a75ff6 Mon Sep 17 00:00:00 2001 From: moyosore Date: Tue, 13 Sep 2022 15:27:26 +0100 Subject: [PATCH 03/19] update connection --- ably/realtime/connection.py | 9 +++++++-- ably/realtime/realtime.py | 4 +++- test/ably/realtimeauthtest.py | 29 ++++++++++++++++++++++++----- test/ably/restsetup.py | 2 ++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index 0fab035a..c870bd15 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -1,6 +1,7 @@ import asyncio import websockets import json +from ably.util.exceptions import AblyAuthException class RealtimeConnection: @@ -13,8 +14,9 @@ async def connect(self): asyncio.create_task(self.connect_impl()) return await self.connected_future + async def connect_impl(self): - async with websockets.connect(f'wss://realtime.ably.io?key={self.ably.key}') as websocket: + async with websockets.connect(f'{self.options.realtime_host}?key={self.ably.key}') as websocket: self.websocket = websocket task = asyncio.create_task(self.ws_read_loop()) await task @@ -26,7 +28,10 @@ async def ws_read_loop(self): action = msg['action'] if (action == 4): # CONNECTED self.connected_future.set_result(msg) - return msg + if (action == 9): # ERROR + error = msg["error"] + self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) + @property def ably(self): diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 475a728e..059a43ec 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -1,4 +1,5 @@ import logging +import os from ably.realtime.connection import RealtimeConnection from ably.rest.auth import Auth from ably.types.options import Options @@ -9,7 +10,7 @@ class AblyRealtime: """Ably Realtime Client""" - def __init__(self, key=None, token=None, token_details=None, **kwargs): + def __init__(self, key=None, **kwargs): """Create an AblyRealtime instance. :Parameters: @@ -22,6 +23,7 @@ def __init__(self, key=None, token=None, token_details=None, **kwargs): else: options = Options(**kwargs) + options.realtime_host = os.environ.get('ABLY_REALTIME_HOST') self.__auth = Auth(self, options) self.__options = options self.key = key diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index 2c759481..f696569b 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -1,21 +1,40 @@ import pytest from ably import Auth, AblyRealtime from ably.util.exceptions import AblyAuthException +from test.ably.restsetup import RestSetup from test.ably.utils import BaseAsyncTestCase class TestRealtimeAuth(BaseAsyncTestCase): async def setUp(self): - self.invalid_key = "some key" - self.valid_key_format = "Vjhddw.owt:R97sjjbdERJdjwer" + self.test_vars = await RestSetup.get_test_vars() + self.valid_key_format = "Vjdw.owt:R97sjjjwer" - def test_auth_with_correct_key_format(self): + async def test_auth_with_valid_key(self): + ably = AblyRealtime(self.test_vars["keys"][0]["key_str"]) + assert Auth.Method.BASIC == ably.auth.auth_mechanism, "Unexpected Auth method mismatch" + assert ably.auth.auth_options.key_name == self.test_vars["keys"][0]['key_name'] + assert ably.auth.auth_options.key_secret == self.test_vars["keys"][0]['key_secret'] + + async def test_auth_incorrect_key(self): + with pytest.raises(AblyAuthException): + AblyRealtime("some invalid key") + + async def test_auth_with_valid_key_format(self): key = self.valid_key_format.split(":") ably = AblyRealtime(self.valid_key_format) assert Auth.Method.BASIC == ably.auth.auth_mechanism, "Unexpected Auth method mismatch" assert ably.auth.auth_options.key_name == key[0] assert ably.auth.auth_options.key_secret == key[1] - def test_auth_incorrect_key_format(self): + # async def test_auth_connection(self): + # ably = AblyRealtime(self.test_vars["keys"][0]["key_str"]) + # conn = await ably.connection.connect() + # assert conn["action"] == 4 + # assert "connectionDetails" in conn + + async def test_auth_invalid_key(self): + ably = AblyRealtime(self.valid_key_format) with pytest.raises(AblyAuthException): - ably = AblyRealtime(self.invalid_key) \ No newline at end of file + await ably.connection.connect() + diff --git a/test/ably/restsetup.py b/test/ably/restsetup.py index 3c681005..9babdd05 100644 --- a/test/ably/restsetup.py +++ b/test/ably/restsetup.py @@ -14,6 +14,7 @@ tls = (os.environ.get('ABLY_TLS') or "true").lower() == "true" host = os.environ.get('ABLY_HOST', 'sandbox-rest.ably.io') +realtime_host = os.environ.get('ABLY_REALTIME_HOST', 'sandbox-realtime.ably.io') environment = os.environ.get('ABLY_ENV') port = 80 @@ -51,6 +52,7 @@ async def get_test_vars(sender=None): "tls_port": tls_port, "tls": tls, "environment": environment, + "realtime_host": realtime_host, "keys": [{ "key_name": "%s.%s" % (app_id, k.get("id", "")), "key_secret": k.get("value", ""), From 91f3f8fc9a48167048a7340c9c99bf8863694b11 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:26:57 +0100 Subject: [PATCH 04/19] Add get_ably_realtime test helper --- test/ably/restsetup.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/ably/restsetup.py b/test/ably/restsetup.py index 9babdd05..efab592d 100644 --- a/test/ably/restsetup.py +++ b/test/ably/restsetup.py @@ -6,6 +6,7 @@ from ably.types.capability import Capability from ably.types.options import Options from ably.util.exceptions import AblyException +from ably.realtime.realtime import AblyRealtime log = logging.getLogger(__name__) @@ -14,7 +15,7 @@ tls = (os.environ.get('ABLY_TLS') or "true").lower() == "true" host = os.environ.get('ABLY_HOST', 'sandbox-rest.ably.io') -realtime_host = os.environ.get('ABLY_REALTIME_HOST', 'sandbox-realtime.ably.io') +realtime_host = 'sandbox-realtime.ably.io' environment = os.environ.get('ABLY_ENV') port = 80 @@ -81,6 +82,20 @@ async def get_ably_rest(cls, **kw): options.update(kw) return AblyRest(**options) + @classmethod + async def get_ably_realtime(cls, **kw): + test_vars = await RestSetup.get_test_vars() + options = { + 'key': test_vars["keys"][0]["key_str"], + 'realtime_host': realtime_host, + 'port': test_vars["port"], + 'tls_port': test_vars["tls_port"], + 'tls': test_vars["tls"], + 'environment': test_vars["environment"], + } + options.update(kw) + return AblyRealtime(**options) + @classmethod async def clear_test_vars(cls): test_vars = RestSetup.__test_vars From ef063949d17b06a34efa2707d845eb9b0c20503a Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:27:21 +0100 Subject: [PATCH 05/19] Use configured realtime_host for websocket connections --- ably/realtime/connection.py | 3 +-- ably/realtime/realtime.py | 2 -- ably/types/options.py | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index c870bd15..bedfda18 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -14,9 +14,8 @@ async def connect(self): asyncio.create_task(self.connect_impl()) return await self.connected_future - async def connect_impl(self): - async with websockets.connect(f'{self.options.realtime_host}?key={self.ably.key}') as websocket: + async with websockets.connect(f'wss://{self.options.realtime_host}?key={self.ably.key}') as websocket: self.websocket = websocket task = asyncio.create_task(self.ws_read_loop()) await task diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 059a43ec..9f44f7ff 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -1,5 +1,4 @@ import logging -import os from ably.realtime.connection import RealtimeConnection from ably.rest.auth import Auth from ably.types.options import Options @@ -23,7 +22,6 @@ def __init__(self, key=None, **kwargs): else: options = Options(**kwargs) - options.realtime_host = os.environ.get('ABLY_REALTIME_HOST') self.__auth = Auth(self, options) self.__options = options self.key = key diff --git a/ably/types/options.py b/ably/types/options.py index 38ef8ed9..441d87b6 100644 --- a/ably/types/options.py +++ b/ably/types/options.py @@ -27,6 +27,9 @@ def __init__(self, client_id=None, log_level=0, tls=True, rest_host=None, from ably import api_version idempotent_rest_publishing = api_version >= '1.2' + if realtime_host is None: + realtime_host = Defaults.realtime_host + self.__client_id = client_id self.__log_level = log_level self.__tls = tls From 82a42f47aa860563e53793540bfaeec98d983082 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:27:33 +0100 Subject: [PATCH 06/19] Update tests to use realtime helper method --- test/ably/realtimeauthtest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index f696569b..626eb12d 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -8,21 +8,21 @@ class TestRealtimeAuth(BaseAsyncTestCase): async def setUp(self): self.test_vars = await RestSetup.get_test_vars() - self.valid_key_format = "Vjdw.owt:R97sjjjwer" + self.valid_key_format = "api:key" async def test_auth_with_valid_key(self): - ably = AblyRealtime(self.test_vars["keys"][0]["key_str"]) + ably = await RestSetup.get_ably_realtime(key=self.test_vars["keys"][0]["key_str"]) assert Auth.Method.BASIC == ably.auth.auth_mechanism, "Unexpected Auth method mismatch" assert ably.auth.auth_options.key_name == self.test_vars["keys"][0]['key_name'] assert ably.auth.auth_options.key_secret == self.test_vars["keys"][0]['key_secret'] async def test_auth_incorrect_key(self): with pytest.raises(AblyAuthException): - AblyRealtime("some invalid key") + await RestSetup.get_ably_realtime(key="some invalid key") async def test_auth_with_valid_key_format(self): key = self.valid_key_format.split(":") - ably = AblyRealtime(self.valid_key_format) + ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) assert Auth.Method.BASIC == ably.auth.auth_mechanism, "Unexpected Auth method mismatch" assert ably.auth.auth_options.key_name == key[0] assert ably.auth.auth_options.key_secret == key[1] @@ -34,7 +34,6 @@ async def test_auth_with_valid_key_format(self): # assert "connectionDetails" in conn async def test_auth_invalid_key(self): - ably = AblyRealtime(self.valid_key_format) + ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) with pytest.raises(AblyAuthException): await ably.connection.connect() - From 8daa1075142b82392e83b0f5f9f484779e1de1bd Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:29:37 +0100 Subject: [PATCH 07/19] Add Realtime.connect method --- ably/realtime/realtime.py | 8 ++++++-- test/ably/realtimeauthtest.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 9f44f7ff..71dc5b38 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -6,6 +6,7 @@ log = logging.getLogger(__name__) + class AblyRealtime: """Ably Realtime Client""" @@ -26,7 +27,10 @@ def __init__(self, key=None, **kwargs): self.__options = options self.key = key self.__connection = RealtimeConnection(self) - + + async def connect(self): + await self.connection.connect() + @property def auth(self): return self.__auth @@ -34,7 +38,7 @@ def auth(self): @property def options(self): return self.__options - + @property def connection(self): """Returns the channels container object""" diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index 626eb12d..e84b5703 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -36,4 +36,4 @@ async def test_auth_with_valid_key_format(self): async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) with pytest.raises(AblyAuthException): - await ably.connection.connect() + await ably.connect() From 1cad09725ef3fbb5c5fa747529f352b38b8551e4 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:36:01 +0100 Subject: [PATCH 08/19] Make Realtime.connect return None when successful --- ably/realtime/connection.py | 4 ++-- test/ably/realtimeauthtest.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index bedfda18..bf93dfbf 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -12,7 +12,7 @@ def __init__(self, realtime): async def connect(self): self.connected_future = asyncio.Future() asyncio.create_task(self.connect_impl()) - return await self.connected_future + await self.connected_future async def connect_impl(self): async with websockets.connect(f'wss://{self.options.realtime_host}?key={self.ably.key}') as websocket: @@ -26,7 +26,7 @@ async def ws_read_loop(self): msg = json.loads(raw) action = msg['action'] if (action == 4): # CONNECTED - self.connected_future.set_result(msg) + self.connected_future.set_result(None) if (action == 9): # ERROR error = msg["error"] self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index e84b5703..7297c019 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -27,11 +27,9 @@ async def test_auth_with_valid_key_format(self): assert ably.auth.auth_options.key_name == key[0] assert ably.auth.auth_options.key_secret == key[1] - # async def test_auth_connection(self): - # ably = AblyRealtime(self.test_vars["keys"][0]["key_str"]) - # conn = await ably.connection.connect() - # assert conn["action"] == 4 - # assert "connectionDetails" in conn + async def test_auth_connection(self): + ably = await RestSetup.get_ably_realtime() + await ably.connect() async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) From 3d1a83527bfc64d27cc3b4d4f4abee776e11f718 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:41:22 +0100 Subject: [PATCH 09/19] Add Connection.state --- ably/realtime/connection.py | 15 ++++++++++++++- test/ably/realtimeauthtest.py | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index bf93dfbf..18485afe 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -2,17 +2,27 @@ import websockets import json from ably.util.exceptions import AblyAuthException +from enum import Enum + + +class ConnectionState(Enum): + INITIALIZED = 'initialized' + CONNECTING = 'connecting' + CONNECTED = 'connected' class RealtimeConnection: def __init__(self, realtime): self.options = realtime.options self.__ably = realtime + self.__state = ConnectionState.INITIALIZED async def connect(self): + self.__state = ConnectionState.CONNECTING self.connected_future = asyncio.Future() asyncio.create_task(self.connect_impl()) await self.connected_future + self.__state = ConnectionState.CONNECTED async def connect_impl(self): async with websockets.connect(f'wss://{self.options.realtime_host}?key={self.ably.key}') as websocket: @@ -30,8 +40,11 @@ async def ws_read_loop(self): if (action == 9): # ERROR error = msg["error"] self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) - @property def ably(self): return self.__ably + + @property + def state(self): + return self.__state diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index 7297c019..bda9a530 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -1,3 +1,4 @@ +from ably.realtime.connection import ConnectionState import pytest from ably import Auth, AblyRealtime from ably.util.exceptions import AblyAuthException @@ -29,7 +30,9 @@ async def test_auth_with_valid_key_format(self): async def test_auth_connection(self): ably = await RestSetup.get_ably_realtime() + assert ably.connection.state == ConnectionState.INITIALIZED await ably.connect() + assert ably.connection.state == ConnectionState.CONNECTED async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) From d5b7d0bdeb43b7b88d8e6f0dd5a679f6499cc684 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:48:39 +0100 Subject: [PATCH 10/19] Add Realtime.close method --- ably/realtime/connection.py | 7 +++++++ ably/realtime/realtime.py | 3 +++ test/ably/realtimeauthtest.py | 3 +++ 3 files changed, 13 insertions(+) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index 18485afe..baa24922 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -9,6 +9,8 @@ class ConnectionState(Enum): INITIALIZED = 'initialized' CONNECTING = 'connecting' CONNECTED = 'connected' + CLOSING = 'closing' + CLOSED = 'closed' class RealtimeConnection: @@ -24,6 +26,11 @@ async def connect(self): await self.connected_future self.__state = ConnectionState.CONNECTED + async def close(self): + self.__state = ConnectionState.CLOSING + await self.websocket.close() + self.__state = ConnectionState.CLOSED + async def connect_impl(self): async with websockets.connect(f'wss://{self.options.realtime_host}?key={self.ably.key}') as websocket: self.websocket = websocket diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 71dc5b38..25f57a2a 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -31,6 +31,9 @@ def __init__(self, key=None, **kwargs): async def connect(self): await self.connection.connect() + async def close(self): + await self.connection.close() + @property def auth(self): return self.__auth diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeauthtest.py index bda9a530..e9110b0d 100644 --- a/test/ably/realtimeauthtest.py +++ b/test/ably/realtimeauthtest.py @@ -33,8 +33,11 @@ async def test_auth_connection(self): assert ably.connection.state == ConnectionState.INITIALIZED await ably.connect() assert ably.connection.state == ConnectionState.CONNECTED + await ably.close() + assert ably.connection.state == ConnectionState.CLOSED async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) with pytest.raises(AblyAuthException): await ably.connect() + await ably.close() From 615a87329a59dc60da93076e2821c04b71d470d1 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:49:37 +0100 Subject: [PATCH 11/19] Move realimteauthtest.py to realtimeinit_test.py --- test/ably/{realtimeauthtest.py => realtimeinit_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/ably/{realtimeauthtest.py => realtimeinit_test.py} (100%) diff --git a/test/ably/realtimeauthtest.py b/test/ably/realtimeinit_test.py similarity index 100% rename from test/ably/realtimeauthtest.py rename to test/ably/realtimeinit_test.py From cbe3a07bb5c756019a520dff4e3d857b556ff358 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 16:52:00 +0100 Subject: [PATCH 12/19] Move connection tests to new file --- test/ably/realtimeconnection_test.py | 25 +++++++++++++++++++++++++ test/ably/realtimeinit_test.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/ably/realtimeconnection_test.py diff --git a/test/ably/realtimeconnection_test.py b/test/ably/realtimeconnection_test.py new file mode 100644 index 00000000..00e32759 --- /dev/null +++ b/test/ably/realtimeconnection_test.py @@ -0,0 +1,25 @@ +from ably.realtime.connection import ConnectionState +import pytest +from ably.util.exceptions import AblyAuthException +from test.ably.restsetup import RestSetup +from test.ably.utils import BaseAsyncTestCase + + +class TestRealtimeAuth(BaseAsyncTestCase): + async def setUp(self): + self.test_vars = await RestSetup.get_test_vars() + self.valid_key_format = "api:key" + + async def test_auth_connection(self): + ably = await RestSetup.get_ably_realtime() + assert ably.connection.state == ConnectionState.INITIALIZED + await ably.connect() + assert ably.connection.state == ConnectionState.CONNECTED + await ably.close() + assert ably.connection.state == ConnectionState.CLOSED + + async def test_auth_invalid_key(self): + ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) + with pytest.raises(AblyAuthException): + await ably.connect() + await ably.close() diff --git a/test/ably/realtimeinit_test.py b/test/ably/realtimeinit_test.py index e9110b0d..a85f9576 100644 --- a/test/ably/realtimeinit_test.py +++ b/test/ably/realtimeinit_test.py @@ -1,6 +1,6 @@ from ably.realtime.connection import ConnectionState import pytest -from ably import Auth, AblyRealtime +from ably import Auth from ably.util.exceptions import AblyAuthException from test.ably.restsetup import RestSetup from test.ably.utils import BaseAsyncTestCase From 16fabf295162295d326c228de758847253b81119 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 17:01:41 +0100 Subject: [PATCH 13/19] Ensure connected_future is resolved once --- ably/realtime/connection.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index baa24922..3e4562f6 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -1,9 +1,12 @@ +import logging import asyncio import websockets import json from ably.util.exceptions import AblyAuthException from enum import Enum +log = logging.getLogger(__name__) + class ConnectionState(Enum): INITIALIZED = 'initialized' @@ -18,6 +21,8 @@ def __init__(self, realtime): self.options = realtime.options self.__ably = realtime self.__state = ConnectionState.INITIALIZED + self.connected_future = None + self.websocket = None async def connect(self): self.__state = ConnectionState.CONNECTING @@ -42,11 +47,18 @@ async def ws_read_loop(self): raw = await self.websocket.recv() msg = json.loads(raw) action = msg['action'] - if (action == 4): # CONNECTED - self.connected_future.set_result(None) - if (action == 9): # ERROR + if action == 4: # CONNECTED + if self.connected_future: + self.connected_future.set_result(None) + self.connected_future = None + else: + log.warn('CONNECTED message receieved but connected_future not set') + if action == 9: # ERROR error = msg["error"] - self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) + if error['nonfatal'] is False: + if self.connected_future: + self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) + self.connected_future = None @property def ably(self): From c479e1240247cf2fed08176b997b927b83884d8f Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 17:08:40 +0100 Subject: [PATCH 14/19] Add some state validation to Connection methods --- ably/realtime/connection.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index 3e4562f6..bd3b21df 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -25,15 +25,27 @@ def __init__(self, realtime): self.websocket = None async def connect(self): - self.__state = ConnectionState.CONNECTING - self.connected_future = asyncio.Future() - asyncio.create_task(self.connect_impl()) - await self.connected_future - self.__state = ConnectionState.CONNECTED + if self.__state == ConnectionState.CONNECTED: + return + + if self.__state == ConnectionState.CONNECTING: + if self.connected_future is None: + log.fatal('Connection state is CONNECTING but connected_future does not exits') + return + await self.connected_future + else: + self.__state = ConnectionState.CONNECTING + self.connected_future = asyncio.Future() + asyncio.create_task(self.connect_impl()) + await self.connected_future + self.__state = ConnectionState.CONNECTED async def close(self): self.__state = ConnectionState.CLOSING - await self.websocket.close() + if self.websocket: + await self.websocket.close() + else: + log.warn('Connection.closed called while connection already closed') self.__state = ConnectionState.CLOSED async def connect_impl(self): From 4d5f00f2e67d4447f47527933224a775b81b2535 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Tue, 13 Sep 2022 17:14:34 +0100 Subject: [PATCH 15/19] Add tests for transient connection states --- test/ably/realtimeconnection_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/ably/realtimeconnection_test.py b/test/ably/realtimeconnection_test.py index 00e32759..134c1f9d 100644 --- a/test/ably/realtimeconnection_test.py +++ b/test/ably/realtimeconnection_test.py @@ -1,3 +1,4 @@ +import asyncio from ably.realtime.connection import ConnectionState import pytest from ably.util.exceptions import AblyAuthException @@ -18,6 +19,22 @@ async def test_auth_connection(self): await ably.close() assert ably.connection.state == ConnectionState.CLOSED + async def test_connecting_state(self): + ably = await RestSetup.get_ably_realtime() + task = asyncio.create_task(ably.connect()) + await asyncio.sleep(0) + assert ably.connection.state == ConnectionState.CONNECTING + await task + await ably.close() + + async def test_closing_state(self): + ably = await RestSetup.get_ably_realtime() + await ably.connect() + task = asyncio.create_task(ably.close()) + await asyncio.sleep(0) + assert ably.connection.state == ConnectionState.CLOSING + await task + async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) with pytest.raises(AblyAuthException): From 7e73898eb0dffaafbfbe94a8731752459ab2dc15 Mon Sep 17 00:00:00 2001 From: moyosore Date: Wed, 14 Sep 2022 08:29:34 +0100 Subject: [PATCH 16/19] add api key check --- ably/realtime/connection.py | 3 ++- ably/realtime/realtime.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index bd3b21df..a9e24341 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -69,7 +69,8 @@ async def ws_read_loop(self): error = msg["error"] if error['nonfatal'] is False: if self.connected_future: - self.connected_future.set_exception(AblyAuthException(error["message"], error["statusCode"], error["code"])) + self.connected_future.set_exception( + AblyAuthException(error["message"], error["statusCode"], error["code"])) self.connected_future = None @property diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 25f57a2a..36cf1cbe 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -21,7 +21,7 @@ def __init__(self, key=None, **kwargs): if key is not None: options = Options(key=key, **kwargs) else: - options = Options(**kwargs) + raise ValueError("Key is missing. Provide an API key") self.__auth = Auth(self, options) self.__options = options @@ -44,5 +44,5 @@ def options(self): @property def connection(self): - """Returns the channels container object""" + """Establish realtime connection""" return self.__connection From b8f3c9adac3f20978cf5300a030fd1da02d34f8e Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Wed, 14 Sep 2022 14:15:28 +0100 Subject: [PATCH 17/19] Change some connection fields to private --- ably/realtime/connection.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index a9e24341..3a154bae 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -21,57 +21,57 @@ def __init__(self, realtime): self.options = realtime.options self.__ably = realtime self.__state = ConnectionState.INITIALIZED - self.connected_future = None - self.websocket = None + self.__connected_future = None + self.__websocket = None async def connect(self): if self.__state == ConnectionState.CONNECTED: return if self.__state == ConnectionState.CONNECTING: - if self.connected_future is None: + if self.__connected_future is None: log.fatal('Connection state is CONNECTING but connected_future does not exits') return - await self.connected_future + await self.__connected_future else: self.__state = ConnectionState.CONNECTING - self.connected_future = asyncio.Future() + self.__connected_future = asyncio.Future() asyncio.create_task(self.connect_impl()) - await self.connected_future + await self.__connected_future self.__state = ConnectionState.CONNECTED async def close(self): self.__state = ConnectionState.CLOSING - if self.websocket: - await self.websocket.close() + if self.__websocket: + await self.__websocket.close() else: log.warn('Connection.closed called while connection already closed') self.__state = ConnectionState.CLOSED async def connect_impl(self): async with websockets.connect(f'wss://{self.options.realtime_host}?key={self.ably.key}') as websocket: - self.websocket = websocket + self.__websocket = websocket task = asyncio.create_task(self.ws_read_loop()) await task async def ws_read_loop(self): while True: - raw = await self.websocket.recv() + raw = await self.__websocket.recv() msg = json.loads(raw) action = msg['action'] if action == 4: # CONNECTED - if self.connected_future: - self.connected_future.set_result(None) - self.connected_future = None + if self.__connected_future: + self.__connected_future.set_result(None) + self.__connected_future = None else: log.warn('CONNECTED message receieved but connected_future not set') if action == 9: # ERROR error = msg["error"] if error['nonfatal'] is False: - if self.connected_future: - self.connected_future.set_exception( + if self.__connected_future: + self.__connected_future.set_exception( AblyAuthException(error["message"], error["statusCode"], error["code"])) - self.connected_future = None + self.__connected_future = None @property def ably(self): From fce2edf7028f6372dfdfc1edb36fed16666697ec Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Wed, 14 Sep 2022 14:18:02 +0100 Subject: [PATCH 18/19] Add failed ConnectionState --- ably/realtime/connection.py | 2 ++ test/ably/realtimeconnection_test.py | 1 + 2 files changed, 3 insertions(+) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index 3a154bae..d5c79f0d 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -14,6 +14,7 @@ class ConnectionState(Enum): CONNECTED = 'connected' CLOSING = 'closing' CLOSED = 'closed' + FAILED = 'failed' class RealtimeConnection: @@ -68,6 +69,7 @@ async def ws_read_loop(self): if action == 9: # ERROR error = msg["error"] if error['nonfatal'] is False: + self.__state = ConnectionState.FAILED if self.__connected_future: self.__connected_future.set_exception( AblyAuthException(error["message"], error["statusCode"], error["code"])) diff --git a/test/ably/realtimeconnection_test.py b/test/ably/realtimeconnection_test.py index 134c1f9d..929161a7 100644 --- a/test/ably/realtimeconnection_test.py +++ b/test/ably/realtimeconnection_test.py @@ -39,4 +39,5 @@ async def test_auth_invalid_key(self): ably = await RestSetup.get_ably_realtime(key=self.valid_key_format) with pytest.raises(AblyAuthException): await ably.connect() + assert ably.connection.state == ConnectionState.FAILED await ably.close() From 940d0cc49c6754f3c48d6479e9a4e38eac1ebfb8 Mon Sep 17 00:00:00 2001 From: moyosore Date: Wed, 14 Sep 2022 15:32:22 +0100 Subject: [PATCH 19/19] change base type of ProtocolMessageAction to IntEnum fix hanging test --- ably/realtime/connection.py | 13 +++++++++---- ably/realtime/realtime.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ably/realtime/connection.py b/ably/realtime/connection.py index d5c79f0d..6cf3490f 100644 --- a/ably/realtime/connection.py +++ b/ably/realtime/connection.py @@ -3,7 +3,7 @@ import websockets import json from ably.util.exceptions import AblyAuthException -from enum import Enum +from enum import Enum, IntEnum log = logging.getLogger(__name__) @@ -17,6 +17,11 @@ class ConnectionState(Enum): FAILED = 'failed' +class ProtocolMessageAction(IntEnum): + CONNECTED = 4 + ERROR = 9 + + class RealtimeConnection: def __init__(self, realtime): self.options = realtime.options @@ -60,13 +65,13 @@ async def ws_read_loop(self): raw = await self.__websocket.recv() msg = json.loads(raw) action = msg['action'] - if action == 4: # CONNECTED + if action == ProtocolMessageAction.CONNECTED: # CONNECTED if self.__connected_future: self.__connected_future.set_result(None) self.__connected_future = None else: - log.warn('CONNECTED message receieved but connected_future not set') - if action == 9: # ERROR + log.warn('CONNECTED message received but connected_future not set') + if action == ProtocolMessageAction.ERROR: # ERROR error = msg["error"] if error['nonfatal'] is False: self.__state = ConnectionState.FAILED diff --git a/ably/realtime/realtime.py b/ably/realtime/realtime.py index 36cf1cbe..de70e41c 100644 --- a/ably/realtime/realtime.py +++ b/ably/realtime/realtime.py @@ -21,7 +21,7 @@ def __init__(self, key=None, **kwargs): if key is not None: options = Options(key=key, **kwargs) else: - raise ValueError("Key is missing. Provide an API key") + raise ValueError("Key is missing. Provide an API key.") self.__auth = Auth(self, options) self.__options = options