diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c8ac672..aa4c3d2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -85,6 +85,8 @@ jobs: - name: "🧪 Tests" if: success() || failure() run: poetry run pytest tests --test-redis + env: + DJANGO_SETTINGS_MODULE: "tests.backends.django.settings" - name: "📤 Codecov" if: success() || failure() diff --git a/Makefile b/Makefile index 5455800..b60ccea 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -SRC:=cachetory tests +SRC := cachetory tests +DJANGO_SETTINGS_MODULE := tests.backends.django.settings .PHONY: all all: install lint test build @@ -8,6 +9,7 @@ clean: find . -name "*.pyc" -delete rm -rf *.egg-info build rm -rf coverage*.xml .coverage + poetry env remove --all .PHONY: install install: diff --git a/README.md b/README.md index 51604ac..25074f6 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,16 @@ Note the following **caveats**: Dummy backend that always succeeds but never stores anything. Any values get forgotten immediately, and operations behave as if the cache always is empty. +### Django + +![scheme: django](https://img.shields.io/badge/scheme-django://-important) + +| Sync | Async | +|:----------------------------------------|:------------------------------------------| +| `cachetory.backends.sync.DjangoBackend` | `cachetory.backends.async_.DjangoBackend` | + +Adapter for the Django cache framework: allows using a pre-configured Django cache for Cachetory's `Cache`. + ## Supported serializers ### Pickle diff --git a/cachetory/backends/async_/__init__.py b/cachetory/backends/async_/__init__.py index 82a1d41..db3a77a 100644 --- a/cachetory/backends/async_/__init__.py +++ b/cachetory/backends/async_/__init__.py @@ -6,12 +6,14 @@ from .memory import MemoryBackend try: - # noinspection PyUnresolvedReferences from .redis import RedisBackend except ImportError: - _is_redis_available = False -else: - _is_redis_available = True + RedisBackend = None # type: ignore[assignment, misc] + +try: + from .django import DjangoBackend +except ImportError: + DjangoBackend = None # type: ignore[assignment, misc] def from_url(url: str) -> AsyncBackend: @@ -26,9 +28,13 @@ def from_url(url: str) -> AsyncBackend: if scheme == "memory": return MemoryBackend.from_url(url) if scheme in ("redis", "rediss", "redis+unix"): - if not _is_redis_available: + if RedisBackend is None: raise ValueError(f"`{scheme}://` requires `cachetory[redis]` extra") # pragma: no cover return RedisBackend.from_url(url) if scheme == "dummy": return DummyBackend.from_url(url) + if scheme == "django": + if DjangoBackend is None: + raise ValueError(f"`{scheme}://` requires `cachetory[django]` extra") # pragma: no cover + return DjangoBackend.from_url(url) raise ValueError(f"`{scheme}://` is not supported") diff --git a/cachetory/backends/async_/django.py b/cachetory/backends/async_/django.py new file mode 100644 index 0000000..bfa6755 --- /dev/null +++ b/cachetory/backends/async_/django.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import AsyncIterable, Generic, Iterable +from urllib.parse import urlparse + +from django.core.cache import BaseCache, caches # type: ignore[import] +from django.core.cache.backends.base import DEFAULT_TIMEOUT # type: ignore[import] + +from cachetory.interfaces.backends.async_ import AsyncBackend +from cachetory.interfaces.backends.private import WireT +from cachetory.private.datetime import make_time_to_live + +_SENTINEL = object() + + +class DjangoBackend(AsyncBackend[WireT], Generic[WireT]): + """Django cache adapter.""" + + __slots__ = ("_cache",) + + @classmethod + def from_url(cls, url: str) -> DjangoBackend[WireT]: + return DjangoBackend(caches[urlparse(url).hostname]) + + def __init__(self, cache: BaseCache) -> None: + self._cache = cache + + async def get(self, key: str) -> WireT: + if (value := await self._cache.aget(key, _SENTINEL)) is not _SENTINEL: + return value + raise KeyError(key) + + async def get_many(self, *keys: str) -> AsyncIterable[tuple[str, WireT]]: + for item in (await self._cache.aget_many(keys)).items(): + yield item + + async def set( # noqa: A003 + self, + key: str, + value: WireT, + *, + time_to_live: timedelta | None = None, + if_not_exists: bool = False, + ) -> bool: + timeout = self._to_timeout(time_to_live) + if if_not_exists: + await self._cache.aget_or_set(key, value, timeout) + else: + await self._cache.aset(key, value, timeout) + return True + + async def set_many(self, items: Iterable[tuple[str, WireT]]) -> None: + await self._cache.aset_many(dict(items)) + + async def delete(self, key: str) -> bool: + return await self._cache.adelete(key) + + async def clear(self) -> None: + await self._cache.aclear() + + async def expire_in(self, key: str, time_to_live: timedelta | None = None) -> None: + await self._cache.atouch(key, self._to_timeout(time_to_live)) + + async def expire_at(self, key: str, deadline: datetime | None) -> None: + await self.expire_in(key, make_time_to_live(deadline)) + + @staticmethod + def _to_timeout(time_to_live: timedelta | None) -> float: + return time_to_live.total_seconds() if time_to_live is not None else DEFAULT_TIMEOUT diff --git a/cachetory/backends/async_/dummy.py b/cachetory/backends/async_/dummy.py index 9d754b3..87b17cd 100644 --- a/cachetory/backends/async_/dummy.py +++ b/cachetory/backends/async_/dummy.py @@ -11,7 +11,7 @@ class DummyBackend(AsyncBackend[WireT], Generic[WireT]): """Dummy backend that stores nothing.""" @classmethod - def from_url(cls, url: str) -> DummyBackend: + def from_url(cls, url: str) -> DummyBackend[WireT]: return DummyBackend() async def get(self, key: str) -> WireT: # pragma: no cover diff --git a/cachetory/backends/async_/memory.py b/cachetory/backends/async_/memory.py index 26e5a54..05f2eb1 100644 --- a/cachetory/backends/async_/memory.py +++ b/cachetory/backends/async_/memory.py @@ -15,7 +15,7 @@ class MemoryBackend(AsyncBackend[WireT], Generic[WireT]): __slots__ = ("_inner",) @classmethod - def from_url(cls, _url: str) -> MemoryBackend: + def from_url(cls, _url: str) -> MemoryBackend[WireT]: return MemoryBackend() def __init__(self) -> None: diff --git a/cachetory/backends/sync/__init__.py b/cachetory/backends/sync/__init__.py index 6291e85..8bdabd5 100644 --- a/cachetory/backends/sync/__init__.py +++ b/cachetory/backends/sync/__init__.py @@ -6,12 +6,14 @@ from .memory import MemoryBackend try: - # noinspection PyUnresolvedReferences from .redis import RedisBackend except ImportError: - _is_redis_available = False -else: - _is_redis_available = True + RedisBackend = None # type: ignore[assignment, misc] + +try: + from .django import DjangoBackend +except ImportError: + DjangoBackend = None # type: ignore[assignment, misc] def from_url(url: str) -> SyncBackend: @@ -26,9 +28,13 @@ def from_url(url: str) -> SyncBackend: if scheme == "memory": return MemoryBackend.from_url(url) if scheme in ("redis", "rediss", "redis+unix"): - if not _is_redis_available: + if RedisBackend is None: raise ValueError(f"`{scheme}://` requires `cachetory[redis]` extra") # pragma: no cover return RedisBackend.from_url(url) if scheme == "dummy": return DummyBackend.from_url(url) + if scheme == "django": + if DjangoBackend is None: + raise ValueError(f"`{scheme}://` requires `cachetory[django]` extra") # pragma: no cover + return DjangoBackend.from_url(url) raise ValueError(f"`{scheme}://` is not supported") diff --git a/cachetory/backends/sync/django.py b/cachetory/backends/sync/django.py new file mode 100644 index 0000000..2d34195 --- /dev/null +++ b/cachetory/backends/sync/django.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Generic, Iterable +from urllib.parse import urlparse + +from django.core.cache import BaseCache, caches # type: ignore[import] +from django.core.cache.backends.base import DEFAULT_TIMEOUT # type: ignore[import] + +from cachetory.interfaces.backends.private import WireT +from cachetory.interfaces.backends.sync import SyncBackend +from cachetory.private.datetime import make_time_to_live + +_SENTINEL = object() + + +class DjangoBackend(SyncBackend[WireT], Generic[WireT]): + """Django cache adapter.""" + + __slots__ = ("_cache",) + + @classmethod + def from_url(cls, url: str) -> DjangoBackend[WireT]: + return DjangoBackend(caches[urlparse(url).hostname]) + + def __init__(self, cache: BaseCache) -> None: + self._cache = cache + + def get(self, key: str) -> WireT: + if (value := self._cache.get(key, _SENTINEL)) is not _SENTINEL: + return value + raise KeyError(key) + + def get_many(self, *keys: str) -> Iterable[tuple[str, WireT]]: + return self._cache.get_many(keys).items() + + def set( # noqa: A003 + self, + key: str, + value: WireT, + *, + time_to_live: timedelta | None = None, + if_not_exists: bool = False, + ) -> bool: + timeout = self._to_timeout(time_to_live) + if if_not_exists: + self._cache.get_or_set(key, value, timeout) + else: + self._cache.set(key, value, timeout) + return True + + def set_many(self, items: Iterable[tuple[str, WireT]]) -> None: + self._cache.set_many(dict(items)) + + def delete(self, key: str) -> bool: + return self._cache.delete(key) + + def clear(self) -> None: + self._cache.clear() + + def expire_in(self, key: str, time_to_live: timedelta | None = None) -> None: + self._cache.touch(key, self._to_timeout(time_to_live)) + + def expire_at(self, key: str, deadline: datetime | None) -> None: + self.expire_in(key, make_time_to_live(deadline)) + + @staticmethod + def _to_timeout(time_to_live: timedelta | None) -> float: + return time_to_live.total_seconds() if time_to_live is not None else DEFAULT_TIMEOUT diff --git a/cachetory/backends/sync/dummy.py b/cachetory/backends/sync/dummy.py index 138d56b..2302781 100644 --- a/cachetory/backends/sync/dummy.py +++ b/cachetory/backends/sync/dummy.py @@ -11,7 +11,7 @@ class DummyBackend(SyncBackend[WireT], Generic[WireT]): """Dummy backend that stores nothing.""" @classmethod - def from_url(cls, url: str) -> DummyBackend: + def from_url(cls, url: str) -> DummyBackend[WireT]: return DummyBackend() def get(self, key: str) -> WireT: # pragma: no cover diff --git a/cachetory/backends/sync/memory.py b/cachetory/backends/sync/memory.py index 7ea77a0..6be1bb5 100644 --- a/cachetory/backends/sync/memory.py +++ b/cachetory/backends/sync/memory.py @@ -14,7 +14,7 @@ class MemoryBackend(SyncBackend[WireT], Generic[WireT]): __slots__ = ("_entries",) @classmethod - def from_url(cls, _url: str) -> MemoryBackend: + def from_url(cls, _url: str) -> MemoryBackend[WireT]: return MemoryBackend() def __init__(self) -> None: diff --git a/cachetory/backends/sync/redis.py b/cachetory/backends/sync/redis.py index 751e648..b13334a 100644 --- a/cachetory/backends/sync/redis.py +++ b/cachetory/backends/sync/redis.py @@ -12,6 +12,8 @@ class RedisBackend(SyncBackend[bytes]): """Synchronous Redis backend.""" + __slots__ = ("_client",) + @classmethod def from_url(cls, url: str) -> RedisBackend: if url.startswith("redis+"): diff --git a/cachetory/interfaces/backends/async_.py b/cachetory/interfaces/backends/async_.py index 8f8e570..83b1057 100644 --- a/cachetory/interfaces/backends/async_.py +++ b/cachetory/interfaces/backends/async_.py @@ -8,11 +8,11 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from contextlib import AbstractAsyncContextManager +from contextlib import AbstractAsyncContextManager, AbstractContextManager from datetime import datetime, timedelta, timezone from typing import AsyncIterable, Generic, Iterable -from typing_extensions import Protocol +from typing_extensions import Never, Protocol from cachetory.interfaces.backends.private import WireT, WireT_co, WireT_contra @@ -75,6 +75,8 @@ async def set( # noqa: A003 `True` if the value has been successfully set, `False` when `if_not_exists` is true and the key is already existing. """ + + # TODO: just return `None`. raise NotImplementedError async def set_many(self, items: Iterable[tuple[str, WireT_contra]]) -> None: @@ -97,6 +99,7 @@ async def clear(self) -> None: # pragma: no cover class AsyncBackend( + AbstractContextManager, AbstractAsyncContextManager, AsyncBackendRead[WireT], AsyncBackendWrite[WireT], @@ -116,5 +119,11 @@ def from_url(cls, url: str) -> AsyncBackend: # pragma: no cover """ raise NotImplementedError + def __enter__(self) -> Never: + raise RuntimeError("use async context manager protocol instead") + + def __exit__(self, _exc_type, _exc_val, _exc_tb) -> Never: + raise RuntimeError("use async context manager protocol instead") + async def __aexit__(self, exc_type, exc_value, traceback) -> None: return None diff --git a/cachetory/interfaces/backends/sync.py b/cachetory/interfaces/backends/sync.py index bc18b6b..fde39e3 100644 --- a/cachetory/interfaces/backends/sync.py +++ b/cachetory/interfaces/backends/sync.py @@ -75,6 +75,8 @@ def set( # noqa: A003 `True` if the value has been successfully set, `False` when `if_not_exists` is true and the key is already existing. """ + + # TODO: just return `None`. raise NotImplementedError def set_many(self, items: Iterable[tuple[str, WireT_contra]]) -> None: diff --git a/cachetory/private/datetime.py b/cachetory/private/datetime.py index b60186a..77d697f 100644 --- a/cachetory/private/datetime.py +++ b/cachetory/private/datetime.py @@ -1,6 +1,15 @@ +from __future__ import annotations + from datetime import datetime, timedelta, timezone -from typing import Optional + +ZERO_TIMEDELTA = timedelta() -def make_deadline(time_to_live: Optional[timedelta] = None) -> Optional[datetime]: +def make_deadline(time_to_live: timedelta | None = None) -> datetime | None: return datetime.now(timezone.utc) + time_to_live if time_to_live is not None else None + + +def make_time_to_live(deadline: datetime | None = None) -> timedelta | None: + if deadline is None: + return None + return max(deadline - datetime.now(timezone.utc), ZERO_TIMEDELTA) diff --git a/poetry.lock b/poetry.lock index c236fb4..7ced354 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,22 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +optional = true +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + [[package]] name = "async-timeout" version = "4.0.2" @@ -11,38 +28,63 @@ files = [ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = true +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + [[package]] name = "black" -version = "23.3.0" +version = "23.7.0" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] [package.dependencies] @@ -138,13 +180,13 @@ pycparser = "*" [[package]] name = "click" -version = "8.1.3" +version = "8.1.4" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"}, + {file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"}, ] [package.dependencies] @@ -238,30 +280,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.1" +version = "41.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, - {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, - {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, - {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, ] [package.dependencies] @@ -277,6 +323,27 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "django" +version = "4.2.3" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = true +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.3-py3-none-any.whl", hash = "sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039"}, + {file = "Django-4.2.3.tar.gz", hash = "sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + [[package]] name = "exceptiongroup" version = "1.1.2" @@ -427,13 +494,13 @@ files = [ [[package]] name = "platformdirs" -version = "3.8.0" +version = "3.8.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, - {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, + {file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"}, + {file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"}, ] [package.extras] @@ -645,6 +712,22 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +optional = true +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + [[package]] name = "tomli" version = "2.0.1" @@ -696,6 +779,17 @@ files = [ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = true +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "zstd" version = "1.5.5.1" @@ -793,6 +887,7 @@ files = [ ] [extras] +django = ["django"] ormsgpack = ["ormsgpack"] redis = ["redis"] zstd = ["zstd"] @@ -800,4 +895,4 @@ zstd = ["zstd"] [metadata] lock-version = "2.0" python-versions = "^3.8.0" -content-hash = "5da48d920e9398c9a6ab50021aea538afcc812e1a0d114c43ab363db911bf071" +content-hash = "4cd2605cbef8c5c183374fb6b293ac1f3d2eed0227150c3014a0ab0f42e54b57" diff --git a/pyproject.toml b/pyproject.toml index 6a4aa70..6b8da23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,30 +33,32 @@ requires = ["poetry-core", "poetry-dynamic-versioning"] build-backend = "poetry_dynamic_versioning.backend" [tool.poetry.dependencies] -python = "^3.8.0" +django = {version = "^4.0.0", optional = true} +ormsgpack = {version = "^1.2.6", optional = true, markers = "platform_python_implementation == 'CPython'"} pydantic = "^1.10.4" -typing-extensions = "^4.4.0" +python = "^3.8.0" redis = {version = "^4.4.2", optional = true} +typing-extensions = "^4.4.0" zstd = {version = "^1.5.2.6", optional = true} -ormsgpack = {version = "^1.2.6", optional = true, markers = "platform_python_implementation == 'CPython'"} [tool.poetry.extras] +django = ["django"] +ormsgpack = ["ormsgpack"] redis = ["redis"] zstd = ["zstd"] -ormsgpack = ["ormsgpack"] [tool.poetry.group.dev] optional = true [tool.poetry.group.dev.dependencies] -mypy = "^1.3.0" black = "^23.1.0" -ruff = "^0.0.275" +freezegun = "^1.2.2" +mypy = "^1.3.0" pytest = "^7.2.1" pytest-asyncio = "^0.20.3" pytest-cov = "^4.0.0" +ruff = "^0.0.275" types-redis = "^4.4.0.4" -freezegun = "^1.2.2" [tool.black] line-length = 120 diff --git a/tests/backends/async_/test_django.py b/tests/backends/async_/test_django.py new file mode 100644 index 0000000..e7a551b --- /dev/null +++ b/tests/backends/async_/test_django.py @@ -0,0 +1,54 @@ +from typing import AsyncIterable + +import pytest + +from cachetory.backends.async_ import DjangoBackend + + +@pytest.fixture +async def backend() -> AsyncIterable[DjangoBackend[int]]: + async with DjangoBackend[int].from_url("django://default") as backend: + backend.clear() + try: + yield backend + finally: + await backend.clear() + + +async def test_set_get(backend: DjangoBackend[int]) -> None: + await backend.set("foo", 42) + assert await backend.get("foo") == 42 + + +async def test_get_missing(backend: DjangoBackend[int]) -> None: + with pytest.raises(KeyError): + assert await backend.get("foo") is None + + +async def test_set_default(backend: DjangoBackend[int]) -> None: + await backend.set("foo", 42, if_not_exists=True) + await backend.set("foo", 43, if_not_exists=True) + assert await backend.get("foo") == 42 + + +async def test_delete_existing(backend: DjangoBackend[int]) -> None: + await backend.set("foo", 42) + assert await backend.delete("foo") + with pytest.raises(KeyError): + await backend.get("foo") + + +async def test_delete_missing(backend: DjangoBackend[int]) -> None: + assert not await backend.delete("foo") + + +async def test_set_get_many(backend: DjangoBackend[int]) -> None: + await backend.set_many([("foo", 42)]) + assert [value async for value in backend.get_many("foo", "missing")] == [("foo", 42)] + + +async def test_clear(backend: DjangoBackend[int]) -> None: + await backend.set("foo", 42) + await backend.clear() + with pytest.raises(KeyError): + await backend.get("foo") diff --git a/tests/backends/async_/test_redis.py b/tests/backends/async_/test_redis.py index b330e4f..61360d2 100644 --- a/tests/backends/async_/test_redis.py +++ b/tests/backends/async_/test_redis.py @@ -21,21 +21,21 @@ async def backend() -> AsyncIterable[RedisBackend]: @if_redis_enabled @mark.asyncio -async def test_get_existing(backend: RedisBackend): +async def test_get_existing(backend: RedisBackend) -> None: await backend.set("foo", b"hello") assert await backend.get("foo") == b"hello" @if_redis_enabled @mark.asyncio -async def test_get_missing(backend: RedisBackend): +async def test_get_missing(backend: RedisBackend) -> None: with raises(KeyError): await backend.get("foo") @if_redis_enabled @mark.asyncio -async def test_set_default(backend: RedisBackend): +async def test_set_default(backend: RedisBackend) -> None: assert await backend.set("foo", b"hello", if_not_exists=True) assert not await backend.set("foo", b"world", if_not_exists=True) assert await backend.get("foo") == b"hello" @@ -43,7 +43,7 @@ async def test_set_default(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_delete_existing(backend: RedisBackend): +async def test_delete_existing(backend: RedisBackend) -> None: await backend.set("foo", b"hello") assert await backend.delete("foo") with raises(KeyError): @@ -52,13 +52,13 @@ async def test_delete_existing(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_delete_missing(backend: RedisBackend): +async def test_delete_missing(backend: RedisBackend) -> None: assert not await backend.delete("foo") @if_redis_enabled @mark.asyncio -async def test_set_get_many(backend: RedisBackend): +async def test_set_get_many(backend: RedisBackend) -> None: await backend.set_many([("non-empty", b"foo"), ("empty", b"")]) assert [entry async for entry in backend.get_many("non-empty", "missing", "empty")] == [ ("non-empty", b"foo"), @@ -68,7 +68,7 @@ async def test_set_get_many(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_set_with_ttl(backend: RedisBackend): +async def test_set_with_ttl(backend: RedisBackend) -> None: await backend.set("foo", b"bar", time_to_live=timedelta(seconds=0.25)) assert await backend.get("foo") == b"bar" await sleep(0.5) @@ -78,7 +78,7 @@ async def test_set_with_ttl(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_expire_at(backend: RedisBackend): +async def test_expire_at(backend: RedisBackend) -> None: await backend.set("foo", b"bar") await backend.expire_at("foo", make_deadline(timedelta(seconds=0.25))) assert await backend.get("foo") == b"bar" @@ -89,7 +89,7 @@ async def test_expire_at(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_expire_in(backend: RedisBackend): +async def test_expire_in(backend: RedisBackend) -> None: await backend.set("foo", b"bar") await backend.expire_in("foo", timedelta(seconds=0.25)) assert await backend.get("foo") == b"bar" @@ -100,7 +100,7 @@ async def test_expire_in(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_clear(backend: RedisBackend): +async def test_clear(backend: RedisBackend) -> None: await backend.set("foo", b"bar") await backend.clear() with raises(KeyError): @@ -109,6 +109,6 @@ async def test_clear(backend: RedisBackend): @if_redis_enabled @mark.asyncio -async def test_get_empty_value(backend: RedisBackend): +async def test_get_empty_value(backend: RedisBackend) -> None: await backend.set("foo", b"") assert await backend.get("foo") == b"" diff --git a/tests/backends/django/__init__.py b/tests/backends/django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/backends/django/settings.py b/tests/backends/django/settings.py new file mode 100644 index 0000000..27e6f0b --- /dev/null +++ b/tests/backends/django/settings.py @@ -0,0 +1,6 @@ +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "unique-snowflake", + }, +} diff --git a/tests/backends/sync/test_django.py b/tests/backends/sync/test_django.py new file mode 100644 index 0000000..898519b --- /dev/null +++ b/tests/backends/sync/test_django.py @@ -0,0 +1,54 @@ +from typing import Iterable + +import pytest + +from cachetory.backends.sync import DjangoBackend + + +@pytest.fixture +def backend() -> Iterable[DjangoBackend[int]]: + with DjangoBackend[int].from_url("django://default") as backend: + backend.clear() + try: + yield backend + finally: + backend.clear() + + +def test_set_get(backend: DjangoBackend[int]) -> None: + backend.set("foo", 42) + assert backend.get("foo") == 42 + + +def test_get_missing(backend: DjangoBackend[int]) -> None: + with pytest.raises(KeyError): + assert backend.get("foo") is None + + +async def test_set_default(backend: DjangoBackend[int]) -> None: + backend.set("foo", 42, if_not_exists=True) + backend.set("foo", 43, if_not_exists=True) + assert backend.get("foo") == 42 + + +async def test_delete_existing(backend: DjangoBackend[int]) -> None: + backend.set("foo", 42) + assert backend.delete("foo") + with pytest.raises(KeyError): + backend.get("foo") + + +async def test_delete_missing(backend: DjangoBackend[int]) -> None: + assert not backend.delete("foo") + + +async def test_set_get_many(backend: DjangoBackend[int]) -> None: + backend.set_many([("foo", 42)]) + assert list(backend.get_many("foo", "missing")) == [("foo", 42)] + + +async def test_clear(backend: DjangoBackend[int]) -> None: + backend.set("foo", 42) + backend.clear() + with pytest.raises(KeyError): + backend.get("foo") diff --git a/tests/backends/sync/test_redis.py b/tests/backends/sync/test_redis.py index b1fb3ed..ef18704 100644 --- a/tests/backends/sync/test_redis.py +++ b/tests/backends/sync/test_redis.py @@ -20,26 +20,26 @@ def backend() -> Iterable[RedisBackend]: @if_redis_enabled -def test_get_existing(backend: RedisBackend): +def test_get_existing(backend: RedisBackend) -> None: backend.set("foo", b"hello") assert backend.get("foo") == b"hello" @if_redis_enabled -async def test_get_missing(backend: RedisBackend): +async def test_get_missing(backend: RedisBackend) -> None: with raises(KeyError): backend.get("foo") @if_redis_enabled -async def test_set_default(backend: RedisBackend): +async def test_set_default(backend: RedisBackend) -> None: assert backend.set("foo", b"hello", if_not_exists=True) assert not backend.set("foo", b"world", if_not_exists=True) assert backend.get("foo") == b"hello" @if_redis_enabled -async def test_delete_existing(backend: RedisBackend): +async def test_delete_existing(backend: RedisBackend) -> None: backend.set("foo", b"hello") assert backend.delete("foo") with raises(KeyError): @@ -47,18 +47,18 @@ async def test_delete_existing(backend: RedisBackend): @if_redis_enabled -async def test_delete_missing(backend: RedisBackend): +async def test_delete_missing(backend: RedisBackend) -> None: assert not backend.delete("foo") @if_redis_enabled -async def test_set_get_many(backend: RedisBackend): +async def test_set_get_many(backend: RedisBackend) -> None: backend.set_many([("non-empty", b"foo"), ("empty", b"")]) assert list(backend.get_many("non-empty", "missing", "empty")) == [("non-empty", b"foo"), ("empty", b"")] @if_redis_enabled -async def test_set_with_ttl(backend: RedisBackend): +async def test_set_with_ttl(backend: RedisBackend) -> None: backend.set("foo", b"bar", time_to_live=timedelta(seconds=0.25)) assert backend.get("foo") == b"bar" sleep(0.5) @@ -67,7 +67,7 @@ async def test_set_with_ttl(backend: RedisBackend): @if_redis_enabled -async def test_expire_at(backend: RedisBackend): +async def test_expire_at(backend: RedisBackend) -> None: backend.set("foo", b"bar") backend.expire_at("foo", make_deadline(timedelta(seconds=0.25))) assert backend.get("foo") == b"bar" @@ -77,7 +77,7 @@ async def test_expire_at(backend: RedisBackend): @if_redis_enabled -async def test_expire_in(backend: RedisBackend): +async def test_expire_in(backend: RedisBackend) -> None: backend.set("foo", b"bar") backend.expire_in("foo", timedelta(seconds=0.25)) assert backend.get("foo") == b"bar" @@ -87,7 +87,7 @@ async def test_expire_in(backend: RedisBackend): @if_redis_enabled -async def test_clear(backend: RedisBackend): +async def test_clear(backend: RedisBackend) -> None: backend.set("foo", b"bar") backend.clear() with raises(KeyError): @@ -95,6 +95,6 @@ async def test_clear(backend: RedisBackend): @if_redis_enabled -def test_get_empty_value(backend: RedisBackend): +def test_get_empty_value(backend: RedisBackend) -> None: backend.set("foo", b"") assert backend.get("foo") == b"" diff --git a/tests/private/test_datetime.py b/tests/private/test_datetime.py new file mode 100644 index 0000000..12cc6ad --- /dev/null +++ b/tests/private/test_datetime.py @@ -0,0 +1,17 @@ +from datetime import datetime, timedelta, timezone + +from cachetory.private.datetime import ZERO_TIMEDELTA, make_time_to_live + + +def test_make_time_to_live_none() -> None: + assert make_time_to_live(None) is None + + +def test_make_time_to_live_negative() -> None: + assert make_time_to_live(datetime.now(timezone.utc) - timedelta(seconds=10.0)) == ZERO_TIMEDELTA + + +def test_make_time_to_live_positive() -> None: + time_to_live = make_time_to_live(datetime.now(timezone.utc) + timedelta(seconds=10.0)) + assert time_to_live is not None + assert time_to_live > ZERO_TIMEDELTA