moka-py 0.2.4__cp314-cp314-win32.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.

Potentially problematic release.


This version of moka-py might be problematic. Click here for more details.

moka_py/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ import asyncio as _asyncio
2
+ from functools import _make_key
3
+ from functools import wraps as _wraps
4
+ from typing import Any as _Any
5
+
6
+ from .moka_py import Moka
7
+ from .moka_py import get_version as _get_version
8
+
9
+ __all__ = ["VERSION", "Moka", "cached"]
10
+
11
+ VERSION = _get_version()
12
+
13
+
14
+ def cached(
15
+ maxsize=128,
16
+ typed=False,
17
+ *,
18
+ ttl=None,
19
+ tti=None,
20
+ wait_concurrent=False,
21
+ policy="tiny_lfu",
22
+ ):
23
+ """Cache decorator for sync and async functions with TTL/TTI and optional concurrent-waiting.
24
+
25
+ - For sync functions: returns cached value if present, otherwise computes and stores it.
26
+ - For async functions: returns an awaitable; with wait_concurrent=True a single shared task is created per key
27
+ so concurrent awaiters share the same result or exception.
28
+ """
29
+ cache = Moka(maxsize, ttl=ttl, tti=tti, policy=policy)
30
+ empty = object()
31
+
32
+ def dec(fn):
33
+ if _asyncio.iscoroutinefunction(fn):
34
+
35
+ @_wraps(fn)
36
+ async def inner(*args, **kwargs):
37
+ key = _make_key(args, kwargs, typed)
38
+ if wait_concurrent:
39
+ # Store a shared Task in cache while computation is in-flight
40
+ def init() -> _Any:
41
+ return _asyncio.create_task(fn(*args, **kwargs))
42
+
43
+ task = cache.get_with(key, init)
44
+ return await task
45
+ else:
46
+ maybe_value = cache.get(key, empty)
47
+ if maybe_value is not empty:
48
+ return maybe_value
49
+ value = await fn(*args, **kwargs)
50
+ cache.set(key, value)
51
+ return value
52
+ else:
53
+
54
+ @_wraps(fn)
55
+ def inner(*args, **kwargs):
56
+ key = _make_key(args, kwargs, typed)
57
+ if wait_concurrent:
58
+ return cache.get_with(key, lambda: fn(*args, **kwargs))
59
+ else:
60
+ maybe_value = cache.get(key, empty)
61
+ if maybe_value is not empty:
62
+ return maybe_value
63
+ value = fn(*args, **kwargs)
64
+ cache.set(key, value)
65
+ return value
66
+
67
+ inner.cache_clear = cache.clear
68
+ return inner
69
+
70
+ return dec
moka_py/__init__.pyi ADDED
@@ -0,0 +1,55 @@
1
+ from collections.abc import Callable, Hashable
2
+ from typing import Any, Generic, Literal, TypeVar, overload
3
+
4
+ K = TypeVar("K", bound=Hashable)
5
+ V = TypeVar("V")
6
+ D = TypeVar("D")
7
+ Fn = TypeVar("Fn", bound=Callable[..., Any])
8
+ Cause = Literal["explicit", "size", "expired", "replaced"]
9
+ Policy = Literal["tiny_lfu", "lru"]
10
+
11
+ class Moka(Generic[K, V]):
12
+ def __init__(
13
+ self,
14
+ capacity: int,
15
+ ttl: int | float | None = None,
16
+ tti: int | float | None = None,
17
+ eviction_listener: Callable[[K, V, Cause], None] | None = None,
18
+ policy: Policy = "tiny_lfu",
19
+ ): ...
20
+ def set(self, key: K, value: V) -> None: ...
21
+ @overload
22
+ def get(self, key: K, default: D) -> V | D: ...
23
+ @overload
24
+ def get(self, key: K, default: D | None = None) -> V | D | None: ...
25
+ def get_with(self, key: K, initializer: Callable[[], V]) -> V:
26
+ """Lookup or initialize a value for the key.
27
+
28
+ If multiple threads call `get_with` with the same key, only one calls `initializer`,
29
+ the others wait until the value is set.
30
+ """
31
+
32
+ @overload
33
+ def remove(self, key: K, default: D) -> V | D: ...
34
+ @overload
35
+ def remove(self, key: K, default: D | None = None) -> V | D | None: ...
36
+ def clear(self) -> None: ...
37
+ def count(self) -> int: ...
38
+
39
+ def cached(
40
+ maxsize: int = 128,
41
+ typed: bool = False,
42
+ *,
43
+ ttl: int | float | None = None,
44
+ tti: int | float | None = None,
45
+ wait_concurrent: bool = False,
46
+ policy: Policy = "tiny_lfu",
47
+ ) -> Callable[[Fn], Fn]:
48
+ """Decorator for caching function results in a thread-safe in-memory cache.
49
+
50
+ - If the decorated function is synchronous: returns the cached value or computes and stores it.
51
+ - If the decorated function is asynchronous: returns an awaitable which yields the cached result.
52
+ - If wait_concurrent=True: concurrent calls with the same arguments wait on a single in-flight computation.
53
+ For async functions this is implemented via a shared asyncio.Task; all awaiters receive the same result
54
+ or the same exception.
55
+ """
Binary file
moka_py/py.typed ADDED
File without changes
@@ -0,0 +1,334 @@
1
+ Metadata-Version: 2.4
2
+ Name: moka-py
3
+ Version: 0.2.4
4
+ Classifier: Programming Language :: Python :: 3.9
5
+ Classifier: Programming Language :: Python :: 3.10
6
+ Classifier: Programming Language :: Python :: 3.11
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Classifier: Typing :: Typed
11
+ Classifier: Programming Language :: Rust
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
14
+ License-File: LICENSE
15
+ Summary: A high performance caching library for Python written in Rust
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
18
+ Project-URL: Homepage, https://github.com/deliro/moka-py
19
+ Project-URL: Repository, https://github.com/deliro/moka-py
20
+ Project-URL: Issues, https://github.com/deliro/moka-py/issues
21
+
22
+ # moka-py
23
+
24
+ **moka-py** is a Python binding to the [Moka](https://github.com/moka-rs/moka) cache written in Rust. It brings Moka’s high-performance, feature‑rich caching to Python.
25
+
26
+ ## Features
27
+
28
+ - **Synchronous cache:** Thread-safe in-memory caching for Python.
29
+ - **TTL:** Evicts entries after a configurable time to live (TTL).
30
+ - **TTI:** Evicts entries after a configurable time to idle (TTI).
31
+ - **Size-based eviction:** Removes items when capacity is exceeded using TinyLFU or LRU.
32
+ - **Concurrency:** Optimized for high-throughput, concurrent access.
33
+ - **Fully typed:** `mypy` and `pyright` friendly.
34
+
35
+ ## Installation
36
+
37
+ Install with `uv`:
38
+
39
+ ```bash
40
+ uv add moka-py
41
+ ```
42
+
43
+ Or with `poetry`:
44
+
45
+ ```bash
46
+ poetry add moka-py
47
+ ```
48
+
49
+ Or with `pip`:
50
+
51
+ ```bash
52
+ pip install moka-py
53
+ ```
54
+
55
+ ## Table of Contents
56
+
57
+ - [Installation](#installation)
58
+ - [Features](#features)
59
+ - [Usage](#usage)
60
+ - [Using moka_py.Moka](#using-moka_pymoka)
61
+ - [@cached decorator](#as-a-decorator)
62
+ - [Async support](#async-support)
63
+ - [Coalesce concurrent calls (wait_concurrent)](#coalesce-concurrent-calls-wait_concurrent)
64
+ - [Eviction listener](#eviction-listener)
65
+ - [Removing entries](#removing-entries)
66
+ - [How it works](#how-it-works)
67
+ - [Eviction policies](#eviction-policies)
68
+ - [Performance](#performance)
69
+ - [License](#license)
70
+
71
+ ## Usage
72
+
73
+ ### Using moka_py.Moka
74
+
75
+ ```python
76
+ from time import sleep
77
+ from moka_py import Moka
78
+
79
+
80
+ # Create a cache with a capacity of 100 entries, with a TTL of 10.0 seconds
81
+ # and a TTI of 0.1 seconds. Entries are always removed after 10 seconds
82
+ # and are removed after 0.1 seconds if there are no `get`s happened for this time.
83
+ #
84
+ # Both TTL and TTI settings are optional. In the absence of an entry,
85
+ # the corresponding policy will not expire it.
86
+
87
+ # The default eviction policy is "tiny_lfu" which is optimal for most workloads,
88
+ # but you can choose "lru" as well.
89
+ cache: Moka[str, list[int]] = Moka(capacity=100, ttl=10.0, tti=0.1, policy="lru")
90
+
91
+ # Insert a value.
92
+ cache.set("key", [3, 2, 1])
93
+
94
+ # Retrieve the value.
95
+ assert cache.get("key") == [3, 2, 1]
96
+
97
+ # Wait for 0.1+ seconds, and the entry will be automatically evicted.
98
+ sleep(0.12)
99
+ assert cache.get("key") is None
100
+ ```
101
+
102
+ ### As a decorator
103
+
104
+ moka-py can be used as a drop-in replacement for `@lru_cache()` with TTL + TTI support:
105
+
106
+ ```python
107
+ from time import sleep
108
+ from moka_py import cached
109
+
110
+
111
+ calls = []
112
+
113
+
114
+ @cached(maxsize=1024, ttl=5.0, tti=0.05)
115
+ def f(x, y):
116
+ calls.append((x, y))
117
+ return x + y
118
+
119
+
120
+ assert f(1, 2) == 3 # calls computations
121
+ assert f(1, 2) == 3 # gets from the cache
122
+ assert len(calls) == 1
123
+ sleep(0.06)
124
+ assert f(1, 2) == 3 # calls computations again (since TTI has passed)
125
+ assert len(calls) == 2
126
+ ```
127
+
128
+ ### Async support
129
+
130
+ Unlike `@lru_cache()`, `@moka_py.cached()` supports async functions:
131
+
132
+ ```python
133
+ import asyncio
134
+ from time import perf_counter
135
+ from moka_py import cached
136
+
137
+
138
+ calls = []
139
+
140
+
141
+ @cached(maxsize=1024, ttl=5.0, tti=0.1)
142
+ async def f(x, y):
143
+ calls.append((x, y))
144
+ await asyncio.sleep(0.05)
145
+ return x + y
146
+
147
+
148
+ start = perf_counter()
149
+ assert asyncio.run(f(5, 6)) == 11
150
+ assert asyncio.run(f(5, 6)) == 11 # from cache
151
+ elapsed = perf_counter() - start
152
+ assert elapsed < 0.2
153
+ assert len(calls) == 1
154
+ ```
155
+
156
+ ### Coalesce concurrent calls (wait_concurrent)
157
+
158
+ `moka-py` can synchronize threads on keys
159
+
160
+ ```python
161
+ import moka_py
162
+ from typing import Any
163
+ from time import sleep
164
+ import threading
165
+ from decimal import Decimal
166
+
167
+
168
+ calls = []
169
+
170
+
171
+ @moka_py.cached(ttl=5, wait_concurrent=True)
172
+ def get_user(id_: int) -> dict[str, Any]:
173
+ calls.append(id_)
174
+ sleep(0.02) # simulate an HTTP request (short for tests)
175
+ return {
176
+ "id": id_,
177
+ "first_name": "Jack",
178
+ "last_name": "Pot",
179
+ }
180
+
181
+
182
+ def process_request(path: str, user_id: int) -> None:
183
+ user = get_user(user_id)
184
+ ...
185
+
186
+
187
+ def charge_money(from_user_id: int, amount: Decimal) -> None:
188
+ user = get_user(from_user_id)
189
+ ...
190
+
191
+
192
+ if __name__ == '__main__':
193
+ request_processing = threading.Thread(target=process_request, args=("/user/info/123", 123))
194
+ money_charging = threading.Thread(target=charge_money, args=(123, Decimal("3.14")))
195
+ request_processing.start()
196
+ money_charging.start()
197
+ request_processing.join()
198
+ money_charging.join()
199
+
200
+ # Only one call occurred. Without `wait_concurrent`, each thread would issue its own HTTP request
201
+ # before the cache entry is set.
202
+ assert len(calls) == 1
203
+ ```
204
+
205
+ ### Async wait_concurrent
206
+
207
+ When using `wait_concurrent=True` with async functions, `moka-py` creates a shared `asyncio.Task` per cache key. All
208
+ concurrent callers `await` the same task and receive the same result or exception. This eliminates duplicate in-flight
209
+ work for identical arguments.
210
+
211
+ ### Eviction listener
212
+
213
+ `moka-py` supports an eviction listener, called whenever a key is removed.
214
+ The listener must be a three-argument function `(key, value, cause)` and uses positional arguments only.
215
+
216
+ Possible reasons:
217
+
218
+ 1. `"expired"`: The entry's expiration timestamp has passed.
219
+ 2. `"explicit"`: The entry was manually removed by the user (`.remove()` is called).
220
+ 3. `"replaced"`: The entry itself was not actually removed, but its value was replaced by the user (`.set()` is
221
+ called for an existing entry).
222
+ 4. `"size"`: The entry was evicted due to size constraints.
223
+
224
+ ```python
225
+ from typing import Literal
226
+ from moka_py import Moka
227
+ from time import sleep
228
+
229
+
230
+ def key_evicted(
231
+ k: str,
232
+ v: list[int],
233
+ cause: Literal["explicit", "size", "expired", "replaced"]
234
+ ):
235
+ events.append((k, v, cause))
236
+
237
+
238
+ events: list[tuple[str, list[int], str]] = []
239
+
240
+
241
+ moka: Moka[str, list[int]] = Moka(2, eviction_listener=key_evicted, ttl=0.5)
242
+ moka.set("hello", [1, 2, 3])
243
+ moka.set("hello", [3, 2, 1]) # replaced
244
+ moka.set("foo", [4]) # expired
245
+ moka.set("baz", "size")
246
+ moka.remove("foo") # explicit
247
+ sleep(1.0)
248
+ moka.get("anything") # this will trigger eviction for expired
249
+
250
+ causes = {c for _, _, c in events}
251
+ assert causes == {"size", "expired", "replaced", "explicit"}, events
252
+ ```
253
+
254
+ > IMPORTANT NOTES
255
+ > 1) The listener is not called just-in-time. `moka` has no background threads or tasks; it runs only during cache operations.
256
+ > 2) The listener must not raise exceptions. If it does, the exception may surface from any `moka-py` method on any thread.
257
+ > 3) Keep the listener fast. Heavy work (especially I/O) will slow `.get()`, `.set()`, etc. Offload via `ThreadPoolExecutor.submit()` or `asyncio.create_task()`
258
+
259
+ ### Removing entries
260
+
261
+ Remove an entry with `Moka.remove(key)`. It returns the previous value if present; otherwise `None`.
262
+
263
+ ```python
264
+ from moka_py import Moka
265
+
266
+
267
+ c = Moka(128)
268
+ c.set("hello", "world")
269
+ assert c.remove("hello") == "world"
270
+ assert c.get("hello") is None
271
+ ```
272
+
273
+ If `None` is a valid cached value, distinguish it from absence using `Moka.remove(key, default=...)`:
274
+
275
+ ```python
276
+ from moka_py import Moka
277
+
278
+
279
+ c = Moka(128)
280
+ c.set("hello", None)
281
+ assert c.remove("hello", default="WAS_NOT_SET") is None # None was set explicitly
282
+
283
+ # Now the entry "hello" does not exist, so `default` is returned
284
+ assert c.remove("hello", default="WAS_NOT_SET") == "WAS_NOT_SET"
285
+ ```
286
+
287
+ ## How it works
288
+
289
+ `Moka` stores Python object references
290
+ (by [`Py_INCREF`](https://docs.python.org/3/c-api/refcounting.html#c.Py_INCREF)) and does not serialize or deserialize values.
291
+ You can use any Python object as a value and any hashable object as a key (`__hash__` is used).
292
+ Mutable objects remain mutable:
293
+
294
+ ```python
295
+ from moka_py import Moka
296
+
297
+
298
+ c = Moka(128)
299
+ my_list = [1, 2, 3]
300
+ c.set("hello", my_list)
301
+ still_the_same = c.get("hello")
302
+ still_the_same.append(4)
303
+ assert my_list == [1, 2, 3, 4]
304
+ ```
305
+
306
+ ## Eviction policies
307
+
308
+ `moka-py` uses TinyLFU by default, with an LRU option. Learn more in the
309
+ [Moka wiki](https://github.com/moka-rs/moka/wiki#admission-and-eviction-policies).
310
+
311
+ ## Performance
312
+
313
+ *Measured using MacBook Pro 2021 with Apple M1 Pro processor and 16GiB RAM*
314
+
315
+ ```
316
+ -------------------------------------------------------------------------------------------- benchmark: 9 tests -------------------------------------------------------------------------------------------
317
+ Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Mops/s) Rounds Iterations
318
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
319
+ test_bench_remove 100.8775 (1.0) 108.9191 (1.0) 102.6757 (1.0) 3.4992 (34.54) 101.0640 (1.0) 2.4234 (15.49) 1;1 9.7394 (1.0) 5 10000000
320
+ test_bench_get[lru-False] 112.8452 (1.12) 113.0924 (1.04) 112.9415 (1.10) 0.1013 (1.0) 112.9176 (1.12) 0.1565 (1.0) 1;0 8.8541 (0.91) 5 10000000
321
+ test_bench_get[tiny_lfu-False] 135.0147 (1.34) 135.6069 (1.25) 135.2916 (1.32) 0.2246 (2.22) 135.2849 (1.34) 0.3164 (2.02) 2;0 7.3914 (0.76) 5 10000000
322
+ test_bench_get[lru-True] 135.1628 (1.34) 135.7813 (1.25) 135.4712 (1.32) 0.2231 (2.20) 135.4765 (1.34) 0.2477 (1.58) 2;0 7.3816 (0.76) 5 10000000
323
+ test_bench_get[tiny_lfu-True] 135.2461 (1.34) 135.6612 (1.25) 135.4463 (1.32) 0.1802 (1.78) 135.4026 (1.34) 0.3192 (2.04) 2;0 7.3830 (0.76) 5 10000000
324
+ test_bench_get_with 290.5307 (2.88) 291.0418 (2.67) 290.8393 (2.83) 0.1893 (1.87) 290.8867 (2.88) 0.1873 (1.20) 2;0 3.4383 (0.35) 5 10000000
325
+ test_bench_set[tiny_lfu] 515.7514 (5.11) 518.6080 (4.76) 517.4876 (5.04) 1.1196 (11.05) 517.6572 (5.12) 1.5465 (9.88) 2;0 1.9324 (0.20) 5 1912971
326
+ test_bench_set_str_key 516.1032 (5.12) 533.7330 (4.90) 525.7461 (5.12) 6.3386 (62.57) 526.8491 (5.21) 6.1052 (39.01) 2;0 1.9021 (0.20) 5 1918471
327
+ test_bench_set[lru] 637.3014 (6.32) 644.4533 (5.92) 640.3571 (6.24) 2.8981 (28.61) 639.8821 (6.33) 4.6131 (29.48) 2;0 1.5616 (0.16) 5 1581738
328
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
329
+ ```
330
+
331
+ ## License
332
+
333
+ `moka-py` is distributed under the [MIT license](LICENSE).
334
+
@@ -0,0 +1,8 @@
1
+ moka_py-0.2.4.dist-info/METADATA,sha256=smR5TI9XIB2U2d9vomqBrQvZmhsYOhrv_HbUlDCirno,11787
2
+ moka_py-0.2.4.dist-info/WHEEL,sha256=jjLk7AkOzjZGXfuKORLFExurHA-15F78Q65HbI_nDWE,92
3
+ moka_py-0.2.4.dist-info/licenses/LICENSE,sha256=NQUiZ0-64Z10N5MEJ0x-_jUxxw8mWPDnehhZsIaeEmE,1090
4
+ moka_py/__init__.py,sha256=MiNiXB3PXYgwaZRIAx0TuS9KIBrCEdo_yE3LxL_RKzI,2361
5
+ moka_py/__init__.pyi,sha256=lrue1vx4eUWkEW4bWeeAg7y5oT6yfc3vv89mTdosLho,2130
6
+ moka_py/moka_py.cp314-win32.pyd,sha256=V1pXFr_rk29BvTZkrTxy0twUgQy3rfQ5z2LcnyARSwg,343040
7
+ moka_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ moka_py-0.2.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.9.6)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-win32
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Roman Kitaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.