moka-py 0.1.13__cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl → 0.3.0__cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.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.
moka_py/__init__.py CHANGED
@@ -1,32 +1,57 @@
1
1
  import asyncio as _asyncio
2
- from functools import wraps as _wraps, _make_key
3
- from .moka_py import Moka, get_version as _get_version
2
+ import inspect as _inspect
3
+ from functools import _make_key
4
+ from functools import wraps as _wraps
5
+ from typing import Any as _Any
4
6
 
7
+ from .moka_py import Moka
8
+ from .moka_py import get_version as _get_version
5
9
 
6
- __all__ = ["Moka", "cached", "VERSION"]
10
+ __all__ = ["VERSION", "Moka", "cached"]
7
11
 
8
12
  VERSION = _get_version()
9
13
 
10
14
 
11
- def cached(maxsize=128, typed=False, *, ttl=None, tti=None, wait_concurrent=False):
12
- cache = Moka(maxsize, ttl, tti)
15
+ def cached(
16
+ maxsize=128,
17
+ typed=False,
18
+ *,
19
+ ttl=None,
20
+ tti=None,
21
+ wait_concurrent=False,
22
+ policy="tiny_lfu",
23
+ ):
24
+ """Cache decorator for sync and async functions with TTL/TTI and optional concurrent-waiting.
25
+
26
+ - For sync functions: returns cached value if present, otherwise computes and stores it.
27
+ - For async functions: returns an awaitable; with wait_concurrent=True a single shared task is created per key
28
+ so concurrent awaiters share the same result or exception.
29
+ """
30
+ cache = Moka(maxsize, ttl=ttl, tti=tti, policy=policy)
13
31
  empty = object()
14
32
 
15
33
  def dec(fn):
16
- if _asyncio.iscoroutinefunction(fn):
17
- if wait_concurrent:
18
- raise NotImplementedError("wait_concurrent is not yet supported for async functions")
34
+ if _inspect.iscoroutinefunction(fn):
19
35
 
20
36
  @_wraps(fn)
21
37
  async def inner(*args, **kwargs):
22
38
  key = _make_key(args, kwargs, typed)
23
- maybe_value = cache.get(key, empty)
24
- if maybe_value is not empty:
25
- return maybe_value
26
- value = await fn(*args, **kwargs)
27
- cache.set(key, value)
28
- return value
39
+ if wait_concurrent:
40
+ # Store a shared Task in cache while computation is in-flight
41
+ def init() -> _Any:
42
+ return _asyncio.create_task(fn(*args, **kwargs))
43
+
44
+ task = cache.get_with(key, init)
45
+ return await task
46
+ else:
47
+ maybe_value = cache.get(key, empty)
48
+ if maybe_value is not empty:
49
+ return maybe_value
50
+ value = await fn(*args, **kwargs)
51
+ cache.set(key, value)
52
+ return value
29
53
  else:
54
+
30
55
  @_wraps(fn)
31
56
  def inner(*args, **kwargs):
32
57
  key = _make_key(args, kwargs, typed)
moka_py/__init__.pyi CHANGED
@@ -1,51 +1,67 @@
1
- from typing import TypeVar, Optional, Generic, Hashable, Union, Callable, Any, overload, Literal
2
-
1
+ from collections.abc import Callable, Hashable
2
+ from typing import Any, Generic, Literal, TypeVar, overload
3
3
 
4
4
  K = TypeVar("K", bound=Hashable)
5
5
  V = TypeVar("V")
6
6
  D = TypeVar("D")
7
7
  Fn = TypeVar("Fn", bound=Callable[..., Any])
8
8
  Cause = Literal["explicit", "size", "expired", "replaced"]
9
-
9
+ Policy = Literal["tiny_lfu", "lru"]
10
10
 
11
11
  class Moka(Generic[K, V]):
12
12
  def __init__(
13
- self,
14
- capacity: int,
15
- ttl: Optional[Union[int, float]] = None,
16
- tti: Optional[Union[int, float]] = None,
17
- eviction_listener: Optional[Callable[[K, V, Cause], None]] = None,
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",
18
19
  ): ...
19
-
20
- def set(self, key: K, value: V) -> None: ...
21
-
20
+ def set(
21
+ self,
22
+ key: K,
23
+ value: V,
24
+ ttl: int | float | None = None,
25
+ tti: int | float | None = None,
26
+ ) -> None: ...
22
27
  @overload
23
- def get(self, key: K, default: D) -> Union[V, D]: ...
24
-
28
+ def get(self, key: K, default: D) -> V | D: ...
25
29
  @overload
