moka-py 0.1.17__cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl → 0.3.0__cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.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 +38 -13
- moka_py/__init__.pyi +44 -35
- moka_py/moka_py.cpython-313t-powerpc64le-linux-gnu.so +0 -0
- moka_py-0.3.0.dist-info/METADATA +446 -0
- moka_py-0.3.0.dist-info/RECORD +8 -0
- moka_py-0.3.0.dist-info/WHEEL +5 -0
- moka_py-0.1.17.dist-info/METADATA +0 -323
- moka_py-0.1.17.dist-info/RECORD +0 -8
- moka_py-0.1.17.dist-info/WHEEL +0 -4
- {moka_py-0.1.17.dist-info → moka_py-0.3.0.dist-info}/licenses/LICENSE +0 -0
moka_py/__init__.py
CHANGED
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
import asyncio as _asyncio
|
|
2
|
-
|
|
3
|
-
from
|
|
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__ = ["
|
|
10
|
+
__all__ = ["VERSION", "Moka", "cached"]
|
|
7
11
|
|
|
8
12
|
VERSION = _get_version()
|
|
9
13
|
|
|
10
14
|
|
|
11
|
-
def cached(
|
|
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
|
+
"""
|
|
12
30
|
cache = Moka(maxsize, ttl=ttl, tti=tti, policy=policy)
|
|
13
31
|
empty = object()
|
|
14
32
|
|
|
15
33
|
def dec(fn):
|
|
16
|
-
if
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,5 +1,5 @@
|
|
|
1
|
-
from
|
|
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")
|
|
@@ -8,51 +8,60 @@ Fn = TypeVar("Fn", bound=Callable[..., Any])
|
|
|
8
8
|
Cause = Literal["explicit", "size", "expired", "replaced"]
|
|
9
9
|
Policy = Literal["tiny_lfu", "lru"]
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
class Moka(Generic[K, V]):
|
|
13
12
|
def __init__(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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",
|
|
20
19
|
): ...
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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: ...
|
|
24
27
|
@overload
|
|
25
|
-
def get(self, key: K, default: D) ->
|
|
26
|
-
|
|
28
|
+
def get(self, key: K, default: D) -> V | D: ...
|
|
27
29
|
@overload
|
|
28
|
-
def get(self, key: K, default:
|
|
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.
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Lookups for a key in the cache and only if there is no value set, calls the `initializer`
|
|
33
|
-
function to set the key's value.
|
|
34
|
-
If multiple threads call `get_with` with the same key, only one of them calls
|
|
35
|
-
`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.
|
|
36
42
|
"""
|
|
37
43
|
|
|
38
44
|
@overload
|
|
39
|
-
def remove(self, key: K, default: D) ->
|
|
40
|
-
|
|
45
|
+
def remove(self, key: K, default: D) -> V | D: ...
|
|
41
46
|
@overload
|
|
42
|
-
def remove(self, key: K, default:
|
|
43
|
-
|
|
47
|
+
def remove(self, key: K, default: D | None = None) -> V | D | None: ...
|
|
44
48
|
def clear(self) -> None: ...
|
|
45
|
-
|
|
46
49
|
def count(self) -> int: ...
|
|
47
50
|
|
|
48
|
-
|
|
49
51
|
def cached(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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",
|
|
57
59
|
) -> Callable[[Fn], Fn]:
|
|
58
|
-
|
|
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
|
+
"""
|
|
Binary file
|
|
@@ -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-313t-powerpc64le-linux-gnu.so,sha256=IFqxRZofwsfXB9zVv_aeAjXjtkigaKaQfyLmQB5ge3E,726040
|
|
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=2yo4KOUoBa6tMWvxk21okE8Ycb5W22kL8Ukm23Q3bcA,151
|
|
7
|
+
moka_py-0.3.0.dist-info/licenses/LICENSE,sha256=CUj5ca53JXgIACVKNEOFOlbMWtxY4RXXj9cELIv2R04,1069
|
|
8
|
+
moka_py-0.3.0.dist-info/RECORD,,
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: moka-py
|
|
3
|
-
Version: 0.1.17
|
|
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 TinyLFU or LRU
|
|
25
|
-
policy.
|
|
26
|
-
- **Concurrency:** Optimized for high-performance, concurrent access in multithreaded environments.
|
|
27
|
-
- **Fully typed:** `mypy` and `pyright` friendly.
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
30
|
-
|
|
31
|
-
You can install `moka-py` using `uv`:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
uv add moka-py
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`poetry`:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
poetry add moka-py
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Or, if you still stick to `pip` for some reason:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
pip install moka-py
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Table of Contents
|
|
50
|
-
|
|
51
|
-
- [Installation](#installation)
|
|
52
|
-
- [Features](#features)
|
|
53
|
-
- [Usage](#usage)
|
|
54
|
-
- [Using moka_py.Moka](#using-moka_pymoka)
|
|
55
|
-
- [@cached decorator](#as-a-decorator)
|
|
56
|
-
- [async support](#async-support)
|
|
57
|
-
- [Do not call a function if another function is in progress](#do-not-call-a-function-if-another-function-is-in-progress)
|
|
58
|
-
- [Eviction listener](#eviction-listener)
|
|
59
|
-
- [Removing entries](#removing-entries)
|
|
60
|
-
- [How it works](#how-it-works)
|
|
61
|
-
- [Eviction policies](#eviction-policies)
|
|
62
|
-
- [Performance](#performance)
|
|
63
|
-
- [License](#license)
|
|
64
|
-
|
|
65
|
-
## Usage
|
|
66
|
-
|
|
67
|
-
### Using moka_py.Moka
|
|
68
|
-
|
|
69
|
-
```python
|
|
70
|
-
from time import sleep
|
|
71
|
-
from moka_py import Moka
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Create a cache with a capacity of 100 entries, with a TTL of 30 seconds
|
|
75
|
-
# and a TTI of 5.2 seconds. Entries are always removed after 30 seconds
|
|
76
|
-
# and are removed after 5.2 seconds if there are no `get`s happened for this time.
|
|
77
|
-
#
|
|
78
|
-
# Both TTL and TTI settings are optional. In the absence of an entry,
|
|
79
|
-
# the corresponding policy will not expire it.
|
|
80
|
-
|
|
81
|
-
# The default eviction policy is "tiny_lfu" which is optimal for most workloads,
|
|
82
|
-
# but you can choose "lru" as well.
|
|
83
|
-
cache: Moka[str, list[int]] = Moka(capacity=100, ttl=30, tti=5.2, policy="lru")
|
|
84
|
-
|
|
85
|
-
# Insert a value.
|
|
86
|
-
cache.set("key", [3, 2, 1])
|
|
87
|
-
|
|
88
|
-
# Retrieve the value.
|
|
89
|
-
assert cache.get("key") == [3, 2, 1]
|
|
90
|
-
|
|
91
|
-
# Wait for 5.2+ seconds, and the entry will be automatically evicted.
|
|
92
|
-
sleep(5.3)
|
|
93
|
-
assert cache.get("key") is None
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### As a decorator
|
|
97
|
-
|
|
98
|
-
moka-py can be used as a drop-in replacement for `@lru_cache()` with TTL + TTI support:
|
|
99
|
-
|
|
100
|
-
```python
|
|
101
|
-
from time import sleep
|
|
102
|
-
from moka_py import cached
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@cached(maxsize=1024, ttl=10.0, tti=1.0)
|
|
106
|
-
def f(x, y):
|
|
107
|
-
print("hard computations")
|
|
108
|
-
return x + y
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
f(1, 2) # calls computations
|
|
112
|
-
f(1, 2) # gets from the cache
|
|
113
|
-
sleep(1.1)
|
|
114
|
-
f(1, 2) # calls computations (since TTI has passed)
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Async support
|
|
118
|
-
|
|
119
|
-
Unlike `@lru_cache()`, `@moka_py.cached()` supports async functions:
|
|
120
|
-
|
|
121
|
-
```python
|
|
122
|
-
import asyncio
|
|
123
|
-
from time import perf_counter
|
|
124
|
-
from moka_py import cached
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@cached(maxsize=1024, ttl=10.0, tti=1.0)
|
|
128
|
-
async def f(x, y):
|
|
129
|
-
print("http request happening")
|
|
130
|
-
await asyncio.sleep(2.0)
|
|
131
|
-
return x + y
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
start = perf_counter()
|
|
135
|
-
assert asyncio.run(f(5, 6)) == 11
|
|
136
|
-
assert asyncio.run(f(5, 6)) == 11 # got from cache
|
|
137
|
-
assert perf_counter() - start < 4.0
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Do not call a function if another function is in progress
|
|
141
|
-
|
|
142
|
-
moka-py can synchronize threads on keys
|
|
143
|
-
|
|
144
|
-
```python
|
|
145
|
-
import moka_py
|
|
146
|
-
from typing import Any
|
|
147
|
-
from time import sleep
|
|
148
|
-
import threading
|
|
149
|
-
from decimal import Decimal
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
calls = []
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@moka_py.cached(ttl=5, wait_concurrent=True)
|
|
156
|
-
def get_user(id_: int) -> dict[str, Any]:
|
|
157
|
-
calls.append(id_)
|
|
158
|
-
sleep(0.3) # simulation of HTTP request
|
|
159
|
-
return {
|
|
160
|
-
"id": id_,
|
|
161
|
-
"first_name": "Jack",
|
|
162
|
-
"last_name": "Pot",
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def process_request(path: str, user_id: int) -> None:
|
|
167
|
-
user = get_user(user_id)
|
|
168
|
-
print(f"user #{user_id} came to {path}, their info is {user}")
|
|
169
|
-
...
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def charge_money(from_user_id: int, amount: Decimal) -> None:
|
|
173
|
-
user = get_user(from_user_id)
|
|
174
|
-
print(f"charging {amount} money from user #{from_user_id} ({user['first_name']} {user['last_name']})")
|
|
175
|
-
...
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == '__main__':
|
|
179
|
-
request_processing = threading.Thread(target=process_request, args=("/user/info/123", 123))
|
|
180
|
-
money_charging = threading.Thread(target=charge_money, args=(123, Decimal("3.14")))
|
|
181
|
-
request_processing.start()
|
|
182
|
-
money_charging.start()
|
|
183
|
-
request_processing.join()
|
|
184
|
-
money_charging.join()
|
|
185
|
-
|
|
186
|
-
# only one call occurred. without the `wait_concurrent` option, each thread would go for an HTTP request
|
|
187
|
-
# since no cache key was set
|
|
188
|
-
assert len(calls) == 1
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
> **_ATTENTION:_** `wait_concurrent` is not yet supported for async functions and will throw `NotImplementedError`
|
|
192
|
-
|
|
193
|
-
### Eviction listener
|
|
194
|
-
|
|
195
|
-
moka-py supports adding of an eviction listener that's called whenever a key is dropped
|
|
196
|
-
from the cache for some reason. The listener must be a 3-arguments function `(key, value, cause)`. The arguments
|
|
197
|
-
are passed as positional (not keyword).
|
|
198
|
-
|
|
199
|
-
There are 4 reasons why a key may be dropped:
|
|
200
|
-
|
|
201
|
-
1. `"expired"`: The entry's expiration timestamp has passed.
|
|
202
|
-
2. `"explicit"`: The entry was manually removed by the user (`.remove()` is called).
|
|
203
|
-
3. `"replaced"`: The entry itself was not actually removed, but its value was replaced by the user (`.set()` is
|
|
204
|
-
called for an existing entry).
|
|
205
|
-
4. `"size"`: The entry was evicted due to size constraints.
|
|
206
|
-
|
|
207
|
-
```python
|
|
208
|
-
from typing import Literal
|
|
209
|
-
from moka_py import Moka
|
|
210
|
-
from time import sleep
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def key_evicted(
|
|
214
|
-
k: str,
|
|
215
|
-
v: list[int],
|
|
216
|
-
cause: Literal["explicit", "size", "expired", "replaced"]
|
|
217
|
-
):
|
|
218
|
-
print(f"entry {k}:{v} was evicted. {cause=}")
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
moka: Moka[str, list[int]] = Moka(2, eviction_listener=key_evicted, ttl=0.1)
|
|
222
|
-
moka.set("hello", [1, 2, 3])
|
|
223
|
-
moka.set("hello", [3, 2, 1])
|
|
224
|
-
moka.set("foo", [4])
|
|
225
|
-
moka.set("bar", [])
|
|
226
|
-
sleep(1)
|
|
227
|
-
moka.get("foo")
|
|
228
|
-
|
|
229
|
-
# will print
|
|
230
|
-
# entry hello:[1, 2, 3] was evicted. cause='replaced'
|
|
231
|
-
# entry bar:[] was evicted. cause='size'
|
|
232
|
-
# entry hello:[3, 2, 1] was evicted. cause='expired'
|
|
233
|
-
# entry foo:[4] was evicted. cause='expired'
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
> **_IMPORTANT NOTES_**:
|
|
237
|
-
> 1. It's not guaranteed that the listener will be called just in time. Also, the underlying `moka` doesn't use any
|
|
238
|
-
background threads or tasks, hence, the listener is never called in "background"
|
|
239
|
-
> 2. The listener must never raise any kind of `Exception`. If an exception is raised, it might be raised to any of the
|
|
240
|
-
moka-py method in any of the threads that call this method.
|
|
241
|
-
> 3. The listener must be fast. Since it's called only when you're interacting with `moka-py` (via `.get()` / `.set()` /
|
|
242
|
-
etc.), the listener will slow down these operations. It's terrible idea to do some sort of IO in the listener. If
|
|
243
|
-
you need so, run a `ThreadPoolExecutor` somewhere and call `.submit()` inside of the listener or commit an async
|
|
244
|
-
task via `asyncio.create_task()`
|
|
245
|
-
|
|
246
|
-
### Removing entries
|
|
247
|
-
|
|
248
|
-
An entry can be removed using `Moka.remove(key)`. If a value was set, it is returned; otherwise, `None` is returned.
|
|
249
|
-
|
|
250
|
-
```python
|
|
251
|
-
from moka_py import Moka
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
c = Moka(128)
|
|
255
|
-
c.set("hello", "world")
|
|
256
|
-
assert c.remove("hello") == "world"
|
|
257
|
-
assert c.get("hello") is None
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
In some cases you may want `None`s to be a valid cache value. In this case you need to distinguish between `None` as a
|
|
261
|
-
value and `None` as the absence of a value. Use `Moka.remove(key, default=...)`:
|
|
262
|
-
|
|
263
|
-
```python
|
|
264
|
-
from moka_py import Moka
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
c = Moka(128)
|
|
268
|
-
c.set("hello", None)
|
|
269
|
-
assert c.remove("hello", default="WAS_NOT_SET") is None # None is returned since is was set
|
|
270
|
-
|
|
271
|
-
# Now entry with key "hello" doesn't exist so `default` argument is returned
|
|
272
|
-
assert c.remove("hello", default="WAS_NOT_SET") == "WAS_NOT_SET"
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## How it works
|
|
276
|
-
|
|
277
|
-
`Moka` object stores Python object references
|
|
278
|
-
(by [`INCREF`ing](https://docs.python.org/3/c-api/refcounting.html#c.Py_INCREF) `PyObject`s) and doesn't use
|
|
279
|
-
serialization or deserialization. This means you can use any Python object as a value and any Hashable object as a
|
|
280
|
-
key (`Moka` calls keys' `__hash__` magic methods). But also you need to remember that mutable objects stored in `Moka`
|
|
281
|
-
are still mutable:
|
|
282
|
-
|
|
283
|
-
```python
|
|
284
|
-
from moka_py import Moka
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
c = Moka(128)
|
|
288
|
-
my_list = [1, 2, 3]
|
|
289
|
-
c.set("hello", my_list)
|
|
290
|
-
still_the_same = c.get("hello")
|
|
291
|
-
still_the_same.append(4)
|
|
292
|
-
assert my_list == [1, 2, 3, 4]
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Eviction policies
|
|
296
|
-
|
|
297
|
-
moka-py uses the TinyLFU eviction policy as default, with LRU option. You can learn more about the
|
|
298
|
-
policies [here](https://github.com/moka-rs/moka/wiki#admission-and-eviction-policies)
|
|
299
|
-
|
|
300
|
-
## Performance
|
|
301
|
-
|
|
302
|
-
*Measured using MacBook Pro 2021 with Apple M1 Pro processor and 16GiB RAM*
|
|
303
|
-
|
|
304
|
-
```
|
|
305
|
-
-------------------------------------------------------------------------------------------- benchmark: 9 tests -------------------------------------------------------------------------------------------
|
|
306
|
-
Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Mops/s) Rounds Iterations
|
|
307
|
-
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
308
|
-
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
|
|
309
|
-
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
|
|
310
|
-
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
|
|
311
|
-
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
|
|
312
|
-
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
|
|
313
|
-
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
|
|
314
|
-
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
|
|
315
|
-
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
|
|
316
|
-
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
|
|
317
|
-
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## License
|
|
321
|
-
|
|
322
|
-
moka-py is distributed under the [MIT license](LICENSE)
|
|
323
|
-
|
moka_py-0.1.17.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
moka_py-0.1.17.dist-info/METADATA,sha256=rEdC2hZGxuIcXCOGHmqgNGucFYjNJdFjzoCLNI9LEGs,11766
|
|
2
|
-
moka_py-0.1.17.dist-info/WHEEL,sha256=JVm7IbAw8AUzBxiizeYyxa0y-VZmmJpnXKbn70sXhTw,132
|
|
3
|
-
moka_py-0.1.17.dist-info/licenses/LICENSE,sha256=CUj5ca53JXgIACVKNEOFOlbMWtxY4RXXj9cELIv2R04,1069
|
|
4
|
-
moka_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
moka_py/__init__.py,sha256=Vm6tQsXweLtjD_QjBSWtJRquv9g7wq3zrQZJ7EBQGko,1572
|
|
6
|
-
moka_py/__init__.pyi,sha256=wfOnFVeRxogZpBwhUMWqlAVw_0yAa5xuCO28GJNTkGg,1777
|
|
7
|
-
moka_py/moka_py.cpython-313t-powerpc64le-linux-gnu.so,sha256=IhN9ZgF2hvqQgw3sl5Z44cc7i9BUyoQU6MuOZZkcRc4,726016
|
|
8
|
-
moka_py-0.1.17.dist-info/RECORD,,
|
moka_py-0.1.17.dist-info/WHEEL
DELETED
|
File without changes
|