From db69581d3d8dca82c798b5ef92fe96278f9949f8 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:10:29 +0300 Subject: [PATCH 1/7] Make the client ready for the 1.3.0 release - Reduce the dependency count from 2 to 1 by using sync and async compatible httpx. This is what we do for our other SDKs. To match the settings of the requests, we use infinite timeout similarly for httpx. - Add ruff checks to CI and reformat code with ruff. - Fix JSON tests, which was failing due to some duplicate test file names. - Bump version to 1.3.0 --- .env.example | 2 + .github/workflows/test.yaml | 8 + pyproject.toml | 15 +- ...st_arrappend.py => test_json_arrappend.py} | 18 +- ...test_arrindex.py => test_json_arrindex.py} | 16 +- ...st_arrinsert.py => test_json_arrinsert.py} | 16 +- .../{test_arrlen.py => test_json_arrlen.py} | 11 +- .../{test_arrpop.py => test_json_arrpop.py} | 6 +- .../{test_arrtrim.py => test_json_arrtrim.py} | 6 +- .../{test_clear.py => test_json_clear.py} | 10 +- ...est_delete_json.py => test_json_delete.py} | 6 +- .../{test_forget.py => test_json_forget.py} | 6 +- .../{test_get_json.py => test_json_get.py} | 16 +- tests/commands/json/test_json_merge.py | 43 ++++ .../{test_mget_json.py => test_json_mget.py} | 17 +- tests/commands/json/test_json_mset.py | 42 ++++ ...st_numincrby.py => test_json_numincrby.py} | 0 ...st_nummultby.py => test_json_nummultby.py} | 0 .../{test_objkeys.py => test_json_objkeys.py} | 12 +- .../{test_objlen.py => test_json_objlen.py} | 7 +- .../json/{test_resp.py => test_json_resp.py} | 4 +- tests/commands/json/test_json_set.py | 79 ++++++ ...st_strappend.py => test_json_strappend.py} | 1 - ...est_strlen_json.py => test_json_strlen.py} | 1 - .../{test_toggle.py => test_json_toggle.py} | 1 - .../{test_type_json.py => test_json_type.py} | 13 +- tests/commands/json/test_merge.py | 34 --- tests/commands/json/test_mset.py | 27 --- tests/commands/json/test_set_json.py | 72 ------ tests/conftest.py | 29 ++- tests/execute_on_http.py | 21 +- tests/test_formatters.py | 7 +- tests/test_http.py | 190 ++++----------- tests/test_read_your_writes.py | 4 +- upstash_redis/__init__.py | 2 +- upstash_redis/asyncio/client.py | 187 +++----------- upstash_redis/client.py | 123 +++------- upstash_redis/commands.py | 78 +++--- upstash_redis/commands.pyi | 229 ++++++++++++------ upstash_redis/http.py | 223 ++++++++++------- 40 files changed, 796 insertions(+), 786 deletions(-) create mode 100644 .env.example rename tests/commands/json/{test_arrappend.py => test_json_arrappend.py} (73%) rename tests/commands/json/{test_arrindex.py => test_json_arrindex.py} (75%) rename tests/commands/json/{test_arrinsert.py => test_json_arrinsert.py} (74%) rename tests/commands/json/{test_arrlen.py => test_json_arrlen.py} (82%) rename tests/commands/json/{test_arrpop.py => test_json_arrpop.py} (92%) rename tests/commands/json/{test_arrtrim.py => test_json_arrtrim.py} (93%) rename tests/commands/json/{test_clear.py => test_json_clear.py} (75%) rename tests/commands/json/{test_delete_json.py => test_json_delete.py} (86%) rename tests/commands/json/{test_forget.py => test_json_forget.py} (86%) rename tests/commands/json/{test_get_json.py => test_json_get.py} (68%) create mode 100644 tests/commands/json/test_json_merge.py rename tests/commands/json/{test_mget_json.py => test_json_mget.py} (68%) create mode 100644 tests/commands/json/test_json_mset.py rename tests/commands/json/{test_numincrby.py => test_json_numincrby.py} (100%) rename tests/commands/json/{test_nummultby.py => test_json_nummultby.py} (100%) rename tests/commands/json/{test_objkeys.py => test_json_objkeys.py} (81%) rename tests/commands/json/{test_objlen.py => test_json_objlen.py} (82%) rename tests/commands/json/{test_resp.py => test_json_resp.py} (77%) create mode 100644 tests/commands/json/test_json_set.py rename tests/commands/json/{test_strappend.py => test_json_strappend.py} (99%) rename tests/commands/json/{test_strlen_json.py => test_json_strlen.py} (99%) rename tests/commands/json/{test_toggle.py => test_json_toggle.py} (99%) rename tests/commands/json/{test_type_json.py => test_json_type.py} (88%) delete mode 100644 tests/commands/json/test_merge.py delete mode 100644 tests/commands/json/test_mset.py delete mode 100644 tests/commands/json/test_set_json.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..369256d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +UPSTASH_REDIS_REST_URL="" +UPSTASH_REDIS_REST_TOKEN="" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 38b5cb4..fff914b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,6 +24,14 @@ jobs: - name: Set up Poetry environment run: poetry install --no-root + - name: Run ruff + run: | + poetry run ruff check . + + - name: Run ruff format + run: | + poetry run ruff format --check . + - name: Run mypy run: | poetry run mypy --show-error-codes . diff --git a/pyproject.toml b/pyproject.toml index af032e7..27d2556 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "upstash-redis" -version = "1.2.0" +version = "1.3.0" description = "Serverless Redis SDK from Upstash" license = "MIT" authors = ["Upstash ", "Zgîmbău Tudor "] @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database", "Topic :: Database :: Front-Ends", @@ -30,14 +31,14 @@ packages = [{ include = "upstash_redis" }] [tool.poetry.dependencies] python = "^3.8" -aiohttp = "^3.8.4" -requests = "^2.31.0" +httpx = ">=0.23.0, <1" [tool.poetry.group.dev.dependencies] -pytest = "^7.3.0" -pytest-asyncio = "^0.21.0" -mypy = "^1.4.1" -types-requests = "^2.31.0" +pytest = "^8.3.4" +pytest-asyncio = "^0.24.0" +python-dotenv = "^1.0.1" +mypy = "^1.14.1" +ruff = "^0.9.7" [build-system] requires = ["poetry-core"] diff --git a/tests/commands/json/test_arrappend.py b/tests/commands/json/test_json_arrappend.py similarity index 73% rename from tests/commands/json/test_arrappend.py rename to tests/commands/json/test_json_arrappend.py index b470c02..dc6746c 100644 --- a/tests/commands/json/test_arrappend.py +++ b/tests/commands/json/test_json_arrappend.py @@ -7,7 +7,7 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_arrappend" - value: JSONValueT = {"int": 1, "array": [], "object": {"array": [ 1 ]}} + value: JSONValueT = {"int": 1, "array": [], "object": {"array": [1]}} redis.json.set(json_key, "$", value) yield redis.delete(json_key) @@ -18,21 +18,25 @@ def test_arrappend_single_element(redis: Redis): path = "$.array" assert redis.json.arrappend(key, path, 1) == [1] - assert redis.json.arrappend(key, path, 'new val') == [2] + assert redis.json.arrappend(key, path, "new val") == [2] assert redis.json.arrappend(key, path, 1.5) == [3] assert redis.json.arrappend(key, path, True) == [4] assert redis.json.arrappend(key, path, [1]) == [5] assert redis.json.arrappend(key, path, {"key": "value"}) == [6] - assert redis.json.get(key, path) == [[1, 'new val', 1.5, True, [1], {"key": "value"} ]] + assert redis.json.get(key, path) == [ + [1, "new val", 1.5, True, [1], {"key": "value"}] + ] def test_arrappend_multiple_elements(redis: Redis): key = "json_arrappend" path = "$.array" - new_values: List[JSONValueT] = [1, 'new val', 1.5, True, [1], {"key": "value"}] + new_values: List[JSONValueT] = [1, "new val", 1.5, True, [1], {"key": "value"}] assert redis.json.arrappend(key, path, *new_values) == [6] - assert redis.json.get(key, path) == [[1, 'new val', 1.5, True, [1], {"key": "value"}]] + assert redis.json.get(key, path) == [ + [1, "new val", 1.5, True, [1], {"key": "value"}] + ] def test_arrappend_nonarray_path(redis: Redis): @@ -40,8 +44,8 @@ def test_arrappend_nonarray_path(redis: Redis): path = "$.int" new_value = 9 - assert redis.json.arrappend(key, path, new_value) == [ None ] - assert redis.json.get(key, path) == [ 1 ] + assert redis.json.arrappend(key, path, new_value) == [None] + assert redis.json.get(key, path) == [1] def test_arrappend_wildcard(redis: Redis): diff --git a/tests/commands/json/test_arrindex.py b/tests/commands/json/test_json_arrindex.py similarity index 75% rename from tests/commands/json/test_arrindex.py rename to tests/commands/json/test_json_arrindex.py index 6096a68..2b1788e 100644 --- a/tests/commands/json/test_arrindex.py +++ b/tests/commands/json/test_json_arrindex.py @@ -6,20 +6,26 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_arrindex" - value: JSONValueT = {"array": [1, 'test', ['a'], 1.5, {'test': 1}], "int": 1, "object": {"array": [ 0, 1 ]}} + value: JSONValueT = { + "array": [1, "test", ["a"], 1.5, {"test": 1}], + "int": 1, + "object": {"array": [0, 1]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) + def test_arrindex_existing_element(redis: Redis): key = "json_arrindex" path = "$.array" assert redis.json.arrindex(key, path, 1) == [0] - assert redis.json.arrindex(key, path, 'test') == [1] - assert redis.json.arrindex(key, path, ['a']) == [2] + assert redis.json.arrindex(key, path, "test") == [1] + assert redis.json.arrindex(key, path, ["a"]) == [2] assert redis.json.arrindex(key, path, 1.5) == [3] - assert redis.json.arrindex(key, path, {'test': 1}) == [4] + assert redis.json.arrindex(key, path, {"test": 1}) == [4] + def test_arrindex_nonexisting_element(redis: Redis): key = "json_arrindex" @@ -28,6 +34,7 @@ def test_arrindex_nonexisting_element(redis: Redis): assert redis.json.arrindex(key, path, value) == [-1] + def test_arrindex_nonarray_path(redis: Redis): key = "json_arrindex" path = "$.int" @@ -35,6 +42,7 @@ def test_arrindex_nonarray_path(redis: Redis): assert redis.json.arrindex(key, path, value) == [None] + def test_arrindex_wildcard(redis: Redis): key = "json_arrindex" path = "$..array" diff --git a/tests/commands/json/test_arrinsert.py b/tests/commands/json/test_json_arrinsert.py similarity index 74% rename from tests/commands/json/test_arrinsert.py rename to tests/commands/json/test_json_arrinsert.py index 5b626d8..03d7253 100644 --- a/tests/commands/json/test_arrinsert.py +++ b/tests/commands/json/test_json_arrinsert.py @@ -16,13 +16,15 @@ def test_arrinsert_single_element(redis: Redis): key = "json_arrinsert" path = "$.array" - assert redis.json.arrinsert(key, path, 0,1) == [1] - assert redis.json.arrinsert(key, path, 0, 'new val') == [2] - assert redis.json.arrinsert(key, path, 0, 1.5) == [3] - assert redis.json.arrinsert(key, path, 0, True) == [4] - assert redis.json.arrinsert(key, path, 0, [1]) == [5] - assert redis.json.arrinsert(key, path, 0, {"key": "value"}) == [6] - assert redis.json.get(key, path) == [[ {"key": "value"}, [1],True , 1.5, 'new val', 1 ]] + assert redis.json.arrinsert(key, path, 0, 1) == [1] + assert redis.json.arrinsert(key, path, 0, "new val") == [2] + assert redis.json.arrinsert(key, path, 0, 1.5) == [3] + assert redis.json.arrinsert(key, path, 0, True) == [4] + assert redis.json.arrinsert(key, path, 0, [1]) == [5] + assert redis.json.arrinsert(key, path, 0, {"key": "value"}) == [6] + assert redis.json.get(key, path) == [ + [{"key": "value"}, [1], True, 1.5, "new val", 1] + ] def test_arrinsert_multiple_elements(redis: Redis): diff --git a/tests/commands/json/test_arrlen.py b/tests/commands/json/test_json_arrlen.py similarity index 82% rename from tests/commands/json/test_arrlen.py rename to tests/commands/json/test_json_arrlen.py index e4a6ba2..beef6ce 100644 --- a/tests/commands/json/test_arrlen.py +++ b/tests/commands/json/test_json_arrlen.py @@ -6,11 +6,16 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_arrlen" - value: JSONValueT = {"array": [1, 2, 3, 4], "int": 1, "object": {"array": [1, 2, 3]}} + value: JSONValueT = { + "array": [1, 2, 3, 4], + "int": 1, + "object": {"array": [1, 2, 3]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) + def test_arrlen_existing_element(redis: Redis): key = "json_arrlen" path = "$.array" @@ -18,6 +23,7 @@ def test_arrlen_existing_element(redis: Redis): length = redis.json.arrlen(key, path) assert length == [4] + def test_arrlen_nonarray_path(redis: Redis): key = "json_arrlen" path = "$.int" @@ -25,9 +31,10 @@ def test_arrlen_nonarray_path(redis: Redis): length = redis.json.arrlen(key, path) assert length == [None] + def test_arrlen_wildcard(redis: Redis): key = "json_arrlen" path = "$..array" length = redis.json.arrlen(key, path) - assert length == [3, 4] \ No newline at end of file + assert length == [3, 4] diff --git a/tests/commands/json/test_arrpop.py b/tests/commands/json/test_json_arrpop.py similarity index 92% rename from tests/commands/json/test_arrpop.py rename to tests/commands/json/test_json_arrpop.py index da19cb4..e8c0932 100644 --- a/tests/commands/json/test_arrpop.py +++ b/tests/commands/json/test_json_arrpop.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_arrpop" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [5, 6, 7]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [5, 6, 7]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) diff --git a/tests/commands/json/test_arrtrim.py b/tests/commands/json/test_json_arrtrim.py similarity index 93% rename from tests/commands/json/test_arrtrim.py rename to tests/commands/json/test_json_arrtrim.py index 3bf6915..e0a1489 100644 --- a/tests/commands/json/test_arrtrim.py +++ b/tests/commands/json/test_json_arrtrim.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_arrtrim" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [5, 6, 7]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [5, 6, 7]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) diff --git a/tests/commands/json/test_clear.py b/tests/commands/json/test_json_clear.py similarity index 75% rename from tests/commands/json/test_clear.py rename to tests/commands/json/test_json_clear.py index cd590bc..cd7320f 100644 --- a/tests/commands/json/test_clear.py +++ b/tests/commands/json/test_json_clear.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_clear" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) @@ -18,7 +22,9 @@ def test_clear_single_element(redis: Redis): removed_element = redis.json.clear(key, path) assert removed_element == 1 - assert redis.json.get(key) == [{"int": 1, "array": [], "object": {"array": [1, 2, 3]}}] + assert redis.json.get(key) == [ + {"int": 1, "array": [], "object": {"array": [1, 2, 3]}} + ] def test_clear_wildcard(redis: Redis): diff --git a/tests/commands/json/test_delete_json.py b/tests/commands/json/test_json_delete.py similarity index 86% rename from tests/commands/json/test_delete_json.py rename to tests/commands/json/test_json_delete.py index 6c223e5..fcd9a2d 100644 --- a/tests/commands/json/test_delete_json.py +++ b/tests/commands/json/test_json_delete.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_delete" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) diff --git a/tests/commands/json/test_forget.py b/tests/commands/json/test_json_forget.py similarity index 86% rename from tests/commands/json/test_forget.py rename to tests/commands/json/test_json_forget.py index 471e3c9..ad8d988 100644 --- a/tests/commands/json/test_forget.py +++ b/tests/commands/json/test_json_forget.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_forget" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) diff --git a/tests/commands/json/test_get_json.py b/tests/commands/json/test_json_get.py similarity index 68% rename from tests/commands/json/test_get_json.py rename to tests/commands/json/test_json_get.py index 5c38152..3ea4902 100644 --- a/tests/commands/json/test_get_json.py +++ b/tests/commands/json/test_json_get.py @@ -6,7 +6,11 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_get" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) @@ -15,7 +19,9 @@ def setup_json(redis: Redis): def test_get(redis: Redis): key = "json_get" - assert redis.json.get(key) == [{"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}}] + assert redis.json.get(key) == [ + {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} + ] def test_get_path(redis: Redis): @@ -27,7 +33,11 @@ def test_get_path(redis: Redis): def test_get_multiple_path(redis: Redis): key = "json_get" paths = ["$.array", "$.object", "$.int"] - assert redis.json.get(key, *paths) == {"$.array": [[1, 2, 3, 4]], "$.object": [{"array": [1, 2, 3]}], "$.int": [1]} + assert redis.json.get(key, *paths) == { + "$.array": [[1, 2, 3, 4]], + "$.object": [{"array": [1, 2, 3]}], + "$.int": [1], + } def test_get_nonexisting_key(redis: Redis): diff --git a/tests/commands/json/test_json_merge.py b/tests/commands/json/test_json_merge.py new file mode 100644 index 0000000..b45d975 --- /dev/null +++ b/tests/commands/json/test_json_merge.py @@ -0,0 +1,43 @@ +import pytest + +from upstash_redis import Redis +from upstash_redis.typing import JSONValueT + + +@pytest.fixture(autouse=True) +def setup_json(redis: Redis): + json_key = "json_merge" + value: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } + redis.json.set(json_key, "$", value) + yield + redis.delete(json_key) + + +def test_merge(redis: Redis): + key = "json_merge" + + assert redis.json.merge(key, "$", {"str": "test", "int": 2}) is True + assert redis.json.get(key) == [ + {"int": 2, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}, "str": "test"} + ] + + +def test_merge_nonexisting_key(redis: Redis): + key = "json_merge_nonexisting" + + assert redis.json.merge(key, "$", {"str": "test", "int": 2}) is True + assert redis.json.get(key) == [{"int": 2, "str": "test"}] + + +def test_merge_wildcard(redis: Redis): + key = "json_merge" + path = "$..array" + + assert redis.json.merge(key, path, [2, 2, 3, 4]) is True + assert redis.json.get(key) == [ + {"int": 1, "array": [2, 2, 3, 4], "object": {"array": [2, 2, 3, 4]}} + ] diff --git a/tests/commands/json/test_mget_json.py b/tests/commands/json/test_json_mget.py similarity index 68% rename from tests/commands/json/test_mget_json.py rename to tests/commands/json/test_json_mget.py index 271bd80..ad52613 100644 --- a/tests/commands/json/test_mget_json.py +++ b/tests/commands/json/test_json_mget.py @@ -6,8 +6,16 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_keys = ["json_mget_1", "json_mget_2"] - value1: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} - value2: JSONValueT = {"int": 2, "array": [2, 2, 3, 4], "object": {"array": [2, 2, 3]}} + value1: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } + value2: JSONValueT = { + "int": 2, + "array": [2, 2, 3, 4], + "object": {"array": [2, 2, 3]}, + } redis.json.set(json_keys[0], "$", value1) redis.json.set(json_keys[1], "$", value2) yield @@ -33,4 +41,7 @@ def test_mget_wildcard(redis: Redis): keys = ["json_mget_1", "json_mget_2"] path = "$..array" - assert redis.json.mget(keys, path) == [[[1, 2, 3], [1, 2, 3, 4]], [[2, 2, 3], [2, 2, 3, 4]]] + assert redis.json.mget(keys, path) == [ + [[1, 2, 3], [1, 2, 3, 4]], + [[2, 2, 3], [2, 2, 3, 4]], + ] diff --git a/tests/commands/json/test_json_mset.py b/tests/commands/json/test_json_mset.py new file mode 100644 index 0000000..32777ae --- /dev/null +++ b/tests/commands/json/test_json_mset.py @@ -0,0 +1,42 @@ +import pytest + +from upstash_redis import Redis +from upstash_redis.typing import JSONValueT + + +@pytest.fixture(autouse=True) +def setup_json(redis: Redis): + redis.json.delete("json_mset_1") + redis.json.delete("json_mset_2") + yield + redis.json.delete("json_mset_1") + redis.json.delete("json_mset_2") + + +def test_mset(redis: Redis): + json_key_1 = "json_mset_1" + json_key_2 = "json_mset_2" + value1: JSONValueT = { + "int": 1, + "array": [1, 2, 3, 4], + "object": {"array": [1, 2, 3]}, + } + value2: JSONValueT = { + "int": 2, + "array": [2, 2, 3, 4], + "object": {"array": [2, 2, 3]}, + } + + assert ( + redis.json.mset([(json_key_1, "$", value1), (json_key_2, "$", value2)]) is True + ) + assert redis.json.mget([json_key_1, json_key_2], "$.int") == [[1], [2]] + assert redis.json.mset([(json_key_1, "$.int", 2), (json_key_2, "$.int", 3)]) is True + assert redis.json.mget([json_key_1, json_key_2], "$.int") == [[2], [3]] + assert ( + redis.json.mset( + [(json_key_1, "$..array[0]", 2), (json_key_2, "$..array[0]", 3)] + ) + is True + ) + assert redis.json.mget([json_key_1, json_key_2], "$..array[0]") == [[2, 2], [3, 3]] diff --git a/tests/commands/json/test_numincrby.py b/tests/commands/json/test_json_numincrby.py similarity index 100% rename from tests/commands/json/test_numincrby.py rename to tests/commands/json/test_json_numincrby.py diff --git a/tests/commands/json/test_nummultby.py b/tests/commands/json/test_json_nummultby.py similarity index 100% rename from tests/commands/json/test_nummultby.py rename to tests/commands/json/test_json_nummultby.py diff --git a/tests/commands/json/test_objkeys.py b/tests/commands/json/test_json_objkeys.py similarity index 81% rename from tests/commands/json/test_objkeys.py rename to tests/commands/json/test_json_objkeys.py index 659256e..ab653d5 100644 --- a/tests/commands/json/test_objkeys.py +++ b/tests/commands/json/test_json_objkeys.py @@ -1,4 +1,5 @@ import pytest + from upstash_redis import Redis from upstash_redis.typing import JSONValueT @@ -6,7 +7,12 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_objkeys" - value: JSONValueT = {"int": 2, "str": "test", "object": {"int": 3}, "second_object": {"object": {"str": "test", "int": 2}}} + value: JSONValueT = { + "int": 2, + "str": "test", + "object": {"int": 3}, + "second_object": {"object": {"str": "test", "int": 2}}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) @@ -18,7 +24,7 @@ def test_objkeys(redis: Redis): response = redis.json.objkeys(key, path) - assert not response[0] is None + assert response[0] is not None response[0].sort() assert response == [["int", "object", "second_object", "str"]] @@ -44,7 +50,7 @@ def test_objkeys_wildcard(redis: Redis): response = redis.json.objkeys(key, path) for i in response: - assert not i is None + assert i is not None i.sort() response.sort() diff --git a/tests/commands/json/test_objlen.py b/tests/commands/json/test_json_objlen.py similarity index 82% rename from tests/commands/json/test_objlen.py rename to tests/commands/json/test_json_objlen.py index 654c5a7..17efec3 100644 --- a/tests/commands/json/test_objlen.py +++ b/tests/commands/json/test_json_objlen.py @@ -6,7 +6,12 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_objlen" - value: JSONValueT = {"int": 2, "str": "test", "object": {"int": 3}, "second_object": {"object": {"str": "test", "int": 2}}} + value: JSONValueT = { + "int": 2, + "str": "test", + "object": {"int": 3}, + "second_object": {"object": {"str": "test", "int": 2}}, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) diff --git a/tests/commands/json/test_resp.py b/tests/commands/json/test_json_resp.py similarity index 77% rename from tests/commands/json/test_resp.py rename to tests/commands/json/test_json_resp.py index b58c569..62df58e 100644 --- a/tests/commands/json/test_resp.py +++ b/tests/commands/json/test_json_resp.py @@ -15,4 +15,6 @@ def setup_json(redis: Redis): def test_resp(redis: Redis): key = "json_resp" - assert redis.json.resp(key, "$") == [['{', 'object', ['{', 'array', ['[', 1, 2, 3]]]] + assert redis.json.resp(key, "$") == [ + ["{", "object", ["{", "array", ["[", 1, 2, 3]]] + ] diff --git a/tests/commands/json/test_json_set.py b/tests/commands/json/test_json_set.py new file mode 100644 index 0000000..301ae4a --- /dev/null +++ b/tests/commands/json/test_json_set.py @@ -0,0 +1,79 @@ +import pytest + +import upstash_redis.errors +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def setup_json(redis: Redis): + json_key = "json_set" + redis.delete(json_key) + yield + redis.delete(json_key) + + +def test_set(redis: Redis): + key = "json_set" + + assert redis.json.get(key) is None + assert redis.json.set(key, "$", {"test": 1}) is True + assert redis.json.set(key, "$.str", "test") is True + assert redis.json.set(key, "$.int", 1) is True + assert redis.json.set(key, "$.bool", True) is True + assert redis.json.set(key, "$.array", [1, 2, "test"]) is True + assert redis.json.set(key, "$.object", {"int": 1}) is True + assert redis.json.set(key, "$.null", None) is True + assert redis.json.get(key) == [ + { + "int": 1, + "test": 1, + "str": "test", + "bool": True, + "array": [1, 2, "test"], + "object": {"int": 1}, + "null": None, + } + ] + + +def test_set_nonexisting_path(redis: Redis): + key = "json_set" + path = "$.nonexisting_key.str" + + assert redis.json.set(key, "$", {"int": 1}) is True + assert redis.json.get(key) == [{"int": 1}] + with pytest.raises(upstash_redis.errors.UpstashError): + redis.json.set(key, path, "test") + + +def test_set_wildcard(redis: Redis): + key = "json_set" + path = "$..int" + + assert redis.json.set(key, "$", {"int": 1, "obj": {"int": 3}}) is True + assert redis.json.get(key) == [{"int": 1, "obj": {"int": 3}}] + assert redis.json.set(key, path, 2) is True + assert redis.json.get(key) == [{"int": 2, "obj": {"int": 2}}] + + +def test_set_nx(redis: Redis): + key = "json_set" + path = "$.int" + + assert redis.json.set(key, "$", {"int": 1}) is True + assert redis.json.get(key) == [{"int": 1}] + assert redis.json.set(key, "$", {"str": "test"}, nx=True) is False + assert redis.json.get(key) == [{"int": 1}] + assert redis.json.set(key, path, 2, nx=True) is False + assert redis.json.get(key) == [{"int": 1}] + + +def test_set_xx(redis: Redis): + key = "json_set" + + assert redis.json.set(key, "$", {"int": 1}, xx=True) is False + assert redis.json.get(key) is None + assert redis.json.set(key, "$", {"int": 1}) is True + assert redis.json.get(key) == [{"int": 1}] + assert redis.json.set(key, "$", {"str": "test"}, xx=True) is True + assert redis.json.get(key) == [{"str": "test"}] diff --git a/tests/commands/json/test_strappend.py b/tests/commands/json/test_json_strappend.py similarity index 99% rename from tests/commands/json/test_strappend.py rename to tests/commands/json/test_json_strappend.py index d732b55..ee8eb5c 100644 --- a/tests/commands/json/test_strappend.py +++ b/tests/commands/json/test_json_strappend.py @@ -31,4 +31,3 @@ def test_strappend_wildcard(redis: Redis): assert redis.json.strappend(key, path, "_append") == [13, 11] assert redis.json.get(key, path) == ["test_1_append", "test_append"] - diff --git a/tests/commands/json/test_strlen_json.py b/tests/commands/json/test_json_strlen.py similarity index 99% rename from tests/commands/json/test_strlen_json.py rename to tests/commands/json/test_json_strlen.py index b95c2f8..973a3d2 100644 --- a/tests/commands/json/test_strlen_json.py +++ b/tests/commands/json/test_json_strlen.py @@ -29,4 +29,3 @@ def test_strlen_wildcard(redis: Redis): path = "$..str" assert redis.json.strlen(key, path) == [6, 4] - diff --git a/tests/commands/json/test_toggle.py b/tests/commands/json/test_json_toggle.py similarity index 99% rename from tests/commands/json/test_toggle.py rename to tests/commands/json/test_json_toggle.py index 4b5f1c6..7f7de5e 100644 --- a/tests/commands/json/test_toggle.py +++ b/tests/commands/json/test_json_toggle.py @@ -35,4 +35,3 @@ def test_toggle_wildcard(redis: Redis): assert redis.json.get(key, path) == [True, False] assert redis.json.toggle(key, path) == [0, 1] assert redis.json.get(key, path) == [False, True] - diff --git a/tests/commands/json/test_type_json.py b/tests/commands/json/test_json_type.py similarity index 88% rename from tests/commands/json/test_type_json.py rename to tests/commands/json/test_json_type.py index 3a2e3ad..ec351a6 100644 --- a/tests/commands/json/test_type_json.py +++ b/tests/commands/json/test_json_type.py @@ -6,7 +6,14 @@ @pytest.fixture(autouse=True) def setup_json(redis: Redis): json_key = "json_type" - value: JSONValueT = {"number": 1, "array": [1, 2, 3, 4], "object": {"number": 3.1415}, "str": "test", "bool": True, "null": None} + value: JSONValueT = { + "number": 1, + "array": [1, 2, 3, 4], + "object": {"number": 3.1415}, + "str": "test", + "bool": True, + "null": None, + } redis.json.set(json_key, "$", value) yield redis.delete(json_key) @@ -73,7 +80,3 @@ def test_type_wildcard(redis: Redis): path = "$..number" assert redis.json.type(key, path) == ["number", "integer"] - - - - diff --git a/tests/commands/json/test_merge.py b/tests/commands/json/test_merge.py deleted file mode 100644 index db5126e..0000000 --- a/tests/commands/json/test_merge.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -from upstash_redis import Redis -from upstash_redis.typing import JSONValueT - - -@pytest.fixture(autouse=True) -def setup_json(redis: Redis): - json_key = "json_merge" - value: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} - redis.json.set(json_key, "$", value) - yield - redis.delete(json_key) - - -def test_merge(redis: Redis): - key = "json_merge" - - assert redis.json.merge(key, '$', {'str': 'test', 'int': 2}) == True - assert redis.json.get(key) == [{"int": 2, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}, "str": "test"}] - - -def test_merge_nonexisting_key(redis: Redis): - key = "json_merge_nonexisting" - - assert redis.json.merge(key, '$', {'str': 'test', 'int': 2}) == True - assert redis.json.get(key) == [{"int": 2, "str": "test"}] - - -def test_merge_wildcard(redis: Redis): - key = 'json_merge' - path = "$..array" - - assert redis.json.merge(key, path, [2, 2, 3, 4]) == True - assert redis.json.get(key) == [{"int": 1, "array": [2, 2, 3, 4], "object": {"array": [2, 2, 3, 4]}}] diff --git a/tests/commands/json/test_mset.py b/tests/commands/json/test_mset.py deleted file mode 100644 index ee5c50d..0000000 --- a/tests/commands/json/test_mset.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -from upstash_redis import Redis -from upstash_redis.typing import JSONValueT - - -@pytest.fixture(autouse=True) -def setup_json(redis: Redis): - redis.json.delete('json_mset_1') - redis.json.delete('json_mset_2') - yield - redis.json.delete('json_mset_1') - redis.json.delete('json_mset_2') - - -def test_mset(redis: Redis): - json_key_1 = "json_mset_1" - json_key_2 = "json_mset_2" - value1: JSONValueT = {"int": 1, "array": [1, 2, 3, 4], "object": {"array": [1, 2, 3]}} - value2: JSONValueT = {"int": 2, "array": [2, 2, 3, 4], "object": {"array": [2, 2, 3]}} - - assert redis.json.mset([(json_key_1, "$", value1), (json_key_2, "$", value2)]) == True - assert redis.json.mget([json_key_1, json_key_2], "$.int") == [[1], [2]] - assert redis.json.mset([(json_key_1, "$.int", 2), (json_key_2, "$.int", 3)]) == True - assert redis.json.mget([json_key_1, json_key_2], "$.int") == [[2], [3]] - assert redis.json.mset([(json_key_1, "$..array[0]", 2), (json_key_2, "$..array[0]", 3)]) == True - assert redis.json.mget([json_key_1, json_key_2], "$..array[0]") == [[2, 2], [3, 3]] - diff --git a/tests/commands/json/test_set_json.py b/tests/commands/json/test_set_json.py deleted file mode 100644 index 7f1b046..0000000 --- a/tests/commands/json/test_set_json.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest - -import upstash_redis.errors -from upstash_redis import Redis - - -@pytest.fixture(autouse=True) -def setup_json(redis: Redis): - json_key = "json_set" - redis.delete(json_key) - yield - redis.delete(json_key) - - -def test_set(redis: Redis): - key = "json_set" - - assert redis.json.get(key) == None - assert redis.json.set(key, '$', {"test": 1}) == True - assert redis.json.set(key, "$.str", "test") == True - assert redis.json.set(key, "$.int", 1) == True - assert redis.json.set(key, "$.bool", True) == True - assert redis.json.set(key, "$.array", [1, 2, "test"]) == True - assert redis.json.set(key, "$.object", {"int": 1}) == True - assert redis.json.set(key, "$.null", None) == True - assert redis.json.get(key) == [{"int": 1, "test": 1, "str": "test", "bool": True, "array": [1, 2, "test"], "object": {"int": 1}, "null": None}] - - -def test_set_nonexisting_path(redis: Redis): - key = "json_set" - path = "$.nonexisting_key.str" - - assert redis.json.set(key, "$", {"int": 1}) == True - assert redis.json.get(key) == [{"int": 1}] - with pytest.raises(upstash_redis.errors.UpstashError) as ex: - redis.json.set(key, path, "test") - - - -def test_set_wildcard(redis: Redis): - key = "json_set" - path = "$..int" - - assert redis.json.set(key, "$", {"int": 1, "obj":{"int": 3}}) == True - assert redis.json.get(key) == [{"int": 1, "obj":{"int": 3}}] - assert redis.json.set(key, path, 2) == True - assert redis.json.get(key) == [{"int": 2, "obj": {"int": 2}}] - - -def test_set_nx(redis: Redis): - key = "json_set" - path = "$.int" - - assert redis.json.set(key, "$", {"int": 1}) == True - assert redis.json.get(key) == [{"int": 1}] - assert redis.json.set(key, "$", {"str": "test"}, nx=True) == False - assert redis.json.get(key) == [{"int": 1}] - assert redis.json.set(key, path, 2, nx=True) == False - assert redis.json.get(key) == [{"int": 1}] - - -def test_set_xx(redis: Redis): - key = "json_set" - - assert redis.json.set(key, "$", {"int": 1}, xx=True) == False - assert redis.json.get(key) is None - assert redis.json.set(key, "$", {"int": 1}) == True - assert redis.json.get(key) == [{"int": 1}] - assert redis.json.set(key, "$", {"str": "test"}, xx=True) == True - assert redis.json.get(key) == [{"str": "test"}] - - diff --git a/tests/conftest.py b/tests/conftest.py index 456e45c..12dcedc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,31 @@ from os import environ from typing import Dict, List +import dotenv +import httpx import pytest import pytest_asyncio -import requests from upstash_redis import Redis from upstash_redis.asyncio import Redis as AsyncRedis -""" -Flush and fill the testing database with the necessary data. -""" +dotenv.load_dotenv(override=True) + +URL = environ.get( + "UPSTASH_REDIS_REST_URL", + dotenv.dotenv_values().get("UPSTASH_REDIS_REST_URL"), +) -url: str = environ["UPSTASH_REDIS_REST_URL"] + "/pipeline" -token: str = environ["UPSTASH_REDIS_REST_TOKEN"] +TOKEN = environ.get( + "UPSTASH_REDIS_REST_TOKEN", + dotenv.dotenv_values().get("UPSTASH_REDIS_REST_TOKEN"), +) -headers: Dict[str, str] = {"Authorization": f"Bearer {token}"} +HEADERS: Dict[str, str] = {"Authorization": f"Bearer {TOKEN}"} +""" +Flush and fill the testing database with the necessary data. +""" commands: List[List] = [ # Flush the database. ["FLUSHDB"], @@ -116,9 +125,9 @@ def pytest_configure(): - with requests.post(url, headers=headers, json=commands) as r: - if r.status_code != 200: - raise RuntimeError(r.json()["error"]) + r = httpx.post(f"{URL}/pipeline", headers=HEADERS, json=commands) + if r.status_code != 200: + raise RuntimeError(r.json()["error"]) @pytest_asyncio.fixture diff --git a/tests/execute_on_http.py b/tests/execute_on_http.py index 8ea921a..49f58ee 100644 --- a/tests/execute_on_http.py +++ b/tests/execute_on_http.py @@ -1,8 +1,7 @@ from os import environ from typing import Any, Dict -import requests -from aiohttp import ClientSession +import httpx from upstash_redis.typing import RESTResultT @@ -13,21 +12,19 @@ async def execute_on_http(*command_elements: str) -> RESTResultT: - async with ClientSession() as session: - async with session.post( - url=url, headers=headers, json=[*command_elements] - ) as response: - body: Dict[str, Any] = await response.json() + async with httpx.AsyncClient(timeout=None) as client: + response = await client.post(url=url, headers=headers, json=[*command_elements]) + body: Dict[str, Any] = response.json() - # Avoid the [] syntax to prevent KeyError from being raised. - if body.get("error"): - raise Exception(body.get("error")) + # Avoid the [] syntax to prevent KeyError from being raised. + if body.get("error"): + raise Exception(body.get("error")) - return body["result"] + return body["result"] def sync_execute_on_http(*command_elements: str) -> RESTResultT: - response = requests.post(url, headers=headers, json=[*command_elements]) + response = httpx.post(url, headers=headers, json=[*command_elements]) body: Dict[str, Any] = response.json() # Avoid the [] syntax to prevent KeyError from being raised. diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 615e809..794f3a4 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -32,9 +32,10 @@ def test_string_to_json() -> None: def test_string_list_to_json_list() -> None: - assert string_list_to_json_list( - res=['{"a": 1, "b": 2}', '{"c": 3, "d": 4}'] - ) == [{"a": 1, "b": 2}, {"c": 3, "d": 4}] + assert string_list_to_json_list(res=['{"a": 1, "b": 2}', '{"c": 3, "d": 4}']) == [ + {"a": 1, "b": 2}, + {"c": 3, "d": 4}, + ] def test_format_geo_members_with_distance_and_hash_and_coordinates() -> None: diff --git a/tests/test_http.py b/tests/test_http.py index 744cd15..333d3ce 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,30 +1,27 @@ -import asyncio from os import environ from platform import python_version from typing import Any, Dict, Literal, Optional -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from aiohttp import ClientSession from pytest import mark, raises -from requests import Session from upstash_redis import __version__ from upstash_redis.errors import UpstashError -from upstash_redis.http import async_execute, decode, make_headers, sync_execute +from upstash_redis.http import decode, make_headers, AsyncHttpClient, SyncHttpClient @mark.asyncio async def test_async_execute_without_encoding() -> None: - async with ClientSession() as session: + async with AsyncHttpClient( + encoding=None, + retries=0, + retry_interval=0, + ) as client: assert ( - await async_execute( - session=session, + await client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers(environ["UPSTASH_REDIS_REST_TOKEN"], None, False), - retries=0, - retry_interval=0, - encoding=None, command=["SET", "a", "b"], ) == "OK" @@ -33,17 +30,17 @@ async def test_async_execute_without_encoding() -> None: @mark.asyncio async def test_async_execute_with_encoding() -> None: - async with ClientSession() as session: + async with AsyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: assert ( - await async_execute( - session=session, + await client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", command=["SET", "a", "b"], ) == "OK" @@ -52,17 +49,17 @@ async def test_async_execute_with_encoding() -> None: @mark.asyncio async def test_async_execute_with_encoding_and_object() -> None: - async with ClientSession() as session: + async with AsyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: assert ( - await async_execute( - session=session, + await client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", command=["SET", "a", {"b": "c"}], ) == "OK" @@ -71,32 +68,32 @@ async def test_async_execute_with_encoding_and_object() -> None: @mark.asyncio async def test_async_execute_with_invalid_command() -> None: - async with ClientSession() as session: + async with AsyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: with raises(UpstashError): - await async_execute( - session=session, + await client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", # We give one parameter to "SET" instead of two. command=["SET", "a"], ) def test_sync_execute_without_encoding() -> None: - with Session() as session: + with SyncHttpClient( + encoding=None, + retries=0, + retry_interval=0, + ) as client: assert ( - sync_execute( - session=session, + client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers(environ["UPSTASH_REDIS_REST_TOKEN"], None, False), - retries=0, - retry_interval=0, - encoding=None, command=["SET", "a", "b"], ) == "OK" @@ -104,17 +101,17 @@ def test_sync_execute_without_encoding() -> None: def test_sync_execute_with_encoding() -> None: - with Session() as session: + with SyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: assert ( - sync_execute( - session=session, + client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", command=["SET", "a", "b"], ) == "OK" @@ -122,17 +119,17 @@ def test_sync_execute_with_encoding() -> None: def test_sync_execute_with_encoding_and_object() -> None: - with Session() as session: + with SyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: assert ( - sync_execute( - session=session, + client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", command=["SET", "a", {"b": "c"}], ) == "OK" @@ -140,17 +137,17 @@ def test_sync_execute_with_encoding_and_object() -> None: def test_sync_execute_with_invalid_command() -> None: - with Session() as session: + with SyncHttpClient( + encoding="base64", + retries=0, + retry_interval=0, + ) as client: with raises(UpstashError): - sync_execute( - session=session, + client.execute( url=environ["UPSTASH_REDIS_REST_URL"], headers=make_headers( environ["UPSTASH_REDIS_REST_TOKEN"], "base64", False ), - retries=0, - retry_interval=0, - encoding="base64", # We give one parameter to "SET" instead of two. command=["SET", "a"], ) @@ -245,90 +242,3 @@ def test_make_headers_on_aws() -> None: "Upstash-Telemetry-Runtime": f"python@v{python_version()}", "Upstash-Telemetry-Platform": "aws", } - - -@pytest.mark.parametrize("retry_count", [0, 42, 100]) -def test_sync_execute_no_retry_on_success(retry_count: int) -> None: - session = MagicMock() - response = MagicMock() - response.json = MagicMock(return_value={"result": "OK"}) - session.post = MagicMock(return_value=response) - - assert sync_execute(session, "", {}, None, retry_count, 0, []) == "OK" - - assert session.post.call_count == 1 - - -def test_sync_execute_no_retry_on_error_response_from_server() -> None: - session = MagicMock() - response = MagicMock() - response.json = MagicMock(return_value={"error": "expected error"}) - session.post = MagicMock(return_value=response) - - with raises(UpstashError) as e: - sync_execute(session, "", {}, None, 100, 0, []) - - assert str(e.value) == "expected error" - assert session.post.call_count == 1 - - -@pytest.mark.parametrize("retry_count", [0, 42, 100]) -def test_sync_execute_retry_on_post_request_error(retry_count) -> None: - session = MagicMock() - session.post = MagicMock(side_effect=RuntimeError("expected error")) - - with raises(RuntimeError) as e: - sync_execute(session, "", {}, None, retry_count, 0, []) - - assert str(e.value) == "expected error" - - # We start couting retries after the first attempt - assert session.post.call_count == (retry_count + 1) - - -@pytest.mark.parametrize("retry_count", [0, 42, 100]) -@mark.asyncio -async def test_async_execute_no_retry_on_success(retry_count: int) -> None: - session = MagicMock() - response = MagicMock() - f: asyncio.Future[Dict] = asyncio.Future() - f.set_result({"result": "OK"}) - response.json = MagicMock(return_value=f) - session.post = MagicMock(return_value=response) - response.__aenter__.return_value = response - - assert (await async_execute(session, "", {}, None, retry_count, 0, [])) == "OK" - - assert session.post.call_count == 1 - - -@mark.asyncio -async def test_async_execute_no_retry_on_error_response_from_server() -> None: - session = MagicMock() - response = MagicMock() - f: asyncio.Future[Dict] = asyncio.Future() - f.set_result({"error": "expected error"}) - response.json = MagicMock(return_value=f) - session.post = MagicMock(return_value=response) - response.__aenter__.return_value = response - - with raises(UpstashError) as e: - await async_execute(session, "", {}, None, 100, 0, []) - - assert str(e.value) == "expected error" - assert session.post.call_count == 1 - - -@pytest.mark.parametrize("retry_count", [0, 42, 100]) -@mark.asyncio -async def test_async_execute_retry_on_post_request_error(retry_count) -> None: - session = MagicMock() - session.post = MagicMock(side_effect=RuntimeError("expected error")) - - with raises(RuntimeError) as e: - await async_execute(session, "", {}, None, retry_count, 0, []) - - assert str(e.value) == "expected error" - - # We start couting retries after the first attempt - assert session.post.call_count == (retry_count + 1) diff --git a/tests/test_read_your_writes.py b/tests/test_read_your_writes.py index 9a70e2e..b7058b8 100644 --- a/tests/test_read_your_writes.py +++ b/tests/test_read_your_writes.py @@ -116,7 +116,7 @@ def test_should_not_update_sync_state_with_opt_out_ryw(redis: Redis): @pytest.mark.parametrize("async_redis", [{"read_your_writes": False}], indirect=True) @pytest.mark.asyncio async def test_should_not_update_sync_state_with_opt_out_ryw_async( - async_redis: AsyncRedis + async_redis: AsyncRedis, ): initial_token = async_redis._sync_token await async_redis.set("key", "value") @@ -133,7 +133,7 @@ def test_should_update_sync_state_with_default_behavior(redis: Redis): @pytest.mark.asyncio async def test_should_update_sync_state_with_default_behavior_async( - async_redis: AsyncRedis + async_redis: AsyncRedis, ): initial_token = async_redis._sync_token await async_redis.set("key", "value") diff --git a/upstash_redis/__init__.py b/upstash_redis/__init__.py index f4085f5..6482097 100644 --- a/upstash_redis/__init__.py +++ b/upstash_redis/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.2.0" +__version__ = "1.3.0" from upstash_redis.asyncio.client import Redis as AsyncRedis from upstash_redis.client import Redis diff --git a/upstash_redis/asyncio/client.py b/upstash_redis/asyncio/client.py index b8dae49..f805a39 100644 --- a/upstash_redis/asyncio/client.py +++ b/upstash_redis/asyncio/client.py @@ -1,11 +1,14 @@ from os import environ from typing import Any, List, Literal, Optional, Type, Dict, Callable -from aiohttp import ClientSession - -from upstash_redis.commands import AsyncCommands, PipelineCommands, AsyncJsonCommands, PipelineJsonCommands +from upstash_redis.commands import ( + AsyncCommands, + PipelineCommands, + AsyncJsonCommands, + PipelineJsonCommands, +) from upstash_redis.format import cast_response -from upstash_redis.http import async_execute, make_headers +from upstash_redis.http import make_headers, AsyncHttpClient from upstash_redis.typing import RESTResultT @@ -47,23 +50,22 @@ def __init__( :param read_your_writes: when enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. """ - self._url = url - self._token = token - - self._json = AsyncJsonCommands(self) - - self._allow_telemetry = allow_telemetry - - self._rest_encoding: Optional[Literal["base64"]] = rest_encoding - self._rest_retries = rest_retries - self._rest_retry_interval = rest_retry_interval - self._read_your_writes = read_your_writes self._sync_token = "" + # self._allow_telemetry and self._headers need to stay in this class + # for compatibility with the ratelimit library. + self._allow_telemetry = allow_telemetry self._headers = make_headers(token, rest_encoding, allow_telemetry) - self._context_manager: Optional[_SessionContextManager] = None + + self._json = AsyncJsonCommands(self) + self._http = AsyncHttpClient( + encoding=rest_encoding, + retries=rest_retries, + retry_interval=rest_retry_interval, + sync_token_cb=self._update_sync_token, + ) @property def json(self) -> AsyncJsonCommands: @@ -100,9 +102,6 @@ def from_env( ) async def __aenter__(self) -> "Redis": - self._context_manager = _SessionContextManager( - ClientSession(), close_session=False - ) return self async def __aexit__( @@ -117,9 +116,7 @@ async def close(self) -> None: """ Closes the resources associated with the client. """ - if self._context_manager: - await self._context_manager.close_session() - self._context_manager = None + await self._http.close() def _update_sync_token(self, new_token: str): if self._read_your_writes: @@ -133,25 +130,12 @@ async def execute(self, command: List) -> RESTResultT: """ Executes the given command. """ - context_manager = self._context_manager - if not context_manager: - context_manager = _SessionContextManager( - ClientSession(), close_session=True - ) - self._maybe_set_sync_token_header(self._headers) - async with context_manager: - res = await async_execute( - session=context_manager.session, - url=self._url, - headers=self._headers, - encoding=self._rest_encoding, - retries=self._rest_retries, - retry_interval=self._rest_retry_interval, - command=command, - sync_token_cb=self._update_sync_token, - ) - + res = await self._http.execute( + url=self._url, + headers=self._headers, + command=command, + ) return cast_response(command, res) def pipeline(self) -> "AsyncPipeline": @@ -160,16 +144,10 @@ def pipeline(self) -> "AsyncPipeline": """ return AsyncPipeline( url=self._url, - token=self._token, - rest_encoding=self._rest_encoding, - rest_retries=self._rest_retries, - rest_retry_interval=self._rest_retry_interval, - allow_telemetry=self._allow_telemetry, headers=self._headers, - context_manager=self._context_manager, + http=self._http, multi_exec="pipeline", set_sync_token_header_fn=self._maybe_set_sync_token_header, - sync_token_cb=self._update_sync_token, ) def multi(self) -> "AsyncPipeline": @@ -178,16 +156,10 @@ def multi(self) -> "AsyncPipeline": """ return AsyncPipeline( url=self._url, - token=self._token, - rest_encoding=self._rest_encoding, - rest_retries=self._rest_retries, - rest_retry_interval=self._rest_retry_interval, - allow_telemetry=self._allow_telemetry, headers=self._headers, - context_manager=self._context_manager, + http=self._http, multi_exec="multi-exec", set_sync_token_header_fn=self._maybe_set_sync_token_header, - sync_token_cb=self._update_sync_token, ) @@ -195,54 +167,19 @@ class AsyncPipeline(PipelineCommands): def __init__( self, url: str, - token: str, - rest_encoding: Optional[Literal["base64"]] = "base64", - rest_retries: int = 1, - rest_retry_interval: float = 3, # Seconds. - allow_telemetry: bool = True, - context_manager: Optional["_SessionContextManager"] = None, - headers: Optional[Dict[str, str]] = None, - multi_exec: Literal["multi-exec", "pipeline"] = "pipeline", - set_sync_token_header_fn: Optional[Callable[[Dict[str, str]], None]] = None, - sync_token_cb: Optional[Callable[[str], None]] = None, + headers: Dict[str, str], + http: AsyncHttpClient, + multi_exec: Literal["multi-exec", "pipeline"], + set_sync_token_header_fn: Callable[[Dict[str, str]], None], ): - """ - Creates a new blocking Redis client. - - :param url: UPSTASH_REDIS_REST_URL in the console - :param token: UPSTASH_REDIS_REST_TOKEN in the console - :param rest_encoding: the encoding that can be used by the REST API to parse the response before sending it - :param rest_retries: how many times an HTTP request will be retried if it fails - :param rest_retry_interval: how many seconds will be waited between each retry - :param allow_telemetry: whether anonymous telemetry can be collected - :param context_manager: context manager - :param headers: request headers - :param multiexec: Whether multi execution (transaction) or pipelining is to be used - :param set_sync_token_header_fn: Function to set the Upstash-Sync-Token header - :param sync_token_cb: Function to call when a new Upstash-Sync-Token response is received - """ - - self._url = url - self._token = token + self._url = f"{url}/{multi_exec}" + self._headers = headers + self._http = http self._json = PipelineJsonCommands(self) - - self._allow_telemetry = allow_telemetry - - self._rest_encoding: Optional[Literal["base64"]] = rest_encoding - self._rest_retries = rest_retries - self._rest_retry_interval = rest_retry_interval - - self._headers = headers or make_headers(token, rest_encoding, allow_telemetry) - self._context_manager = context_manager or _SessionContextManager( - ClientSession(), close_session=True - ) - self._command_stack: List[List[str]] = [] - self._multi_exec = multi_exec self._set_sync_token_header_fn = set_sync_token_header_fn - self._sync_token_cb = sync_token_cb @property def json(self) -> PipelineJsonCommands: @@ -262,25 +199,14 @@ async def exec(self) -> List[RESTResultT]: """ Executes the commands in the pipeline by sending them as a batch """ - url = f"{self._url}/{self._multi_exec}" - - if self._set_sync_token_header_fn: - self._set_sync_token_header_fn(self._headers) - - context_manager = self._context_manager - async with context_manager: - res: List[RESTResultT] = await async_execute( # type: ignore[assignment] - session=context_manager.session, - url=url, - headers=self._headers, - encoding=self._rest_encoding, - retries=self._rest_retries, - retry_interval=self._rest_retry_interval, - command=self._command_stack, - from_pipeline=True, - sync_token_cb=self._sync_token_cb, - ) + self._set_sync_token_header_fn(self._headers) + res: List[RESTResultT] = await self._http.execute( # type: ignore[assignment] + url=self._url, + headers=self._headers, + command=self._command_stack, + from_pipeline=True, + ) response = [ cast_response(command, response) for command, response in zip(self._command_stack, res) @@ -304,36 +230,3 @@ async def __aexit__( exc_tb: Any, ) -> None: self.reset() - - -class _SessionContextManager: - """ - Allows a session to be re-used in multiple async with - blocks when the `close_session` is False. - - Main use case is to re-use sessions in multiple HTTP - requests when the client is used in an async with block. - - The logic around the places in which we use this class is - required so that the same client can be re-used even in - different event loops, one after another. - """ - - def __init__(self, session: ClientSession, close_session: bool) -> None: - self.session = session - self._close_session = close_session - - async def close_session(self) -> None: - await self.session.close() - - async def __aenter__(self) -> ClientSession: - return self.session - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Any, - ) -> None: - if self._close_session: - await self.close_session() diff --git a/upstash_redis/client.py b/upstash_redis/client.py index a3b420a..c2a9bc8 100644 --- a/upstash_redis/client.py +++ b/upstash_redis/client.py @@ -1,11 +1,14 @@ from os import environ from typing import Any, List, Literal, Optional, Type, Dict, Callable -from requests import Session - -from upstash_redis.commands import Commands, PipelineCommands, JsonCommands, PipelineJsonCommands +from upstash_redis.commands import ( + Commands, + PipelineCommands, + JsonCommands, + PipelineJsonCommands, +) from upstash_redis.format import cast_response -from upstash_redis.http import make_headers, sync_execute +from upstash_redis.http import make_headers, SyncHttpClient from upstash_redis.typing import RESTResultT @@ -49,23 +52,22 @@ def __init__( :param read_your_writes: when enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. """ - self._url = url - self._token = token - - self._json = JsonCommands(self) - - self._allow_telemetry = allow_telemetry - - self._rest_encoding: Optional[Literal["base64"]] = rest_encoding - self._rest_retries = rest_retries - self._rest_retry_interval = rest_retry_interval - self._read_your_writes = read_your_writes self._sync_token = "" + # self._allow_telemetry and self._headers need to stay in this class + # for compatibility with the ratelimit library. + self._allow_telemetry = allow_telemetry self._headers = make_headers(token, rest_encoding, allow_telemetry) - self._session = Session() + + self._json = JsonCommands(self) + self._http = SyncHttpClient( + encoding=rest_encoding, + retries=rest_retries, + retry_interval=rest_retry_interval, + sync_token_cb=self._update_sync_token, + ) @property def json(self) -> JsonCommands: @@ -116,7 +118,7 @@ def close(self): """ Closes the resources associated with the client. """ - self._session.close() + self._http.close() def _update_sync_token(self, new_token: str): if self._read_your_writes: @@ -131,17 +133,11 @@ def execute(self, command: List) -> RESTResultT: Executes the given command. """ self._maybe_set_sync_token_header(self._headers) - res = sync_execute( - session=self._session, + res = self._http.execute( url=self._url, headers=self._headers, - encoding=self._rest_encoding, - retries=self._rest_retries, - retry_interval=self._rest_retry_interval, command=command, - sync_token_cb=self._update_sync_token, ) - return cast_response(command, res) def pipeline(self) -> "Pipeline": @@ -150,16 +146,10 @@ def pipeline(self) -> "Pipeline": """ return Pipeline( url=self._url, - token=self._token, - rest_encoding=self._rest_encoding, - rest_retries=self._rest_retries, - rest_retry_interval=self._rest_retry_interval, - allow_telemetry=self._allow_telemetry, headers=self._headers, - session=self._session, + http=self._http, multi_exec="pipeline", set_sync_token_header_fn=self._maybe_set_sync_token_header, - sync_token_cb=self._update_sync_token, ) def multi(self) -> "Pipeline": @@ -168,16 +158,10 @@ def multi(self) -> "Pipeline": """ return Pipeline( url=self._url, - token=self._token, - rest_encoding=self._rest_encoding, - rest_retries=self._rest_retries, - rest_retry_interval=self._rest_retry_interval, - allow_telemetry=self._allow_telemetry, headers=self._headers, - session=self._session, + http=self._http, multi_exec="multi-exec", set_sync_token_header_fn=self._maybe_set_sync_token_header, - sync_token_cb=self._update_sync_token, ) @@ -185,52 +169,19 @@ class Pipeline(PipelineCommands): def __init__( self, url: str, - token: str, - rest_encoding: Optional[Literal["base64"]] = "base64", - rest_retries: int = 1, - rest_retry_interval: float = 3, # Seconds. - allow_telemetry: bool = True, - headers: Optional[Dict[str, str]] = None, - session: Optional[Session] = None, - multi_exec: Literal["multi-exec", "pipeline"] = "pipeline", - set_sync_token_header_fn: Optional[Callable[[Dict[str, str]], None]] = None, - sync_token_cb: Optional[Callable[[str], None]] = None, + headers: Dict[str, str], + http: SyncHttpClient, + multi_exec: Literal["multi-exec", "pipeline"], + set_sync_token_header_fn: Callable[[Dict[str, str]], None], ): - """ - Creates a new blocking Redis client. - - :param url: UPSTASH_REDIS_REST_URL in the console - :param token: UPSTASH_REDIS_REST_TOKEN in the console - :param rest_encoding: the encoding that can be used by the REST API to parse the response before sending it - :param rest_retries: how many times an HTTP request will be retried if it fails - :param rest_retry_interval: how many seconds will be waited between each retry - :param allow_telemetry: whether anonymous telemetry can be collected - :param headers: request headers - :param session: A Requests session - :param multiexec: Whether multi execution (transaction) or pipelining is to be used - :param set_sync_token_header_fn: Function to set the Upstash-Sync-Token header - :param sync_token_cb: Function to call when a new Upstash-Sync-Token response is received - """ - - self._url = url - self._token = token + self._url = f"{url}/{multi_exec}" + self._headers = headers + self._http = http self._json = PipelineJsonCommands(self) - - self._allow_telemetry = allow_telemetry - - self._rest_encoding: Optional[Literal["base64"]] = rest_encoding - self._rest_retries = rest_retries - self._rest_retry_interval = rest_retry_interval - - self._headers = headers or make_headers(token, rest_encoding, allow_telemetry) - self._session = session or Session() - self._command_stack: List[List[str]] = [] - self._multi_exec = multi_exec self._set_sync_token_header_fn = set_sync_token_header_fn - self._sync_token_cb = sync_token_cb @property def json(self) -> PipelineJsonCommands: @@ -238,7 +189,7 @@ def json(self) -> PipelineJsonCommands: def execute(self, command: List) -> "Pipeline": """ - Adds commnd to the command stack which will be sent as a batch + Adds command to the command stack which will be sent as a batch later :param command: Command to execute @@ -250,21 +201,13 @@ def exec(self) -> List[RESTResultT]: """ Executes the commands in the pipeline by sending them as a batch """ - url = f"{self._url}/{self._multi_exec}" + self._set_sync_token_header_fn(self._headers) - if self._set_sync_token_header_fn: - self._set_sync_token_header_fn(self._headers) - - res: List[RESTResultT] = sync_execute( # type: ignore[assignment] - session=self._session, - url=url, + res: List[RESTResultT] = self._http.execute( # type: ignore[assignment] + url=self._url, headers=self._headers, - encoding=self._rest_encoding, - retries=self._rest_retries, - retry_interval=self._rest_retry_interval, command=self._command_stack, from_pipeline=True, - sync_token_cb=self._sync_token_cb, ) response = [ cast_response(command, response) diff --git a/upstash_redis/commands.py b/upstash_redis/commands.py index ef827e7..28e28a0 100644 --- a/upstash_redis/commands.py +++ b/upstash_redis/commands.py @@ -639,9 +639,9 @@ def renamenx(self, key: str, newkey: str) -> ResponseT: redis.set("key2", "World") # Rename failed because "key2" already exists. - assert redis.renamenx("key1", "key2") == False + assert redis.renamenx("key1", "key2") is False - assert redis.renamenx("key1", "key3") == True + assert redis.renamenx("key1", "key3") is True assert redis.get("key1") is None assert redis.get("key2") == "World" @@ -1559,8 +1559,8 @@ def hsetnx(self, key: str, field: str, value: ValueT) -> ResponseT: Example: ```python - assert redis.hsetnx("myhash", "field1", "Hello") == True - assert redis.hsetnx("myhash", "field1", "World") == False + assert redis.hsetnx("myhash", "field1", "Hello") is True + assert redis.hsetnx("myhash", "field1", "World") is False ``` See https://redis.io/commands/hsetnx @@ -1926,11 +1926,11 @@ def lset(self, key: str, index: int, element: ValueT) -> ResponseT: ```python redis.rpush("mylist", "one", "two", "three") - assert redis.lset("mylist", 1, "Hello") == True + assert redis.lset("mylist", 1, "Hello") is True assert redis.lrange("mylist", 0, -1) == ["one", "Hello", "three"] - assert redis.lset("mylist", 5, "Hello") == False + assert redis.lset("mylist", 5, "Hello") is False assert redis.lrange("mylist", 0, -1) == ["one", "Hello", "three"] ``` @@ -1956,7 +1956,7 @@ def ltrim(self, key: str, start: int, stop: int) -> ResponseT: ```python redis.rpush("mylist", "one", "two", "three") - assert redis.ltrim("mylist", 0, 1) == True + assert redis.ltrim("mylist", 0, 1) is True assert redis.lrange("mylist", 0, -1) == ["one", "two"] ``` @@ -2431,7 +2431,7 @@ def smove(self, source: str, destination: str, member: ValueT) -> ResponseT: redis.sadd("dest", "four") - assert redis.smove("src", "dest", "three") == True + assert redis.smove("src", "dest", "three") is True assert redis.smembers("source") == {"one", "two"} @@ -3269,7 +3269,7 @@ def zrank(self, key: str, member: str) -> ResponseT: assert redis.zrank("myset", "a") == 0 - assert redis.zrank("myset", "d") == None + assert redis.zrank("myset", "d") is None assert redis.zrank("myset", "b") == 1 @@ -3703,7 +3703,7 @@ def getdel(self, key: str) -> ResponseT: assert redis.getdel("key") == "value" - assert redis.get("key") == None + assert redis.get("key") is None ``` See https://redis.io/commands/getdel @@ -4003,15 +4003,15 @@ def set( Example: ```python - assert redis.set("key", "value") == True + assert redis.set("key", "value") is True assert redis.get("key") == "value" # Only set the key if it does not already exist. - assert redis.set("key", "value", nx=True) == False + assert redis.set("key", "value", nx=True) is False # Only set the key if it already exists. - assert redis.set("key", "value", xx=True) == True + assert redis.set("key", "value", xx=True) is True # Get the old value stored at the key. assert redis.set("key", "new-value", get=True) == "value" @@ -4089,10 +4089,10 @@ def setnx(self, key: str, value: ValueT) -> ResponseT: Example: ```python # The key does not exist, so it will be set. - assert redis.setnx("key", "value") == True + assert redis.setnx("key", "value") is True # The key already exists, so it will not be set. - assert redis.setnx("key", "value") == False + assert redis.setnx("key", "value") is False ``` See https://redis.io/commands/setnx @@ -4234,7 +4234,7 @@ class JsonCommands: def __init__(self, client: Commands): self.client = client - def arrappend(self, key: str, path: str = '$', *value: JSONValueT) -> ResponseT: + def arrappend(self, key: str, path: str = "$", *value: JSONValueT) -> ResponseT: """ Appends one or more values to a JSON array stored at a key. @@ -4249,7 +4249,9 @@ def arrappend(self, key: str, path: str = '$', *value: JSONValueT) -> ResponseT: return self.client.execute(command=command) - def arrindex(self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0) -> ResponseT: + def arrindex( + self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 + ) -> ResponseT: """ Returns the index of the first occurrence of a value in a JSON array. @@ -4264,7 +4266,9 @@ def arrindex(self, key: str, path: str, value: JSONValueT, start: int = 0, stop: return self.client.execute(command=command) - def arrinsert(self, key: str, path: str, index: int, *value: JSONValueT) -> ResponseT: + def arrinsert( + self, key: str, path: str, index: int, *value: JSONValueT + ) -> ResponseT: """ Inserts one or more values to a JSON array stored at a key at a specified index. @@ -4279,7 +4283,7 @@ def arrinsert(self, key: str, path: str, index: int, *value: JSONValueT) -> Resp return self.client.execute(command=command) - def arrlen(self, key: str, path: str = '$') -> ResponseT: + def arrlen(self, key: str, path: str = "$") -> ResponseT: """ Returns the length of a JSON array stored at a key. @@ -4289,7 +4293,7 @@ def arrlen(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def arrpop(self, key: str, path: str = '$', index: int = -1) -> ResponseT: + def arrpop(self, key: str, path: str = "$", index: int = -1) -> ResponseT: """ Removes and returns the element at the specified index from a JSON array stored at a key. @@ -4313,7 +4317,7 @@ def arrtrim(self, key: str, path: str, start: int, stop: int) -> ResponseT: return self.client.execute(command=command) - def clear(self, key: str, path: str = '$') -> ResponseT: + def clear(self, key: str, path: str = "$") -> ResponseT: """ Sets the value at a specified path in a JSON document stored at a key to default value of the type. @@ -4323,7 +4327,7 @@ def clear(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def delete(self, key: str, path: str = '$') -> ResponseT: + def delete(self, key: str, path: str = "$") -> ResponseT: """ Removes the value at a specified path in a JSON document stored at a key. @@ -4333,7 +4337,7 @@ def delete(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def forget(self, key: str, path: str = '$') -> ResponseT: + def forget(self, key: str, path: str = "$") -> ResponseT: """ Removes the value at a specified path in a JSON document stored at a key. @@ -4354,9 +4358,10 @@ def get(self, key: str, *path: str) -> ResponseT: if len(path) > 0: command.extend(path) else: - command.append('$') + command.append("$") return self.client.execute(command=command) + def merge(self, key: str, path: str, value: JSONValueT) -> ResponseT: """ Merges the value at a specified path in a JSON document stored at a key. @@ -4380,7 +4385,9 @@ def mget(self, keys: List[str], path: str) -> ResponseT: return self.client.execute(command=command) - def mset(self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]]) -> ResponseT: + def mset( + self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] + ) -> ResponseT: """ Sets the values at specified paths in multiple JSON documents stored at multiple keys. @@ -4419,7 +4426,7 @@ def nummultby(self, key: str, path: str, multiply: int) -> ResponseT: return self.client.execute(command=command) - def objkeys(self, key: str, path: str = '$') -> ResponseT: + def objkeys(self, key: str, path: str = "$") -> ResponseT: """ Returns the object keys in the object at a specified path in a JSON document stored at a key. @@ -4429,7 +4436,7 @@ def objkeys(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def objlen(self, key: str, path: str = '$') -> ResponseT: + def objlen(self, key: str, path: str = "$") -> ResponseT: """ Returns the number of keys in the object at a specified path in a JSON document stored at a key. @@ -4439,7 +4446,7 @@ def objlen(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def resp(self, key: str, path: str = '$') -> ResponseT: + def resp(self, key: str, path: str = "$") -> ResponseT: """ Returns the value at a specified path in redis serialization protocol format. @@ -4449,7 +4456,14 @@ def resp(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def set(self, key: str, path: str, value: JSONValueT, nx: Optional[bool] = None, xx: Optional[bool] = None) -> ResponseT: + def set( + self, + key: str, + path: str, + value: JSONValueT, + nx: Optional[bool] = None, + xx: Optional[bool] = None, + ) -> ResponseT: """ Sets the value at a specified path in a JSON document stored at a key. @@ -4477,7 +4491,7 @@ def strappend(self, key: str, path: str, value: str) -> ResponseT: return self.client.execute(command=command) - def strlen(self, key: str, path: str = '$') -> ResponseT: + def strlen(self, key: str, path: str = "$") -> ResponseT: """ Returns the length of the string value at a specified path in a JSON document stored at a key. @@ -4487,7 +4501,7 @@ def strlen(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def toggle(self, key: str, path: str = '$') -> ResponseT: + def toggle(self, key: str, path: str = "$") -> ResponseT: """ Toggles a boolean value at a specified path in a JSON document stored at a key. @@ -4497,7 +4511,7 @@ def toggle(self, key: str, path: str = '$') -> ResponseT: return self.client.execute(command=command) - def type(self, key: str, path: str = '$') -> ResponseT: + def type(self, key: str, path: str = "$") -> ResponseT: """ Returns the type of the value at a specified path in a JSON document stored at a key. diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index 19d1bd3..f8489f5 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -1500,79 +1500,164 @@ class PipelineCommands: def script_load(self, script: str) -> PipelineCommands: ... class JsonCommands: - def __init__(self, client: Commands):... - def arrappend(self, key: str, path: str = '$', *value: JSONValueT) -> List[Union[int, None]]:... - def arrindex(self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0) -> List[Union[int, None]]:... - def arrinsert(self, key: str, path: str, index: int, *value: JSONValueT) -> List[Union[int, None]]:... - def arrlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - def arrpop(self, key: str, path: str = '$', index: int = -1) -> List[Union[JSONValueT, None]]:... - def arrtrim(self, key: str, path: str = '$', start: int = 0, stop: int = 0) -> List[Union[int, None]]:... - def clear(self, key: str, path: str = '$') -> int:... - def delete(self, key: str, path: str = '$') -> int:... - def forget(self, key: str, path: str = '$') -> int:... - def get(self, key: str, path: Union[str, List[str]] = '$') -> Union[List[Union[JSONValueT, None]], JSONValueT]:... - def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]:... - def mget(self, keys: List[str], path: str) -> List[Union[JSONValueT, None]]:... - def mset(self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]]) -> Literal[True]:... - def numincrby(self, key: str, path: str, increment: int) -> List[Union[int, None]]:... - def nummultby(self, key: str, path: str, multiply: int) -> List[Union[int, None]]:... - def objkeys(self, key: str, path: str = '$') -> List[Union[List[str], None]]:... - def objlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - def resp(self, key: str, path: str = '$') -> List[List[JSONValueT]]:... - def set(self, key: str, path: str, value: JSONValueT, nx: Optional[bool] = None, xx: Optional[bool] = None) -> Literal[True]:... - def strappend(self, key: str, path: str, value: str) -> List[Union[int, None]]:... - def strlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - def toggle(self, key: str, path: str = '$') -> List[Union[Literal[0, 1], None]]:... - def type(self, key: str, path: str = '$') -> List[Literal["string", "integer", "boolean", "object", "array", "number", "null"]]:... + def __init__(self, client: Commands): ... + def arrappend( + self, key: str, path: str = "$", *value: JSONValueT + ) -> List[Union[int, None]]: ... + def arrindex( + self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 + ) -> List[Union[int, None]]: ... + def arrinsert( + self, key: str, path: str, index: int, *value: JSONValueT + ) -> List[Union[int, None]]: ... + def arrlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + def arrpop( + self, key: str, path: str = "$", index: int = -1 + ) -> List[Union[JSONValueT, None]]: ... + def arrtrim( + self, key: str, path: str = "$", start: int = 0, stop: int = 0 + ) -> List[Union[int, None]]: ... + def clear(self, key: str, path: str = "$") -> int: ... + def delete(self, key: str, path: str = "$") -> int: ... + def forget(self, key: str, path: str = "$") -> int: ... + def get( + self, key: str, path: Union[str, List[str]] = "$" + ) -> Union[List[Union[JSONValueT, None]], JSONValueT]: ... + def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... + def mget(self, keys: List[str], path: str) -> List[Union[JSONValueT, None]]: ... + def mset( + self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] + ) -> Literal[True]: ... + def numincrby( + self, key: str, path: str, increment: int + ) -> List[Union[int, None]]: ... + def nummultby( + self, key: str, path: str, multiply: int + ) -> List[Union[int, None]]: ... + def objkeys(self, key: str, path: str = "$") -> List[Union[List[str], None]]: ... + def objlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + def resp(self, key: str, path: str = "$") -> List[List[JSONValueT]]: ... + def set( + self, + key: str, + path: str, + value: JSONValueT, + nx: Optional[bool] = None, + xx: Optional[bool] = None, + ) -> Literal[True]: ... + def strappend(self, key: str, path: str, value: str) -> List[Union[int, None]]: ... + def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + def toggle(self, key: str, path: str = "$") -> List[Union[Literal[0, 1], None]]: ... + def type( + self, key: str, path: str = "$" + ) -> List[ + Literal["string", "integer", "boolean", "object", "array", "number", "null"] + ]: ... class PipelineJsonCommands: - def __init__(self, client: PipelineCommands):... - def arrappend(self, key: str, path: str = '$', *value: JSONValueT) -> PipelineCommands:... - def arrindex(self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0) -> PipelineCommands:... - def arrinsert(self, key: str, path: str, index: int, *value: JSONValueT) -> PipelineCommands:... - def arrlen(self, key: str, path: str = '$') -> PipelineCommands:... - def arrpop(self, key: str, path: str = '$', index: int = -1) -> PipelineCommands:... - def arrtrim(self, key: str, path: str = '$', start: int = 0, stop: int = 0) -> PipelineCommands:... - def clear(self, key: str, path: str = '$') -> PipelineCommands:... - def delete(self, key: str, path: str = '$') -> PipelineCommands:... - def forget(self, key: str, path: str = '$') -> PipelineCommands:... - def get(self, key: str, path: Union[str, List[str]] = '$') -> PipelineCommands:... - def merge(self, key: str, path: str, value: JSONValueT) -> PipelineCommands:... - def mget(self, keys: List[str], path: str) -> PipelineCommands:... - def mset(self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]]) -> PipelineCommands:... - def numincrby(self, key: str, path: str, increment: int) -> PipelineCommands:... - def nummultby(self, key: str, path: str, multiply: int) -> PipelineCommands:... - def objkeys(self, key: str, path: str = '$') -> PipelineCommands:... - def objlen(self, key: str, path: str = '$') -> PipelineCommands:... - def resp(self, key: str, path: str = '$') -> PipelineCommands:... - def set(self, key: str, path: str, value: JSONValueT, nx: Optional[bool] = None, xx: Optional[bool] = None) -> PipelineCommands:... - def strappend(self, key: str, path: str, value: str) -> PipelineCommands:... - def strlen(self, key: str, path: str = '$') -> PipelineCommands:... - def toggle(self, key: str, path: str = '$') -> PipelineCommands:... - def type(self, key: str, path: str = '$') -> PipelineCommands:... + def __init__(self, client: PipelineCommands): ... + def arrappend( + self, key: str, path: str = "$", *value: JSONValueT + ) -> PipelineCommands: ... + def arrindex( + self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 + ) -> PipelineCommands: ... + def arrinsert( + self, key: str, path: str, index: int, *value: JSONValueT + ) -> PipelineCommands: ... + def arrlen(self, key: str, path: str = "$") -> PipelineCommands: ... + def arrpop( + self, key: str, path: str = "$", index: int = -1 + ) -> PipelineCommands: ... + def arrtrim( + self, key: str, path: str = "$", start: int = 0, stop: int = 0 + ) -> PipelineCommands: ... + def clear(self, key: str, path: str = "$") -> PipelineCommands: ... + def delete(self, key: str, path: str = "$") -> PipelineCommands: ... + def forget(self, key: str, path: str = "$") -> PipelineCommands: ... + def get(self, key: str, path: Union[str, List[str]] = "$") -> PipelineCommands: ... + def merge(self, key: str, path: str, value: JSONValueT) -> PipelineCommands: ... + def mget(self, keys: List[str], path: str) -> PipelineCommands: ... + def mset( + self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] + ) -> PipelineCommands: ... + def numincrby(self, key: str, path: str, increment: int) -> PipelineCommands: ... + def nummultby(self, key: str, path: str, multiply: int) -> PipelineCommands: ... + def objkeys(self, key: str, path: str = "$") -> PipelineCommands: ... + def objlen(self, key: str, path: str = "$") -> PipelineCommands: ... + def resp(self, key: str, path: str = "$") -> PipelineCommands: ... + def set( + self, + key: str, + path: str, + value: JSONValueT, + nx: Optional[bool] = None, + xx: Optional[bool] = None, + ) -> PipelineCommands: ... + def strappend(self, key: str, path: str, value: str) -> PipelineCommands: ... + def strlen(self, key: str, path: str = "$") -> PipelineCommands: ... + def toggle(self, key: str, path: str = "$") -> PipelineCommands: ... + def type(self, key: str, path: str = "$") -> PipelineCommands: ... class AsyncJsonCommands: - def __init__(self, client: AsyncCommands):... - async def arrappend(self, key: str, path: str = '$', *value: JSONValueT) -> List[Union[int, None]]:... - async def arrindex(self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0) -> List[Union[int, None]]:... - async def arrinsert(self, key: str, path: str, index: int, *value: JSONValueT) -> List[Union[int, None]]:... - async def arrlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - async def arrpop(self, key: str, path: str = '$', index: int = -1) -> List[Union[JSONValueT, None]]:... - async def arrtrim(self, key: str, path: str = '$', start: int = 0, stop: int = 0) -> List[Union[int, None]]:... - async def clear(self, key: str, path: str = '$') -> int:... - async def delete(self, key: str, path: str = '$') -> int:... - async def forget(self, key: str, path: str = '$') -> int:... - async def get(self, key: str, path: Union[str, List[str]] = '$') -> Union[List[Union[JSONValueT, None]], JSONValueT]:... - async def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]:... - async def mget(self, keys: List[str], path: str) -> List[Union[JSONValueT, None]]:... - async def mset(self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]]) -> Literal[True]:... - async def numincrby(self, key: str, path: str, increment: int) -> List[Union[int, None]]:... - async def nummultby(self, key: str, path: str, multiply: int) -> List[Union[int, None]]:... - async def objkeys(self, key: str, path: str = '$') -> List[Union[List[str], None]]:... - async def objlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - async def resp(self, key: str, path: str = '$') -> List[List[JSONValueT]]:... - async def set(self, key: str, path: str, value: JSONValueT, nx: Optional[bool] = None, xx: Optional[bool] = None) -> Literal[True]:... - async def strappend(self, key: str, path: str, value: str) -> List[Union[int, None]]:... - async def strlen(self, key: str, path: str = '$') -> List[Union[int, None]]:... - async def toggle(self, key: str, path: str = '$') -> List[Union[Literal[0, 1], None]]:... - async def type(self, key: str, path: str = '$') -> List[Literal["string", "integer", "boolean", "object", "array", "number", "null"]]:... \ No newline at end of file + def __init__(self, client: AsyncCommands): ... + async def arrappend( + self, key: str, path: str = "$", *value: JSONValueT + ) -> List[Union[int, None]]: ... + async def arrindex( + self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 + ) -> List[Union[int, None]]: ... + async def arrinsert( + self, key: str, path: str, index: int, *value: JSONValueT + ) -> List[Union[int, None]]: ... + async def arrlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + async def arrpop( + self, key: str, path: str = "$", index: int = -1 + ) -> List[Union[JSONValueT, None]]: ... + async def arrtrim( + self, key: str, path: str = "$", start: int = 0, stop: int = 0 + ) -> List[Union[int, None]]: ... + async def clear(self, key: str, path: str = "$") -> int: ... + async def delete(self, key: str, path: str = "$") -> int: ... + async def forget(self, key: str, path: str = "$") -> int: ... + async def get( + self, key: str, path: Union[str, List[str]] = "$" + ) -> Union[List[Union[JSONValueT, None]], JSONValueT]: ... + async def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... + async def mget( + self, keys: List[str], path: str + ) -> List[Union[JSONValueT, None]]: ... + async def mset( + self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] + ) -> Literal[True]: ... + async def numincrby( + self, key: str, path: str, increment: int + ) -> List[Union[int, None]]: ... + async def nummultby( + self, key: str, path: str, multiply: int + ) -> List[Union[int, None]]: ... + async def objkeys( + self, key: str, path: str = "$" + ) -> List[Union[List[str], None]]: ... + async def objlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + async def resp(self, key: str, path: str = "$") -> List[List[JSONValueT]]: ... + async def set( + self, + key: str, + path: str, + value: JSONValueT, + nx: Optional[bool] = None, + xx: Optional[bool] = None, + ) -> Literal[True]: ... + async def strappend( + self, key: str, path: str, value: str + ) -> List[Union[int, None]]: ... + async def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... + async def toggle( + self, key: str, path: str = "$" + ) -> List[Union[Literal[0, 1], None]]: ... + async def type( + self, key: str, path: str = "$" + ) -> List[ + Literal["string", "integer", "boolean", "object", "array", "number", "null"] + ]: ... diff --git a/upstash_redis/http.py b/upstash_redis/http.py index 5d70aa1..2bc0c20 100644 --- a/upstash_redis/http.py +++ b/upstash_redis/http.py @@ -1,13 +1,12 @@ +import asyncio import os import time -from asyncio import sleep from base64 import b64decode from json import dumps from platform import python_version -from typing import Any, Dict, List, Literal, Optional, Union, Callable +from typing import Any, Dict, List, Literal, Optional, Union, Callable, Type -from aiohttp import ClientSession -from requests import Session +import httpx from upstash_redis import __version__ from upstash_redis.errors import UpstashError @@ -16,7 +15,7 @@ def make_headers( token: str, encoding: Optional[Literal["base64"]], allow_telemetry: bool -) -> Dict[str, str]: +) -> Dict[str, Any]: headers = { "Authorization": f"Bearer {token}", } @@ -40,105 +39,145 @@ def make_headers( return headers -async def async_execute( - session: ClientSession, - url: str, - headers: Dict[str, str], - encoding: Optional[Literal["base64"]], - retries: int, - retry_interval: float, - command: List, - from_pipeline: bool = False, - sync_token_cb: Optional[Callable[[str], None]] = None, -) -> Union[RESTResultT, List[RESTResultT]]: - """ - Execute the given command over the REST API. +class SyncHttpClient: + def __init__( + self, + encoding: Optional[Literal["base64"]], + retries: int, + retry_interval: float, + sync_token_cb: Optional[Callable[[str], None]] = None, + ): + self._encoding = encoding + self._retries = retries + self._retry_interval = retry_interval + self._sync_token_cb = sync_token_cb + self._client = httpx.Client(timeout=None) + + def execute( + self, + url: str, + headers: Dict[str, str], + command: List, + from_pipeline: bool = False, + ) -> Union[RESTResultT, List[RESTResultT]]: + command = _format_command(command, from_pipeline=from_pipeline) + + response: Optional[Dict[str, Any]] = None + last_error: Optional[Exception] = None + + for attempts_left in range(max(0, self._retries), -1, -1): + try: + r = self._client.post(url, headers=headers, json=command) + sync_token = r.headers.get("Upstash-Sync-Token") + if self._sync_token_cb and sync_token: + self._sync_token_cb(sync_token) - :param encoding: the encoding that can be used by the REST API to parse the response before sending it - :param retries: how many times an HTTP request will be retried if it fails - :param retry_interval: how many seconds will be waited between each retry - :param allow_telemetry: whether anonymous telemetry can be collected - :param sync_token_cb: This callback is called with the new Upstash Sync Token after each request to update the client's token - """ + response = r.json() - # Serialize the command; more specifically, write string-incompatible types as JSON strings. - command = _format_command(command, from_pipeline=from_pipeline) + break # Break the loop as soon as we receive a proper response + except Exception as e: + last_error = e + + if attempts_left > 0: + time.sleep(self._retry_interval) - response: Optional[Union[Dict, List[Dict]]] = None - last_error: Optional[Exception] = None + if response is None: + assert last_error is not None - for attempts_left in range(max(0, retries), -1, -1): - try: - async with session.post(url, headers=headers, json=command) as r: + # Exhausted all retries, but no response is received + raise last_error + if not from_pipeline: + return format_response(response, self._encoding) + else: + return [ + format_response(sub_response, self._encoding) # type: ignore[arg-type] + for sub_response in response + ] + + def close(self): + self._client.close() + + def __enter__(self) -> "SyncHttpClient": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Any, + ) -> None: + self.close() + + +class AsyncHttpClient: + def __init__( + self, + encoding: Optional[Literal["base64"]], + retries: int, + retry_interval: float, + sync_token_cb: Optional[Callable[[str], None]] = None, + ): + self._encoding = encoding + self._retries = retries + self._retry_interval = retry_interval + self._sync_token_cb = sync_token_cb + self._client = httpx.AsyncClient(timeout=None) + + async def execute( + self, + url: str, + headers: Dict[str, str], + command: List, + from_pipeline: bool = False, + ) -> Union[RESTResultT, List[RESTResultT]]: + command = _format_command(command, from_pipeline=from_pipeline) + + response: Optional[Union[Dict, List[Dict]]] = None + last_error: Optional[Exception] = None + + for attempts_left in range(max(0, self._retries), -1, -1): + try: + r = await self._client.post(url, headers=headers, json=command) sync_token = r.headers.get("Upstash-Sync-Token") - if sync_token_cb and sync_token: - sync_token_cb(sync_token) + if self._sync_token_cb and sync_token: + self._sync_token_cb(sync_token) - response = await r.json() + response = r.json() break # Break the loop as soon as we receive a proper response - except Exception as e: - last_error = e + except Exception as e: + last_error = e - if attempts_left > 0: - await sleep(retry_interval) + if attempts_left > 0: + await asyncio.sleep(self._retry_interval) - if response is None: - assert last_error is not None + if response is None: + assert last_error is not None - # Exhausted all retries, but no response is received - raise last_error + # Exhausted all retries, but no response is received + raise last_error - if not from_pipeline: - return format_response(response, encoding) # type: ignore[arg-type] - else: - return [format_response(sub_response, encoding) for sub_response in response] - - -def sync_execute( - session: Session, - url: str, - headers: Dict[str, str], - encoding: Optional[Literal["base64"]], - retries: int, - retry_interval: float, - command: List[Any], - from_pipeline: bool = False, - sync_token_cb: Optional[Callable[[str], None]] = None, -) -> Union[RESTResultT, List[RESTResultT]]: - command = _format_command(command, from_pipeline=from_pipeline) - - response: Optional[Dict[str, Any]] = None - last_error: Optional[Exception] = None - - for attempts_left in range(max(0, retries), -1, -1): - try: - r = session.post(url, headers=headers, json=command) - sync_token = r.headers.get("Upstash-Sync-Token") - if sync_token_cb and sync_token: - sync_token_cb(sync_token) - - response = r.json() - - break # Break the loop as soon as we receive a proper response - except Exception as e: - last_error = e - - if attempts_left > 0: - time.sleep(retry_interval) - - if response is None: - assert last_error is not None - - # Exhausted all retries, but no response is received - raise last_error - if not from_pipeline: - return format_response(response, encoding) - else: - return [ - format_response(sub_response, encoding) # type: ignore[arg-type] - for sub_response in response - ] + if not from_pipeline: + return format_response(response, self._encoding) # type: ignore[arg-type] + else: + return [ + format_response(sub_response, self._encoding) + for sub_response in response + ] + + async def close(self): + await self._client.aclose() + + async def __aenter__(self) -> "AsyncHttpClient": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Any, + ) -> None: + await self.close() def format_response( From a2f8e60f576254a9e1ee04da2e9207951c99674f Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:22:04 +0300 Subject: [PATCH 2/7] fix type hints in json commands and do some minor refactors --- upstash_redis/commands.py | 47 +++++++++++++++++++++++--------------- upstash_redis/commands.pyi | 24 +++++++++---------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/upstash_redis/commands.py b/upstash_redis/commands.py index 28e28a0..4745060 100644 --- a/upstash_redis/commands.py +++ b/upstash_redis/commands.py @@ -1,5 +1,4 @@ import datetime -import json from typing import Any, Awaitable, Dict, List, Literal, Mapping, Optional, Tuple, Union from upstash_redis.typing import FloatMinMaxT, ValueT, JSONValueT @@ -4234,7 +4233,7 @@ class JsonCommands: def __init__(self, client: Commands): self.client = client - def arrappend(self, key: str, path: str = "$", *value: JSONValueT) -> ResponseT: + def arrappend(self, key: str, path: str = "$", *values: JSONValueT) -> ResponseT: """ Appends one or more values to a JSON array stored at a key. @@ -4242,10 +4241,12 @@ def arrappend(self, key: str, path: str = "$", *value: JSONValueT) -> ResponseT: See https://redis.io/commands/json.arrappend """ - - value = [f'"{i}"' if type(i) is str else i for i in value] - - command: List = ["JSON.ARRAPPEND", key, path, *value] + command: List = ["JSON.ARRAPPEND", key, path] + for value in values: + if isinstance(value, str): + command.append(f'"{value}"') + else: + command.append(value) return self.client.execute(command=command) @@ -4259,7 +4260,7 @@ def arrindex( See https://redis.io/commands/json.arrindex """ - if type(value) is str: + if isinstance(value, str): value = f'"{value}"' command: List = ["JSON.ARRINDEX", key, path, value, start, stop] @@ -4267,7 +4268,7 @@ def arrindex( return self.client.execute(command=command) def arrinsert( - self, key: str, path: str, index: int, *value: JSONValueT + self, key: str, path: str, index: int, *values: JSONValueT ) -> ResponseT: """ Inserts one or more values to a JSON array stored at a key at a specified index. @@ -4276,10 +4277,12 @@ def arrinsert( See https://redis.io/commands/json.arrinsert """ - - value = [f'"{i}"' if type(i) is str else i for i in value] - - command: List = ["JSON.ARRINSERT", key, path, index, *value] + command: List = ["JSON.ARRINSERT", key, path, index] + for value in values: + if isinstance(value, str): + command.append(f'"{value}"') + else: + command.append(value) return self.client.execute(command=command) @@ -4347,7 +4350,7 @@ def forget(self, key: str, path: str = "$") -> ResponseT: return self.client.execute(command=command) - def get(self, key: str, *path: str) -> ResponseT: + def get(self, key: str, *paths: str) -> ResponseT: """ Returns the value at a specified path in a JSON document stored at a key. @@ -4355,8 +4358,8 @@ def get(self, key: str, *path: str) -> ResponseT: """ command: List = ["JSON.GET", key] - if len(path) > 0: - command.extend(path) + if len(paths) > 0: + command.extend(paths) else: command.append("$") @@ -4370,7 +4373,10 @@ def merge(self, key: str, path: str, value: JSONValueT) -> ResponseT: See https://redis.io/commands/json.merge """ - command: List = ["JSON.MERGE", key, path, json.dumps(value)] + if isinstance(value, str): + value = f'"{value}"' + + command: List = ["JSON.MERGE", key, path, value] return self.client.execute(command=command) @@ -4398,7 +4404,10 @@ def mset( command = ["JSON.MSET"] for key, path, value in key_path_value_tuples: - command.extend([key, path, json.dumps(value)]) + if isinstance(value, str): + value = f'"{value}"' + + command.extend([key, path, value]) return self.client.execute(command=command) @@ -4471,8 +4480,10 @@ def set( See https://redis.io/commands/json.set """ + if isinstance(value, str): + value = f'"{value}"' - command: List = ["JSON.SET", key, path, json.dumps(value)] + command: List = ["JSON.SET", key, path, value] if nx: command.append("NX") diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index f8489f5..3840588 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -1502,26 +1502,26 @@ class PipelineCommands: class JsonCommands: def __init__(self, client: Commands): ... def arrappend( - self, key: str, path: str = "$", *value: JSONValueT + self, key: str, path: str = "$", *values: JSONValueT ) -> List[Union[int, None]]: ... def arrindex( self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 ) -> List[Union[int, None]]: ... def arrinsert( - self, key: str, path: str, index: int, *value: JSONValueT + self, key: str, path: str, index: int, *values: JSONValueT ) -> List[Union[int, None]]: ... def arrlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... def arrpop( self, key: str, path: str = "$", index: int = -1 ) -> List[Union[JSONValueT, None]]: ... def arrtrim( - self, key: str, path: str = "$", start: int = 0, stop: int = 0 + self, key: str, path: str, start: int, stop: int ) -> List[Union[int, None]]: ... def clear(self, key: str, path: str = "$") -> int: ... def delete(self, key: str, path: str = "$") -> int: ... def forget(self, key: str, path: str = "$") -> int: ... def get( - self, key: str, path: Union[str, List[str]] = "$" + self, key: str, *paths: str ) -> Union[List[Union[JSONValueT, None]], JSONValueT]: ... def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... def mget(self, keys: List[str], path: str) -> List[Union[JSONValueT, None]]: ... @@ -1557,25 +1557,25 @@ class JsonCommands: class PipelineJsonCommands: def __init__(self, client: PipelineCommands): ... def arrappend( - self, key: str, path: str = "$", *value: JSONValueT + self, key: str, path: str = "$", *values: JSONValueT ) -> PipelineCommands: ... def arrindex( self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 ) -> PipelineCommands: ... def arrinsert( - self, key: str, path: str, index: int, *value: JSONValueT + self, key: str, path: str, index: int, *values: JSONValueT ) -> PipelineCommands: ... def arrlen(self, key: str, path: str = "$") -> PipelineCommands: ... def arrpop( self, key: str, path: str = "$", index: int = -1 ) -> PipelineCommands: ... def arrtrim( - self, key: str, path: str = "$", start: int = 0, stop: int = 0 + self, key: str, path: str, start: int, stop: int ) -> PipelineCommands: ... def clear(self, key: str, path: str = "$") -> PipelineCommands: ... def delete(self, key: str, path: str = "$") -> PipelineCommands: ... def forget(self, key: str, path: str = "$") -> PipelineCommands: ... - def get(self, key: str, path: Union[str, List[str]] = "$") -> PipelineCommands: ... + def get(self, key: str, *paths: str) -> PipelineCommands: ... def merge(self, key: str, path: str, value: JSONValueT) -> PipelineCommands: ... def mget(self, keys: List[str], path: str) -> PipelineCommands: ... def mset( @@ -1602,26 +1602,26 @@ class PipelineJsonCommands: class AsyncJsonCommands: def __init__(self, client: AsyncCommands): ... async def arrappend( - self, key: str, path: str = "$", *value: JSONValueT + self, key: str, path: str = "$", *values: JSONValueT ) -> List[Union[int, None]]: ... async def arrindex( self, key: str, path: str, value: JSONValueT, start: int = 0, stop: int = 0 ) -> List[Union[int, None]]: ... async def arrinsert( - self, key: str, path: str, index: int, *value: JSONValueT + self, key: str, path: str, index: int, *values: JSONValueT ) -> List[Union[int, None]]: ... async def arrlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... async def arrpop( self, key: str, path: str = "$", index: int = -1 ) -> List[Union[JSONValueT, None]]: ... async def arrtrim( - self, key: str, path: str = "$", start: int = 0, stop: int = 0 + self, key: str, path: str, start: int, stop: int ) -> List[Union[int, None]]: ... async def clear(self, key: str, path: str = "$") -> int: ... async def delete(self, key: str, path: str = "$") -> int: ... async def forget(self, key: str, path: str = "$") -> int: ... async def get( - self, key: str, path: Union[str, List[str]] = "$" + self, key: str, *paths: str ) -> Union[List[Union[JSONValueT, None]], JSONValueT]: ... async def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... async def mget( From a892c85318cac462796811f11df20ed347fea2b3 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:09:22 +0300 Subject: [PATCH 3/7] make json.toggle return boolean list --- tests/commands/json/test_json_toggle.py | 9 +++++---- tests/test_formatters.py | 15 ++++++++++----- upstash_redis/commands.pyi | 6 ++---- upstash_redis/format.py | 17 +++++++---------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/commands/json/test_json_toggle.py b/tests/commands/json/test_json_toggle.py index 7f7de5e..d3608f2 100644 --- a/tests/commands/json/test_json_toggle.py +++ b/tests/commands/json/test_json_toggle.py @@ -1,4 +1,5 @@ import pytest + from upstash_redis import Redis @@ -14,9 +15,9 @@ def test_toggle(redis: Redis): key = "json_toggle" path = "$.bool" - assert redis.json.toggle(key, path) == [0] + assert redis.json.toggle(key, path) == [False] assert redis.json.get(key, path) == [False] - assert redis.json.toggle(key, path) == [1] + assert redis.json.toggle(key, path) == [True] assert redis.json.get(key, path) == [True] @@ -31,7 +32,7 @@ def test_toggle_wildcard(redis: Redis): key = "json_toggle" path = "$..bool" - assert redis.json.toggle(key, path) == [1, 0] + assert redis.json.toggle(key, path) == [True, False] assert redis.json.get(key, path) == [True, False] - assert redis.json.toggle(key, path) == [0, 1] + assert redis.json.toggle(key, path) == [False, True] assert redis.json.get(key, path) == [False, True] diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 794f3a4..313e574 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,5 +1,4 @@ from upstash_redis.format import ( - format_bool_list, format_float_list, format_geo_search_result, format_geopos, @@ -7,6 +6,7 @@ list_to_dict, string_to_json, string_list_to_json_list, + list_to_optional_bool_list, ) from upstash_redis.utils import GeoSearchResult @@ -19,10 +19,6 @@ def test_list_to_dict() -> None: } -def test_format_bool_list() -> None: - assert format_bool_list(raw=[1, 0, 1, 1, 0]) == [True, False, True, True, False] - - def test_format_float_list() -> None: assert format_float_list(raw=["1.1", "2.2", None]) == [1.1, 2.2, None] @@ -165,3 +161,12 @@ def test_format_geo_positions() -> None: def test_format_server_time() -> None: assert format_time(raw=["1620752099", "12"]) == (1620752099, 12) + + +def test_list_to_optional_bool_list() -> None: + assert list_to_optional_bool_list([1, 0, None, 1], None) == [ + True, + False, + None, + True, + ] diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index 3840588..035ba97 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -1547,7 +1547,7 @@ class JsonCommands: ) -> Literal[True]: ... def strappend(self, key: str, path: str, value: str) -> List[Union[int, None]]: ... def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... - def toggle(self, key: str, path: str = "$") -> List[Union[Literal[0, 1], None]]: ... + def toggle(self, key: str, path: str = "$") -> List[Union[bool, None]]: ... def type( self, key: str, path: str = "$" ) -> List[ @@ -1653,9 +1653,7 @@ class AsyncJsonCommands: self, key: str, path: str, value: str ) -> List[Union[int, None]]: ... async def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... - async def toggle( - self, key: str, path: str = "$" - ) -> List[Union[Literal[0, 1], None]]: ... + async def toggle(self, key: str, path: str = "$") -> List[Union[bool, None]]: ... async def type( self, key: str, path: str = "$" ) -> List[ diff --git a/upstash_redis/format.py b/upstash_redis/format.py index de36884..d56fb86 100644 --- a/upstash_redis/format.py +++ b/upstash_redis/format.py @@ -1,8 +1,8 @@ -from typing import Callable, Dict, List, Literal, Optional, Tuple, Union from json import loads +from typing import Callable, Dict, List, Optional, Tuple, Union -from upstash_redis.utils import GeoSearchResult from upstash_redis.typing import RESTResultT +from upstash_redis.utils import GeoSearchResult def list_to_dict(raw: List, command=None) -> Dict: @@ -100,14 +100,6 @@ def format_pubsub_numsub_return(raw: List[Union[str, int]]) -> Dict[str, int]: return list_to_dict(raw=raw) -def format_bool_list(raw: List[Literal[0, 1]]) -> List[bool]: - """ - Format a list of boolean integers. - """ - - return [bool(value) for value in raw] - - def format_time(raw: List[str], command=None) -> Tuple[int, int]: return (int(raw[0]), int(raw[1])) @@ -160,6 +152,10 @@ def list_to_bool_list(res, command): return list(map(bool, res)) +def list_to_optional_bool_list(res, commands): + return [bool(value) if value is not None else None for value in res] + + def float_or_none(res, command): if res is None: return None @@ -312,6 +308,7 @@ def zunion_formatter(res, command): "JSON.NUMINCRBY": string_to_json, "JSON.NUMMULTBY": string_to_json, "JSON.SET": ok_to_bool, + "JSON.TOGGLE": list_to_optional_bool_list, "PFADD": to_bool, "PFMERGE": ok_to_bool, "TIME": format_time, From 869239f6e50c6b23e37dedf86c92996dda7d51b5 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:11:06 +0300 Subject: [PATCH 4/7] make the return type of json.type simpler --- upstash_redis/commands.pyi | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index 035ba97..0abe6f5 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -1548,11 +1548,7 @@ class JsonCommands: def strappend(self, key: str, path: str, value: str) -> List[Union[int, None]]: ... def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... def toggle(self, key: str, path: str = "$") -> List[Union[bool, None]]: ... - def type( - self, key: str, path: str = "$" - ) -> List[ - Literal["string", "integer", "boolean", "object", "array", "number", "null"] - ]: ... + def type(self, key: str, path: str = "$") -> List[str]: ... class PipelineJsonCommands: def __init__(self, client: PipelineCommands): ... @@ -1654,8 +1650,4 @@ class AsyncJsonCommands: ) -> List[Union[int, None]]: ... async def strlen(self, key: str, path: str = "$") -> List[Union[int, None]]: ... async def toggle(self, key: str, path: str = "$") -> List[Union[bool, None]]: ... - async def type( - self, key: str, path: str = "$" - ) -> List[ - Literal["string", "integer", "boolean", "object", "array", "number", "null"] - ]: ... + async def type(self, key: str, path: str = "$") -> List[str]: ... From 35bddc6e7e899ec669a820b597614316ce50a9dd Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:42:09 +0300 Subject: [PATCH 5/7] refactor formatters and fix the return type of hscan and sscan --- tests/test_formatters.py | 52 +++++----- upstash_redis/format.py | 203 ++++++++++++--------------------------- 2 files changed, 89 insertions(+), 166 deletions(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 313e574..318fdd7 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,18 +1,18 @@ from upstash_redis.format import ( - format_float_list, - format_geo_search_result, + to_optional_float_list, + format_geo_search_response, format_geopos, format_time, - list_to_dict, + to_dict, string_to_json, - string_list_to_json_list, - list_to_optional_bool_list, + to_json_list, + to_optional_bool_list, ) from upstash_redis.utils import GeoSearchResult def test_list_to_dict() -> None: - assert list_to_dict(raw=["a", "1", "b", "2", "c", 3]) == { + assert to_dict(["a", "1", "b", "2", "c", 3], None) == { "a": "1", "b": "2", "c": 3, @@ -20,23 +20,23 @@ def test_list_to_dict() -> None: def test_format_float_list() -> None: - assert format_float_list(raw=["1.1", "2.2", None]) == [1.1, 2.2, None] + assert to_optional_float_list(["1.1", "2.2", None], None) == [1.1, 2.2, None] def test_string_to_json() -> None: - assert string_to_json(res='{"a": 1, "b": 2}') == {"a": 1, "b": 2} + assert string_to_json('{"a": 1, "b": 2}', None) == {"a": 1, "b": 2} def test_string_list_to_json_list() -> None: - assert string_list_to_json_list(res=['{"a": 1, "b": 2}', '{"c": 3, "d": 4}']) == [ + assert to_json_list(['{"a": 1, "b": 2}', '{"c": 3, "d": 4}'], None) == [ {"a": 1, "b": 2}, {"c": 3, "d": 4}, ] def test_format_geo_members_with_distance_and_hash_and_coordinates() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "2.51", "100", ["3.12", "4.23"]], ["b", "5.6", "200", ["7.1", "8.2"]], ], @@ -62,8 +62,8 @@ def test_format_geo_members_with_distance_and_hash_and_coordinates() -> None: def test_format_geo_members_with_distance() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "2.51"], ["b", "5.6"], ], @@ -77,8 +77,8 @@ def test_format_geo_members_with_distance() -> None: def test_format_geo_members_with_hash() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "100"], ["b", "200"], ], @@ -92,8 +92,8 @@ def test_format_geo_members_with_hash() -> None: def test_format_geo_members_with_coordinates() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", ["3.12", "4.23"]], ["b", ["7.1", "8.2"]], ], @@ -107,8 +107,8 @@ def test_format_geo_members_with_coordinates() -> None: def test_format_geo_members_with_distance_and_hash() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "2.51", "100"], ["b", "5.6", "200"], ], @@ -122,8 +122,8 @@ def test_format_geo_members_with_distance_and_hash() -> None: def test_format_geo_members_with_distance_and_coordinates() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "2.51", ["3.12", "4.23"]], ["b", "5.6", ["7.1", "8.2"]], ], @@ -137,8 +137,8 @@ def test_format_geo_members_with_distance_and_coordinates() -> None: def test_format_geo_members_with_hash_and_coordinates() -> None: - assert format_geo_search_result( - raw=[ + assert format_geo_search_response( + [ ["a", "100", ["3.12", "4.23"]], ["b", "200", ["7.1", "8.2"]], ], @@ -152,7 +152,7 @@ def test_format_geo_members_with_hash_and_coordinates() -> None: def test_format_geo_positions() -> None: - assert format_geopos(raw=[["1.0", "2.5"], ["3.1", "4.2"], None]) == [ + assert format_geopos([["1.0", "2.5"], ["3.1", "4.2"], None], None) == [ (1.0, 2.5), (3.1, 4.2), None, @@ -160,11 +160,11 @@ def test_format_geo_positions() -> None: def test_format_server_time() -> None: - assert format_time(raw=["1620752099", "12"]) == (1620752099, 12) + assert format_time(["1620752099", "12"], None) == (1620752099, 12) def test_list_to_optional_bool_list() -> None: - assert list_to_optional_bool_list([1, 0, None, 1], None) == [ + assert to_optional_bool_list([1, 0, None, 1], None) == [ True, False, None, diff --git a/upstash_redis/format.py b/upstash_redis/format.py index d56fb86..df04501 100644 --- a/upstash_redis/format.py +++ b/upstash_redis/format.py @@ -5,25 +5,25 @@ from upstash_redis.utils import GeoSearchResult -def list_to_dict(raw: List, command=None) -> Dict: +def to_dict(res: List, _) -> Dict: """ Convert a list that contains ungrouped pairs as consecutive elements (usually field-value or similar) into a dict. """ - return {raw[iterator]: raw[iterator + 1] for iterator in range(0, len(raw), 2)} + return {res[i]: res[i + 1] for i in range(0, len(res), 2)} def format_geopos( - raw: List[Optional[List[str]]], command=None + res: List[Optional[List[str]]], _ ) -> List[Union[Tuple[float, float], None]]: return [ (float(member[0]), float(member[1])) if isinstance(member, List) else None - for member in raw + for member in res ] -def format_geo_search_result( - raw: List[List[Union[str, List[str]]]], +def format_geo_search_response( + res: List[List[Union[str, List[str]]]], with_distance: bool, with_hash: bool, with_coordinates: bool, @@ -50,7 +50,7 @@ def format_geo_search_result( # that has a runtime cost to just please the type checker. Until # there is a better way of doing it, I wil just ignore the errors. - for member in raw: + for member in res: result = GeoSearchResult(member[0]) # type: ignore[arg-type] if with_distance: @@ -83,45 +83,28 @@ def format_geo_search_result( return results -def format_hash_return(raw: List[str], command=None) -> Dict[str, str]: - """ - Format the raw output given by Hash commands, usually the ones that return the field-value - pairs of Hashes. - """ - - return list_to_dict(raw=raw) - - -def format_pubsub_numsub_return(raw: List[Union[str, int]]) -> Dict[str, int]: - """ - Format the raw output returned by "PUBSUB NUMSUB". - """ - - return list_to_dict(raw=raw) - - -def format_time(raw: List[str], command=None) -> Tuple[int, int]: - return (int(raw[0]), int(raw[1])) +def format_time(res: List[str], _) -> Tuple[int, int]: + return int(res[0]), int(res[1]) -def format_sorted_set_return(raw: List[str], command=None) -> List[Tuple[str, float]]: +def format_sorted_set_response(res: List[str], _) -> List[Tuple[str, float]]: """ Format the raw output given by Sorted Set commands, usually the ones that return the member-score pairs of Sorted Sets. """ - it = iter(raw) + it = iter(res) return list(zip(it, map(float, it))) -def format_float_list(raw: List[Optional[str]], command=None) -> List[Optional[float]]: +def to_optional_float_list(res: List[Optional[str]], _) -> List[Optional[float]]: """ Format a list of strings representing floats or None values. """ - return [float(value) if value is not None else None for value in raw] + return [float(value) if value is not None else None for value in res] -def set_formatter(res, command): +def format_set(res, command): options = command[3:] if "GET" in options: @@ -129,61 +112,57 @@ def set_formatter(res, command): return res == "OK" -def string_to_json(res, command=None): +def string_to_json(res, _): if res is None: return None return loads(res) -def string_list_to_json_list(res, command=None): +def to_json_list(res, command): return [string_to_json(value, command) for value in res] -def ok_to_bool(res, command): +def ok_to_bool(res, _): return res == "OK" -def to_bool(res, command): +def to_bool(res, _): return bool(res) -def list_to_bool_list(res, command): +def to_bool_list(res, _): return list(map(bool, res)) -def list_to_optional_bool_list(res, commands): +def to_optional_bool_list(res, _): return [bool(value) if value is not None else None for value in res] -def float_or_none(res, command): +def to_optional_float(res, _): if res is None: return None return float(res) -def to_float(res, command): +def to_float(res, _): return float(res) -def format_scan(res, command): - return (int(res[0]), res[1]) +def format_scan(res, _): + return int(res[0]), res[1] -def hscan_formatter(res, command): - return [int(res[0]), format_hash_return(res[1])] +def format_hscan(res, _): + return int(res[0]), to_dict(res[1], None) -def sscan_formatter(res, command): ## same with scan_formatter - return [int(res[0]), res[1]] +def format_zscan(res, _): + return int(res[0]), format_sorted_set_response(res[1], None) -def zscan_formatter(res, command): ## same with scan_formatter - return [int(res[0]), format_sorted_set_return(res[1])] - - -def zscore_formatter(res, command): +def format_zscore(res, _): return float(res) if res is not None else res @@ -192,20 +171,20 @@ def format_search(res, command): withhash = "WITHHASH" in command withcoord = "WITHCOORD" in command if withdist or withhash or withcoord: - return format_geo_search_result(res, withdist, withhash, withcoord) + return format_geo_search_response(res, withdist, withhash, withcoord) return res def format_hrandfield(res, command): - withvalues = "WITHVALUES" in command - if withvalues: - return format_hash_return(res) + with_values = "WITHVALUES" in command + if with_values: + return to_dict(res, command) return res -def zadd_formatter(res, command): +def format_zadd(res, command): incr = "INCR" in command if incr: return float(res) if res is not None else res @@ -213,66 +192,10 @@ def zadd_formatter(res, command): return res -def format_zdiff(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zinter_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zrandmember_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zrange_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zrangebyscore_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zrevrange_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zrevrangebyscore_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) - - return res - - -def zunion_formatter(res, command): - withscores = "WITHSCORES" in command - if withscores: - return format_sorted_set_return(res) +def format_sorted_set_response_with_score(res, command): + with_scores = "WITHSCORES" in command + if with_scores: + return format_sorted_set_response(res, command) return res @@ -287,7 +210,7 @@ def zunion_formatter(res, command): "RENAME": ok_to_bool, "RENAMENX": to_bool, "SCAN": format_scan, - "GEODIST": float_or_none, + "GEODIST": to_optional_float, "GEOPOS": format_geopos, "GEORADIUS": format_search, "GEORADIUS_RO": format_search, @@ -295,48 +218,48 @@ def zunion_formatter(res, command): "GEORADIUSBYMEMBER_RO": format_search, "GEOSEARCH": format_search, "HEXISTS": to_bool, - "HGETALL": format_hash_return, + "HGETALL": to_dict, "HINCRBYFLOAT": to_float, "HRANDFIELD": format_hrandfield, - "HSCAN": hscan_formatter, + "HSCAN": format_hscan, "HSETNX": to_bool, - "JSON.ARRPOP": string_list_to_json_list, + "JSON.ARRPOP": to_json_list, "JSON.GET": string_to_json, "JSON.MERGE": ok_to_bool, - "JSON.MGET": string_list_to_json_list, + "JSON.MGET": to_json_list, "JSON.MSET": ok_to_bool, "JSON.NUMINCRBY": string_to_json, "JSON.NUMMULTBY": string_to_json, "JSON.SET": ok_to_bool, - "JSON.TOGGLE": list_to_optional_bool_list, + "JSON.TOGGLE": to_optional_bool_list, "PFADD": to_bool, "PFMERGE": ok_to_bool, "TIME": format_time, "SISMEMBER": to_bool, - "SMISMEMBER": list_to_bool_list, + "SMISMEMBER": to_bool_list, "SMOVE": to_bool, - "SSCAN": sscan_formatter, - "ZADD": zadd_formatter, - "ZDIFF": format_zdiff, + "SSCAN": format_scan, + "ZADD": format_zadd, + "ZDIFF": format_sorted_set_response_with_score, "ZINCRBY": to_float, - "ZINTER": zinter_formatter, - "ZMSCORE": format_float_list, - "ZPOPMAX": format_sorted_set_return, - "ZPOPMIN": format_sorted_set_return, - "ZRANDMEMBER": zrandmember_formatter, - "ZRANGE": zrange_formatter, - "ZRANGEBYSCORE": zrangebyscore_formatter, - "ZREVRANGE": zrevrange_formatter, - "ZREVRANGEBYSCORE": zrevrangebyscore_formatter, - "ZSCAN": zscan_formatter, - "ZSCORE": zscore_formatter, - "ZUNION": zunion_formatter, + "ZINTER": format_sorted_set_response_with_score, + "ZMSCORE": to_optional_float_list, + "ZPOPMAX": format_sorted_set_response, + "ZPOPMIN": format_sorted_set_response, + "ZRANDMEMBER": format_sorted_set_response_with_score, + "ZRANGE": format_sorted_set_response_with_score, + "ZRANGEBYSCORE": format_sorted_set_response_with_score, + "ZREVRANGE": format_sorted_set_response_with_score, + "ZREVRANGEBYSCORE": format_sorted_set_response_with_score, + "ZSCAN": format_zscan, + "ZSCORE": format_zscore, + "ZUNION": format_sorted_set_response_with_score, "INCRBYFLOAT": to_float, - "PUBSUB NUMSUB": list_to_dict, + "PUBSUB NUMSUB": to_dict, "FLUSHALL": ok_to_bool, "FLUSHDB": ok_to_bool, "PSETEX": ok_to_bool, - "SET": set_formatter, + "SET": format_set, "SETEX": ok_to_bool, "SETNX": to_bool, "MSET": ok_to_bool, @@ -344,7 +267,7 @@ def zunion_formatter(res, command): "HMSET": ok_to_bool, "LSET": ok_to_bool, "SCRIPT FLUSH": ok_to_bool, - "SCRIPT EXISTS": list_to_bool_list, + "SCRIPT EXISTS": to_bool_list, } From 533ae9720b199ceb4f3dcefa40927d6872c11078 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:57:32 +0300 Subject: [PATCH 6/7] simplify environment variable handling in tests --- tests/conftest.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 12dcedc..b4a4f47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,17 +9,11 @@ from upstash_redis import Redis from upstash_redis.asyncio import Redis as AsyncRedis -dotenv.load_dotenv(override=True) +dotenv.load_dotenv() -URL = environ.get( - "UPSTASH_REDIS_REST_URL", - dotenv.dotenv_values().get("UPSTASH_REDIS_REST_URL"), -) +URL = environ["UPSTASH_REDIS_REST_URL"] -TOKEN = environ.get( - "UPSTASH_REDIS_REST_TOKEN", - dotenv.dotenv_values().get("UPSTASH_REDIS_REST_TOKEN"), -) +TOKEN = environ["UPSTASH_REDIS_REST_TOKEN"] HEADERS: Dict[str, str] = {"Authorization": f"Bearer {TOKEN}"} From 10f8e4802c704c97b1bc49a592361f4355718251 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:35:30 +0300 Subject: [PATCH 7/7] fix json.mget return type --- tests/commands/json/test_json_mget.py | 1 + upstash_redis/commands.pyi | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/commands/json/test_json_mget.py b/tests/commands/json/test_json_mget.py index ad52613..ebbe4d7 100644 --- a/tests/commands/json/test_json_mget.py +++ b/tests/commands/json/test_json_mget.py @@ -1,4 +1,5 @@ import pytest + from upstash_redis import Redis from upstash_redis.typing import JSONValueT diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index 0abe6f5..b13d267 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -1524,7 +1524,9 @@ class JsonCommands: self, key: str, *paths: str ) -> Union[List[Union[JSONValueT, None]], JSONValueT]: ... def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... - def mget(self, keys: List[str], path: str) -> List[Union[JSONValueT, None]]: ... + def mget( + self, keys: List[str], path: str + ) -> List[List[Union[JSONValueT, None]]]: ... def mset( self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] ) -> Literal[True]: ... @@ -1622,7 +1624,7 @@ class AsyncJsonCommands: async def merge(self, key: str, path: str, value: JSONValueT) -> Literal[True]: ... async def mget( self, keys: List[str], path: str - ) -> List[Union[JSONValueT, None]]: ... + ) -> List[List[Union[JSONValueT, None]]]: ... async def mset( self, key_path_value_tuples: List[Tuple[str, str, JSONValueT]] ) -> Literal[True]: ...