From b05cb22a869c70c5b68dc873cea6295efeecd820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 11:50:29 +0200 Subject: [PATCH 1/9] Change DynamoDBCacheBackend to serialize cached value as json string --- CHANGELOG.md | 5 +++ .../contrib/aws/cache_backend.py | 8 +++-- tests/contrib/aws/test_cache_backend.py | 34 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8c954..f16758e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## UNRELEASED + +- Changed `DynamoDBCacheBackend` to serialize cached value as json string. + + ## 0.2.0 (2023-09-25) - Added `CloudflareCacheBackend`. diff --git a/ariadne_graphql_proxy/contrib/aws/cache_backend.py b/ariadne_graphql_proxy/contrib/aws/cache_backend.py index afeada5..c3d66de 100644 --- a/ariadne_graphql_proxy/contrib/aws/cache_backend.py +++ b/ariadne_graphql_proxy/contrib/aws/cache_backend.py @@ -1,3 +1,4 @@ +import json import time from typing import Any, Optional @@ -60,7 +61,10 @@ def _query_by_key(self, key: str, max_ttl: int) -> dict: ) async def set(self, key: str, value: Any, ttl: Optional[int] = None): - item = {self.partition_key: key, self.value_attribute_name: value} + item: dict[str, Any] = { + self.partition_key: key, + self.value_attribute_name: json.dumps(value), + } if ttl: now = int(time.time()) item[self.ttl_attribute] = now + ttl @@ -74,7 +78,7 @@ async def get(self, key: str, default: Any = None) -> Any: if len(items) < 1: return default - return items[0].get(self.value_attribute_name, default) + return json.loads(items[0][self.value_attribute_name]) async def clear_all(self): pass diff --git a/tests/contrib/aws/test_cache_backend.py b/tests/contrib/aws/test_cache_backend.py index 0d6785d..9d09ed8 100644 --- a/tests/contrib/aws/test_cache_backend.py +++ b/tests/contrib/aws/test_cache_backend.py @@ -1,3 +1,4 @@ +import json import os import time @@ -43,13 +44,30 @@ def test_init_raises_dynamodb_cache_error_for_unavailable_table(aws_credentials) @pytest.mark.asyncio -async def test_set_creates_correct_item_in_table(test_table): +@pytest.mark.parametrize( + "test_value", + [ + "test", + 14, + 10.11, + True, + {"val": 5.5}, + {"val": "test"}, + {"val": 22}, + {"val": False}, + ["x", "y", "z"], + [1, 2, 3], + [1.1, 2.2, 3.3], + [True, False, True], + ], +) +async def test_set_creates_correct_item_in_table(test_table, test_value): cache = DynamoDBCacheBackend(table_name="test_table") - await cache.set(key="test_key", value="test_value") + await cache.set(key="test_key", value=test_value) response = test_table.get_item(Key={"key": "test_key"}) - assert response["Item"] == {"key": "test_key", "value": "test_value"} + assert response["Item"] == {"key": "test_key", "value": json.dumps(test_value)} @pytest.mark.asyncio @@ -62,14 +80,14 @@ async def test_set_creates_item_with_ttl(test_table): response = test_table.get_item(Key={"key": "test_key"}) assert response["Item"] == { "key": "test_key", - "value": "test_value", + "value": json.dumps("test_value"), "ttl": int(time.time()) + 300, } @pytest.mark.asyncio async def test_get_returns_value_from_table(test_table): - test_table.put_item(Item={"key": "test_key", "value": "test_value"}) + test_table.put_item(Item={"key": "test_key", "value": json.dumps("test_value")}) cache = DynamoDBCacheBackend(table_name="test_table") assert await cache.get(key="test_key") == "test_value" @@ -86,7 +104,11 @@ async def test_get_returns_default_for_not_exisitng_key(test_table): @freeze_time("2023-01-01 12:00:00") async def test_get_returns_not_expired_item(test_table): test_table.put_item( - Item={"key": "test_key", "value": "test_value", "ttl": int(time.time()) + 900} + Item={ + "key": "test_key", + "value": json.dumps("test_value"), + "ttl": int(time.time()) + 900, + } ) cache = DynamoDBCacheBackend(table_name="test_table") From 282021afa3913be02c41c2124032dd88e73ba056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 15:28:51 +0200 Subject: [PATCH 2/9] Add CacheSerializer to CacheBackend --- .github/workflows/tests.yml | 2 +- ariadne_graphql_proxy/cache/__init__.py | 4 +++ ariadne_graphql_proxy/cache/backend.py | 9 ++++- ariadne_graphql_proxy/cache/serializer.py | 41 +++++++++++++++++++++++ pyproject.toml | 4 +++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 ariadne_graphql_proxy/cache/serializer.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39efddd..c6ef18b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[test,dev,aws] + pip install -e .[test,dev,orjson,aws] - name: Pytest run: | pytest diff --git a/ariadne_graphql_proxy/cache/__init__.py b/ariadne_graphql_proxy/cache/__init__.py index 3c2da6a..9265187 100644 --- a/ariadne_graphql_proxy/cache/__init__.py +++ b/ariadne_graphql_proxy/cache/__init__.py @@ -6,6 +6,7 @@ get_simple_cache_key, ) from .cached_resolver import cached_resolver +from .serializer import CacheSerializer, JSONCacheSerializer, NoopCacheSerializer from .simple_cached_resolver import simple_cached_resolver __all__ = [ @@ -17,4 +18,7 @@ "get_operation_cache_key", "get_simple_cache_key", "simple_cached_resolver", + "CacheSerializer", + "JSONCacheSerializer", + "NoopCacheSerializer", ] diff --git a/ariadne_graphql_proxy/cache/backend.py b/ariadne_graphql_proxy/cache/backend.py index 9386c3e..26f6b0e 100644 --- a/ariadne_graphql_proxy/cache/backend.py +++ b/ariadne_graphql_proxy/cache/backend.py @@ -1,8 +1,13 @@ from time import time from typing import Any, Dict, Optional, Tuple +from .serializer import CacheSerializer, NoopCacheSerializer + class CacheBackend: + def __init__(self, serializer: Optional[CacheSerializer] = None) -> None: + self.serializer: CacheSerializer = serializer or NoopCacheSerializer() + async def set(self, key: str, value: Any, ttl: Optional[int] = None): raise NotImplementedError("Cache backends need to define custom 'set' method.") @@ -18,7 +23,9 @@ async def clear_all(self): class InMemoryCache(CacheBackend): _cache: Dict[str, Tuple[Any, Optional[int]]] - def __init__(self): + def __init__(self, serializer: Optional[CacheSerializer] = None): + super().__init__(serializer) + self._cache = {} async def set(self, key: str, value: Any, ttl: Optional[int] = None): diff --git a/ariadne_graphql_proxy/cache/serializer.py b/ariadne_graphql_proxy/cache/serializer.py new file mode 100644 index 0000000..bb54930 --- /dev/null +++ b/ariadne_graphql_proxy/cache/serializer.py @@ -0,0 +1,41 @@ +from typing import Any + +try: + import orjson as json + + USING_ORJSON = True +except ImportError: + import json # type: ignore[no-redef] + + USING_ORJSON = False + + +class CacheSerializer: + def serialize(self, value: Any) -> str: + raise NotImplementedError( + "Cache serializer needs to define custom 'serialize' method." + ) + + def deserialize(self, value: str) -> Any: + raise NotImplementedError( + "Cache serializer needs to define custom 'deserialize' method." + ) + + +class NoopCacheSerializer(CacheSerializer): + def serialize(self, value: Any) -> str: + return value + + def deserialize(self, value: str) -> Any: + return value + + +class JSONCacheSerializer(CacheSerializer): + def serialize(self, value: Any) -> str: + if USING_ORJSON: + return json.dumps(value).decode() + + return json.dumps(value) # type: ignore + + def deserialize(self, value: str) -> Any: + return json.loads(value) diff --git a/pyproject.toml b/pyproject.toml index 4af409d..76b6cde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,10 @@ test = [ "ruff", ] +orjson = [ + "orjson", +] + aws = [ "asgiref", "boto3", From 9156bfaa60a0a279ddbdcab7c1732eb41eea4c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 15:30:05 +0200 Subject: [PATCH 3/9] Add serializer argument to DynamoDBCacheBackend --- .../contrib/aws/cache_backend.py | 14 ++++-- tests/contrib/aws/test_cache_backend.py | 48 ++++++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/ariadne_graphql_proxy/contrib/aws/cache_backend.py b/ariadne_graphql_proxy/contrib/aws/cache_backend.py index c3d66de..54afbcb 100644 --- a/ariadne_graphql_proxy/contrib/aws/cache_backend.py +++ b/ariadne_graphql_proxy/contrib/aws/cache_backend.py @@ -1,8 +1,11 @@ -import json import time from typing import Any, Optional -from ariadne_graphql_proxy.cache import CacheBackend +from ariadne_graphql_proxy.cache import ( + CacheBackend, + CacheSerializer, + JSONCacheSerializer, +) try: from asgiref.sync import sync_to_async @@ -25,7 +28,10 @@ def __init__( partition_key: str = "key", ttl_attribute: str = "ttl", session: Optional[Session] = None, + serializer: Optional[CacheSerializer] = None, ) -> None: + super().__init__(serializer or JSONCacheSerializer()) + self.table_name = table_name self.partition_key = partition_key self.ttl_attribute = ttl_attribute @@ -63,7 +69,7 @@ def _query_by_key(self, key: str, max_ttl: int) -> dict: async def set(self, key: str, value: Any, ttl: Optional[int] = None): item: dict[str, Any] = { self.partition_key: key, - self.value_attribute_name: json.dumps(value), + self.value_attribute_name: self.serializer.serialize(value), } if ttl: now = int(time.time()) @@ -78,7 +84,7 @@ async def get(self, key: str, default: Any = None) -> Any: if len(items) < 1: return default - return json.loads(items[0][self.value_attribute_name]) + return self.serializer.deserialize(items[0][self.value_attribute_name]) async def clear_all(self): pass diff --git a/tests/contrib/aws/test_cache_backend.py b/tests/contrib/aws/test_cache_backend.py index 9d09ed8..6046c7e 100644 --- a/tests/contrib/aws/test_cache_backend.py +++ b/tests/contrib/aws/test_cache_backend.py @@ -1,12 +1,13 @@ -import json import os import time +from typing import Any import boto3 import pytest from freezegun import freeze_time from moto import mock_dynamodb +from ariadne_graphql_proxy.cache import CacheSerializer from ariadne_graphql_proxy.contrib.aws import DynamoDBCacheBackend, DynamoDBCacheError @@ -67,7 +68,27 @@ async def test_set_creates_correct_item_in_table(test_table, test_value): await cache.set(key="test_key", value=test_value) response = test_table.get_item(Key={"key": "test_key"}) - assert response["Item"] == {"key": "test_key", "value": json.dumps(test_value)} + assert response["Item"] == { + "key": "test_key", + "value": cache.serializer.serialize(test_value), + } + + +@pytest.mark.asyncio +async def test_set_uses_custom_serializer(test_table): + class CustomSerializer(CacheSerializer): + def serialize(self, value: Any) -> str: + return str(value) + "-serialized" + + cache = DynamoDBCacheBackend(table_name="test_table", serializer=CustomSerializer()) + + await cache.set(key="test_key", value="abcd") + + response = test_table.get_item(Key={"key": "test_key"}) + assert response["Item"] == { + "key": "test_key", + "value": "abcd-serialized", + } @pytest.mark.asyncio @@ -80,19 +101,34 @@ async def test_set_creates_item_with_ttl(test_table): response = test_table.get_item(Key={"key": "test_key"}) assert response["Item"] == { "key": "test_key", - "value": json.dumps("test_value"), + "value": cache.serializer.serialize("test_value"), "ttl": int(time.time()) + 300, } @pytest.mark.asyncio async def test_get_returns_value_from_table(test_table): - test_table.put_item(Item={"key": "test_key", "value": json.dumps("test_value")}) cache = DynamoDBCacheBackend(table_name="test_table") + test_table.put_item( + Item={"key": "test_key", "value": cache.serializer.serialize("test_value")} + ) assert await cache.get(key="test_key") == "test_value" +@pytest.mark.asyncio +async def test_get_uses_custom_serializer(test_table): + class CustomSerializer(CacheSerializer): + def deserialize(self, value: str) -> Any: + return value + "-deserialized" + + test_table.put_item(Item={"key": "test_key", "value": "test_value"}) + + cache = DynamoDBCacheBackend(table_name="test_table", serializer=CustomSerializer()) + + assert await cache.get(key="test_key") == "test_value-deserialized" + + @pytest.mark.asyncio async def test_get_returns_default_for_not_exisitng_key(test_table): cache = DynamoDBCacheBackend(table_name="test_table") @@ -103,14 +139,14 @@ async def test_get_returns_default_for_not_exisitng_key(test_table): @pytest.mark.asyncio @freeze_time("2023-01-01 12:00:00") async def test_get_returns_not_expired_item(test_table): + cache = DynamoDBCacheBackend(table_name="test_table") test_table.put_item( Item={ "key": "test_key", - "value": json.dumps("test_value"), + "value": cache.serializer.serialize("test_value"), "ttl": int(time.time()) + 900, } ) - cache = DynamoDBCacheBackend(table_name="test_table") assert await cache.get(key="test_key") == "test_value" From 6663b535007ca66e1b189378c2ba725858c2effb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 15:58:55 +0200 Subject: [PATCH 4/9] Add serializer argument to CloudflareCacheBackend --- .../contrib/cloudflare/cache_backend.py | 14 ++++-- .../contrib/cloudflare/test_cache_backend.py | 50 +++++++++++++++++-- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ariadne_graphql_proxy/contrib/cloudflare/cache_backend.py b/ariadne_graphql_proxy/contrib/cloudflare/cache_backend.py index fab57ea..0ad870b 100644 --- a/ariadne_graphql_proxy/contrib/cloudflare/cache_backend.py +++ b/ariadne_graphql_proxy/contrib/cloudflare/cache_backend.py @@ -1,9 +1,12 @@ -import json from typing import Any, Dict, Optional import httpx -from ...cache import CacheBackend +from ariadne_graphql_proxy.cache import ( + CacheBackend, + CacheSerializer, + JSONCacheSerializer, +) class CloudflareCacheError(Exception): @@ -25,7 +28,10 @@ def __init__( namespace_id: str, headers: Optional[Dict[str, str]] = None, base_url: str = "https://api.cloudflare.com/client/v4", + serializer: Optional[CacheSerializer] = None, ) -> None: + super().__init__(serializer or JSONCacheSerializer()) + self.base_url = base_url self.account_id = account_id self.namespace_id = namespace_id @@ -51,7 +57,7 @@ async def set(self, key: str, value: Any, ttl: Optional[int] = None): f"accounts/{self.account_id}/" f"storage/kv/namespaces/{self.namespace_id}/" f"values/{key}", - files={"value": json.dumps({"value": value}), "metadata": "{}"}, + files={"value": self.serializer.serialize(value), "metadata": "{}"}, params={"expiration_ttl": ttl} if ttl is not None else {}, ) @@ -66,7 +72,7 @@ async def get(self, key: str, default: Any = None) -> Any: ) if response.is_success: - return response.json()["value"] + return self.serializer.deserialize(response.content.decode()) return default diff --git a/tests/contrib/cloudflare/test_cache_backend.py b/tests/contrib/cloudflare/test_cache_backend.py index 6f24ea7..026a747 100644 --- a/tests/contrib/cloudflare/test_cache_backend.py +++ b/tests/contrib/cloudflare/test_cache_backend.py @@ -1,7 +1,9 @@ import json +from typing import Any import pytest +from ariadne_graphql_proxy.cache import CacheSerializer from ariadne_graphql_proxy.contrib.cloudflare import ( CloudflareCacheBackend, CloudflareCacheError, @@ -123,7 +125,29 @@ async def test_set_sends_correct_payload_to_cache_value(httpx_mock, list_keys_js request = httpx_mock.get_request(method="PUT") assert {f.name: f.file for f in request.stream.fields} == { - "value": json.dumps({"value": "test_value"}), + "value": cache.serializer.serialize("test_value"), + "metadata": "{}", + } + + +@pytest.mark.asyncio +async def test_set_uses_custom_serializer(httpx_mock, list_keys_json): + httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) + httpx_mock.add_response(method="PUT", status_code=200) + + class CustomSerializer(CacheSerializer): + def serialize(self, value: Any) -> str: + return str(value) + "-serialized" + + cache = CloudflareCacheBackend( + account_id="acc_id", namespace_id="kv_id", serializer=CustomSerializer() + ) + + await cache.set("key", "test_value") + + request = httpx_mock.get_request(method="PUT") + assert {f.name: f.file for f in request.stream.fields} == { + "value": "test_value-serialized", "metadata": "{}", } @@ -143,7 +167,7 @@ async def test_set_sends_request_with_provided_ttl(httpx_mock, list_keys_json): @pytest.mark.asyncio async def test_get_retrives_value_from_correct_url(httpx_mock, list_keys_json): httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) - httpx_mock.add_response(method="GET", status_code=200, json={"value": "test_value"}) + httpx_mock.add_response(method="GET", status_code=200, content=b'"test_value"') cache = CloudflareCacheBackend( account_id="acc_id", namespace_id="kv_id", base_url="https://base.url" ) @@ -169,7 +193,7 @@ async def test_get_sends_request_with_correct_headers( headers, httpx_mock, list_keys_json ): httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) - httpx_mock.add_response(method="GET", status_code=200, json={"value": "test_value"}) + httpx_mock.add_response(method="GET", status_code=200, content=b'"test_value"') cache = CloudflareCacheBackend( account_id="acc_id", namespace_id="kv_id", headers=headers ) @@ -183,7 +207,7 @@ async def test_get_sends_request_with_correct_headers( @pytest.mark.asyncio async def test_get_returns_retrieved_value(httpx_mock, list_keys_json): httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) - httpx_mock.add_response(method="GET", status_code=200, json={"value": "test_value"}) + httpx_mock.add_response(method="GET", status_code=200, content=b'"test_value"') cache = CloudflareCacheBackend(account_id="acc_id", namespace_id="kv_id") value = await cache.get("key") @@ -191,6 +215,24 @@ async def test_get_returns_retrieved_value(httpx_mock, list_keys_json): assert value == "test_value" +@pytest.mark.asyncio +async def test_get_uses_custom_serializer(httpx_mock, list_keys_json): + httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) + httpx_mock.add_response(method="GET", status_code=200, content=b'"test_value"') + + class CustomSerializer(CacheSerializer): + def deserialize(self, value: str) -> Any: + return value.strip("'\"") + "-deserialized" + + cache = CloudflareCacheBackend( + account_id="acc_id", namespace_id="kv_id", serializer=CustomSerializer() + ) + + value = await cache.get("key") + + assert value == "test_value-deserialized" + + @pytest.mark.asyncio async def test_get_returns_default_value_for_404_response(httpx_mock, list_keys_json): httpx_mock.add_response(method="GET", status_code=200, json=list_keys_json) From 699f3330b91d61f7b00a7637b9c6adbc748784f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 16:26:06 +0200 Subject: [PATCH 5/9] Add tests to cache serializers --- tests/cache/test_serializer.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/cache/test_serializer.py diff --git a/tests/cache/test_serializer.py b/tests/cache/test_serializer.py new file mode 100644 index 0000000..1d2497b --- /dev/null +++ b/tests/cache/test_serializer.py @@ -0,0 +1,55 @@ +import pytest + +from ariadne_graphql_proxy.cache import JSONCacheSerializer, NoopCacheSerializer + + +@pytest.fixture +def mocked_lib(mocker): + return mocker.patch("ariadne_graphql_proxy.cache.serializer.json") + + +@pytest.fixture +def mocked_json(mocker, mocked_lib): + mocker.patch("ariadne_graphql_proxy.cache.serializer.USING_ORJSON", False) + return mocked_lib + + +@pytest.fixture +def mocked_orjson(mocker, mocked_lib): + mocker.patch("ariadne_graphql_proxy.cache.serializer.USING_ORJSON", True) + return mocked_lib + + +def test_noop_serialize_returns_not_changed_value(): + assert NoopCacheSerializer().serialize("abc") == "abc" + + +def test_noop_deserialize_returns_not_changed_value(): + assert NoopCacheSerializer().deserialize("abc") == "abc" + + +def test_json_serialize_calls_json_dumps_with_decode_if_orjson_is_available( + mocked_orjson, +): + JSONCacheSerializer().serialize("test value") + + assert mocked_orjson.dumps.called_with("test value") + assert mocked_orjson.dumps().decode.called + + +def test_json_serialize_calls_json_dumps_if_orjson_is_not_available(mocked_json): + JSONCacheSerializer().serialize("test value") + + assert mocked_json.dumps.called_with("test value") + + +def test_json_deserialize_calls_orjson_loads_if_orjson_is_available(mocked_orjson): + JSONCacheSerializer().deserialize("test value") + + assert mocked_orjson.loads.called_with("test value") + + +def test_json_deserialize_calls_json_loads_if_orjson_is_not_available(mocked_json): + JSONCacheSerializer().deserialize("test value") + + assert mocked_json.loads.called_with("test value") From 50f2ef2924f2ca8be0d434cf2e94a0bc4a292d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 16:55:13 +0200 Subject: [PATCH 6/9] Add information about cache serializers to guide --- GUIDE.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/GUIDE.md b/GUIDE.md index a0ded8c..91ee04f 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -630,7 +630,7 @@ To enable cache, `cache` and `cache_key` need to be set. ### Custom cache backends -Custom cache backends should extend `ariadne_graphql_proxy.cache.CacheBackend` class and need to implement `set` and `get` methods: +Custom cache backends should extend `ariadne_graphql_proxy.cache.CacheBackend` class and need to implement `set` and `get` methods: ```python class CacheBackend: @@ -641,6 +641,15 @@ class CacheBackend: ... ``` +`__init__` methods takes `serializer` argument which defaults to `NoopCacheSerializer()` and can be overriden: + +```python +class CacheBackend: + def __init__(self, serializer: Optional[CacheSerializer] = None) -> None: + self.serializer: CacheSerializer = serializer or NoopCacheSerializer() +``` + + They can also optionally implement `clear_all` method, but its not used by Ariadne GraphQL Proxy outside of tests: ```python @@ -650,6 +659,22 @@ class CacheBackend: ``` +### Cche serializers + +Currently only `NoopCacheSerializer` and `JSONCacheSerializer` are provided, but developers may implement their own serializers. + +Custom cache serializers should extend `ariadne_graphql_proxy.cache.CacheSerializer` class and need to implement `serialize` and `deserialize` methods: + +```python +class CacheSerializer: + def serialize(self, value: Any) -> str: + ... + + def deserialize(self, value: str) -> Any: + ... +``` + + ### `CloudflareCacheBackend` `CloudflareCacheBackend` uses Cloudflare's [key value storage](https://developers.cloudflare.com/workers/learning/how-kv-works/) for caching. It can be imported from `ariadne_graphql_proxy.contrib.cloudflare` and requires following arguments: @@ -658,6 +683,7 @@ class CacheBackend: - `namespace_id`: `str`: Id of worker's KV Namespace. - `headers`: `Optional[Dict[str, str]]`: Headers attached to every api call, defaults to `{}`. - `base_url`: `str`: Cloudflare API base url, defaults to `"https://api.cloudflare.com/client/v4"`. +- `serializer`: `Optional[CacheSerializer]`: serialiser used to process cached and retrieved values, defaults to `ariadne_graphql_proxy.cache.JSONCacheSerializer()`. ```python from ariadne_graphql_proxy.contrib.cloudflare import CloudflareCacheBackend @@ -687,6 +713,7 @@ It can be imported from `ariadne_graphql_proxy.contrib.aws` and requires followi - `partition_key`: `str`: Partition key, defaults to `key`. - `ttl_attribute`: `str`: TTL attribute, defaults to `ttl`. - `session`: `Optional[Session]`: Instance of `boto3.session.Session`, defaults to `Session()` which reads configuration values according to these [docs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#guide-configuration). +- `serializer`: `Optional[CacheSerializer]`: serialiser used to process cached and retrieved values, defaults to `ariadne_graphql_proxy.cache.JSONCacheSerializer()`. ```python from ariadne_graphql_proxy.contrib.aws import DynamoDBCacheBackend From 04dedb9702e5932a1034117f8bdc8be0e9dac6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 17:11:13 +0200 Subject: [PATCH 7/9] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f16758e..b09696c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## UNRELEASED -- Changed `DynamoDBCacheBackend` to serialize cached value as json string. +- Added `CacheSerializer`, `NoopCacheSerializer` and `JSONCacheSerializer`. Changed `CacheBackend`, `InMemoryCache`, `CloudflareCacheBackend` and `DynamoDBCacheBackend` to handle `serializer` argument. ## 0.2.0 (2023-09-25) From 538edbfa6be016c9e37f82ab2ea6ee42b69f82b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Thu, 12 Oct 2023 17:12:49 +0200 Subject: [PATCH 8/9] Fix wording --- GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDE.md b/GUIDE.md index 91ee04f..76e47f8 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -659,7 +659,7 @@ class CacheBackend: ``` -### Cche serializers +### Cache serializers Currently only `NoopCacheSerializer` and `JSONCacheSerializer` are provided, but developers may implement their own serializers. From 51fca732de0484564b1452fcc0a4594dce770226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sopi=C5=84ski?= Date: Fri, 13 Oct 2023 10:47:27 +0200 Subject: [PATCH 9/9] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Pitoń --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09696c..81f6413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## UNRELEASED -- Added `CacheSerializer`, `NoopCacheSerializer` and `JSONCacheSerializer`. Changed `CacheBackend`, `InMemoryCache`, `CloudflareCacheBackend` and `DynamoDBCacheBackend` to handle `serializer` argument. +- Added `CacheSerializer`, `NoopCacheSerializer` and `JSONCacheSerializer`. Changed `CacheBackend`, `InMemoryCache`, `CloudflareCacheBackend` and `DynamoDBCacheBackend` to accept `serializer` initialization option. ## 0.2.0 (2023-09-25)