26
- def get(self, key: K, default: Optional[D] = None) -> Optional[Union[V, D]]: ...
30
+ def get(self, key: K, default: D | None = None) -> V | D | None: ...
31
+ def get_with(
32
+ self,
33
+ key: K,
34
+ initializer: Callable[[], V],
35
+ ttl: int | float | None = None,
36
+ tti: int | float | None = None,
37
+ ) -> V:
38
+ """Lookup or initialize a value for the key.
27
39
 
28
- def get_with(self, key: K, initializer: Callable[[], V]) -> V:
29
- """
30
- Lookups for a key in the cache and only if there is no value set, calls the `initializer`
31
- function to set the key's value.
32
- If multiple threads call `get_with` with the same key, only one of them calls
33
- `initializer`, and the others wait until the value is set.
40
+ If multiple threads call `get_with` with the same key, only one calls `initializer`,
41
+ the others wait until the value is set.
34
42
  """
35
43
 
36
- def remove(self, key: K) -> Optional[V]: ...
37
-
44
+ @overload
45
+ def remove(self, key: K, default: D) -> V | D: ...
46
+ @overload
47
+ def remove(self, key: K, default: D | None = None) -> V | D | None: ...
38
48
  def clear(self) -> None: ...
39
-
40
49
  def count(self) -> int: ...
41
50
 
42
-
43
51
  def cached(
44
- maxsize: int = 128,
45
- typed: bool = False,
46
- *,
47
- ttl: Optional[Union[int, float]] = None,
48
- tti: Optional[Union[int, float]] = None,
49
- wait_concurrent: bool = False,
52
+ maxsize: int = 128,
53
+ typed: bool = False,
54
+ *,
55
+ ttl: int | float | None = None,
56
+ tti: int | float | None = None,
57
+ wait_concurrent: bool = False,
58
+ policy: Policy = "tiny_lfu",
50
59
  ) -> Callable[[Fn], Fn]:
51
- ...
60
+ """Decorator for caching function results in a thread-safe in-memory cache.
61
+
62
+ - If the decorated function is synchronous: returns the cached value or computes and stores it.
63
+ - If the decorated function is asynchronous: returns an awaitable which yields the cached result.
64
+ - If wait_concurrent=True: concurrent calls with the same arguments wait on a single in-flight computation.
65
+ For async functions this is implemented via a shared asyncio.Task; all awaiters receive the same result
66
+ or the same exception.
67
+ """
@@ -0,0 +1,446 @@
1
+ Metadata-Version: 2.4
2
+ Name: moka-py
3
+ Version: 0.3.0
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: Issues, https://github.com/deliro/moka-py/issues
20
+ Project-URL: Repository, https://github.com/deliro/moka-py
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
+ - **Per-entry TTL / TTI:** Override the cache-wide TTL or TTI on individual entries.
32
+ - **Size-based eviction:** Removes items when capacity is exceeded using TinyLFU or LRU.
33
+ - **Concurrency:** Optimized for high-throughput, concurrent access.
34
+ - **Fully typed:** `mypy` and `pyright` friendly.
35
+
36
+ ## Installation
37
+
38
+ Install with `uv`:
39
+
40
+ ```bash
41
+ uv add moka-py
42
+ ```
43
+
44
+ Or with `poetry`:
45
+
46
+ ```bash
47
+ poetry add moka-py
48
+ ```
49
+
50
+ Or with `pip`:
51
+
52
+ ```bash
53
+ pip install moka-py
54
+ ```
55
+
56
+ ## Table of Contents
57
+
58
+ - [Installation](#installation)
59
+ - [Features](#features)
60
+ - [Usage](#usage)
61
+ - [Using moka_py.Moka](#using-moka_pymoka)
62
+ - [Per-entry TTL / TTI](#per-entry-ttl--tti)
63
+ - [@cached decorator](#as-a-decorator)
64
+ - [Async support](#async-support)
65
+ - [Coalesce concurrent calls (wait_concurrent)](#coalesce-concurrent-calls-wait_concurrent)
66
+ - [Eviction listener](#eviction-listener)
67
+ - [Removing entries](#removing-entries)
68
+ - [How it works](#how-it-works)
69
+ - [Eviction policies](#eviction-policies)
70
+ - [Performance](#performance)
71
+ - [License](#license)
72
+
73
+ ## Usage
74
+
75
+ ### Using moka_py.Moka
76
+
77
+ ```python
78
+ from time import sleep
79
+ from moka_py import Moka
80
+
81
+
82
+ # Create a cache with a capacity of 100 entries, with a TTL of 10.0 seconds
83
+ # and a TTI of 0.1 seconds. Entries are always removed after 10 seconds
84
+ # and are removed after 0.1 seconds if there are no `get`s happened for this time.
85
+ #
86
+ # Both TTL and TTI settings are optional. In the absence of an entry,
87
+ # the corresponding policy will not expire it.
88
+
89
+ # The default eviction policy is "tiny_lfu" which is optimal for most workloads,
90
+ # but you can choose "lru" as well.
91
+ cache: Moka[str, list[int]] = Moka(capacity=100, ttl=10.0, tti=0.1, policy="lru")
92
+
93
+ # Insert a value.
94
+ cache.set("key", [3, 2, 1])
95
+
96
+ # Retrieve the value.
97
+ assert cache.get("key") == [3, 2, 1]
98
+
99
+ # Wait for 0.1+ seconds, and the entry will be automatically evicted.
100
+ sleep(0.12)
101
+ assert cache.get("key") is None
102
+ ```
103
+
104
+ ### Per-entry TTL / TTI
105
+
106
+ By default, TTL and TTI are set once for the entire cache. You can also set them
107
+ per entry by passing `ttl` and/or `tti` to `set()` or `get_with()`:
108
+
109
+ ```python
110
+ from time import sleep
111
+ from moka_py import Moka
112
+
113
+
114
+ cache = Moka(100)
115
+
116
+ cache.set("short-lived", "value", ttl=0.5)
117
+ cache.set("session", {"user": "alice"}, ttl=3600.0)
118
+ cache.set("idle-sensitive", "value", tti=1.0)
119
+ cache.set("both", "value", ttl=60.0, tti=5.0)
120
+
121
+ # Entries without per-entry ttl/tti never expire (unless the cache has global settings).
122
+ cache.set("permanent", "value")
123
+
124
+ sleep(0.6)
125
+ assert cache.get("short-lived") is None # expired after 0.5s
126
+ assert cache.get("session") is not None # still alive
127
+ assert cache.get("permanent") is not None
128
+ ```
129
+
130
+ `get_with()` accepts the same parameters:
131
+
132
+ ```python
133
+ from moka_py import Moka
134
+
135
+
136
+ cache = Moka(100)
137
+
138
+ value = cache.get_with("key", lambda: "computed", ttl=30.0)
139
+ ```
140
+
141
+ #### Concurrent `get_with` with different TTL / TTI
142
+
143
+ `get_with()` guarantees that only **one** thread executes the initializer for a given key (stampede protection).
144
+ When multiple threads call `get_with()` for the same key concurrently with **different** `ttl`/`tti` values,
145
+ the thread that wins the race runs its initializer — and its `ttl`/`tti` values are stored with the entry.
146
+ All other threads receive the same cached value and their `ttl`/`tti` parameters are **silently ignored**.
147
+
148
+ ```python
149
+ import threading
150
+ from moka_py import Moka
151
+
152
+
153
+ cache = Moka(100)
154
+
155
+ # Thread A: get_with("k", compute, ttl=1.0)
156
+ # Thread B: get_with("k", compute, ttl=60.0)
157
+ #
158
+ # If thread A wins, the entry expires in 1 second.
159
+ # If thread B wins, the entry expires in 60 seconds.
160
+ # The loser's ttl is discarded — it is NOT merged or compared.
161
+ ```
162
+
163
+ #### Interaction with cache-wide TTL / TTI
164
+
165
+ When the cache is constructed with global `ttl` or `tti` **and** an entry specifies its own, the entry
166
+ expires at whichever deadline comes **first**.
167
+
168
+ > **WARNING**
169
+ >
170
+ > Per-entry TTL / TTI can only make an entry expire **sooner** than the cache-wide
171
+ > policy, not later. This is a technical limitation of the underlying
172
+ > [Moka](https://github.com/moka-rs/moka) library: global and per-entry expiration
173
+ > are evaluated independently, and the earliest deadline wins.
174
+ >
175
+ > If you need entries with different lifetimes that can **exceed** a common default,
176
+ > do not set global `ttl`/`tti` on the cache. Use per-entry values exclusively instead.
177
+
178
+ ```python
179
+ from moka_py import Moka
180
+
181
+ # Do this:
182
+ cache = Moka(1000)
183
+ cache.set("short", "v", ttl=60.0)
184
+ cache.set("long", "v", ttl=300.0) # works as expected
185
+
186
+ # NOT this — "long" will still expire in 60 s:
187
+ cache = Moka(1000, ttl=60.0)
188
+ cache.set("long", "v", ttl=300.0) # capped at 60 s by the global policy
189
+ ```
190
+
191
+ ```python
192
+ from time import sleep
193
+ from moka_py import Moka
194
+
195
+
196
+ # Global TTL of 10 seconds.
197
+ cache = Moka(100, ttl=10.0)
198
+
199
+ # This entry will expire in 0.5 s (per-entry TTL wins, it is shorter).
200
+ cache.set("fast", "value", ttl=0.5)
201
+
202
+ # This entry keeps the global 10 s TTL (per-entry TTL=20 s is longer, so global wins).
203
+ cache.set("slow", "value", ttl=20.0)
204
+
205
+ sleep(0.6)
206
+ assert cache.get("fast") is None
207
+ assert cache.get("slow") is not None
208
+ ```
209
+
210
+ ### As a decorator
211
+
212
+ moka-py can be used as a drop-in replacement for `@lru_cache()` with TTL + TTI support:
213
+
214
+ ```python
215
+ from time import sleep
216
+ from moka_py import cached
217
+
218
+
219
+ calls = []
220
+
221
+
222
+ @cached(maxsize=1024, ttl=5.0, tti=0.05)
223
+ def f(x, y):
224
+ calls.append((x, y))
225
+ return x + y
226
+
227
+
228
+ assert f(1, 2) == 3 # calls computations
229
+ assert f(1, 2) == 3 # gets from the cache
230
+ assert len(calls) == 1
231
+ sleep(0.06)
232
+ assert f(1, 2) == 3 # calls computations again (since TTI has passed)
233
+ assert len(calls) == 2
234
+ ```
235
+
236
+ ### Async support
237
+
238
+ Unlike `@lru_cache()`, `@moka_py.cached()` supports async functions:
239
+
240
+ ```python
241
+ import asyncio
242
+ from time import perf_counter
243
+ from moka_py import cached
244
+
245
+
246
+ calls = []
247
+
248
+
249
+ @cached(maxsize=1024, ttl=5.0, tti=0.1)
250
+ async def f(x, y):
251
+ calls.append((x, y))
252
+ await asyncio.sleep(0.05)
253
+ return x + y
254
+
255
+
256
+ start = perf_counter()
257
+ assert asyncio.run(f(5, 6)) == 11
258
+ assert asyncio.run(f(5, 6)) == 11 # from cache
259
+ elapsed = perf_counter() - start
260
+ assert elapsed < 0.2
261
+ assert len(calls) == 1
262
+ ```
263
+
264
+ ### Coalesce concurrent calls (wait_concurrent)
265
+
266
+ `moka-py` can synchronize threads on keys
267
+
268
+ ```python
269
+ import moka_py
270
+ from typing import Any
271
+ from time import sleep
272
+ import threading
273
+ from decimal import Decimal
274
+
275
+
276
+ calls = []
277
+
278
+
279
+ @moka_py.cached(ttl=5, wait_concurrent=True)
280
+ def get_user(id_: int) -> dict[str, Any]:
281
+ calls.append(id_)
282
+ sleep(0.02) # simulate an HTTP request (short for tests)
283
+ return {
284
+ "id": id_,
285
+ "first_name": "Jack",
286
+ "last_name": "Pot",
287
+ }
288
+
289
+
290
+ def process_request(path: str, user_id: int) -> None:
291
+ user = get_user(user_id)
292
+ ...
293
+
294
+
295
+ def charge_money(from_user_id: int, amount: Decimal) -> None:
296
+ user = get_user(from_user_id)
297
+ ...
298
+
299
+
300
+ if __name__ == '__main__':
301
+ request_processing = threading.Thread(target=process_request, args=("/user/info/123", 123))
302
+ money_charging = threading.Thread(target=charge_money, args=(123, Decimal("3.14")))
303
+ request_processing.start()
304
+ money_charging.start()
305
+ request_processing.join()
306
+ money_charging.join()
307
+
308
+ # Only one call occurred. Without `wait_concurrent`, each thread would issue its own HTTP request
309
+ # before the cache entry is set.
310
+ assert len(calls) == 1
311
+ ```
312
+
313
+ ### Async wait_concurrent
314
+
315
+ When using `wait_concurrent=True` with async functions, `moka-py` creates a shared `asyncio.Task` per cache key. All
316
+ concurrent callers `await` the same task and receive the same result or exception. This eliminates duplicate in-flight
317
+ work for identical arguments.
318
+
319
+ ### Eviction listener
320
+
321
+ `moka-py` supports an eviction listener, called whenever a key is removed.
322
+ The listener must be a three-argument function `(key, value, cause)` and uses positional arguments only.
323
+
324
+ Possible reasons:
325
+
326
+ 1. `"expired"`: The entry's expiration timestamp has passed.
327
+ 2. `"explicit"`: The entry was manually removed by the user (`.remove()` is called).
328
+ 3. `"replaced"`: The entry itself was not actually removed, but its value was replaced by the user (`.set()` is
329
+ called for an existing entry).
330
+ 4. `"size"`: The entry was evicted due to size constraints.
331
+
332
+ ```python
333
+ from typing import Literal
334
+ from moka_py import Moka
335
+ from time import sleep
336
+
337
+
338
+ def key_evicted(
339
+ k: str,
340
+ v: list[int],
341
+ cause: Literal["explicit", "size", "expired", "replaced"]
342
+ ):
343
+ events.append((k, v, cause))
344
+
345
+
346
+ events: list[tuple[str, list[int], str]] = []
347
+
348
+
349
+ moka: Moka[str, list[int]] = Moka(2, eviction_listener=key_evicted, ttl=0.5)
350
+ moka.set("hello", [1, 2, 3])
351
+ moka.set("hello", [3, 2, 1]) # replaced
352
+ moka.set("foo", [4]) # expired
353
+ moka.set("baz", "size")
354
+ moka.remove("foo") # explicit
355
+ sleep(1.0)
356
+ moka.get("anything") # this will trigger eviction for expired
357
+
358
+ causes = {c for _, _, c in events}
359
+ assert causes == {"size", "expired", "replaced", "explicit"}, events
360
+ ```
361
+
362
+ > IMPORTANT NOTES
363
+ > 1) The listener is not called just-in-time. `moka` has no background threads or tasks; it runs only during cache operations.
364
+ > 2) The listener must not raise exceptions. If it does, the exception may surface from any `moka-py` method on any thread.
365
+ > 3) Keep the listener fast. Heavy work (especially I/O) will slow `.get()`, `.set()`, etc. Offload via `ThreadPoolExecutor.submit()` or `asyncio.create_task()`
366
+ > 4) **Per-entry TTL / TTI and the eviction listener.** Per-entry expiry fires the
367
+ > listener with `"expired"` just like global TTL/TTI does. The notification is
368
+ > delivered lazily during subsequent cache operations (e.g. `get`, `set`) after
369
+ > the per-entry deadline passes — it is not instant.
370
+
371
+ ### Removing entries
372
+
373
+ Remove an entry with `Moka.remove(key)`. It returns the previous value if present; otherwise `None`.
374
+
375
+ ```python
376
+ from moka_py import Moka
377
+
378
+
379
+ c = Moka(128)
380
+ c.set("hello", "world")
381
+ assert c.remove("hello") == "world"
382
+ assert c.get("hello") is None
383
+ ```
384
+
385
+ If `None` is a valid cached value, distinguish it from absence using `Moka.remove(key, default=...)`:
386
+
387
+ ```python
388
+ from moka_py import Moka
389
+
390
+
391
+ c = Moka(128)
392
+ c.set("hello", None)
393
+ assert c.remove("hello", default="WAS_NOT_SET") is None # None was set explicitly
394
+
395
+ # Now the entry "hello" does not exist, so `default` is returned
396
+ assert c.remove("hello", default="WAS_NOT_SET") == "WAS_NOT_SET"
397
+ ```
398
+
399
+ ## How it works
400
+
401
+ `Moka` stores Python object references
402
+ (by [`Py_INCREF`](https://docs.python.org/3/c-api/refcounting.html#c.Py_INCREF)) and does not serialize or deserialize values.
403
+ You can use any Python object as a value and any hashable object as a key (`__hash__` is used).
404
+ Mutable objects remain mutable:
405
+
406
+ ```python
407
+ from moka_py import Moka
408
+
409
+
410
+ c = Moka(128)
411
+ my_list = [1, 2, 3]
412
+ c.set("hello", my_list)
413
+ still_the_same = c.get("hello")
414
+ still_the_same.append(4)
415
+ assert my_list == [1, 2, 3, 4]
416
+ ```
417
+
418
+ ## Eviction policies
419
+
420
+ `moka-py` uses TinyLFU by default, with an LRU option. Learn more in the
421
+ [Moka wiki](https://github.com/moka-rs/moka/wiki#admission-and-eviction-policies).
422
+
423
+ ## Performance
424
+
425
+ *Measured using MacBook Pro 14-inch, Nov 2024 with Apple M4 Pro processor and 24GiB RAM*
426
+
427
+ ```
428
+ -------------------------------------------------------------------------------------------- benchmark: 9 tests -------------------------------------------------------------------------------------------
429
+ Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Mops/s) Rounds Iterations
430
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
431
+ test_bench_remove 68.1140 (1.0) 68.2812 (1.0) 68.1806 (1.0) 0.0671 (1.0) 68.1621 (1.0) 0.1000 (1.0) 1;0 14.6669 (1.0) 5 10000000
432
+ test_bench_get[lru-False] 77.5126 (1.14) 78.2797 (1.15) 77.7823 (1.14) 0.2947 (4.39) 77.6792 (1.14) 0.2913 (2.91) 1;0 12.8564 (0.88) 5 10000000
433
+ test_bench_get[tiny_lfu-False] 78.0985 (1.15) 78.8168 (1.15) 78.4920 (1.15) 0.2678 (3.99) 78.4868 (1.15) 0.3429 (3.43) 2;0 12.7401 (0.87) 5 10000000
434
+ test_bench_get[lru-True] 89.1512 (1.31) 89.6459 (1.31) 89.4480 (1.31) 0.1910 (2.85) 89.5190 (1.31) 0.2458 (2.46) 2;0 11.1797 (0.76) 5 10000000
435
+ test_bench_get[tiny_lfu-True] 91.4891 (1.34) 91.9214 (1.35) 91.6827 (1.34) 0.1867 (2.78) 91.7339 (1.35) 0.3141 (3.14) 2;0 10.9072 (0.74) 5 10000000
436
+ test_bench_get_with 137.0672 (2.01) 137.8738 (2.02) 137.4143 (2.02) 0.3182 (4.74) 137.2839 (2.01) 0.4530 (4.53) 2;0 7.2773 (0.50) 5 10000000
437
+ test_bench_set_str_key 354.1709 (5.20) 355.5768 (5.21) 354.9073 (5.21) 0.5631 (8.39) 355.0415 (5.21) 0.8900 (8.90) 2;0 2.8176 (0.19) 5 1408297
438
+ test_bench_set[tiny_lfu] 355.6927 (5.22) 356.9633 (5.23) 356.3647 (5.23) 0.5645 (8.41) 356.4059 (5.23) 1.0390 (10.40) 2;0 2.8061 (0.19) 5 1405450
439
+ test_bench_set[lru] 388.7005 (5.71) 389.5825 (5.71) 389.1170 (5.71) 0.3837 (5.72) 389.0796 (5.71) 0.6915 (6.92) 2;0 2.5699 (0.18) 5 1295615
440
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
441
+ ```
442
+
443
+ ## License
444
+
445
+ `moka-py` is distributed under the [MIT license](LICENSE).
446
+
@@ -0,0 +1,8 @@
1
+ moka_py/__init__.py,sha256=WMkLJ34xVZ9eEnWsnfkOYxM_cWbo3IztW_3gUvmJ2V0,2318
2
+ moka_py/__init__.pyi,sha256=1VMXf8_CKuik1EGAJzJ04kN0BgSZyPlxz7ceSKsTO5w,2297
3
+ moka_py/moka_py.cpython-39-s390x-linux-gnu.so,sha256=2vGOYq5nDZrU6AFqy552WtElfHrn0HQgS6vPOdkr98E,705664
4
+ moka_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ moka_py-0.3.0.dist-info/METADATA,sha256=cbEf4W_gAb5iDndFWBTdVZ_ApdxrhoGdGsAaxDFUZKU,15095
6
+ moka_py-0.3.0.dist-info/WHEEL,sha256=XcaP7mD6aXEAQuMVCzXaOXD_TDNfWD8zFnptryRUioM,141
7
+ moka_py-0.3.0.dist-info/licenses/LICENSE,sha256=CUj5ca53JXgIACVKNEOFOlbMWtxY4RXXj9cELIv2R04,1069
8
+ moka_py-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-cp39-manylinux_2_17_s390x
5
+ Tag: cp39-cp39-manylinux2014_s390x
@@ -1,224 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: moka-py
3
- Version: 0.1.13
4
- Classifier: Programming Language :: Rust
5
- Classifier: Programming Language :: Python :: Implementation :: CPython
6
- Classifier: Programming Language :: Python :: Implementation :: PyPy
7
- License-File: LICENSE
8
- Requires-Python: >=3.9
9
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
10
-
11
- # moka-py
12
-
13
- * * *
14
-
15
- **moka-py** is a Python binding for the highly efficient [Moka](https://github.com/moka-rs/moka) caching library written
16
- in Rust. This library allows you to leverage the power of Moka's high-performance, feature-rich cache in your Python
17
- projects.
18
-
19
- ## Features
20
-
21
- - **Synchronous Cache:** Supports thread-safe, in-memory caching for Python applications.
22
- - **TTL Support:** Automatically evicts entries after a configurable time-to-live (TTL).
23
- - **TTI Support:** Automatically evicts entries after a configurable time-to-idle (TTI).
24
- - **Size-based Eviction:** Automatically removes items when the cache exceeds its size limit using the TinyLFU policy.
25
- - **Concurrency:** Optimized for high-performance, concurrent access in multi-threaded environments.
26
-
27
- ## Installation
28
-
29
- You can install `moka-py` using `pip`:
30
-
31
- ```bash
32
- pip install moka-py
33
- ```
34
-
35
- ## Quick Start
36
-
37
- ```python
38
- from time import sleep
39
- from moka_py import Moka
40
-
41
-
42
- # Create a cache with a capacity of 100 entries, with a TTL of 30 seconds
43
- # and a TTI of 5.2 seconds. Entries are always removed after 30 seconds
44
- # and are removed after 5.2 seconds if there are no `get`s happened for this time.
45
- #
46
- # Both TTL and TTI settings are optional. In the absence of an entry,
47
- # the corresponding policy will not expire it.
48
- cache: Moka[str, list[int]] = Moka(capacity=100, ttl=30, tti=5.2)
49
-
50
- # Insert a value.
51
- cache.set("key", [3, 2, 1])
52
-
53
- # Retrieve the value.
54
- assert cache.get("key") == [3, 2, 1]
55
-
56
- # Wait for 5.2+ seconds, and the entry will be automatically evicted.
57
- sleep(5.3)
58
- assert cache.get("key") is None
59
- ```
60
-
61
- moka-py can be used as a drop-in replacement for `@lru_cache()` with TTL + TTI support:
62
-
63
- ```python
64
- from time import sleep
65
- from moka_py import cached
66
-
67
-
68
- @cached(maxsize=1024, ttl=10.0, tti=1.0)
69
- def f(x, y):
70
- print("hard computations")
71
- return x + y
72
-
73
-
74
- f(1, 2) # calls computations
75
- f(1, 2) # gets from the cache
76
- sleep(1.1)
77
- f(1, 2) # calls computations (since TTI has passed)
78
- ```
79
-
80
- But unlike `@lru_cache()`, `@moka_py.cached()` also supports async functions:
81
-
82
- ```python
83
- import asyncio
84
- from time import perf_counter
85
- from moka_py import cached
86
-
87
-
88
- @cached(maxsize=1024, ttl=10.0, tti=1.0)
89
- async def f(x, y):
90
- print("http request happening")
91
- await asyncio.sleep(2.0)
92
- return x + y
93
-
94
-
95
- start = perf_counter()
96
- assert asyncio.run(f(5, 6)) == 11
97
- assert asyncio.run(f(5, 6)) == 11 # got from cache
98
- assert perf_counter() - start < 4.0
99
- ```
100
-
101
- moka-py can synchronize threads on keys
102
-
103
- ```python
104
- import moka_py
105
- from typing import Any
106
- from time import sleep
107
- import threading
108
- from decimal import Decimal
109
-
110
-
111
- calls = []
112
-
113
-
114
- @moka_py.cached(ttl=5, wait_concurrent=True)
115
- def get_user(id_: int) -> dict[str, Any]:
116
- calls.append(id_)
117
- sleep(0.3) # simulation of HTTP request
118
- return {
119
- "id": id_,
120
- "first_name": "Jack",
121
- "last_name": "Pot",
122
- }
123
-
124
-
125
- def process_request(path: str, user_id: int) -> None:
126
- user = get_user(user_id)
127
- print(f"user #{user_id} came to {path}, their info is {user}")
128
- ...
129
-
130
-
131
- def charge_money(from_user_id: int, amount: Decimal) -> None:
132
- user = get_user(from_user_id)
133
- print(f"charging {amount} money from user #{from_user_id} ({user['first_name']} {user['last_name']})")
134
- ...
135
-
136
-
137
- if __name__ == '__main__':
138
- request_processing = threading.Thread(target=process_request, args=("/user/info/123", 123))
139
- money_charging = threading.Thread(target=charge_money, args=(123, Decimal("3.14")))
140
- request_processing.start()
141
- money_charging.start()
142
- request_processing.join()
143
- money_charging.join()
144
-
145
- # only one call occurred. without the `wait_concurrent` option, each thread would go for an HTTP request
146
- # since no cache key was set
147
- assert len(calls) == 1
148
- ```
149
-
150
- > **_ATTENTION:_** `wait_concurrent` is not yet supported for async functions and will throw `NotImplementedError`
151
-
152
- ## Eviction listener
153
-
154
- moka-py supports adding of an eviction listener that's called whenever a key is dropped
155
- from the cache for some reason. The listener must be a 3-arguments function `(key, value, cause)`. The arguments
156
- are passed as positional (not keyword).
157
-
158
- There are 4 reasons why a key may be dropped:
159
-
160
- 1. `"expired"`: The entry's expiration timestamp has passed.
161
- 2. `"explicit"`: The entry was manually removed by the user (`.remove()` is called).
162
- 3. `"replaced"`: The entry itself was not actually removed, but its value was replaced by the user (`.set()` is
163
- called for an existing entry).
164
- 4. `"size"`: The entry was evicted due to size constraints.
165
-
166
- ```python
167
- from typing import Literal
168
- from moka_py import Moka
169
- from time import sleep
170
-
171
-
172
- def key_evicted(
173
- k: str,
174
- v: list[int],
175
- cause: Literal["explicit", "size", "expired", "replaced"]
176
- ):
177
- print(f"entry {k}:{v} was evicted. {cause=}")
178
-
179
-
180
- moka: Moka[str, list[int]] = Moka(2, eviction_listener=key_evicted, ttl=0.1)
181
- moka.set("hello", [1, 2, 3])
182
- moka.set("hello", [3, 2, 1])
183
- moka.set("foo", [4])
184
- moka.set("bar", [])
185
- sleep(1)
186
- moka.get("foo")
187
-
188
- # will print
189
- # entry hello:[1, 2, 3] was evicted. cause='replaced'
190
- # entry bar:[] was evicted. cause='size'
191
- # entry hello:[3, 2, 1] was evicted. cause='expired'
192
- # entry foo:[4] was evicted. cause='expired'
193
- ```
194
-
195
- > **_IMPORTANT NOTES_**:
196
- > 1. It's not guaranteed that the listener will be called just in time. Also, the underlying `moka` doesn't use any
197
- background threads or tasks, hence, the listener is never called in "background"
198
- > 2. The listener must never raise any kind of `Exception`. If an exception is raised, it might be raised to any of the
199
- moka-py method in any of the threads that call this method.
200
- > 3. The listener must be fast. Since it's called only when you're interacting with `moka-py` (via `.get()` / `.set()` /
201
- etc.), the listener will slow down these operations. It's terrible idea to do some sort of IO in the listener. If
202
- you need so, run a `ThreadPoolExecutor` somewhere and call `.submit()` inside of the listener or commit an async
203
- task via `asyncio.create_task()`
204
-
205
- ## Performance
206
-
207
- *Measured using MacBook Pro 2021 with Apple M1 Pro processor and 16GiB RAM*
208
-
209
- ```
210
- ------------------------------------------------------------------------------------------- benchmark: 5 tests -------------------------------------------------------------------------------------------
211
- Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Mops/s) Rounds Iterations
212
- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
213
- test_bench_get_non_existent 206.3389 (1.0) 208.9872 (1.0) 207.0240 (1.0) 1.1154 (4.27) 206.5119 (1.0) 0.9932 (2.73) 1;1 4.8304 (1.0) 5 10000000
214
- test_bench_get 224.4981 (1.09) 229.1849 (1.10) 225.8305 (1.09) 1.9252 (7.37) 224.9832 (1.09) 1.8345 (5.05) 1;0 4.4281 (0.92) 5 10000000
215
- test_bench_get_with 248.2484 (1.20) 248.9123 (1.19) 248.5142 (1.20) 0.2612 (1.0) 248.5172 (1.20) 0.3634 (1.0) 2;0 4.0239 (0.83) 5 2020760
216
- test_bench_set_huge 676.6090 (3.28) 692.0143 (3.31) 683.5817 (3.30) 6.5151 (24.94) 684.8168 (3.32) 10.9585 (30.16) 2;0 1.4629 (0.30) 5 1000000
217
- test_bench_set 723.4063 (3.51) 770.0967 (3.68) 738.1940 (3.57) 18.5167 (70.89) 733.0997 (3.55) 18.1077 (49.83) 1;0 1.3547 (0.28) 5 1000000
218
- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
219
- ```
220
-
221
- ## License
222
-
223
- moka-py is distributed under the [MIT license](LICENSE)
224
-
@@ -1,8 +0,0 @@
1
- moka_py-0.1.13.dist-info/METADATA,sha256=0Ja8eVRcbkV6vATVmnrJRTBVJ4355CUsXQXV2Q10fnc,8331
2
- moka_py-0.1.13.dist-info/WHEEL,sha256=N-TSPwwwA2EqYtB-qCH5JjcTDipiRUuZBsjy1JM4bA4,125
3
- moka_py-0.1.13.dist-info/licenses/LICENSE,sha256=CUj5ca53JXgIACVKNEOFOlbMWtxY4RXXj9cELIv2R04,1069
4
- moka_py/__init__.py,sha256=7P0yGnJ8hNt5Hg_ndBEidy-8YWKyh6WhViNbBt7DOhI,1530
5
- moka_py/__init__.pyi,sha256=rlvQzwMrub2jTK8Mtr0tH246Fp86_oC1WZkZEYg9DRk,1534
6
- moka_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- moka_py/moka_py.cpython-39-s390x-linux-gnu.so,sha256=ExCwkun00Cmjn_j-pcBFB49lc09hemQ4jzbeiittT60,1059592
8
- moka_py-0.1.13.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: maturin (1.7.8)
3
- Root-Is-Purelib: false
4
- Tag: cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x