From ac7b626b0cff0e7890ca9d1e7b2c2e39119982b8 Mon Sep 17 00:00:00 2001 From: pgautier404 Date: Thu, 13 Jul 2023 16:07:05 -0700 Subject: [PATCH] feat: add pubsub (#353) * feat: adding pubsub * chore: support multiple subscription channels * chore: adding subscription items initial draft * chore: improved logging and error handling * chore: add topic validation * chore: adding synchronous functionality * fix: fix interceptor processing * chore: linting cleanup * chore: cleanup * chore: split topic subscribe response into synch and async classes * chore: typing, linting, and formatting work * chore: docstrings and other cleanup * chore: support more graceful cancellation of async streams * chore: add tests and fix usage of behaves_like decorator throughout * chore: fix up cancelled error a bit * chore: add docstrings * fix: formatting fixes * fix: correct an accidentally changed type * fix: more linting/formatting * chore: refactoring test shared behaviors * fix: fixing examples * fix: correct docstring * chore: remove redundant test cases * fix: add missing docstring for subscription item method * chore: change topic subscription responses into iterators * chore: update tests to use iterators * fix: test fixes --- CONTRIBUTING.md | 2 +- examples/py310/readme.py | 6 +- poetry.lock | 236 +++++++++--------- pyproject.toml | 2 +- src/momento/__init__.py | 14 +- src/momento/config/__init__.py | 4 +- src/momento/config/topic_configuration.py | 35 +++ src/momento/config/topic_configurations.py | 24 ++ src/momento/internal/_utilities/__init__.py | 1 + .../internal/_utilities/_data_validation.py | 4 + .../aio/_add_header_client_interceptor.py | 34 +++ src/momento/internal/aio/_scs_grpc_manager.py | 74 +++++- .../internal/aio/_scs_pubsub_client.py | 121 +++++++++ .../_add_header_client_interceptor.py | 34 +++ .../internal/synchronous/_scs_grpc_manager.py | 68 ++++- .../synchronous/_scs_pubsub_client.py | 121 +++++++++ src/momento/responses/__init__.py | 15 +- src/momento/responses/pubsub/__init__.py | 0 src/momento/responses/pubsub/publish.py | 28 +++ src/momento/responses/pubsub/subscribe.py | 173 +++++++++++++ .../responses/pubsub/subscription_item.py | 34 +++ src/momento/responses/response.py | 4 + src/momento/topic_client.py | 97 +++++++ src/momento/topic_client_async.py | 97 +++++++ src/momento/typing.py | 1 + tests/asserts.py | 4 +- tests/conftest.py | 38 ++- tests/momento/cache_client/test_dictionary.py | 87 ++++--- .../cache_client/test_dictionary_async.py | 87 ++++--- tests/momento/cache_client/test_list.py | 88 ++++--- tests/momento/cache_client/test_list_async.py | 88 ++++--- tests/momento/cache_client/test_scalar.py | 4 +- .../momento/cache_client/test_scalar_async.py | 4 +- tests/momento/cache_client/test_set.py | 52 ++-- tests/momento/cache_client/test_set_async.py | 52 ++-- tests/momento/cache_client/test_sorted_set.py | 100 +++++--- .../cache_client/test_sorted_set_async.py | 100 +++++--- tests/momento/topic_client/__init__.py | 0 .../momento/topic_client/shared_behaviors.py | 56 +++++ .../topic_client/shared_behaviors_async.py | 58 +++++ tests/momento/topic_client/test_topics.py | 69 +++++ .../momento/topic_client/test_topics_async.py | 77 ++++++ 42 files changed, 1777 insertions(+), 416 deletions(-) create mode 100644 src/momento/config/topic_configuration.py create mode 100644 src/momento/config/topic_configurations.py create mode 100644 src/momento/internal/aio/_scs_pubsub_client.py create mode 100644 src/momento/internal/synchronous/_scs_pubsub_client.py create mode 100644 src/momento/responses/pubsub/__init__.py create mode 100644 src/momento/responses/pubsub/publish.py create mode 100644 src/momento/responses/pubsub/subscribe.py create mode 100644 src/momento/responses/pubsub/subscription_item.py create mode 100644 src/momento/topic_client.py create mode 100644 src/momento/topic_client_async.py create mode 100644 tests/momento/topic_client/__init__.py create mode 100644 tests/momento/topic_client/shared_behaviors.py create mode 100644 tests/momento/topic_client/shared_behaviors_async.py create mode 100644 tests/momento/topic_client/test_topics.py create mode 100644 tests/momento/topic_client/test_topics_async.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd2a76e1..0fa8c410 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ pyenv install 3.10.4 NB: These are examples. You will need one of each (3.7, 3.8, 3.9, 3.10) but the patch version ("13" in "3.7.13") can be the latest one. - +
### Configure poetry to use your particular python version diff --git a/examples/py310/readme.py b/examples/py310/readme.py index 7719ffea..e6ac0fd8 100644 --- a/examples/py310/readme.py +++ b/examples/py310/readme.py @@ -9,9 +9,9 @@ timedelta(seconds=60) ) -create_cache_response = cache_client.create_cache("cache") -set_response = cache_client.set("cache", "my-key", "my-value") -get_response = cache_client.get(_CACHE_NAME, _KEY) +cache_client.create_cache("cache") +cache_client.set("cache", "my-key", "my-value") +get_response = cache_client.get("cache", "my-key") match get_response: case CacheGet.Hit() as hit: print(f"Got value: {hit.value_string}") diff --git a/poetry.lock b/poetry.lock index 76b42617..8c30d32b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "black" @@ -116,61 +116,61 @@ pydocstyle = ">=2.1" [[package]] name = "grpcio" -version = "1.54.2" +version = "1.56.0" description = "HTTP/2-based RPC framework" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.54.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:40e1cbf69d6741b40f750f3cccc64326f927ac6145a9914d33879e586002350c"}, - {file = "grpcio-1.54.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2288d76e4d4aa7ef3fe7a73c1c470b66ea68e7969930e746a8cd8eca6ef2a2ea"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c0e3155fc5335ec7b3b70f15230234e529ca3607b20a562b6c75fb1b1218874c"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bf88004fe086c786dc56ef8dd6cb49c026833fdd6f42cb853008bce3f907148"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be88c081e33f20630ac3343d8ad9f1125f32987968e9c8c75c051c9800896e8"}, - {file = "grpcio-1.54.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:33d40954199bddbb6a78f8f6f2b2082660f381cd2583ec860a6c2fa7c8400c08"}, - {file = "grpcio-1.54.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b52d00d1793d290c81ad6a27058f5224a7d5f527867e5b580742e1bd211afeee"}, - {file = "grpcio-1.54.2-cp310-cp310-win32.whl", hash = "sha256:881d058c5ccbea7cc2c92085a11947b572498a27ef37d3eef4887f499054dca8"}, - {file = "grpcio-1.54.2-cp310-cp310-win_amd64.whl", hash = "sha256:0212e2f7fdf7592e4b9d365087da30cb4d71e16a6f213120c89b4f8fb35a3ab3"}, - {file = "grpcio-1.54.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:1e623e0cf99a0ac114f091b3083a1848dbc64b0b99e181473b5a4a68d4f6f821"}, - {file = "grpcio-1.54.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:66233ccd2a9371158d96e05d082043d47dadb18cbb294dc5accfdafc2e6b02a7"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:4cb283f630624ebb16c834e5ac3d7880831b07cbe76cb08ab7a271eeaeb8943e"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a1e601ee31ef30a9e2c601d0867e236ac54c922d32ed9f727b70dd5d82600d5"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8da84bbc61a4e92af54dc96344f328e5822d574f767e9b08e1602bb5ddc254a"}, - {file = "grpcio-1.54.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5008964885e8d23313c8e5ea0d44433be9bfd7e24482574e8cc43c02c02fc796"}, - {file = "grpcio-1.54.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a2f5a1f1080ccdc7cbaf1171b2cf384d852496fe81ddedeb882d42b85727f610"}, - {file = "grpcio-1.54.2-cp311-cp311-win32.whl", hash = "sha256:b74ae837368cfffeb3f6b498688a123e6b960951be4dec0e869de77e7fa0439e"}, - {file = "grpcio-1.54.2-cp311-cp311-win_amd64.whl", hash = "sha256:8cdbcbd687e576d48f7886157c95052825ca9948c0ed2afdc0134305067be88b"}, - {file = "grpcio-1.54.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:782f4f8662a2157c4190d0f99eaaebc602899e84fb1e562a944e5025929e351c"}, - {file = "grpcio-1.54.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:714242ad0afa63a2e6dabd522ae22e1d76e07060b5af2ddda5474ba4f14c2c94"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:f900ed4ad7a0f1f05d35f955e0943944d5a75f607a836958c6b8ab2a81730ef2"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96a41817d2c763b1d0b32675abeb9179aa2371c72aefdf74b2d2b99a1b92417b"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70fcac7b94f4c904152809a050164650ac81c08e62c27aa9f156ac518029ebbe"}, - {file = "grpcio-1.54.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fd6c6c29717724acf9fc1847c4515d57e4dc12762452457b9cb37461f30a81bb"}, - {file = "grpcio-1.54.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c2392f5b5d84b71d853918687d806c1aa4308109e5ca158a16e16a6be71041eb"}, - {file = "grpcio-1.54.2-cp37-cp37m-win_amd64.whl", hash = "sha256:51630c92591d6d3fe488a7c706bd30a61594d144bac7dee20c8e1ce78294f474"}, - {file = "grpcio-1.54.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:b04202453941a63b36876a7172b45366dc0cde10d5fd7855c0f4a4e673c0357a"}, - {file = "grpcio-1.54.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:89dde0ac72a858a44a2feb8e43dc68c0c66f7857a23f806e81e1b7cc7044c9cf"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:09d4bfd84686cd36fd11fd45a0732c7628308d094b14d28ea74a81db0bce2ed3"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fc2b4edb938c8faa4b3c3ea90ca0dd89b7565a049e8e4e11b77e60e4ed2cc05"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61f7203e2767800edee7a1e1040aaaf124a35ce0c7fe0883965c6b762defe598"}, - {file = "grpcio-1.54.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e416c8baf925b5a1aff31f7f5aecc0060b25d50cce3a5a7255dc5cf2f1d4e5eb"}, - {file = "grpcio-1.54.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dc80c9c6b608bf98066a038e0172013a49cfa9a08d53335aefefda2c64fc68f4"}, - {file = "grpcio-1.54.2-cp38-cp38-win32.whl", hash = "sha256:8d6192c37a30a115f4663592861f50e130caed33efc4eec24d92ec881c92d771"}, - {file = "grpcio-1.54.2-cp38-cp38-win_amd64.whl", hash = "sha256:46a057329938b08e5f0e12ea3d7aed3ecb20a0c34c4a324ef34e00cecdb88a12"}, - {file = "grpcio-1.54.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:2296356b5c9605b73ed6a52660b538787094dae13786ba53080595d52df13a98"}, - {file = "grpcio-1.54.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:c72956972e4b508dd39fdc7646637a791a9665b478e768ffa5f4fe42123d5de1"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:9bdbb7624d65dc0ed2ed8e954e79ab1724526f09b1efa88dcd9a1815bf28be5f"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c44e1a765b31e175c391f22e8fc73b2a2ece0e5e6ff042743d8109b5d2eff9f"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cc928cfe6c360c1df636cf7991ab96f059666ac7b40b75a769410cc6217df9c"}, - {file = "grpcio-1.54.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a08920fa1a97d4b8ee5db2f31195de4a9def1a91bc003544eb3c9e6b8977960a"}, - {file = "grpcio-1.54.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4864f99aac207e3e45c5e26c6cbb0ad82917869abc2f156283be86c05286485c"}, - {file = "grpcio-1.54.2-cp39-cp39-win32.whl", hash = "sha256:b38b3de8cff5bc70f8f9c615f51b48eff7313fc9aca354f09f81b73036e7ddfa"}, - {file = "grpcio-1.54.2-cp39-cp39-win_amd64.whl", hash = "sha256:be48496b0e00460717225e7680de57c38be1d8629dc09dadcd1b3389d70d942b"}, - {file = "grpcio-1.54.2.tar.gz", hash = "sha256:50a9f075eeda5097aa9a182bb3877fe1272875e45370368ac0ee16ab9e22d019"}, + {file = "grpcio-1.56.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:fb34ace11419f1ae321c36ccaa18d81cd3f20728cd191250be42949d6845bb2d"}, + {file = "grpcio-1.56.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:008767c0aed4899e657b50f2e0beacbabccab51359eba547f860e7c55f2be6ba"}, + {file = "grpcio-1.56.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:17f47aeb9be0da5337f9ff33ebb8795899021e6c0741ee68bd69774a7804ca86"}, + {file = "grpcio-1.56.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43c50d810cc26349b093bf2cfe86756ab3e9aba3e7e681d360930c1268e1399a"}, + {file = "grpcio-1.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187b8f71bad7d41eea15e0c9812aaa2b87adfb343895fffb704fb040ca731863"}, + {file = "grpcio-1.56.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:881575f240eb5db72ddca4dc5602898c29bc082e0d94599bf20588fb7d1ee6a0"}, + {file = "grpcio-1.56.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c243b158dd7585021d16c50498c4b2ec0a64a6119967440c5ff2d8c89e72330e"}, + {file = "grpcio-1.56.0-cp310-cp310-win32.whl", hash = "sha256:8b3b2c7b5feef90bc9a5fa1c7f97637e55ec3e76460c6d16c3013952ee479cd9"}, + {file = "grpcio-1.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:03a80451530fd3b8b155e0c4480434f6be669daf7ecba56f73ef98f94222ee01"}, + {file = "grpcio-1.56.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:64bd3abcf9fb4a9fa4ede8d0d34686314a7075f62a1502217b227991d9ca4245"}, + {file = "grpcio-1.56.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:fdc3a895791af4addbb826808d4c9c35917c59bb5c430d729f44224e51c92d61"}, + {file = "grpcio-1.56.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:4f84a6fd4482e5fe73b297d4874b62a535bc75dc6aec8e9fe0dc88106cd40397"}, + {file = "grpcio-1.56.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14e70b4dda3183abea94c72d41d5930c333b21f8561c1904a372d80370592ef3"}, + {file = "grpcio-1.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b5ce42a5ebe3e04796246ba50357f1813c44a6efe17a37f8dc7a5c470377312"}, + {file = "grpcio-1.56.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8219f17baf069fe8e42bd8ca0b312b875595e43a70cabf397be4fda488e2f27d"}, + {file = "grpcio-1.56.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:defdd14b518e6e468466f799aaa69db0355bca8d3a5ea75fb912d28ba6f8af31"}, + {file = "grpcio-1.56.0-cp311-cp311-win32.whl", hash = "sha256:50f4daa698835accbbcc60e61e0bc29636c0156ddcafb3891c987e533a0031ba"}, + {file = "grpcio-1.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:59c4e606993a47146fbeaf304b9e78c447f5b9ee5641cae013028c4cca784617"}, + {file = "grpcio-1.56.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:b1f4b6f25a87d80b28dd6d02e87d63fe1577fe6d04a60a17454e3f8077a38279"}, + {file = "grpcio-1.56.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:c2148170e01d464d41011a878088444c13413264418b557f0bdcd1bf1b674a0e"}, + {file = "grpcio-1.56.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:0409de787ebbf08c9d2bca2bcc7762c1efe72eada164af78b50567a8dfc7253c"}, + {file = "grpcio-1.56.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66f0369d27f4c105cd21059d635860bb2ea81bd593061c45fb64875103f40e4a"}, + {file = "grpcio-1.56.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38fdf5bd0a1c754ce6bf9311a3c2c7ebe56e88b8763593316b69e0e9a56af1de"}, + {file = "grpcio-1.56.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:79d4c5911d12a7aa671e5eb40cbb50a830396525014d2d6f254ea2ba180ce637"}, + {file = "grpcio-1.56.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5d2fc471668a7222e213f86ef76933b18cdda6a51ea1322034478df8c6519959"}, + {file = "grpcio-1.56.0-cp37-cp37m-win_amd64.whl", hash = "sha256:991224fd485e088d3cb5e34366053691a4848a6b7112b8f5625a411305c26691"}, + {file = "grpcio-1.56.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:c6f36621aabecbaff3e70c4d1d924c76c8e6a7ffec60c331893640a4af0a8037"}, + {file = "grpcio-1.56.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:1eadd6de258901929223f422ffed7f8b310c0323324caf59227f9899ea1b1674"}, + {file = "grpcio-1.56.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:72836b5a1d4f508ffbcfe35033d027859cc737972f9dddbe33fb75d687421e2e"}, + {file = "grpcio-1.56.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f92a99ab0c7772fb6859bf2e4f44ad30088d18f7c67b83205297bfb229e0d2cf"}, + {file = "grpcio-1.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa08affbf672d051cd3da62303901aeb7042a2c188c03b2c2a2d346fc5e81c14"}, + {file = "grpcio-1.56.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2db108b4c8e29c145e95b0226973a66d73ae3e3e7fae00329294af4e27f1c42"}, + {file = "grpcio-1.56.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8674fdbd28266d8efbcddacf4ec3643f76fe6376f73283fd63a8374c14b0ef7c"}, + {file = "grpcio-1.56.0-cp38-cp38-win32.whl", hash = "sha256:bd55f743e654fb050c665968d7ec2c33f03578a4bbb163cfce38024775ff54cc"}, + {file = "grpcio-1.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:c63bc5ac6c7e646c296fed9139097ae0f0e63f36f0864d7ce431cce61fe0118a"}, + {file = "grpcio-1.56.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c0bc9dda550785d23f4f025be614b7faa8d0293e10811f0f8536cf50435b7a30"}, + {file = "grpcio-1.56.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:d596408bab632ec7b947761e83ce6b3e7632e26b76d64c239ba66b554b7ee286"}, + {file = "grpcio-1.56.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:76b6e6e1ee9bda32e6e933efd61c512e9a9f377d7c580977f090d1a9c78cca44"}, + {file = "grpcio-1.56.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7beb84ebd0a3f732625124b73969d12b7350c5d9d64ddf81ae739bbc63d5b1ed"}, + {file = "grpcio-1.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83ec714bbbe9b9502177c842417fde39f7a267031e01fa3cd83f1ca49688f537"}, + {file = "grpcio-1.56.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4feee75565d1b5ab09cb3a5da672b84ca7f6dd80ee07a50f5537207a9af543a4"}, + {file = "grpcio-1.56.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b4638a796778329cc8e142e4f57c705adb286b3ba64e00b0fa91eeb919611be8"}, + {file = "grpcio-1.56.0-cp39-cp39-win32.whl", hash = "sha256:437af5a7673bca89c4bc0a993382200592d104dd7bf55eddcd141cef91f40bab"}, + {file = "grpcio-1.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:4241a1c2c76e748023c834995cd916570e7180ee478969c2d79a60ce007bc837"}, + {file = "grpcio-1.56.0.tar.gz", hash = "sha256:4c08ee21b3d10315b8dc26f6c13917b20ed574cdbed2d2d80c53d5508fdcc0f2"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.54.2)"] +protobuf = ["grpcio-tools (>=1.56.0)"] [[package]] name = "importlib-metadata" @@ -224,42 +224,42 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "libcst" -version = "0.4.9" +version = "0.4.10" description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "libcst-0.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f9e42085c403e22201e5c41e707ef73e4ea910ad9fc67983ceee2368097f54e"}, - {file = "libcst-0.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1266530bf840cc40633a04feb578bb4cac1aa3aea058cc3729e24eab09a8e996"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9679177391ccb9b0cdde3185c22bf366cb672457c4b7f4031fcb3b5e739fbd6"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d67bc87e0d8db9434f2ea063734938a320f541f4c6da1074001e372f840f385d"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e316da5a126f2a9e1d7680f95f907b575f082a35e2f8bd5620c59b2aaaebfe0a"}, - {file = "libcst-0.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:7415569ab998a85b0fc9af3a204611ea7fadb2d719a12532c448f8fc98f5aca4"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:15ded11ff7f4572f91635e02b519ae959f782689fdb4445bbebb7a3cc5c71d75"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b266867b712a120fad93983de432ddb2ccb062eb5fd2bea748c9a94cb200c36"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045b3b0b06413cdae6e9751b5f417f789ffa410f2cb2815e3e0e0ea6bef10ec0"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e799add8fba4976628b9c1a6768d73178bf898f0ed1bd1322930c2d3db9063ba"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10479371d04ee8dc978c889c1774bbf6a83df88fa055fcb0159a606f6679c565"}, - {file = "libcst-0.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:7a98286cbbfa90a42d376900c875161ad02a5a2a6b7c94c0f7afd9075e329ce4"}, - {file = "libcst-0.4.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:400166fc4efb9aa06ce44498d443aa78519082695b1894202dd73cd507d2d712"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46123863fba35cc84f7b54dd68826419cabfd9504d8a101c7fe3313ea03776f9"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27be8db54c0e5fe440021a771a38b81a7dbc23cd630eb8b0e9828b7717f9b702"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:132bec627b064bd567e7e4cd6c89524d02842151eb0d8f5f3f7ffd2579ec1b09"}, - {file = "libcst-0.4.9-cp37-cp37m-win_amd64.whl", hash = "sha256:596860090aeed3ee6ad1e59c35c6c4110a57e4e896abf51b91cae003ec720a11"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4487608258109f774300466d4ca97353df29ae6ac23d1502e13e5509423c9d5"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa53993e9a2853efb3ed3605da39f2e7125df6430f613eb67ef886c1ce4f94b5"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ce794483d4c605ef0f5b199a49fb6996f9586ca938b7bfef213bd13858d7ab"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786e562b54bbcd17a060d1244deeef466b7ee07fe544074c252c4a169e38f1ee"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794250d2359edd518fb698e5d21c38a5bdfc5e4a75d0407b4c19818271ce6742"}, - {file = "libcst-0.4.9-cp38-cp38-win_amd64.whl", hash = "sha256:76491f67431318c3145442e97dddcead7075b074c59eac51be7cc9e3fffec6ee"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cf48d7aec6dc54b02aec0b1bb413c5bb3b02d852fd6facf1f05c7213e61a176"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b3348c6b7711a5235b133bd8e11d22e903c388db42485b8ceb5f2aa0fae9b9f"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e33b66762efaa014c38819efae5d8f726dd823e32d5d691035484411d2a2a69"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1350d375d3fb9b20a6cf10c09b2964baca9be753a033dde7c1aced49d8e58387"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3822056dc13326082362db35b3f649e0f4a97e36ddb4e487441da8e0fb9db7b3"}, - {file = "libcst-0.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:183636141b839aa35b639e100883813744523bc7c12528906621121731b28443"}, - {file = "libcst-0.4.9.tar.gz", hash = "sha256:01786c403348f76f274dbaf3888ae237ffb73e6ed6973e65eba5c1fc389861dd"}, + {file = "libcst-0.4.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8fa0ec646ed7bce984d0ee9dbf514af278050bdb16a4fb986e916ace534eebc6"}, + {file = "libcst-0.4.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3cb3b7821eac00713844cda079583230c546a589b22ed5f03f2ddc4f985c384b"}, + {file = "libcst-0.4.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7acfa747112ae40b032739661abd7c81aff37191294f7c2dab8bbd72372e78f"}, + {file = "libcst-0.4.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1312e293b864ef3cb4b09534ed5f104c2dc45b680233c68bf76237295041c781"}, + {file = "libcst-0.4.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76884b1afe475e8e68e704bf26eb9f9a2867029643e58f2f26a0286e3b6e998e"}, + {file = "libcst-0.4.10-cp310-cp310-win_amd64.whl", hash = "sha256:1069b808a711db5cd47538f27eb2c73206317aa0d8b5a3500b23aab24f86eb2e"}, + {file = "libcst-0.4.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:50be085346a35812535c7f876319689e15a7bfd1bd8efae8fd70589281d944b6"}, + {file = "libcst-0.4.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb9f10e5763e361e8bd8ff765fc0f1bcf744f242ff8b6d3e50ffec4dda3972ac"}, + {file = "libcst-0.4.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfeeabb528b5df7b4be1817b584ce79e9a1a66687bd72f6de9c22272462812f1"}, + {file = "libcst-0.4.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5648aeae8c90a2abab1f7b1bf205769a0179ed2cfe1ea7f681f6885e87b8b193"}, + {file = "libcst-0.4.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a144f20aff4643b00374facf8409d30c7935db8176e5b2a07e1fd44004db2c1f"}, + {file = "libcst-0.4.10-cp311-cp311-win_amd64.whl", hash = "sha256:a10adc2e8ea2dda2b70eabec631ead2fc4a7a7ab633d6c2b690823c698b8431a"}, + {file = "libcst-0.4.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58fe90458a26a55358207f74abf8a05dff51d662069f070b4bd308a000a80c09"}, + {file = "libcst-0.4.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:999fbbe467f61cbce9e6e054f86cd1c5ffa3740fd3dc8ebdd600db379f699256"}, + {file = "libcst-0.4.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83ee7e7be4efac4c140a97d772e1f6b3553f98fa5f46ad78df5dfe51e5a4aa4d"}, + {file = "libcst-0.4.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:158478e8f45578fb26621b3dc0fe275f9e004297e9afdcf08936ecda05681174"}, + {file = "libcst-0.4.10-cp37-cp37m-win_amd64.whl", hash = "sha256:5ed101fee1af7abea3684fcff7fab5b170ceea4040756f54c15c870539daec66"}, + {file = "libcst-0.4.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:349f2b4ee4b982fe254c65c78d941fc96299f3c422b79f95ef8c7bba2b7f0f90"}, + {file = "libcst-0.4.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7cfa4d4beb84d0d63247aca27f1a15c63984512274c5b23040f8b4ba511036d7"}, + {file = "libcst-0.4.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24582506da24e31f2644f862f11413a6b80fbad68d15194bfcc3f7dfebf2ec5e"}, + {file = "libcst-0.4.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cdf2d0157438d3d52d310b0b6be31ff99bed19de489b2ebd3e2a4cd9946da45"}, + {file = "libcst-0.4.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a677103d2f1ab0e50bc3a7cc6c96c7d64bcbac826d785e4cbf5ee9aaa9fcfa25"}, + {file = "libcst-0.4.10-cp38-cp38-win_amd64.whl", hash = "sha256:a8fdfd4a7d301adb785aa4b98e4a7cca45c5ff8cfb460b485d081efcfaaeeab7"}, + {file = "libcst-0.4.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b1569d87536bed4e9c11dd5c94a137dc0bce2a2b05961489c6016bf4521bb7cf"}, + {file = "libcst-0.4.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:72dff8783ac79cd10f2bd2fde0b28f262e9a22718ae26990948ba6131b85ca8b"}, + {file = "libcst-0.4.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76adc53660ef094ff83f77a2550a7e00d1cab8e5e63336e071c17c09b5a89fe2"}, + {file = "libcst-0.4.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3e9d9fdd9a9b9b8991936ff1c07527ce7ef396c8233280ba9a7137e72c2e48e"}, + {file = "libcst-0.4.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e1b4cbaf7b1cdad5fa3eababe42d5b46c0d52afe13c5ba4eac2495fc57630ea"}, + {file = "libcst-0.4.10-cp39-cp39-win_amd64.whl", hash = "sha256:bcbd07cec3d7a7be6f0299b0c246e085e3d6cc8af367e2c96059183b97c2e2fe"}, + {file = "libcst-0.4.10.tar.gz", hash = "sha256:b98a829d96e8b209fb761b00cd1bacc27c70eae77d00e57976e5ae2c718c3f81"}, ] [package.dependencies] @@ -268,7 +268,7 @@ typing-extensions = ">=3.7.4.2" typing-inspect = ">=0.4.0" [package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==22.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.0.1)", "usort (==1.0.5)"] +dev = ["Sphinx (>=5.1.1)", "black (==23.1.0)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.10)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.1.0)", "usort (==1.0.6)"] [[package]] name = "mccabe" @@ -284,14 +284,14 @@ files = [ [[package]] name = "momento-wire-types" -version = "0.64.0" +version = "0.67.0" description = "Momento Client Proto Generated Files" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "momento_wire_types-0.64.0-py3-none-any.whl", hash = "sha256:30a5d523cef9209c0863db25e6344044b0c7240fea183a41c1433ca83cefea5b"}, - {file = "momento_wire_types-0.64.0.tar.gz", hash = "sha256:5d3647210d49d0c3032a74ae5f5cc012a9faf826786272e1436a3d84d70a8bd5"}, + {file = "momento_wire_types-0.67.0-py3-none-any.whl", hash = "sha256:b596b45fe20534afba57c57cad50f70cc2b77c0d090646165d4bce66165ed290"}, + {file = "momento_wire_types-0.67.0.tar.gz", hash = "sha256:64fb30794940e6004b4e678b52b8b2728e3fce4390ac427a38054615795165c4"}, ] [package.dependencies] @@ -380,33 +380,33 @@ files = [ [[package]] name = "platformdirs" -version = "3.5.1" +version = "3.8.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, - {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, ] [package.dependencies] -typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.dependencies] @@ -418,25 +418,25 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "4.23.1" +version = "4.23.3" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.23.1-cp310-abi3-win32.whl", hash = "sha256:410bcc0a5b279f634d3e16082ce221dfef7c3392fac723500e2e64d1806dd2be"}, - {file = "protobuf-4.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:32e78beda26d7a101fecf15d7a4a792278a0d26a31bc327ff05564a9d68ab8ee"}, - {file = "protobuf-4.23.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f9510cac91e764e86acd74e2b7f7bc5e6127a7f3fb646d7c8033cfb84fd1176a"}, - {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:346990f634272caac1f09efbcfbbacb23098b1f606d172534c6fa2d9758bb436"}, - {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3ce113b3f3362493bddc9069c2163a38f240a9ed685ff83e7bcb756b05e1deb0"}, - {file = "protobuf-4.23.1-cp37-cp37m-win32.whl", hash = "sha256:2036a3a1e7fc27f973fa0a7888dce712393af644f4695385f117886abc792e39"}, - {file = "protobuf-4.23.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3b8905eafe4439076e1f58e9d1fa327025fd2777cf90f14083092ae47f77b0aa"}, - {file = "protobuf-4.23.1-cp38-cp38-win32.whl", hash = "sha256:5b9cd6097e6acae48a68cb29b56bc79339be84eca65b486910bb1e7a30e2b7c1"}, - {file = "protobuf-4.23.1-cp38-cp38-win_amd64.whl", hash = "sha256:decf119d54e820f298ee6d89c72d6b289ea240c32c521f00433f9dc420595f38"}, - {file = "protobuf-4.23.1-cp39-cp39-win32.whl", hash = "sha256:91fac0753c3c4951fbb98a93271c43cc7cf3b93cf67747b3e600bb1e5cc14d61"}, - {file = "protobuf-4.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac50be82491369a9ec3710565777e4da87c6d2e20404e0abb1f3a8f10ffd20f0"}, - {file = "protobuf-4.23.1-py3-none-any.whl", hash = "sha256:65f0ac96ef67d7dd09b19a46aad81a851b6f85f89725577f16de38f2d68ad477"}, - {file = "protobuf-4.23.1.tar.gz", hash = "sha256:95789b569418a3e32a53f43d7763be3d490a831e9c08042539462b6d972c2d7e"}, + {file = "protobuf-4.23.3-cp310-abi3-win32.whl", hash = "sha256:514b6bbd54a41ca50c86dd5ad6488afe9505901b3557c5e0f7823a0cf67106fb"}, + {file = "protobuf-4.23.3-cp310-abi3-win_amd64.whl", hash = "sha256:cc14358a8742c4e06b1bfe4be1afbdf5c9f6bd094dff3e14edb78a1513893ff5"}, + {file = "protobuf-4.23.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2991f5e7690dab569f8f81702e6700e7364cc3b5e572725098215d3da5ccc6ac"}, + {file = "protobuf-4.23.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:08fe19d267608d438aa37019236db02b306e33f6b9902c3163838b8e75970223"}, + {file = "protobuf-4.23.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3b01a5274ac920feb75d0b372d901524f7e3ad39c63b1a2d55043f3887afe0c1"}, + {file = "protobuf-4.23.3-cp37-cp37m-win32.whl", hash = "sha256:aca6e86a08c5c5962f55eac9b5bd6fce6ed98645d77e8bfc2b952ecd4a8e4f6a"}, + {file = "protobuf-4.23.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0149053336a466e3e0b040e54d0b615fc71de86da66791c592cc3c8d18150bf8"}, + {file = "protobuf-4.23.3-cp38-cp38-win32.whl", hash = "sha256:84ea0bd90c2fdd70ddd9f3d3fc0197cc24ecec1345856c2b5ba70e4d99815359"}, + {file = "protobuf-4.23.3-cp38-cp38-win_amd64.whl", hash = "sha256:3bcbeb2bf4bb61fe960dd6e005801a23a43578200ea8ceb726d1f6bd0e562ba1"}, + {file = "protobuf-4.23.3-cp39-cp39-win32.whl", hash = "sha256:5cb9e41188737f321f4fce9a4337bf40a5414b8d03227e1d9fbc59bc3a216e35"}, + {file = "protobuf-4.23.3-cp39-cp39-win_amd64.whl", hash = "sha256:29660574cd769f2324a57fb78127cda59327eb6664381ecfe1c69731b83e8288"}, + {file = "protobuf-4.23.3-py3-none-any.whl", hash = "sha256:447b9786ac8e50ae72cae7a2eec5c5df6a9dbf9aa6f908f1b8bda6032644ea62"}, + {file = "protobuf-4.23.3.tar.gz", hash = "sha256:7a92beb30600332a52cdadbedb40d33fd7c8a0d7f549c440347bc606fb3fe34b"}, ] [[package]] @@ -505,14 +505,14 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" -version = "7.3.1" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, - {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -525,7 +525,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -733,26 +733,26 @@ types-docutils = "*" [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.0-py3-none-any.whl", hash = "sha256:5d8c9dac95c27d20df12fb1d97b9793ab8b2af8a3a525e68c80e21060c161771"}, + {file = "typing_extensions-4.7.0.tar.gz", hash = "sha256:935ccf31549830cda708b42289d44b6f74084d616a00be651601a4f968e77c82"}, ] [[package]] name = "typing-inspect" -version = "0.8.0" +version = "0.9.0" description = "Runtime inspection utilities for typing module." category = "dev" optional = false python-versions = "*" files = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, ] [package.dependencies] @@ -778,4 +778,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "2ad82f57de28ac79bf502ae000e2a08dad1082d30de8f4443b4924666c6176f3" +content-hash = "8f5859da31f64cc57d348adc8877578f2b5d55497d9b410c0d0f1bfd8349ad52" diff --git a/pyproject.toml b/pyproject.toml index d76c0fc8..faa0dd53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ exclude = ["src/momento/internal/codegen.py"] [tool.poetry.dependencies] python = "^3.7" -momento-wire-types = "^0.64" +momento-wire-types = "^0.67" grpcio = "^1.46.0" # note if you bump this presigned url test need be updated pyjwt = "^2.4.0" diff --git a/src/momento/__init__.py b/src/momento/__init__.py index 197d7ed4..213ca4e5 100644 --- a/src/momento/__init__.py +++ b/src/momento/__init__.py @@ -12,9 +12,19 @@ from .auth import CredentialProvider from .cache_client import CacheClient from .cache_client_async import CacheClientAsync -from .config import Configurations +from .config import Configurations, TopicConfigurations +from .topic_client import TopicClient +from .topic_client_async import TopicClientAsync logging.getLogger("momentosdk").addHandler(logging.NullHandler()) logs.initialize_momento_logging() -__all__ = ["CredentialProvider", "Configurations", "CacheClient", "CacheClientAsync"] +__all__ = [ + "CredentialProvider", + "Configurations", + "TopicConfigurations", + "CacheClient", + "CacheClientAsync", + "TopicClient", + "TopicClientAsync", +] diff --git a/src/momento/config/__init__.py b/src/momento/config/__init__.py index 577d1bc0..283c347c 100644 --- a/src/momento/config/__init__.py +++ b/src/momento/config/__init__.py @@ -2,5 +2,7 @@ from .configuration import Configuration from .configurations import Configurations +from .topic_configuration import TopicConfiguration +from .topic_configurations import TopicConfigurations -__all__ = ["Configuration", "Configurations"] +__all__ = ["Configuration", "Configurations", "TopicConfiguration", "TopicConfigurations"] diff --git a/src/momento/config/topic_configuration.py b/src/momento/config/topic_configuration.py new file mode 100644 index 00000000..8c45ff3a --- /dev/null +++ b/src/momento/config/topic_configuration.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod + + +class TopicConfigurationBase(ABC): + @abstractmethod + def get_max_subscriptions(self) -> int: + pass + + @abstractmethod + def with_max_subscriptions(self, max_subscriptions: int) -> TopicConfiguration: + pass + + +class TopicConfiguration(TopicConfigurationBase): + """Configuration options for Momento topic client.""" + + def __init__(self, max_subscriptions: int = 0): + """Instantiate a configuration. + + Args: + max_subscriptions (int): The maximum number of subscriptions the client is expected + to handle. Because each gRPC channel can handle 100 connections, we must explicitly + open multiple channels to accommodate the load. NOTE: if the number of connection + attempts exceeds the number the channels can support, program execution will block + and hang. + """ + self._max_subscriptions = max_subscriptions + + def get_max_subscriptions(self) -> int: + return self._max_subscriptions + + def with_max_subscriptions(self, max_subscriptions: int) -> TopicConfiguration: + return TopicConfiguration(max_subscriptions) diff --git a/src/momento/config/topic_configurations.py b/src/momento/config/topic_configurations.py new file mode 100644 index 00000000..229e0920 --- /dev/null +++ b/src/momento/config/topic_configurations.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from .topic_configuration import TopicConfiguration + + +class TopicConfigurations: + class Default(TopicConfiguration): + """Provides the recommended default configuration for topic clients.""" + + @staticmethod + def latest() -> TopicConfigurations.Default: + """Provides the latest recommended configuration for topics. + + This configuration will be updated every time there is a new version of the laptop configuration. + """ + return TopicConfigurations.Default.v1() + + @staticmethod + def v1() -> TopicConfigurations.Default: + """Provides the v1 recommended configuration for topics. + + This configuration is guaranteed not to change in future releases of the Momento Python SDK. + """ + return TopicConfigurations.Default(max_subscriptions=0) diff --git a/src/momento/internal/_utilities/__init__.py b/src/momento/internal/_utilities/__init__.py index 07dc9e1f..98016efe 100644 --- a/src/momento/internal/_utilities/__init__.py +++ b/src/momento/internal/_utilities/__init__.py @@ -10,6 +10,7 @@ _validate_request_timeout, _validate_set_name, _validate_timedelta_ttl, + _validate_topic_name, _validate_ttl, ) from ._momento_version import momento_version diff --git a/src/momento/internal/_utilities/_data_validation.py b/src/momento/internal/_utilities/_data_validation.py index b39d2b2b..7e8b6bca 100644 --- a/src/momento/internal/_utilities/_data_validation.py +++ b/src/momento/internal/_utilities/_data_validation.py @@ -50,6 +50,10 @@ def _validate_sorted_set_name(sorted_set_name: str) -> None: _validate_name(sorted_set_name, "Sorted set name") +def _validate_topic_name(topic_name: str) -> None: + _validate_name(topic_name, "Topic name") + + def _validate_sorted_set_score(score: float) -> float: if isinstance(score, float): return score diff --git a/src/momento/internal/aio/_add_header_client_interceptor.py b/src/momento/internal/aio/_add_header_client_interceptor.py index baedc9bc..5ddd2fff 100644 --- a/src/momento/internal/aio/_add_header_client_interceptor.py +++ b/src/momento/internal/aio/_add_header_client_interceptor.py @@ -16,6 +16,40 @@ def __init__(self, name: str, value: str): self.value = value +class AddHeaderStreamingClientInterceptor(grpc.aio.UnaryStreamClientInterceptor): + are_only_once_headers_sent = False + + def __init__(self, headers: list[Header]): + self._headers_to_add_once: list[Header] = list( + filter(lambda header: header.name in header.once_only_headers, headers) + ) + self.headers_to_add_every_time = list( + filter(lambda header: header.name not in header.once_only_headers, headers) + ) + + async def intercept_unary_stream( + self, + continuation: Callable[ + [grpc.aio._interceptor.ClientCallDetails, grpc.aio._typing.RequestType], + grpc.aio._call.UnaryStreamCall, + ], + client_call_details: grpc.aio._interceptor.ClientCallDetails, + request: grpc.aio._typing.RequestType, + ) -> grpc.aio._call.UnaryStreamCall | grpc.aio._typing.ResponseType: + + new_client_call_details = sanitize_client_call_details(client_call_details) + + for header in self.headers_to_add_every_time: + new_client_call_details.metadata.add(header.name, header.value) + + if not AddHeaderStreamingClientInterceptor.are_only_once_headers_sent: + for header in self._headers_to_add_once: + new_client_call_details.metadata.add(header.name, header.value) + AddHeaderStreamingClientInterceptor.are_only_once_headers_sent = True + + return await continuation(new_client_call_details, request) + + class AddHeaderClientInterceptor(grpc.aio.UnaryUnaryClientInterceptor): are_only_once_headers_sent = False diff --git a/src/momento/internal/aio/_scs_grpc_manager.py b/src/momento/internal/aio/_scs_grpc_manager.py index 75d96e88..86bf92d6 100644 --- a/src/momento/internal/aio/_scs_grpc_manager.py +++ b/src/momento/internal/aio/_scs_grpc_manager.py @@ -1,20 +1,27 @@ from __future__ import annotations +from typing import Optional + import grpc from momento_wire_types import cacheclient_pb2_grpc as cache_client +from momento_wire_types import cachepubsub_pb2_grpc as pubsub_client from momento_wire_types import controlclient_pb2_grpc as control_client from momento.auth import CredentialProvider -from momento.config import Configuration +from momento.config import Configuration, TopicConfiguration from momento.internal._utilities import momento_version from momento.retry import RetryStrategy -from ._add_header_client_interceptor import AddHeaderClientInterceptor, Header +from ._add_header_client_interceptor import ( + AddHeaderClientInterceptor, + AddHeaderStreamingClientInterceptor, + Header, +) from ._retry_interceptor import RetryInterceptor class _ControlGrpcManager: - """Internal gRPC control mananger.""" + """Internal gRPC control manager.""" version = momento_version @@ -33,7 +40,7 @@ def async_stub(self) -> control_client.ScsControlStub: class _DataGrpcManager: - """Internal gRPC data mananger.""" + """Internal gRPC data manager.""" version = momento_version @@ -65,12 +72,63 @@ def async_stub(self) -> cache_client.ScsStub: return cache_client.ScsStub(self._secure_channel) # type: ignore[no-untyped-call] -def _interceptors(auth_token: str, retry_strategy: RetryStrategy) -> list[grpc.aio.ClientInterceptor]: +class _PubsubGrpcManager: + """Internal gRPC pubsub manager.""" + + version = momento_version + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + self._secure_channel = grpc.aio.secure_channel( + target=credential_provider.cache_endpoint, + credentials=grpc.ssl_channel_credentials(), + interceptors=_interceptors(credential_provider.auth_token, None), + ) + + async def close(self) -> None: + await self._secure_channel.close() + + def async_stub(self) -> pubsub_client.PubsubStub: + return pubsub_client.PubsubStub(self._secure_channel) # type: ignore[no-untyped-call] + + +class _PubsubGrpcStreamManager: + """Internal gRPC pubsub stream manager.""" + + version = momento_version + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + self._secure_channel = grpc.aio.secure_channel( + target=credential_provider.cache_endpoint, + credentials=grpc.ssl_channel_credentials(), + interceptors=_stream_interceptors(credential_provider.auth_token), + ) + + async def close(self) -> None: + await self._secure_channel.close() + + def async_stub(self) -> pubsub_client.PubsubStub: + return pubsub_client.PubsubStub(self._secure_channel) # type: ignore[no-untyped-call] + + +def _interceptors(auth_token: str, retry_strategy: Optional[RetryStrategy] = None) -> list[grpc.aio.ClientInterceptor]: headers = [ Header("authorization", auth_token), Header("agent", f"python:{_ControlGrpcManager.version}"), ] - return [ - AddHeaderClientInterceptor(headers), - RetryInterceptor(retry_strategy), + return list( + filter( + None, + [ + AddHeaderClientInterceptor(headers), + RetryInterceptor(retry_strategy) if retry_strategy else None, + ], + ) + ) + + +def _stream_interceptors(auth_token: str) -> list[grpc.aio.UnaryStreamClientInterceptor]: + headers = [ + Header("authorization", auth_token), + Header("agent", f"python:{_PubsubGrpcStreamManager.version}"), ] + return [AddHeaderStreamingClientInterceptor(headers)] diff --git a/src/momento/internal/aio/_scs_pubsub_client.py b/src/momento/internal/aio/_scs_pubsub_client.py new file mode 100644 index 00000000..d9e6c924 --- /dev/null +++ b/src/momento/internal/aio/_scs_pubsub_client.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import math + +from momento_wire_types import cachepubsub_pb2 as pubsub_pb +from momento_wire_types import cachepubsub_pb2_grpc as pubsub_grpc + +from momento import logs +from momento.auth import CredentialProvider +from momento.config import TopicConfiguration +from momento.errors import convert_error +from momento.internal._utilities import _validate_cache_name, _validate_topic_name +from momento.internal.aio._scs_grpc_manager import ( + _PubsubGrpcManager, + _PubsubGrpcStreamManager, +) +from momento.responses import ( + TopicPublish, + TopicPublishResponse, + TopicSubscribe, + TopicSubscribeResponse, +) + + +class _ScsPubsubClient: + """Internal pubsub client.""" + + stream_topic_manager_count = 0 + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + endpoint = credential_provider.cache_endpoint + self._logger = logs.logger + self._logger.debug("Pubsub client instantiated with endpoint: %s", endpoint) + self._endpoint = endpoint + + num_subscriptions = configuration.get_max_subscriptions() + # Default to a single channel and scale up if necessary. Each channel can support + # 100 subscriptions. Issuing more subscribe requests than you have channels to handle + # will cause the last request to hang indefinitely, so it's important to get this right. + num_channels = 1 + if num_subscriptions > 0: + num_channels = math.ceil(num_subscriptions / 100.0) + self._logger.debug(f"creating {num_channels} subscription channels") + + self._grpc_manager = _PubsubGrpcManager(configuration, credential_provider) + self._stream_managers = [ + _PubsubGrpcStreamManager(configuration, credential_provider) for i in range(0, num_channels) + ] + + @property + def endpoint(self) -> str: + return self._endpoint + + async def publish(self, cache_name: str, topic_name: str, value: str | bytes) -> TopicPublishResponse: + try: + _validate_cache_name(cache_name) + _validate_topic_name(topic_name) + + if isinstance(value, str): + topic_value = pubsub_pb._TopicValue(text=value) + else: + topic_value = pubsub_pb._TopicValue(binary=value) + + request = pubsub_pb._PublishRequest( + cache_name=cache_name, + topic=topic_name, + value=topic_value, + ) + + await self._get_stub().Publish( # type: ignore[misc] + request, + ) + return TopicPublish.Success() + except Exception as e: + self._log_request_error("publish", e) + return TopicPublish.Error(convert_error(e)) + + async def subscribe(self, cache_name: str, topic_name: str) -> TopicSubscribeResponse: + try: + _validate_cache_name(cache_name) + _validate_topic_name(topic_name) + + request = pubsub_pb._SubscriptionRequest( + cache_name=cache_name, + topic=topic_name, + # TODO: resume_at_topic_sequence_number + ) + stream = self._get_stream_stub().Subscribe( # type: ignore[misc] + request, + ) + + # Ping the stream to provide a nice error message if the cache does not exist. + msg: pubsub_pb._SubscriptionItem = await stream.read() # type: ignore[misc] + msg_type: str = msg.WhichOneof("kind") + if msg_type == "heartbeat": + # The first message to a new subscription is always a heartbeat. + pass + else: + err = Exception(f"expected a heartbeat message but got '{msg_type}'") + self._log_request_error("subscribe", err) + return TopicSubscribe.Error(convert_error(err)) + return TopicSubscribe.SubscriptionAsync(cache_name, topic_name, client_stream=stream) # type: ignore[misc] + except Exception as e: + self._log_request_error("subscribe", e) + return TopicSubscribe.Error(convert_error(e)) + + def _log_request_error(self, request_type: str, e: Exception) -> None: + self._logger.warning(f"{request_type} failed with exception: {e}") + + def _get_stub(self) -> pubsub_grpc.PubsubStub: + return self._grpc_manager.async_stub() + + def _get_stream_stub(self) -> pubsub_grpc.PubsubStub: + stub = self._stream_managers[self.stream_topic_manager_count % len(self._stream_managers)].async_stub() + self.stream_topic_manager_count += 1 + return stub + + async def close(self) -> None: + await self._grpc_manager.close() + for stream_client in self._stream_managers: + await stream_client.close() diff --git a/src/momento/internal/synchronous/_add_header_client_interceptor.py b/src/momento/internal/synchronous/_add_header_client_interceptor.py index aadbdbf4..e99205d4 100644 --- a/src/momento/internal/synchronous/_add_header_client_interceptor.py +++ b/src/momento/internal/synchronous/_add_header_client_interceptor.py @@ -26,6 +26,40 @@ class _ClientCallDetails( pass +class AddHeaderStreamingClientInterceptor(grpc.UnaryStreamClientInterceptor): + are_only_once_headers_sent = False + + def __init__(self, headers: list[Header]): + self._headers_to_add_once: list[Header] = list( + filter(lambda header: header.name in header.once_only_headers, headers) + ) + self.headers_to_add_every_time = list( + filter(lambda header: header.name not in header.once_only_headers, headers) + ) + + def intercept_unary_stream( + self, + continuation: Callable[ + [grpc.ClientCallDetails, RequestType], + grpc.Call, + ], + client_call_details: grpc.ClientCallDetails, + request: RequestType, + ) -> grpc.Call | ResponseType: + + new_client_call_details = sanitize_client_call_details(client_call_details) + + for header in self.headers_to_add_every_time: + new_client_call_details.metadata.append((header.name, header.value)) + + if not AddHeaderStreamingClientInterceptor.are_only_once_headers_sent: + for header in self._headers_to_add_once: + new_client_call_details.metadata.append((header.name, header.value)) + AddHeaderStreamingClientInterceptor.are_only_once_headers_sent = True + + return continuation(new_client_call_details, request) + + class AddHeaderClientInterceptor(grpc.UnaryUnaryClientInterceptor): are_only_once_headers_sent = False diff --git a/src/momento/internal/synchronous/_scs_grpc_manager.py b/src/momento/internal/synchronous/_scs_grpc_manager.py index 31d18d88..06a89643 100644 --- a/src/momento/internal/synchronous/_scs_grpc_manager.py +++ b/src/momento/internal/synchronous/_scs_grpc_manager.py @@ -1,14 +1,18 @@ from __future__ import annotations +from typing import Optional + import grpc from momento_wire_types import cacheclient_pb2_grpc as cache_client +from momento_wire_types import cachepubsub_pb2_grpc as pubsub_client from momento_wire_types import controlclient_pb2_grpc as control_client from momento.auth import CredentialProvider -from momento.config import Configuration +from momento.config import Configuration, TopicConfiguration from momento.internal._utilities import momento_version from momento.internal.synchronous._add_header_client_interceptor import ( AddHeaderClientInterceptor, + AddHeaderStreamingClientInterceptor, Header, ) from momento.internal.synchronous._retry_interceptor import RetryInterceptor @@ -58,6 +62,64 @@ def stub(self) -> cache_client.ScsStub: return self._stub -def _interceptors(auth_token: str, retry_strategy: RetryStrategy) -> list[grpc.UnaryUnaryClientInterceptor]: +class _PubsubGrpcManager: + """Internal gRPC pubsub manager.""" + + version = momento_version + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + self._secure_channel = grpc.secure_channel( + target=credential_provider.cache_endpoint, + credentials=grpc.ssl_channel_credentials(), + ) + intercept_channel = grpc.intercept_channel( + self._secure_channel, *_interceptors(credential_provider.auth_token, None) + ) + self._stub = pubsub_client.PubsubStub(intercept_channel) # type: ignore[no-untyped-call] + + def close(self) -> None: + self._secure_channel.close() + + def stub(self) -> pubsub_client.PubsubStub: + return self._stub + + +class _PubsubGrpcStreamManager: + """Internal gRPC pubsub stream manager.""" + + version = momento_version + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + self._secure_channel = grpc.secure_channel( + target=credential_provider.cache_endpoint, + credentials=grpc.ssl_channel_credentials(), + ) + intercept_channel = grpc.intercept_channel( + self._secure_channel, *_stream_interceptors(credential_provider.auth_token) + ) + self._stub = pubsub_client.PubsubStub(intercept_channel) # type: ignore[no-untyped-call] + + def close(self) -> None: + self._secure_channel.close() + + def stub(self) -> pubsub_client.PubsubStub: + return self._stub + + +def _interceptors( + auth_token: str, retry_strategy: Optional[RetryStrategy] = None +) -> list[grpc.UnaryUnaryClientInterceptor]: headers = [Header("authorization", auth_token), Header("agent", f"python:{_ControlGrpcManager.version}")] - return [AddHeaderClientInterceptor(headers), RetryInterceptor(retry_strategy)] + return list( + filter( + None, [AddHeaderClientInterceptor(headers), RetryInterceptor(retry_strategy) if retry_strategy else None] + ) + ) + + +def _stream_interceptors(auth_token: str) -> list[grpc.UnaryStreamClientInterceptor]: + headers = [ + Header("authorization", auth_token), + Header("agent", f"python:{_PubsubGrpcStreamManager.version}"), + ] + return [AddHeaderStreamingClientInterceptor(headers)] diff --git a/src/momento/internal/synchronous/_scs_pubsub_client.py b/src/momento/internal/synchronous/_scs_pubsub_client.py new file mode 100644 index 00000000..e33753e3 --- /dev/null +++ b/src/momento/internal/synchronous/_scs_pubsub_client.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import math + +from momento_wire_types import cachepubsub_pb2 as pubsub_pb +from momento_wire_types import cachepubsub_pb2_grpc as pubsub_grpc + +from momento import logs +from momento.auth import CredentialProvider +from momento.config import TopicConfiguration +from momento.errors import convert_error +from momento.internal._utilities import _validate_cache_name, _validate_topic_name +from momento.internal.synchronous._scs_grpc_manager import ( + _PubsubGrpcManager, + _PubsubGrpcStreamManager, +) +from momento.responses import ( + TopicPublish, + TopicPublishResponse, + TopicSubscribe, + TopicSubscribeResponse, +) + + +class _ScsPubsubClient: + """Internal pubsub client.""" + + stream_topic_manager_count = 0 + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + endpoint = credential_provider.cache_endpoint + self._logger = logs.logger + self._logger.debug("Pubsub client instantiated with endpoint: %s", endpoint) + self._endpoint = endpoint + + num_subscriptions = configuration.get_max_subscriptions() + # Default to a single channel and scale up if necessary. Each channel can support + # 100 subscriptions. Issuing more subscribe requests than you have channels to handle + # will cause the last request to hang indefinitely, so it's important to get this right. + num_channels = 1 + if num_subscriptions > 0: + num_channels = math.ceil(num_subscriptions / 100.0) + self._logger.debug(f"creating {num_channels} subscription channels") + + self._grpc_manager = _PubsubGrpcManager(configuration, credential_provider) + self._stream_managers = [ + _PubsubGrpcStreamManager(configuration, credential_provider) for i in range(0, num_channels) + ] + + @property + def endpoint(self) -> str: + return self._endpoint + + def publish(self, cache_name: str, topic_name: str, value: str | bytes) -> TopicPublishResponse: + try: + _validate_cache_name(cache_name) + _validate_topic_name(topic_name) + + if isinstance(value, str): + topic_value = pubsub_pb._TopicValue(text=value) + else: + topic_value = pubsub_pb._TopicValue(binary=value) + + request = pubsub_pb._PublishRequest( + cache_name=cache_name, + topic=topic_name, + value=topic_value, + ) + + self._get_stub().Publish( + request, + ) + return TopicPublish.Success() + except Exception as e: + self._log_request_error("publish", e) + return TopicPublish.Error(convert_error(e)) + + def subscribe(self, cache_name: str, topic_name: str) -> TopicSubscribeResponse: + try: + _validate_cache_name(cache_name) + _validate_topic_name(topic_name) + + request = pubsub_pb._SubscriptionRequest( + cache_name=cache_name, + topic=topic_name, + # TODO: resume_at_topic_sequence_number + ) + stream = self._get_stream_stub().Subscribe( # type: ignore[misc] + request, + ) + + # Ping the stream to provide a nice error message if the cache does not exist. + msg: pubsub_pb._SubscriptionItem = stream.next() # type: ignore[misc] + msg_type: str = msg.WhichOneof("kind") + if msg_type == "heartbeat": + # The first message to a new subscription is always a heartbeat. + pass + else: + err = Exception(f"expected a heartbeat message but got '{msg_type}'") + self._log_request_error("subscribe", err) + return TopicSubscribe.Error(convert_error(err)) + return TopicSubscribe.Subscription(cache_name, topic_name, client_stream=stream) # type: ignore[misc] + except Exception as e: + self._log_request_error("subscribe", e) + return TopicSubscribe.Error(convert_error(e)) + + def _log_request_error(self, request_type: str, e: Exception) -> None: + self._logger.warning(f"{request_type} failed with exception: {e}") + + def _get_stub(self) -> pubsub_grpc.PubsubStub: + return self._grpc_manager.stub() + + def _get_stream_stub(self) -> pubsub_grpc.PubsubStub: + stub = self._stream_managers[self.stream_topic_manager_count % len(self._stream_managers)].stub() + self.stream_topic_manager_count += 1 + return stub + + def close(self) -> None: + self._grpc_manager.close() + for stream_client in self._stream_managers: + stream_client.close() diff --git a/src/momento/responses/__init__.py b/src/momento/responses/__init__.py index ad25fa87..8e0b1919 100644 --- a/src/momento/responses/__init__.py +++ b/src/momento/responses/__init__.py @@ -107,7 +107,13 @@ CacheSortedSetPutElements, CacheSortedSetPutElementsResponse, ) -from .response import CacheResponse, ControlResponse +from .pubsub.publish import TopicPublish, TopicPublishResponse +from .pubsub.subscribe import TopicSubscribe, TopicSubscribeResponse +from .pubsub.subscription_item import ( + TopicSubscriptionItem, + TopicSubscriptionItemResponse, +) +from .response import CacheResponse, ControlResponse, PubsubResponse __all__ = [ "CreateCache", @@ -191,6 +197,13 @@ "CacheSortedSetPutElementsResponse", "CacheSortedSetFetch", "CacheSortedSetFetchResponse", + "TopicPublish", + "TopicPublishResponse", + "TopicSubscribe", + "TopicSubscribeResponse", + "TopicSubscriptionItem", + "TopicSubscriptionItemResponse", "CacheResponse", "ControlResponse", + "PubsubResponse", ] diff --git a/src/momento/responses/pubsub/__init__.py b/src/momento/responses/pubsub/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/momento/responses/pubsub/publish.py b/src/momento/responses/pubsub/publish.py new file mode 100644 index 00000000..da04890d --- /dev/null +++ b/src/momento/responses/pubsub/publish.py @@ -0,0 +1,28 @@ +from abc import ABC + +from ..mixins import ErrorResponseMixin +from ..response import PubsubResponse + + +class TopicPublishResponse(PubsubResponse): + """Parent response type for a topic `publish` request. + + Its subtypes are: + - `TopicPublish.Success` + - `TopicPublish.Error` + """ + + +class TopicPublish(ABC): + """Groups all `TopicPublishResponse` derived types under a common namespace.""" + + class Success(TopicPublishResponse): + """Indicates the request was successful.""" + + class Error(TopicPublishResponse, ErrorResponseMixin): + """Contains information about an error returned from a request. + + This includes: + - `error_code`: `MomentoErrorCode` value for the error. + - `message`: a detailed error message. + """ diff --git a/src/momento/responses/pubsub/subscribe.py b/src/momento/responses/pubsub/subscribe.py new file mode 100644 index 00000000..3b7a6295 --- /dev/null +++ b/src/momento/responses/pubsub/subscribe.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from abc import ABC +from concurrent.futures._base import CancelledError +from typing import Optional + +from grpc._channel import _MultiThreadedRendezvous +from grpc.aio._interceptor import InterceptedUnaryStreamCall +from momento_wire_types import cachepubsub_pb2 + +from ... import logs +from ...errors import MomentoErrorCode, SdkException +from ..mixins import ErrorResponseMixin +from ..response import PubsubResponse +from .subscription_item import TopicSubscriptionItem, TopicSubscriptionItemResponse + + +class TopicSubscribeResponse(PubsubResponse): + """Parent response type for a topic `publish` request. + + Its subtypes are: + - `TopicSubscribe.Subscription` + - `TopicSubscribe.SubscriptionAsync` + - `TopicSubscribe.Error` + + Both the ``Subscription` and the `SubscriptionAsync` response subtypes are + iterators that yield subscription items. + """ + + +class TopicSubscribe(ABC): + """Groups all `TopicSubscribeResponse` derived types under a common namespace.""" + + class SubscriptionBase(TopicSubscribeResponse): + """Base class for common logic shared between async and synchronous subscriptions.""" + + _logger = logs.logger + _last_known_sequence_number: Optional[int] = None + + def _process_result(self, result: cachepubsub_pb2._SubscriptionItem) -> Optional[TopicSubscriptionItemResponse]: + msg_type: str = result.WhichOneof("kind") + if msg_type == "item": + self._last_known_sequence_number = result.item.topic_sequence_number + value = result.item.value + value_type: str = value.WhichOneof("kind") + if value_type == "text": + return TopicSubscriptionItem.Success(bytes(value.text, "utf-8")) + elif value_type == "bytes": + return TopicSubscriptionItem.Success(value.bytes) + elif msg_type == "heartbeat": + self._logger.debug("client stream received heartbeat") + return None + elif msg_type == "discontinuity": + self._logger.debug("client stream received discontinuity") + return None + self._logger.debug(f"client stream received unknown type: {msg_type}") + return None + + class SubscriptionAsync(SubscriptionBase): + """Provides the async version of a topic subscription.""" + + def __init__(self, cache_name: str, topic_name: str, client_stream: InterceptedUnaryStreamCall): + self._cache_name = cache_name + self._topic_name = topic_name + self._client_stream = client_stream # type: ignore[misc] + + def __aiter__(self) -> TopicSubscribe.SubscriptionAsync: + return self + + async def __anext__(self) -> TopicSubscriptionItemResponse: + """Retrieves the next published item from the subscription. + + The item method returns a response object that is resolved to a type-safe object + of one of the following: + + - TopicSubscriptionItem.Success + - TopicSubscriptionItem.Error + + Pattern matching can be used to operate on the appropriate subtype. + For example, in python 3.10+ if you're receiving a subscription item:: + + async for response in subscription: + match response: + case TopicSubscriptionItem.Success(): + return response.value_string # value_bytes is also available + case TopicSubscriptionItem.Error(): + ...there was an error retrieving the item... + + or equivalently in earlier versions of python:: + + async for response in subscription: + if isinstance(response, TopicSubscriptionItem.Success): + return response.value_string # value_bytes is also available + elif isinstance(response, TopicSubscriptionItem.Error): + ...there was an error retrieving the item... + """ + while True: + try: + result: cachepubsub_pb2._SubscriptionItem = await self._client_stream.read() # type: ignore[misc] + item = self._process_result(result) + # if item is null, we've received a heartbeat or discontinuity and should continue + if item is not None: + return item + except (CancelledError, StopAsyncIteration) as e: + err = SdkException( + f"Client subscription has been cancelled by {type(e)}", + MomentoErrorCode.CANCELLED_ERROR, + message_wrapper="Error reading item from topic subscription", + ) + self._logger.debug(f"Client stream read has been cancelled: {type(e)}") + return TopicSubscriptionItem.Error(err) + except Exception as e: + self._logger.debug(f"Error reading from client stream: {type(e)}") + # TODO: attempt reconnect + continue + + class Subscription(SubscriptionBase): + """Provides the synchronous version of a topic subscription.""" + + def __init__(self, cache_name: str, topic_name: str, client_stream: _MultiThreadedRendezvous): + self._cache_name = cache_name + self._topic_name = topic_name + self._client_stream = client_stream # type: ignore[misc] + + def __iter__(self) -> TopicSubscribe.Subscription: + return self + + def __next__(self) -> TopicSubscriptionItemResponse: + """Retrieves the next published item from the subscription. + + The item method returns a response object that is resolved to a type-safe object + of one of the following: + + - TopicSubscriptionItem.Success + - TopicSubscriptionItem.Error + + Pattern matching can be used to operate on the appropriate subtype. + For example, in python 3.10+ if you're receiving a subscription item:: + + for response in subscription: + match response: + case TopicSubscriptionItem.Success(): + return response.value_string # value_bytes is also available + case TopicSubscriptionItem.Error(): + ...there was an error retrieving the item... + + or equivalently in earlier versions of python:: + + for response in subscription: + if isinstance(response, TopicSubscriptionItem.Success): + return response.value_string # value_bytes is also available + elif isinstance(response, TopicSubscriptionItem.Error): + ...there was an error retrieving the item... + """ + while True: + try: + result: cachepubsub_pb2._SubscriptionItem = self._client_stream.next() # type: ignore[misc] + item = self._process_result(result) + # if item is null, we've received a heartbeat or discontinuity and should continue + if item is not None: + return item + except Exception as e: + self._logger.debug("Error reading from client stream: %s", e) + # TODO: attempt reconnect + continue + + class Error(TopicSubscribeResponse, ErrorResponseMixin): + """Contains information about an error returned from a request. + + This includes: + - `error_code`: `MomentoErrorCode` value for the error. + - `message`: a detailed error message. + """ diff --git a/src/momento/responses/pubsub/subscription_item.py b/src/momento/responses/pubsub/subscription_item.py new file mode 100644 index 00000000..d7ed0590 --- /dev/null +++ b/src/momento/responses/pubsub/subscription_item.py @@ -0,0 +1,34 @@ +from abc import ABC +from dataclasses import dataclass + +from ..mixins import ErrorResponseMixin, ValueStringMixin +from ..response import PubsubResponse + + +class TopicSubscriptionItemResponse(PubsubResponse): + """Parent response type for a topic subscription's `item` request. + + Its subtypes are: + - `TopicSubscriptionItem.Success` + - `TopicSubscriptionItem.Error` + """ + + +class TopicSubscriptionItem(ABC): + """Groups all `TopicSubscriptionItemResponse` derived types under a common namespace.""" + + @dataclass + class Success(TopicSubscriptionItemResponse, ValueStringMixin): + """Indicates the request was successful.""" + + value_bytes: bytes + """The item returned from the subscription for the specified topic. Use the + `value_string` property to access the value as a string.""" + + class Error(TopicSubscriptionItemResponse, ErrorResponseMixin): + """Contains information about an error returned from a request. + + This includes: + - `error_code`: `MomentoErrorCode` value for the error. + - `messsage`: a detailed error message. + """ diff --git a/src/momento/responses/response.py b/src/momento/responses/response.py index cdad5c47..c82ef212 100644 --- a/src/momento/responses/response.py +++ b/src/momento/responses/response.py @@ -126,3 +126,7 @@ class CacheResponse(Response): class ControlResponse(Response): ... + + +class PubsubResponse(Response): + ... diff --git a/src/momento/topic_client.py b/src/momento/topic_client.py new file mode 100644 index 00000000..a24a7ba9 --- /dev/null +++ b/src/momento/topic_client.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from types import TracebackType +from typing import Optional, Type + +from momento import logs +from momento.auth import CredentialProvider +from momento.config import TopicConfiguration +from momento.internal.synchronous._scs_pubsub_client import _ScsPubsubClient +from momento.responses import TopicPublishResponse, TopicSubscribeResponse + + +class TopicClient: + """Synchronous Topic Client. + + Publish and subscribe methods return a response object unique to each request. + The response object is resolved to a type-safe object of one of several + sub-types. See the documentation for each response type for details. + + Pattern matching can be used to operate on the appropriate subtype. + For example, in python 3.10+ if you're subscribing to a topic:: + + response = client.subscribe(cache_name, topic_name) + match response: + case TopicSubscribe.Subscription(): + ...the subscription was successful... + case TopicSubscribe.Error(): + ...there was an error trying to subscribe... + + or equivalently in earlier versions of python:: + + response = client.subscribe(cache_name, topic_name) + if isinstance(response, TopicSubscribe.Subscription): + ... + elif isinstance(response, TopicSubscribe.Error): + ... + else: + raise Exception("This should never happen") + """ + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + """Instantiate a client. + + Args: + configuration (TopicConfiguration): An object holding configuration settings for communication + with the server. + credential_provider: (CredentialProvider): An object holding the auth token and endpoint information. + + Example: + from momento import CredentialProvider, TopicClient, TopicConfigurations + + configuration = TopicConfigurations.Default.v1() + credential_provider = CredentialProvider.from_environment_variable("MOMENTO_AUTH_TOKEN") + client = TopicClient(configuration, credential_provider) + """ + self._logger = logs.logger + self._cache_endpoint = credential_provider.cache_endpoint + self._pubsub_client = _ScsPubsubClient(configuration, credential_provider) + + def __enter__(self) -> TopicClient: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self._pubsub_client.close() + + def publish(self, cache_name: str, topic_name: str, value: str | bytes) -> TopicPublishResponse: + """Publishes a message to a topic. + + Args: + cache_name (str): Name of the cache to publish to. + topic_name (str): Name of the topic to publish to. + value (str|bytes): The value to publish + + Returns: + TopicPublishResponse + """ + return self._pubsub_client.publish(cache_name, topic_name, value) + + def subscribe(self, cache_name: str, topic_name: str) -> TopicSubscribeResponse: + """Subscribes to a topic. + + Args: + cache_name (str): The cache to subscribe to. + topic_name (str): The topic to subscribe to. + + Returns: + TopicSubscribeResponse + """ + return self._pubsub_client.subscribe(cache_name, topic_name) + + def close(self) -> None: + self._pubsub_client.close() diff --git a/src/momento/topic_client_async.py b/src/momento/topic_client_async.py new file mode 100644 index 00000000..da4c34cd --- /dev/null +++ b/src/momento/topic_client_async.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from types import TracebackType +from typing import Optional, Type + +from momento import logs +from momento.auth import CredentialProvider +from momento.config import TopicConfiguration +from momento.internal.aio._scs_pubsub_client import _ScsPubsubClient +from momento.responses import TopicPublishResponse, TopicSubscribeResponse + + +class TopicClientAsync: + """Asynchronous Topic Client. + + Publish and subscribe methods return a response object unique to each request. + The response object is resolved to a type-safe object of one of several + sub-types. See the documentation for each response type for details. + + Pattern matching can be used to operate on the appropriate subtype. + For example, in python 3.10+ if you're subscribing to a topic:: + + response = client_async.subscribe(cache_name, topic_name) + match response: + case TopicSubscribe.SubscriptionAsync(): + ...the subscription was successful... + case TopicSubscribe.Error(): + ...there was an error trying to subscribe... + + or equivalently in earlier versions of python:: + + response = client_async.subscribe(cache_name, topic_name) + if isinstance(response, TopicSubscribe.SubscriptionAsync): + ... + elif isinstance(response, TopicSubscribe.Error): + ... + else: + raise Exception("This should never happen") + """ + + def __init__(self, configuration: TopicConfiguration, credential_provider: CredentialProvider): + """Instantiate a client. + + Args: + configuration (TopicConfiguration): An object holding configuration settings for communication + with the server. + credential_provider: (CredentialProvider): An object holding the auth token and endpoint information. + + Example: + from momento import CredentialProvider, TopicClientAsync, TopicConfigurations + + configuration = TopicConfigurations.Default.v1() + credential_provider = CredentialProvider.from_environment_variable("MOMENTO_AUTH_TOKEN") + client = TopicClientAsync(configuration, credential_provider) + """ + self._logger = logs.logger + self._cache_endpoint = credential_provider.cache_endpoint + self._pubsub_client = _ScsPubsubClient(configuration, credential_provider) + + async def __aenter__(self) -> TopicClientAsync: + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self._pubsub_client.close() + + async def publish(self, cache_name: str, topic_name: str, value: str | bytes) -> TopicPublishResponse: + """Publishes a message to a topic. + + Args: + cache_name (str): Name of the cache to publish to. + topic_name (str): Name of the topic to publish to. + value (str|bytes): The value to publish + + Returns: + TopicPublishResponse + """ + return await self._pubsub_client.publish(cache_name, topic_name, value) + + async def subscribe(self, cache_name: str, topic_name: str) -> TopicSubscribeResponse: + """Subscribes to a topic. + + Args: + cache_name (str): The cache to subscribe to. + topic_name (str): The topic to subscribe to. + + Returns: + TopicSubscribeResponse + """ + return await self._pubsub_client.subscribe(cache_name, topic_name) + + async def close(self) -> None: + await self._pubsub_client.close() diff --git a/src/momento/typing.py b/src/momento/typing.py index 5d8188ec..0583f04d 100644 --- a/src/momento/typing.py +++ b/src/momento/typing.py @@ -3,6 +3,7 @@ from typing import Iterable, List, Mapping, Set, Union TCacheName = str +TTopicName = str TMomentoValue = Union[str, bytes] diff --git a/tests/asserts.py b/tests/asserts.py index 254281e4..170e6b7c 100644 --- a/tests/asserts.py +++ b/tests/asserts.py @@ -2,12 +2,12 @@ from momento.errors.error_details import MomentoErrorCode from momento.responses.mixins import ErrorResponseMixin -from momento.responses.response import CacheResponse +from momento.responses.response import Response # Custom assertions def assert_response_is_error( - response: CacheResponse, + response: Response, *, error_code: Optional[MomentoErrorCode] = None, inner_exception_message: Optional[str] = None, diff --git a/tests/conftest.py b/tests/conftest.py index fced1430..f4f8056a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,8 +9,16 @@ import pytest import pytest_asyncio -from momento import CacheClient, CacheClientAsync, Configurations, CredentialProvider -from momento.config import Configuration +from momento import ( + CacheClient, + CacheClientAsync, + Configurations, + CredentialProvider, + TopicClient, + TopicClientAsync, + TopicConfigurations, +) +from momento.config import Configuration, TopicConfiguration from momento.typing import ( TCacheName, TDictionaryField, @@ -28,6 +36,7 @@ TSortedSetName, TSortedSetScore, TSortedSetValues, + TTopicName, ) from tests.utils import unique_test_cache_name, uuid_bytes, uuid_str @@ -36,6 +45,7 @@ ####################### TEST_CONFIGURATION = Configurations.Laptop.latest() +TEST_TOPIC_CONFIGURATION = TopicConfigurations.Default.latest() TEST_AUTH_PROVIDER = CredentialProvider.from_environment_variable("TEST_AUTH_TOKEN") @@ -43,6 +53,8 @@ if not TEST_CACHE_NAME: raise RuntimeError("Integration tests require TEST_CACHE_NAME env var; see README for more details.") +TEST_TOPIC_NAME: Optional[str] = "my-topic" + DEFAULT_TTL_SECONDS: timedelta = timedelta(seconds=60) BAD_AUTH_TOKEN: str = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiIsImNwIjoiY29udHJvbC5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSIsImMiOiJjYWNoZS5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSJ9.gdghdjjfjyehhdkkkskskmmls76573jnajhjjjhjdhnndy" # noqa: E501 @@ -68,11 +80,21 @@ def configuration() -> Configuration: return TEST_CONFIGURATION +@pytest.fixture(scope="session") +def topic_configuration() -> TopicConfiguration: + return TEST_TOPIC_CONFIGURATION + + @pytest.fixture(scope="session") def cache_name() -> TCacheName: return cast(str, TEST_CACHE_NAME) +@pytest.fixture(scope="session") +def topic_name() -> TTopicName: + return cast(str, TEST_TOPIC_NAME) + + @pytest.fixture def list_name() -> TListName: return uuid_str() @@ -258,6 +280,18 @@ async def client_async() -> AsyncIterator[CacheClientAsync]: await _client.delete_cache(cast(str, TEST_CACHE_NAME)) +@pytest.fixture(scope="session") +def topic_client() -> Iterator[TopicClient]: + with TopicClient(TEST_TOPIC_CONFIGURATION, TEST_AUTH_PROVIDER) as _client: + yield _client + + +@pytest.fixture(scope="session") +async def topic_client_async() -> AsyncIterator[TopicClientAsync]: + async with TopicClientAsync(TEST_TOPIC_CONFIGURATION, TEST_AUTH_PROVIDER) as _topic_client: + yield _topic_client + + TUniqueCacheName = Callable[[CacheClient], str] diff --git a/tests/momento/cache_client/test_dictionary.py b/tests/momento/cache_client/test_dictionary.py index f10b8ced..916a3da8 100644 --- a/tests/momento/cache_client/test_dictionary.py +++ b/tests/momento/cache_client/test_dictionary.py @@ -375,11 +375,13 @@ def with_wrong_container_throws_exception(dictionary_value_validator: TDictionar assert response.error_code == MomentoErrorCode.INVALID_ARGUMENT_ERROR -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_getter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_getter, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_get_field() -> None: @fixture def cache_name_validator( @@ -426,11 +428,13 @@ def misses_when_the_dictionary_does_not_exist( assert isinstance(get_response, CacheDictionaryGetField.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_getter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_fields_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_getter, + a_dictionary_name_validator, + a_dictionary_fields_validator, +) def describe_dictionary_get_fields() -> None: @fixture def cache_name_validator( @@ -531,9 +535,7 @@ def it_excludes_misses_from_dictionary( assert missing_key not in get_response.value_dictionary_string_string -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_name_validator) +@behaves_like(a_cache_name_validator, a_connection_validator, a_dictionary_name_validator) def describe_dictionary_fetch() -> None: @fixture def cache_name_validator(client: CacheClient, dictionary_name: TDictionaryName) -> TCacheNameValidator: @@ -557,10 +559,12 @@ def misses_when_the_dictionary_does_not_exist( assert isinstance(fetch_response, CacheDictionaryFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_increment() -> None: @fixture def cache_name_validator( @@ -676,11 +680,13 @@ def it_errors_on_bad_initial_values( assert increment_response.error_code == MomentoErrorCode.FAILED_PRECONDITION_ERROR -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_remover) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_remover, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_remove_field() -> None: @fixture def cache_name_validator( @@ -744,12 +750,13 @@ def it_removes_a_field( assert fetch_response.value_dictionary_string_string == {extra_field: extra_value} -# TODO these don't work for this case? -# @behaves_like(a_cache_name_validator) -# @behaves_like(a_connection_validator) -@behaves_like(a_dictionary_remover) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_fields_validator) +@behaves_like( + a_dictionary_remover, + a_dictionary_name_validator, + a_dictionary_fields_validator, + a_cache_name_validator, + a_connection_validator, +) def describe_dictionary_remove_fields() -> None: @fixture def cache_name_validator( @@ -819,11 +826,13 @@ def it_removes_multiple_items( assert fetch_response.value_dictionary_string_string == {extra_field: extra_value} -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_setter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_value_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_setter, + a_dictionary_name_validator, + a_dictionary_value_validator, +) def describe_dictionary_set_field() -> None: @fixture def cache_name_validator( @@ -903,11 +912,13 @@ def dictionary_value_validator( ) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_setter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_items_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_setter, + a_dictionary_name_validator, + a_dictionary_items_validator, +) def describe_dictionary_set_fields() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/cache_client/test_dictionary_async.py b/tests/momento/cache_client/test_dictionary_async.py index e9116151..750901cd 100644 --- a/tests/momento/cache_client/test_dictionary_async.py +++ b/tests/momento/cache_client/test_dictionary_async.py @@ -377,11 +377,13 @@ async def with_wrong_container_throws_exception(dictionary_value_validator: TDic assert response.error_code == MomentoErrorCode.INVALID_ARGUMENT_ERROR -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_getter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_getter, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_get_field() -> None: @fixture def cache_name_validator( @@ -430,11 +432,13 @@ async def misses_when_the_dictionary_does_not_exist( assert isinstance(get_response, CacheDictionaryGetField.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_getter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_fields_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_getter, + a_dictionary_name_validator, + a_dictionary_fields_validator, +) def describe_dictionary_get_fields() -> None: @fixture def cache_name_validator( @@ -535,9 +539,7 @@ async def it_excludes_misses_from_dictionary( assert missing_key not in get_response.value_dictionary_string_string -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_name_validator) +@behaves_like(a_cache_name_validator, a_connection_validator, a_dictionary_name_validator) def describe_dictionary_fetch() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync, dictionary_name: TDictionaryName) -> TCacheNameValidator: @@ -561,10 +563,12 @@ async def misses_when_the_dictionary_does_not_exist( assert isinstance(fetch_response, CacheDictionaryFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_increment() -> None: @fixture def cache_name_validator( @@ -684,11 +688,13 @@ async def it_errors_on_bad_initial_values( assert increment_response.error_code == MomentoErrorCode.FAILED_PRECONDITION_ERROR -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_remover) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_field_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_remover, + a_dictionary_name_validator, + a_dictionary_field_validator, +) def describe_dictionary_remove_field() -> None: @fixture def cache_name_validator( @@ -756,12 +762,13 @@ async def it_removes_a_field( assert fetch_response.value_dictionary_string_string == {extra_field: extra_value} -# TODO these don't work for this case? -# @behaves_like(a_cache_name_validator) -# @behaves_like(a_connection_validator) -@behaves_like(a_dictionary_remover) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_fields_validator) +@behaves_like( + a_dictionary_remover, + a_dictionary_name_validator, + a_dictionary_fields_validator, + a_cache_name_validator, + a_connection_validator, +) def describe_dictionary_remove_fields() -> None: @fixture def cache_name_validator( @@ -833,11 +840,13 @@ async def it_removes_multiple_items( assert fetch_response.value_dictionary_string_string == {extra_field: extra_value} -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_setter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_value_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_setter, + a_dictionary_name_validator, + a_dictionary_value_validator, +) def describe_dictionary_set_field() -> None: @fixture def cache_name_validator( @@ -917,11 +926,13 @@ def dictionary_value_validator( ) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_dictionary_setter) -@behaves_like(a_dictionary_name_validator) -@behaves_like(a_dictionary_items_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_dictionary_setter, + a_dictionary_name_validator, + a_dictionary_items_validator, +) def describe_dictionary_set_fields() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/cache_client/test_list.py b/tests/momento/cache_client/test_list.py index a0e448d1..1d54aa79 100644 --- a/tests/momento/cache_client/test_list.py +++ b/tests/momento/cache_client/test_list.py @@ -292,11 +292,13 @@ def with_other_value_type_it_errors( assert resp.message == "Invalid argument passed to Momento client: Unsupported type for value: " -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_concatenator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_concatenator, +) def describe_list_concatenate_back() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -368,11 +370,13 @@ def it_truncates_the_front( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_concatenator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_concatenator, +) def describe_list_concatenate_front() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -445,9 +449,11 @@ def it_truncates_the_back( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_fetch() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -470,9 +476,11 @@ def misses_when_the_list_does_not_exist(client: CacheClient, cache_name: TCacheN assert isinstance(resp, CacheListFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_length() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -506,9 +514,11 @@ def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListLength.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_pop_back() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -542,9 +552,11 @@ def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListPopBack.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_pop_front() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -578,11 +590,13 @@ def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListPopFront.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_pusher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_pusher, +) def describe_list_push_back() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -641,11 +655,13 @@ def it_truncates_the_front( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_pusher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_pusher, +) def describe_list_push_front() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: @@ -705,9 +721,11 @@ def it_truncates_the_back( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_remove_value() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: diff --git a/tests/momento/cache_client/test_list_async.py b/tests/momento/cache_client/test_list_async.py index 12e3f266..985665c2 100644 --- a/tests/momento/cache_client/test_list_async.py +++ b/tests/momento/cache_client/test_list_async.py @@ -295,11 +295,13 @@ async def with_other_value_type_it_errors( assert resp.message == "Invalid argument passed to Momento client: Unsupported type for value: " -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_concatenator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_concatenator, +) def describe_list_concatenate_back() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -371,11 +373,13 @@ async def it_truncates_the_front( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_concatenator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_concatenator, +) def describe_list_concatenate_front() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -448,9 +452,11 @@ async def it_truncates_the_back( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_fetch() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -475,9 +481,11 @@ async def misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_length() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -511,9 +519,11 @@ async def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListLength.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_pop_back() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -547,9 +557,11 @@ async def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListPopBack.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_pop_front() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -583,11 +595,13 @@ async def it_misses_when_the_list_does_not_exist( assert isinstance(resp, CacheListPopFront.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_pusher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_pusher, +) def describe_list_push_back() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -646,11 +660,13 @@ async def it_truncates_the_front( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_adder) -@behaves_like(a_list_name_validator) -@behaves_like(a_list_pusher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_adder, + a_list_name_validator, + a_list_pusher, +) def describe_list_push_front() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: @@ -710,9 +726,11 @@ async def it_truncates_the_back( assert False -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_list_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_list_name_validator, +) def describe_list_remove_value() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: diff --git a/tests/momento/cache_client/test_scalar.py b/tests/momento/cache_client/test_scalar.py index cd71fe1a..0e35afdf 100644 --- a/tests/momento/cache_client/test_scalar.py +++ b/tests/momento/cache_client/test_scalar.py @@ -123,9 +123,7 @@ def with_byte_key_values(client: CacheClient, cache_name: str) -> None: assert get_resp.value_bytes == value -@behaves_like(a_cache_name_validator) -@behaves_like(a_key_validator) -@behaves_like(a_connection_validator) +@behaves_like(a_cache_name_validator, a_key_validator, a_connection_validator) def describe_get() -> None: @fixture def cache_name_validator(client: CacheClient) -> TCacheNameValidator: diff --git a/tests/momento/cache_client/test_scalar_async.py b/tests/momento/cache_client/test_scalar_async.py index 86b93391..f92c2abe 100644 --- a/tests/momento/cache_client/test_scalar_async.py +++ b/tests/momento/cache_client/test_scalar_async.py @@ -133,9 +133,7 @@ async def with_byte_key_values(client_async: CacheClientAsync, cache_name: str) assert get_resp.value_bytes == value -@behaves_like(a_cache_name_validator) -@behaves_like(a_key_validator) -@behaves_like(a_connection_validator) +@behaves_like(a_cache_name_validator, a_key_validator, a_connection_validator) def describe_get() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync) -> TCacheNameValidator: diff --git a/tests/momento/cache_client/test_set.py b/tests/momento/cache_client/test_set.py index e5408f8f..b83f877a 100644 --- a/tests/momento/cache_client/test_set.py +++ b/tests/momento/cache_client/test_set.py @@ -165,11 +165,13 @@ def with_bad_set_name_it_returns_invalid(set_name_validator: TCacheNameValidator assert response.inner_exception.message == "Set name must be a string" -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_adder) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_adder, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_add_element() -> None: @fixture def cache_name_validator(client: CacheClient, set_name: TSetName, element: TSetElement) -> TCacheNameValidator: @@ -252,11 +254,13 @@ def it_adds_a_byte_element(client: CacheClient, cache_name: TCacheName, set_name assert fetch_resp.value_set_bytes == {element1, element2} -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_adder) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_adder, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_add_elements() -> None: @fixture def cache_name_validator( @@ -346,9 +350,11 @@ def it_adds_byte_elements( assert fetch_resp.value_set_bytes == elements_bytes -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, +) def describe_set_fetch() -> None: @fixture def cache_name_validator(client: CacheClient, set_name: TSetName) -> TCacheNameValidator: @@ -380,10 +386,12 @@ def when_the_set_does_not_exist_it_misses(client: CacheClient, cache_name: TCach assert isinstance(resp, CacheSetFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_remove_element() -> None: @fixture def cache_name_validator(client: CacheClient, set_name: TSetName, element: TSetElement) -> TCacheNameValidator: @@ -462,10 +470,12 @@ def it_removes_a_byte_element( assert fetch_resp.value_set_bytes == new_elements -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_remove_elements() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/cache_client/test_set_async.py b/tests/momento/cache_client/test_set_async.py index 852d6746..db70855f 100644 --- a/tests/momento/cache_client/test_set_async.py +++ b/tests/momento/cache_client/test_set_async.py @@ -166,11 +166,13 @@ async def with_bad_set_name_it_returns_invalid(set_name_validator: TCacheNameVal assert response.inner_exception.message == "Set name must be a string" -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_adder) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_adder, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_add_element() -> None: @fixture def cache_name_validator( @@ -261,11 +263,13 @@ async def it_adds_a_byte_element( assert fetch_resp.value_set_bytes == {element1, element2} -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_adder) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_adder, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_add_elements() -> None: @fixture def cache_name_validator( @@ -355,9 +359,11 @@ async def it_adds_byte_elements( assert fetch_resp.value_set_bytes == elements_bytes -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, +) def describe_set_fetch() -> None: @fixture def cache_name_validator(client_async: CacheClientAsync, set_name: TSetName) -> TCacheNameValidator: @@ -393,10 +399,12 @@ async def when_the_set_does_not_exist_it_misses( assert isinstance(resp, CacheSetFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_remove_element() -> None: @fixture def cache_name_validator( @@ -479,10 +487,12 @@ async def it_removes_a_byte_element( assert fetch_resp.value_set_bytes == new_elements -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_set_name_validator) -@behaves_like(a_set_which_takes_an_element) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_set_name_validator, + a_set_which_takes_an_element, +) def describe_set_remove_elements() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/cache_client/test_sorted_set.py b/tests/momento/cache_client/test_sorted_set.py index b69dd0d0..bd15e3f7 100644 --- a/tests/momento/cache_client/test_sorted_set.py +++ b/tests/momento/cache_client/test_sorted_set.py @@ -486,10 +486,12 @@ def with_bytes_field_succeeds( assert isinstance(fetch_response, CacheSortedSetFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_setter) -@behaves_like(a_sorted_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_setter, + a_sorted_set_name_validator, +) def describe_sorted_set_put_field() -> None: @fixture def cache_name_validator( @@ -561,10 +563,12 @@ def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_setter) -@behaves_like(a_sorted_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_setter, + a_sorted_set_name_validator, +) def describe_sorted_set_put_fields() -> None: @fixture def cache_name_validator( @@ -623,10 +627,12 @@ def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_fetcher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_fetcher, +) def describe_sorted_set_fetch_by_rank() -> None: @fixture def cache_name_validator( @@ -658,10 +664,12 @@ def sorted_set_fetcher( return partial(client.sorted_set_fetch_by_rank, cache_name=cache_name, sorted_set_name=sorted_set_name) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_fetcher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_fetcher, +) def describe_sorted_set_fetch_by_score() -> None: @fixture def cache_name_validator( @@ -691,10 +699,12 @@ def sorted_set_fetcher( return partial(client.sorted_set_fetch_by_score, cache_name=cache_name, sorted_set_name=sorted_set_name) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_score_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_score_getter, +) def describe_sorted_set_get_score() -> None: @fixture def cache_name_validator( @@ -728,10 +738,12 @@ def sorted_set_score_getter(client: CacheClient) -> TSortedSetScoreGetter: return partial(client.sorted_set_get_score) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_score_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_score_getter, +) def describe_sorted_set_get_scores() -> None: @fixture def cache_name_validator( @@ -777,10 +789,12 @@ def _sorted_set_score_getter( return _sorted_set_score_getter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_rank_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_rank_getter, +) def describe_sorted_set_get_rank() -> None: @fixture def cache_name_validator( @@ -814,10 +828,12 @@ def sorted_set_rank_getter(client: CacheClient) -> TSortedSetRankGetter: return partial(client.sorted_set_get_rank) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_setter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_setter, +) def describe_sorted_set_increment_score() -> None: @fixture def cache_name_validator( @@ -873,10 +889,12 @@ def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_remover) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_remover, +) def describe_sorted_set_remove_element() -> None: @fixture def cache_name_validator( @@ -912,10 +930,12 @@ def sorted_set_remover(client: CacheClient) -> TSortedSetRemover: return partial(client.sorted_set_remove_element) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_remover) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_remover, +) def describe_sorted_set_remove_elements() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/cache_client/test_sorted_set_async.py b/tests/momento/cache_client/test_sorted_set_async.py index 97bf69c2..f0d17a4c 100644 --- a/tests/momento/cache_client/test_sorted_set_async.py +++ b/tests/momento/cache_client/test_sorted_set_async.py @@ -488,10 +488,12 @@ async def with_bytes_field_succeeds( assert isinstance(fetch_response, CacheSortedSetFetch.Miss) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_setter) -@behaves_like(a_sorted_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_setter, + a_sorted_set_name_validator, +) def describe_sorted_set_put_field() -> None: @fixture def cache_name_validator( @@ -563,10 +565,12 @@ async def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_setter) -@behaves_like(a_sorted_set_name_validator) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_setter, + a_sorted_set_name_validator, +) def describe_sorted_set_put_fields() -> None: @fixture def cache_name_validator( @@ -625,10 +629,12 @@ async def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_fetcher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_fetcher, +) def describe_sorted_set_fetch_by_rank() -> None: @fixture def cache_name_validator( @@ -660,10 +666,12 @@ def sorted_set_fetcher( return partial(client_async.sorted_set_fetch_by_rank, cache_name=cache_name, sorted_set_name=sorted_set_name) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_fetcher) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_fetcher, +) def describe_sorted_set_fetch_by_score() -> None: @fixture def cache_name_validator( @@ -693,10 +701,12 @@ def sorted_set_fetcher( return partial(client_async.sorted_set_fetch_by_score, cache_name=cache_name, sorted_set_name=sorted_set_name) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_score_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_score_getter, +) def describe_sorted_set_get_score() -> None: @fixture def cache_name_validator( @@ -732,10 +742,12 @@ def sorted_set_score_getter(client_async: CacheClientAsync) -> TSortedSetScoreGe return partial(client_async.sorted_set_get_score) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_score_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_score_getter, +) def describe_sorted_set_get_scores() -> None: @fixture def cache_name_validator( @@ -783,10 +795,12 @@ async def _sorted_set_score_getter( return _sorted_set_score_getter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_rank_getter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_rank_getter, +) def describe_sorted_set_get_rank() -> None: @fixture def cache_name_validator( @@ -822,10 +836,12 @@ def sorted_set_rank_getter(client_async: CacheClientAsync) -> TSortedSetRankGett return partial(client_async.sorted_set_get_rank) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_setter) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_setter, +) def describe_sorted_set_increment_score() -> None: @fixture def cache_name_validator( @@ -883,10 +899,12 @@ async def _sorted_set_setter( return _sorted_set_setter -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_remover) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_remover, +) def describe_sorted_set_remove_element() -> None: @fixture def cache_name_validator( @@ -924,10 +942,12 @@ def sorted_set_remover(client_async: CacheClientAsync) -> TSortedSetRemover: return partial(client_async.sorted_set_remove_element) -@behaves_like(a_cache_name_validator) -@behaves_like(a_connection_validator) -@behaves_like(a_sorted_set_name_validator) -@behaves_like(a_sorted_set_remover) +@behaves_like( + a_cache_name_validator, + a_connection_validator, + a_sorted_set_name_validator, + a_sorted_set_remover, +) def describe_sorted_set_remove_elements() -> None: @fixture def cache_name_validator( diff --git a/tests/momento/topic_client/__init__.py b/tests/momento/topic_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/momento/topic_client/shared_behaviors.py b/tests/momento/topic_client/shared_behaviors.py new file mode 100644 index 00000000..1aa6c195 --- /dev/null +++ b/tests/momento/topic_client/shared_behaviors.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing_extensions import Protocol + +from momento.errors import MomentoErrorCode +from momento.responses import PubsubResponse +from momento.typing import TCacheName, TTopicName +from tests.asserts import assert_response_is_error +from tests.utils import uuid_str + + +class TCacheNameValidator(Protocol): + def __call__(self, cache_name: TCacheName) -> PubsubResponse: + ... + + +def a_cache_name_validator() -> None: + def with_non_existent_cache_name_it_throws_not_found(cache_name_validator: TCacheNameValidator) -> None: + cache_name = uuid_str() + response = cache_name_validator(cache_name=cache_name) + assert_response_is_error(response, error_code=MomentoErrorCode.NOT_FOUND_ERROR) + + def with_null_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = cache_name_validator(cache_name=None) # type: ignore + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must be a string", + ) + + def with_empty_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = cache_name_validator(cache_name="") + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must not be empty", + ) + + def with_bad_cache_name_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = cache_name_validator(cache_name=1) # type: ignore + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must be a string", + ) + + +class TTopicValidator(Protocol): + def __call__(self, topic_name: TTopicName) -> PubsubResponse: + ... + + +def a_topic_validator() -> None: + def with_null_topic_throws_exception(cache_name: str, topic_validator: TTopicValidator) -> None: + response = topic_validator(topic_name=None) # type: ignore + assert_response_is_error(response, error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR) diff --git a/tests/momento/topic_client/shared_behaviors_async.py b/tests/momento/topic_client/shared_behaviors_async.py new file mode 100644 index 00000000..dd0befc7 --- /dev/null +++ b/tests/momento/topic_client/shared_behaviors_async.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Awaitable + +from typing_extensions import Protocol + +from momento.errors import MomentoErrorCode +from momento.responses import PubsubResponse +from momento.typing import TCacheName, TTopicName +from tests.asserts import assert_response_is_error +from tests.utils import uuid_str + + +class TCacheNameValidator(Protocol): + def __call__(self, cache_name: TCacheName) -> Awaitable[PubsubResponse]: + ... + + +def a_cache_name_validator() -> None: + async def with_non_existent_cache_name_it_throws_not_found(cache_name_validator: TCacheNameValidator) -> None: + cache_name = uuid_str() + response = await cache_name_validator(cache_name=cache_name) + assert_response_is_error(response, error_code=MomentoErrorCode.NOT_FOUND_ERROR) + + async def with_null_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = await cache_name_validator(cache_name=None) # type: ignore + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must be a string", + ) + + async def with_empty_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = await cache_name_validator(cache_name="") + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must not be empty", + ) + + async def with_bad_cache_name_throws_exception(cache_name_validator: TCacheNameValidator) -> None: + response = await cache_name_validator(cache_name=1) # type: ignore + assert_response_is_error( + response, + error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, + inner_exception_message="Cache name must be a string", + ) + + +class TTopicValidator(Protocol): + def __call__(self, topic_name: TTopicName) -> Awaitable[PubsubResponse]: + ... + + +def a_topic_validator() -> None: + async def with_null_topic_throws_exception(cache_name: str, topic_validator: TTopicValidator) -> None: + response = await topic_validator(topic_name=None) # type: ignore + assert_response_is_error(response, error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR) diff --git a/tests/momento/topic_client/test_topics.py b/tests/momento/topic_client/test_topics.py new file mode 100644 index 00000000..7060a7d9 --- /dev/null +++ b/tests/momento/topic_client/test_topics.py @@ -0,0 +1,69 @@ +from functools import partial + +from pytest import fixture +from pytest_describe import behaves_like + +from momento import CacheClient, TopicClient +from momento.responses import TopicPublish, TopicSubscribe, TopicSubscriptionItem +from tests.utils import uuid_str + +from .shared_behaviors import ( + TCacheNameValidator, + TTopicValidator, + a_cache_name_validator, + a_topic_validator, +) + + +@behaves_like(a_cache_name_validator, a_topic_validator) +def describe_publish() -> None: + @fixture + def cache_name_validator(topic_client: TopicClient) -> TCacheNameValidator: + topic_name = uuid_str() + value = uuid_str() + return partial(topic_client.publish, topic_name=topic_name, value=value) + + @fixture + def topic_validator(topic_client: TopicClient) -> TTopicValidator: + cache_name = uuid_str() + value = uuid_str() + return partial(topic_client.publish, cache_name=cache_name, value=value) + + def publish_happy_path(client: CacheClient, topic_client: TopicClient, cache_name: str) -> None: + topic = uuid_str() + value = uuid_str() + + resp = topic_client.publish(cache_name, topic_name=topic, value=value) + assert isinstance(resp, TopicPublish.Success) + + +@behaves_like(a_cache_name_validator, a_topic_validator) +def describe_subscribe() -> None: + @fixture + def cache_name_validator(topic_client: TopicClient) -> TCacheNameValidator: + topic_name = uuid_str() + return partial(topic_client.subscribe, topic_name=topic_name) + + @fixture + def topic_validator(topic_client: TopicClient) -> TTopicValidator: + cache_name = uuid_str() + return partial(topic_client.subscribe, cache_name=cache_name) + + def subscribe_happy_path(client: CacheClient, topic_client: TopicClient, cache_name: str) -> None: + topic = uuid_str() + value = uuid_str() + + subscribe_response = topic_client.subscribe(cache_name, topic_name=topic) + assert isinstance(subscribe_response, TopicSubscribe.Subscription) + + _ = topic_client.publish(cache_name, topic_name=topic, value=value) + + item_response = next(subscribe_response) + assert isinstance(item_response, TopicSubscriptionItem.Success) + assert item_response.value_string == value + + def succeeds_with_nonexistent_topic(client: CacheClient, topic_client: TopicClient, cache_name: str) -> None: + topic = uuid_str() + + resp = topic_client.subscribe(cache_name, topic) + assert isinstance(resp, TopicSubscribe.Subscription) diff --git a/tests/momento/topic_client/test_topics_async.py b/tests/momento/topic_client/test_topics_async.py new file mode 100644 index 00000000..8e8b8a74 --- /dev/null +++ b/tests/momento/topic_client/test_topics_async.py @@ -0,0 +1,77 @@ +from functools import partial + +from pytest import fixture +from pytest_describe import behaves_like + +from momento import CacheClientAsync, TopicClientAsync +from momento.responses import TopicPublish, TopicSubscribe, TopicSubscriptionItem +from tests.utils import uuid_str + +from .shared_behaviors_async import ( + TCacheNameValidator, + TTopicValidator, + a_cache_name_validator, + a_topic_validator, +) + + +@behaves_like(a_cache_name_validator, a_topic_validator) +def describe_publish() -> None: + @fixture + def cache_name_validator(topic_client_async: TopicClientAsync) -> TCacheNameValidator: + topic_name = uuid_str() + value = uuid_str() + return partial(topic_client_async.publish, topic_name=topic_name, value=value) + + @fixture + def topic_validator(topic_client_async: TopicClientAsync) -> TTopicValidator: + cache_name = uuid_str() + value = uuid_str() + return partial(topic_client_async.publish, cache_name=cache_name, value=value) + + async def publish_happy_path( + client: CacheClientAsync, topic_client_async: TopicClientAsync, cache_name: str + ) -> None: + topic = uuid_str() + value = uuid_str() + + resp = await topic_client_async.publish(cache_name, topic_name=topic, value=value) + assert isinstance(resp, TopicPublish.Success) + + +@behaves_like(a_cache_name_validator, a_topic_validator) +def describe_subscribe() -> None: + @fixture + def cache_name_validator(topic_client_async: TopicClientAsync) -> TCacheNameValidator: + topic_name = uuid_str() + return partial(topic_client_async.subscribe, topic_name=topic_name) + + @fixture + def topic_validator(topic_client_async: TopicClientAsync) -> TTopicValidator: + cache_name = uuid_str() + return partial(topic_client_async.subscribe, cache_name=cache_name) + + async def subscribe_happy_path( + client: CacheClientAsync, topic_client_async: TopicClientAsync, cache_name: str + ) -> None: + topic = uuid_str() + value = uuid_str() + + subscribe_response = await topic_client_async.subscribe(cache_name, topic_name=topic) + assert isinstance(subscribe_response, TopicSubscribe.SubscriptionAsync) + + item_task = subscribe_response.__anext__() + publish_response = await topic_client_async.publish(cache_name, topic_name=topic, value=value) + + print(publish_response) + item_response = await item_task + assert isinstance(item_response, TopicSubscriptionItem.Success) + assert item_response.value_string == value + + async def succeeds_with_nonexistent_topic( + client: CacheClientAsync, topic_client_async: TopicClientAsync, cache_name: str + ) -> None: + topic = uuid_str() + + resp = await topic_client_async.subscribe(cache_name, topic) + assert isinstance(resp, TopicSubscribe.SubscriptionAsync)