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.
Files changed (41) hide show
  1. persista/__init__.py +13 -0
  2. persista/cache/__init__.py +27 -0
  3. persista/cache/async_ttl.py +237 -0
  4. persista/cache/interface.py +245 -0
  5. persista/cache/ttl.py +233 -0
  6. persista/cache/utils.py +52 -0
  7. persista/py.typed +0 -0
  8. persista/store/__init__.py +70 -0
  9. persista/store/async_in_memory.py +157 -0
  10. persista/store/async_postgres.py +517 -0
  11. persista/store/async_redis.py +326 -0
  12. persista/store/async_sqlite.py +559 -0
  13. persista/store/base.py +541 -0
  14. persista/store/duckdb.py +455 -0
  15. persista/store/in_memory.py +149 -0
  16. persista/store/lmdb.py +299 -0
  17. persista/store/postgres.py +445 -0
  18. persista/store/redis.py +297 -0
  19. persista/store/sqlite.py +488 -0
  20. persista/store/types.py +15 -0
  21. persista/store/validation.py +111 -0
  22. persista/testing/__init__.py +2 -0
  23. persista/testing/fixtures.py +102 -0
  24. persista/utils/__init__.py +1 -0
  25. persista/utils/duckdb.py +42 -0
  26. persista/utils/http_httpx.py +258 -0
  27. persista/utils/http_requests.py +165 -0
  28. persista/utils/imports/__init__.py +97 -0
  29. persista/utils/imports/aiosqlite.py +89 -0
  30. persista/utils/imports/duckdb.py +89 -0
  31. persista/utils/imports/faker.py +89 -0
  32. persista/utils/imports/httpx.py +89 -0
  33. persista/utils/imports/lmdb.py +89 -0
  34. persista/utils/imports/psycopg.py +89 -0
  35. persista/utils/imports/redis.py +89 -0
  36. persista/utils/imports/requests.py +89 -0
  37. persista/utils/imports/urllib3.py +89 -0
  38. persista-0.0.1.dist-info/METADATA +289 -0
  39. persista-0.0.1.dist-info/RECORD +41 -0
  40. persista-0.0.1.dist-info/WHEEL +4 -0
  41. 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