acachetools 0.1.0__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.
@@ -0,0 +1,205 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable, MutableMapping
3
+ from functools import update_wrapper
4
+ from inspect import iscoroutinefunction
5
+ from typing import Any, ParamSpec, Protocol, TypeVar, cast, runtime_checkable
6
+
7
+ from cachetools.keys import hashkey, methodkey
8
+
9
+ P = ParamSpec("P")
10
+ R = TypeVar("R", covariant=True)
11
+
12
+
13
+ @runtime_checkable
14
+ class CachedAsyncFunction(Protocol[P, R]):
15
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Awaitable[R]: ...
16
+
17
+ cache: MutableMapping[Any, Any]
18
+ cache_clear: Callable[[], None]
19
+
20
+
21
+ @runtime_checkable
22
+ class CachedAsyncMethod(Protocol[P, R]):
23
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Awaitable[R]: ...
24
+
25
+ cache: Callable[[Any], MutableMapping[Any, Any]]
26
+ cache_clear: Callable[[Any], None]
27
+
28
+
29
+ async def _run_cached(
30
+ cache_store: MutableMapping[Any, asyncio.Future[R]],
31
+ cache_key: Any,
32
+ coro_factory: Callable[[], Awaitable[R]],
33
+ ) -> R:
34
+ while True:
35
+ future = cache_store.get(cache_key)
36
+
37
+ # Cache hit
38
+ if future is not None:
39
+ if future.cancelled():
40
+ # Cached Future was cancelled
41
+ if cache_store.get(cache_key) is future:
42
+ cache_store.pop(cache_key, None)
43
+ continue
44
+
45
+ if not future.done():
46
+ # Another task is still computing this key
47
+ # Wait for the shared result instead of recomputing
48
+ try:
49
+ return await asyncio.shield(future)
50
+ except asyncio.CancelledError:
51
+ # The caller was cancelled while waiting
52
+ continue
53
+
54
+ # Cached computation completed
55
+ try:
56
+ return future.result()
57
+ except Exception:
58
+ # Failed results are not cached
59
+ if cache_store.get(cache_key) is future:
60
+ cache_store.pop(cache_key, None)
61
+ continue
62
+
63
+ # Cache miss
64
+ loop = asyncio.get_running_loop()
65
+ shared_future: asyncio.Future[R] = loop.create_future()
66
+ existing = cache_store.setdefault(
67
+ cache_key,
68
+ shared_future,
69
+ )
70
+
71
+ # Another task already registered a Future for this cache key.
72
+ if existing is not shared_future:
73
+ continue
74
+
75
+ try:
76
+ result = await coro_factory()
77
+
78
+ if not shared_future.done():
79
+ shared_future.set_result(result)
80
+
81
+ return result
82
+
83
+ except asyncio.CancelledError:
84
+ # The owner task was cancelled
85
+ if cache_store.get(cache_key) is shared_future:
86
+ cache_store.pop(cache_key, None)
87
+
88
+ if not shared_future.done():
89
+ shared_future.cancel()
90
+
91
+ raise
92
+
93
+ except Exception as exc:
94
+ # Exceptions are not cached
95
+ if cache_store.get(cache_key) is shared_future:
96
+ cache_store.pop(cache_key, None)
97
+
98
+ if not shared_future.done():
99
+ shared_future.set_exception(exc)
100
+ # Future exception was never retrieved
101
+ shared_future.exception()
102
+
103
+ raise
104
+
105
+
106
+ def _clear_cache(
107
+ cache_store: MutableMapping[Any, asyncio.Future[Any]],
108
+ ) -> None:
109
+ for future in list(cache_store.values()):
110
+ if not future.done():
111
+ future.cancel()
112
+
113
+ cache_store.clear()
114
+
115
+
116
+ def cached(
117
+ cache: MutableMapping[Any, Any] | None = None,
118
+ *,
119
+ key: Callable[..., Any] = hashkey,
120
+ info: bool = False,
121
+ lock: Any | None = None,
122
+ ) -> Callable[[Callable[P, Awaitable[R]]], CachedAsyncFunction[P, R]]:
123
+ if info:
124
+ raise NotImplementedError("acachetools does not support `info`.")
125
+ if lock is not None:
126
+ raise NotImplementedError("acachetools does not support `lock`.")
127
+
128
+ cache_store = cast(
129
+ MutableMapping[Any, asyncio.Future[R]],
130
+ {} if cache is None else cache,
131
+ )
132
+
133
+ def decorator(
134
+ fn: Callable[P, Awaitable[R]],
135
+ ) -> CachedAsyncFunction[P, R]:
136
+ if not iscoroutinefunction(fn):
137
+ raise TypeError(f"Expected Coroutine function, got {fn}")
138
+
139
+ async def wrapper(
140
+ *args: P.args,
141
+ **kwargs: P.kwargs,
142
+ ) -> R:
143
+ return await _run_cached(
144
+ cache_store=cache_store,
145
+ cache_key=key(*args, **kwargs),
146
+ coro_factory=lambda: fn(*args, **kwargs),
147
+ )
148
+
149
+ def cache_clear() -> None:
150
+ _clear_cache(cache_store)
151
+
152
+ wrapped = update_wrapper(wrapper, fn)
153
+ wrapped.cache = cache_store # type: ignore[attr-defined]
154
+ wrapped.cache_clear = cache_clear # type: ignore[attr-defined]
155
+ return wrapped # type: ignore[return-value]
156
+
157
+ return decorator
158
+
159
+
160
+ def cachedmethod(
161
+ cache: Callable[[Any], MutableMapping[Any, Any]],
162
+ *,
163
+ key: Callable[..., Any] = methodkey,
164
+ lock: Callable[[Any], Any] | None = None,
165
+ ) -> Callable[[Callable[..., Awaitable[R]]], CachedAsyncMethod[P, R]]:
166
+ if lock is not None:
167
+ raise NotImplementedError("acachetools does not support `lock`.")
168
+
169
+ def decorator(method: Callable[..., Awaitable[R]]) -> CachedAsyncMethod[P, R]:
170
+ if not iscoroutinefunction(method):
171
+ raise TypeError(f"Expected coroutine function, got {method!r}")
172
+
173
+ async def wrapper(
174
+ self: Any,
175
+ *args: P.args,
176
+ **kwargs: P.kwargs,
177
+ ) -> R:
178
+ cache_store = cast(
179
+ MutableMapping[Any, asyncio.Future[R]],
180
+ cache(self),
181
+ )
182
+ return await _run_cached(
183
+ cache_store=cache_store,
184
+ cache_key=key(self, *args, **kwargs),
185
+ coro_factory=lambda: method(
186
+ self,
187
+ *args,
188
+ **kwargs,
189
+ ),
190
+ )
191
+
192
+ def cache_clear(self: Any) -> None:
193
+ cache_store = cast(
194
+ MutableMapping[Any, asyncio.Future[Any]],
195
+ cache(self),
196
+ )
197
+ _clear_cache(cache_store)
198
+
199
+ wrapped = update_wrapper(wrapper, method)
200
+ wrapped.cache = cache # type: ignore[attr-defined]
201
+ wrapped.cache_clear = cache_clear # type: ignore[attr-defined]
202
+
203
+ return wrapped # type: ignore[return-value]
204
+
205
+ return decorator
acachetools/py.typed ADDED
File without changes
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.3
2
+ Name: acachetools
3
+ Version: 0.1.0
4
+ Summary: Async cachetools decorators with stampede protection.
5
+ Author: RektPunk
6
+ Author-email: RektPunk <rektpunk@gmail.com>
7
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
8
+ Requires-Dist: cachetools>=7.1.4
9
+ Requires-Python: >=3.10
10
+ Project-URL: repository, https://github.com/RektPunk/acachetools
11
+ Description-Content-Type: text/markdown
12
+
13
+ <div style="text-align: center;">
14
+ <img src="https://capsule-render.vercel.app/api?type=transparent&fontColor=0047AB&text=acachetools&height=120&fontSize=90">
15
+ </div>
16
+
17
+ **acachetools** provides asyncio-compatible versions of `cachetools.cached()` and `cachetools.cachedmethod()`. Concurrent calls for the same cache key share a single in-flight computation, preventing cache stampedes.
18
+
19
+ ## Installation
20
+ ```bash
21
+ pip install acachetools
22
+ ```
23
+
24
+ ## Usage
25
+ ```python
26
+ from cachetools import TTLCache
27
+ from acachetools import cached
28
+
29
+ # Compatible with TTLCache, LRUCache, LFUCache, RRCache, etc.
30
+ @cached(cache=TTLCache(maxsize=1024, ttl=600))
31
+ async def foo(bar: int):
32
+ ...
33
+ ```
@@ -0,0 +1,5 @@
1
+ acachetools/__init__.py,sha256=r7EHPnydQ7r6MxzQh7cPlFuG1kfRZTtbRGOdgQ-_l9M,6348
2
+ acachetools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ acachetools-0.1.0.dist-info/WHEEL,sha256=s49dN1sxqzkgPplo4QuUaKomil-_cbDzeLK4-pZKD-A,81
4
+ acachetools-0.1.0.dist-info/METADATA,sha256=72Zx2NSeE_0-74GTjhymsTDXLTmsNruLHGN5DRARusw,1083
5
+ acachetools-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any