persista 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- persista/__init__.py +13 -0
- persista/cache/__init__.py +27 -0
- persista/cache/async_ttl.py +237 -0
- persista/cache/interface.py +245 -0
- persista/cache/ttl.py +233 -0
- persista/cache/utils.py +52 -0
- persista/py.typed +0 -0
- persista/store/__init__.py +70 -0
- persista/store/async_in_memory.py +157 -0
- persista/store/async_postgres.py +517 -0
- persista/store/async_redis.py +326 -0
- persista/store/async_sqlite.py +559 -0
- persista/store/base.py +541 -0
- persista/store/duckdb.py +455 -0
- persista/store/in_memory.py +149 -0
- persista/store/lmdb.py +299 -0
- persista/store/postgres.py +445 -0
- persista/store/redis.py +297 -0
- persista/store/sqlite.py +488 -0
- persista/store/types.py +15 -0
- persista/store/validation.py +111 -0
- persista/testing/__init__.py +2 -0
- persista/testing/fixtures.py +102 -0
- persista/utils/__init__.py +1 -0
- persista/utils/duckdb.py +42 -0
- persista/utils/http_httpx.py +258 -0
- persista/utils/http_requests.py +165 -0
- persista/utils/imports/__init__.py +97 -0
- persista/utils/imports/aiosqlite.py +89 -0
- persista/utils/imports/duckdb.py +89 -0
- persista/utils/imports/faker.py +89 -0
- persista/utils/imports/httpx.py +89 -0
- persista/utils/imports/lmdb.py +89 -0
- persista/utils/imports/psycopg.py +89 -0
- persista/utils/imports/redis.py +89 -0
- persista/utils/imports/requests.py +89 -0
- persista/utils/imports/urllib3.py +89 -0
- persista-0.0.1.dist-info/METADATA +289 -0
- persista-0.0.1.dist-info/RECORD +41 -0
- persista-0.0.1.dist-info/WHEEL +4 -0
- persista-0.0.1.dist-info/licenses/LICENSE +28 -0
persista/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
r"""Root package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["__version__"]
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version(__name__)
|
|
11
|
+
except PackageNotFoundError: # pragma: no cover
|
|
12
|
+
# Package is not installed, fallback if needed
|
|
13
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
r"""Contain caches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AsyncTTLCache",
|
|
7
|
+
"TTLCache",
|
|
8
|
+
"async_cached",
|
|
9
|
+
"cached",
|
|
10
|
+
"get_async_ttl_cache",
|
|
11
|
+
"get_ttl_cache",
|
|
12
|
+
"make_key",
|
|
13
|
+
"set_async_ttl_cache",
|
|
14
|
+
"set_ttl_cache",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
from persista.cache.async_ttl import AsyncTTLCache
|
|
18
|
+
from persista.cache.interface import (
|
|
19
|
+
async_cached,
|
|
20
|
+
cached,
|
|
21
|
+
get_async_ttl_cache,
|
|
22
|
+
get_ttl_cache,
|
|
23
|
+
set_async_ttl_cache,
|
|
24
|
+
set_ttl_cache,
|
|
25
|
+
)
|
|
26
|
+
from persista.cache.ttl import TTLCache
|
|
27
|
+
from persista.cache.utils import make_key
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
r"""Provide an async TTL cache backed by any ``AsyncBaseStore``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["AsyncTTLCache"]
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import time
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from persista.cache.utils import make_key
|
|
12
|
+
from persista.store.async_in_memory import AsyncInMemoryStore
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Awaitable, Callable
|
|
16
|
+
|
|
17
|
+
from persista.store.base import AsyncBaseStore
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncTTLCache:
|
|
23
|
+
"""Async cache with per-entry expiry, backed by any
|
|
24
|
+
:class:`~persista.store.base.AsyncBaseStore`.
|
|
25
|
+
|
|
26
|
+
Each entry is wrapped as ``{"value": value, "expires_at":
|
|
27
|
+
expires_at}`` before being written to the store, since
|
|
28
|
+
:class:`~persista.store.base.AsyncBaseStore` only accepts ``dict``
|
|
29
|
+
values. If the backing store is one that serializes values (e.g.
|
|
30
|
+
a SQLite- or Redis-backed store), cached values must be
|
|
31
|
+
JSON-serializable.
|
|
32
|
+
|
|
33
|
+
``expires_at`` is a Unix timestamp (``time.time()``), not a
|
|
34
|
+
monotonic clock reading, because entries may be read back by a
|
|
35
|
+
different process or after a restart of this one. Expiry is
|
|
36
|
+
checked lazily on :meth:`get`: an expired entry is only evicted
|
|
37
|
+
the next time it is looked up, not proactively at its expiry
|
|
38
|
+
time.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
store: The backing store. Defaults to a new
|
|
42
|
+
:class:`~persista.store.async_in_memory.AsyncInMemoryStore`.
|
|
43
|
+
default_ttl: The default time-to-live, in seconds, applied to
|
|
44
|
+
entries whose ``ttl`` is not explicitly set. Must be
|
|
45
|
+
positive.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If ``default_ttl`` is not positive.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
```pycon
|
|
52
|
+
>>> import asyncio
|
|
53
|
+
>>> from persista.cache import AsyncTTLCache
|
|
54
|
+
>>> async def main():
|
|
55
|
+
... cache = AsyncTTLCache(default_ttl=60)
|
|
56
|
+
... await cache.set("greeting", "hello")
|
|
57
|
+
... print(await cache.get("greeting"))
|
|
58
|
+
...
|
|
59
|
+
>>> asyncio.run(main())
|
|
60
|
+
hello
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, store: AsyncBaseStore | None = None, default_ttl: float = 300) -> None:
|
|
66
|
+
if default_ttl <= 0:
|
|
67
|
+
msg = f"default_ttl must be a positive number, got {default_ttl}"
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
self._store: AsyncBaseStore = store if store is not None else AsyncInMemoryStore()
|
|
70
|
+
self.default_ttl = default_ttl
|
|
71
|
+
|
|
72
|
+
async def get(self, key: str) -> Any | None:
|
|
73
|
+
"""Retrieve a value by its key.
|
|
74
|
+
|
|
75
|
+
If the entry has expired, it is evicted from the backing
|
|
76
|
+
store as a side effect of this call, before ``None`` is
|
|
77
|
+
returned.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
key: The key to look up.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The cached value, or ``None`` if the key is missing or
|
|
84
|
+
its entry has expired. This means a cached value of
|
|
85
|
+
``None`` is indistinguishable from a cache miss.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
```pycon
|
|
89
|
+
>>> import asyncio
|
|
90
|
+
>>> from persista.cache import AsyncTTLCache
|
|
91
|
+
>>> async def main():
|
|
92
|
+
... cache = AsyncTTLCache()
|
|
93
|
+
... await cache.set("greeting", "hello")
|
|
94
|
+
... print(await cache.get("greeting"))
|
|
95
|
+
... print(await cache.get("missing"))
|
|
96
|
+
...
|
|
97
|
+
>>> asyncio.run(main())
|
|
98
|
+
hello
|
|
99
|
+
None
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
"""
|
|
103
|
+
entry = await self._store.get(key)
|
|
104
|
+
if entry is None:
|
|
105
|
+
return None
|
|
106
|
+
if time.time() > entry["expires_at"]:
|
|
107
|
+
await self._store.delete(key) # expired, evict
|
|
108
|
+
return None
|
|
109
|
+
return entry["value"]
|
|
110
|
+
|
|
111
|
+
async def set(self, key: str, value: Any, ttl: float | None = None) -> None:
|
|
112
|
+
"""Add a value to the cache.
|
|
113
|
+
|
|
114
|
+
Calling this again with an existing key overwrites the
|
|
115
|
+
previous value and resets its expiry.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
key: The key to set.
|
|
119
|
+
value: The value to cache. Must be JSON-serializable if
|
|
120
|
+
the backing store serializes values (see the class
|
|
121
|
+
docstring).
|
|
122
|
+
ttl: The time-to-live, in seconds, before the entry
|
|
123
|
+
expires. Defaults to ``self.default_ttl``. Must be
|
|
124
|
+
positive.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If ``ttl`` is not positive.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```pycon
|
|
131
|
+
>>> import asyncio
|
|
132
|
+
>>> from persista.cache import AsyncTTLCache
|
|
133
|
+
>>> async def main():
|
|
134
|
+
... cache = AsyncTTLCache()
|
|
135
|
+
... await cache.set("greeting", "hello")
|
|
136
|
+
... print(await cache.get("greeting"))
|
|
137
|
+
... await cache.set("greeting", "bonjour")
|
|
138
|
+
... print(await cache.get("greeting"))
|
|
139
|
+
...
|
|
140
|
+
>>> asyncio.run(main())
|
|
141
|
+
hello
|
|
142
|
+
bonjour
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
"""
|
|
146
|
+
ttl = ttl if ttl is not None else self.default_ttl
|
|
147
|
+
if ttl <= 0:
|
|
148
|
+
msg = f"ttl must be a positive number, got {ttl}"
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
await self._store.set(key, {"value": value, "expires_at": time.time() + ttl})
|
|
151
|
+
|
|
152
|
+
async def clear(self) -> None:
|
|
153
|
+
"""Remove every entry from the cache, expired or not.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
```pycon
|
|
157
|
+
>>> import asyncio
|
|
158
|
+
>>> from persista.cache import AsyncTTLCache
|
|
159
|
+
>>> async def main():
|
|
160
|
+
... cache = AsyncTTLCache()
|
|
161
|
+
... await cache.set("greeting", "hello")
|
|
162
|
+
... await cache.clear()
|
|
163
|
+
... print(await cache.get("greeting"))
|
|
164
|
+
...
|
|
165
|
+
>>> asyncio.run(main())
|
|
166
|
+
None
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
"""
|
|
170
|
+
await self._store.clear()
|
|
171
|
+
|
|
172
|
+
def memoize(
|
|
173
|
+
self, ttl: float | None = None
|
|
174
|
+
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
|
|
175
|
+
"""Decorate an async function so its return values are cached.
|
|
176
|
+
|
|
177
|
+
The cache key is derived from the decorated function's
|
|
178
|
+
qualified name (``__qualname__``) and call arguments, via
|
|
179
|
+
:func:`~persista.cache.utils.make_key`, so calls with equal
|
|
180
|
+
arguments share a cached result. Call arguments must always
|
|
181
|
+
be JSON-serializable, regardless of the backing store; the
|
|
182
|
+
return value must additionally be JSON-serializable if the
|
|
183
|
+
backing store serializes values (see the class docstring).
|
|
184
|
+
Because the key is based on ``__qualname__`` rather than
|
|
185
|
+
object identity, two distinct functions defined with the same
|
|
186
|
+
qualified name (e.g. two calls to the same factory returning
|
|
187
|
+
a closure) share their cache entries.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
ttl: The time-to-live, in seconds, applied to cached
|
|
191
|
+
results. Defaults to ``self.default_ttl``. Must be
|
|
192
|
+
positive.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
A decorator that wraps an async function with caching.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: If ``ttl`` is not positive.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
```pycon
|
|
202
|
+
>>> import asyncio
|
|
203
|
+
>>> from persista.cache import AsyncTTLCache
|
|
204
|
+
>>> cache = AsyncTTLCache()
|
|
205
|
+
>>> calls = []
|
|
206
|
+
>>> @cache.memoize(ttl=60)
|
|
207
|
+
... async def square(x):
|
|
208
|
+
... calls.append(x)
|
|
209
|
+
... return x * x
|
|
210
|
+
...
|
|
211
|
+
>>> async def main():
|
|
212
|
+
... print(await square(4))
|
|
213
|
+
... print(await square(4)) # served from the cache, not re-computed
|
|
214
|
+
...
|
|
215
|
+
>>> asyncio.run(main())
|
|
216
|
+
16
|
|
217
|
+
16
|
|
218
|
+
>>> calls
|
|
219
|
+
[4]
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
|
|
225
|
+
@functools.wraps(func)
|
|
226
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
227
|
+
key = make_key(func.__qualname__, args, kwargs)
|
|
228
|
+
cached = await self.get(key)
|
|
229
|
+
if cached is not None:
|
|
230
|
+
return cached
|
|
231
|
+
result = await func(*args, **kwargs)
|
|
232
|
+
await self.set(key, result, ttl=ttl)
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
return wrapper
|
|
236
|
+
|
|
237
|
+
return decorator
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
r"""Provide module-level access to shared default caches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"async_cached",
|
|
7
|
+
"cached",
|
|
8
|
+
"get_async_ttl_cache",
|
|
9
|
+
"get_ttl_cache",
|
|
10
|
+
"set_async_ttl_cache",
|
|
11
|
+
"set_ttl_cache",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import functools
|
|
16
|
+
import inspect
|
|
17
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
18
|
+
|
|
19
|
+
from persista.cache.async_ttl import AsyncTTLCache
|
|
20
|
+
from persista.cache.ttl import TTLCache
|
|
21
|
+
from persista.cache.utils import make_key
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Awaitable, Callable
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
_state = {"cache": TTLCache(), "async_cache": AsyncTTLCache()}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_ttl_cache() -> TTLCache:
|
|
32
|
+
"""Return the shared default cache.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The shared default :class:`~persista.cache.ttl.TTLCache`
|
|
36
|
+
instance, used by :func:`cached` when no explicit cache is
|
|
37
|
+
given.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
```pycon
|
|
41
|
+
>>> from persista.cache.interface import get_ttl_cache
|
|
42
|
+
>>> cache = get_ttl_cache()
|
|
43
|
+
>>> cache.set("greeting", "hello")
|
|
44
|
+
>>> cache.get("greeting")
|
|
45
|
+
'hello'
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
"""
|
|
49
|
+
return _state["cache"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set_ttl_cache(cache: TTLCache) -> None:
|
|
53
|
+
"""Replace the shared default cache.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
cache: The :class:`~persista.cache.ttl.TTLCache` instance to
|
|
57
|
+
install as the new shared default, in place of the one
|
|
58
|
+
returned by :func:`get_ttl_cache`.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```pycon
|
|
62
|
+
>>> from persista.cache import TTLCache
|
|
63
|
+
>>> from persista.cache import get_ttl_cache, set_ttl_cache
|
|
64
|
+
>>> set_ttl_cache(TTLCache(default_ttl=60))
|
|
65
|
+
>>> get_ttl_cache().default_ttl
|
|
66
|
+
60
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
_state["cache"] = cache
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_async_ttl_cache() -> AsyncTTLCache:
|
|
74
|
+
"""Return the shared default async cache.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The shared default :class:`~persista.cache.async_ttl.AsyncTTLCache`
|
|
78
|
+
instance, used by :func:`async_cached` when no explicit cache
|
|
79
|
+
is given.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```pycon
|
|
83
|
+
>>> import asyncio
|
|
84
|
+
>>> from persista.cache.interface import get_async_ttl_cache
|
|
85
|
+
>>> async def main():
|
|
86
|
+
... cache = get_async_ttl_cache()
|
|
87
|
+
... await cache.set("greeting", "hello")
|
|
88
|
+
... print(await cache.get("greeting"))
|
|
89
|
+
...
|
|
90
|
+
>>> asyncio.run(main())
|
|
91
|
+
hello
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
"""
|
|
95
|
+
return _state["async_cache"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def set_async_ttl_cache(cache: AsyncTTLCache) -> None:
|
|
99
|
+
"""Replace the shared default async cache.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
cache: The :class:`~persista.cache.async_ttl.AsyncTTLCache`
|
|
103
|
+
instance to install as the new shared default, in place of
|
|
104
|
+
the one returned by :func:`get_async_ttl_cache`.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
```pycon
|
|
108
|
+
>>> from persista.cache import AsyncTTLCache
|
|
109
|
+
>>> from persista.cache import get_async_ttl_cache, set_async_ttl_cache
|
|
110
|
+
>>> set_async_ttl_cache(AsyncTTLCache(default_ttl=60))
|
|
111
|
+
>>> get_async_ttl_cache().default_ttl
|
|
112
|
+
60
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
"""
|
|
116
|
+
_state["async_cache"] = cache
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cached(ttl: int | None = None) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
120
|
+
"""Cache a function's return values in the shared default cache.
|
|
121
|
+
|
|
122
|
+
Works on both sync and async functions (``async def``), by
|
|
123
|
+
looking up :func:`get_ttl_cache` on every call, so replacing the
|
|
124
|
+
shared cache via :func:`set_ttl_cache` also changes where
|
|
125
|
+
already-decorated functions store their results.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
ttl: The time-to-live, in seconds, applied to cached results.
|
|
129
|
+
Defaults to the cache's ``default_ttl``. Must be positive.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A decorator that wraps a function with caching.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If ``ttl`` is not positive.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
```pycon
|
|
139
|
+
>>> from persista.cache import cached
|
|
140
|
+
>>> calls = []
|
|
141
|
+
>>> @cached(ttl=60)
|
|
142
|
+
... def square(x):
|
|
143
|
+
... calls.append(x)
|
|
144
|
+
... return x * x
|
|
145
|
+
...
|
|
146
|
+
>>> square(4)
|
|
147
|
+
16
|
|
148
|
+
>>> square(4) # served from the cache, not re-computed
|
|
149
|
+
16
|
|
150
|
+
>>> calls
|
|
151
|
+
[4]
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
157
|
+
if inspect.iscoroutinefunction(func):
|
|
158
|
+
|
|
159
|
+
@functools.wraps(func)
|
|
160
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
161
|
+
cache = get_ttl_cache()
|
|
162
|
+
key = make_key(func.__qualname__, args, kwargs)
|
|
163
|
+
result = cache.get(key)
|
|
164
|
+
if result is not None:
|
|
165
|
+
return result
|
|
166
|
+
result = await func(*args, **kwargs)
|
|
167
|
+
cache.set(key, result, ttl=ttl)
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
return async_wrapper
|
|
171
|
+
|
|
172
|
+
@functools.wraps(func)
|
|
173
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
174
|
+
cache = get_ttl_cache()
|
|
175
|
+
key = make_key(func.__qualname__, args, kwargs)
|
|
176
|
+
result = cache.get(key)
|
|
177
|
+
if result is not None:
|
|
178
|
+
return result
|
|
179
|
+
result = func(*args, **kwargs)
|
|
180
|
+
cache.set(key, result, ttl=ttl)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
return wrapper
|
|
184
|
+
|
|
185
|
+
return decorator
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def async_cached(
|
|
189
|
+
ttl: int | None = None,
|
|
190
|
+
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
|
|
191
|
+
"""Cache an async function's return values in the shared default
|
|
192
|
+
async cache.
|
|
193
|
+
|
|
194
|
+
Looks up :func:`get_async_ttl_cache` on every call, so replacing
|
|
195
|
+
the shared cache via :func:`set_async_ttl_cache` also changes
|
|
196
|
+
where already-decorated functions store their results.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
ttl: The time-to-live, in seconds, applied to cached results.
|
|
200
|
+
Defaults to the cache's ``default_ttl``. Must be positive.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A decorator that wraps an async function with caching.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If ``ttl`` is not positive.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
```pycon
|
|
210
|
+
>>> import asyncio
|
|
211
|
+
>>> from persista.cache import async_cached
|
|
212
|
+
>>> calls = []
|
|
213
|
+
>>> @async_cached(ttl=60)
|
|
214
|
+
... async def square(x):
|
|
215
|
+
... calls.append(x)
|
|
216
|
+
... return x * x
|
|
217
|
+
...
|
|
218
|
+
>>> async def main():
|
|
219
|
+
... print(await square(4))
|
|
220
|
+
... print(await square(4)) # served from the cache, not re-computed
|
|
221
|
+
...
|
|
222
|
+
>>> asyncio.run(main())
|
|
223
|
+
16
|
|
224
|
+
16
|
|
225
|
+
>>> calls
|
|
226
|
+
[4]
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
|
|
232
|
+
@functools.wraps(func)
|
|
233
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
234
|
+
cache = get_async_ttl_cache()
|
|
235
|
+
key = make_key(func.__qualname__, args, kwargs)
|
|
236
|
+
result = await cache.get(key)
|
|
237
|
+
if result is not None:
|
|
238
|
+
return result
|
|
239
|
+
result = await func(*args, **kwargs)
|
|
240
|
+
await cache.set(key, result, ttl=ttl)
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
return wrapper
|
|
244
|
+
|
|
245
|
+
return decorator
|