rust-py-cache 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- rust_py_cache-0.2.1/CHANGELOG.md +36 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/Cargo.lock +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/Cargo.toml +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/PKG-INFO +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/pyproject.toml +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/python/rust_py_cache/__init__.py +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/python/rust_py_cache/decorators.py +44 -16
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_features.py +61 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.github/workflows/ci.yml +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.github/workflows/release.yml +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.gitignore +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/PUBLISHING.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/README.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/ROADMAP.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/examples/django_example.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/examples/fastapi_example.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/cache.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/entry.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/lib.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/stats.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/sweeper.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/ttl.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_cache_basic.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_delete.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_eviction.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_set_get.py +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todas as mudanças relevantes deste projeto são registradas aqui.
|
|
4
|
+
|
|
5
|
+
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
|
6
|
+
e o versionamento segue [SemVer](https://semver.org/lang/pt-BR/).
|
|
7
|
+
|
|
8
|
+
## [0.2.1] - 2026-08-11
|
|
9
|
+
|
|
10
|
+
### Corrigido
|
|
11
|
+
|
|
12
|
+
- `@cache.cached` sobre função `async def` guardava no cache o objeto coroutine
|
|
13
|
+
em vez do resultado do `await`, quebrando o decorator já na primeira chamada
|
|
14
|
+
com `TypeError: cannot pickle 'coroutine' object`. O decorator agora detecta
|
|
15
|
+
`inspect.iscoroutinefunction` e gera um wrapper `async` que faz o `await`
|
|
16
|
+
antes do `set`. (BACKLOG2 P0-2)
|
|
17
|
+
|
|
18
|
+
## [0.2.0]
|
|
19
|
+
|
|
20
|
+
### Adicionado
|
|
21
|
+
|
|
22
|
+
- Expiração em background via `Sweeper` (thread dedicada com `Weak<T>`,
|
|
23
|
+
encerramento limpo no `Drop`).
|
|
24
|
+
- Evicção por `max_size` com política LRU.
|
|
25
|
+
- Exemplos para FastAPI e Django.
|
|
26
|
+
|
|
27
|
+
## [0.1.x]
|
|
28
|
+
|
|
29
|
+
### Adicionado
|
|
30
|
+
|
|
31
|
+
- `Cache` com `set` / `get(default=...)` / `delete` / `exists` / `keys` /
|
|
32
|
+
`len` / `clear` / `cleanup_expired` / `stats`.
|
|
33
|
+
- TTL por entrada com expiração preguiçosa.
|
|
34
|
+
- `stats()` cumulativo baseado em `AtomicU64`.
|
|
35
|
+
- Decorator de memoização `@cache.cached(ttl=..., key=...)`.
|
|
36
|
+
- Core em Rust sobre `DashMap`, valores serializados com `pickle`.
|
|
@@ -6,6 +6,7 @@ chave e `functools.wraps` — tudo muito mais natural aqui.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import functools
|
|
9
|
+
import inspect
|
|
9
10
|
|
|
10
11
|
# Sentinela para distinguir "ausente no cache" de "valor None legítimo".
|
|
11
12
|
# `cache.get(key, _MISSING)` devolve `_MISSING` só quando a chave não existe.
|
|
@@ -37,25 +38,52 @@ def cached(self, ttl=None, key=None):
|
|
|
37
38
|
@cache.cached(ttl=60)
|
|
38
39
|
def soma(a, b):
|
|
39
40
|
return a + b
|
|
41
|
+
|
|
42
|
+
@cache.cached(ttl=60)
|
|
43
|
+
async def busca(id):
|
|
44
|
+
return await api.get(id)
|
|
45
|
+
|
|
46
|
+
Funções `async def` são detectadas e recebem um wrapper `async`, que guarda
|
|
47
|
+
no cache o resultado do `await` — não o objeto coroutine. Cachear a coroutine
|
|
48
|
+
faria a segunda chamada levantar `RuntimeError: cannot reuse already awaited
|
|
49
|
+
coroutine`.
|
|
40
50
|
"""
|
|
41
51
|
|
|
52
|
+
def _resolve_key(func, args, kwargs):
|
|
53
|
+
if key is None:
|
|
54
|
+
return _default_key(func, args, kwargs)
|
|
55
|
+
if callable(key):
|
|
56
|
+
return key(*args, **kwargs)
|
|
57
|
+
return key
|
|
58
|
+
|
|
42
59
|
def decorator(func):
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
if inspect.iscoroutinefunction(func):
|
|
61
|
+
|
|
62
|
+
@functools.wraps(func)
|
|
63
|
+
async def wrapper(*args, **kwargs):
|
|
64
|
+
cache_key = _resolve_key(func, args, kwargs)
|
|
65
|
+
|
|
66
|
+
hit = self.get(cache_key, _MISSING)
|
|
67
|
+
if hit is not _MISSING:
|
|
68
|
+
return hit
|
|
69
|
+
|
|
70
|
+
result = await func(*args, **kwargs)
|
|
71
|
+
self.set(cache_key, result, ttl=ttl)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
|
|
76
|
+
@functools.wraps(func)
|
|
77
|
+
def wrapper(*args, **kwargs):
|
|
78
|
+
cache_key = _resolve_key(func, args, kwargs)
|
|
79
|
+
|
|
80
|
+
hit = self.get(cache_key, _MISSING)
|
|
81
|
+
if hit is not _MISSING:
|
|
82
|
+
return hit
|
|
83
|
+
|
|
84
|
+
result = func(*args, **kwargs)
|
|
85
|
+
self.set(cache_key, result, ttl=ttl)
|
|
86
|
+
return result
|
|
59
87
|
|
|
60
88
|
# Expõe a chave-base para inspeção/invalidação manual em testes.
|
|
61
89
|
wrapper.__wrapped__ = func
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Testes de exists/keys/stats/cleanup_expired/clear e decorator — Etapas 8-17."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import time
|
|
4
5
|
|
|
5
6
|
from rust_py_cache import Cache
|
|
@@ -110,3 +111,63 @@ def test_decorator_caches_none_result():
|
|
|
110
111
|
assert returns_none() is None
|
|
111
112
|
assert returns_none() is None
|
|
112
113
|
assert calls["n"] == 1 # None foi cacheado, não recomputou
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_decorator_caches_async_function():
|
|
117
|
+
"""P0-2: `@cached` sobre `async def` deve cachear o RESULTADO do await.
|
|
118
|
+
|
|
119
|
+
Antes da correção, o wrapper síncrono guardava o objeto coroutine: a
|
|
120
|
+
primeira chamada devolvia o valor certo e a segunda levantava
|
|
121
|
+
`RuntimeError: cannot reuse already awaited coroutine`.
|
|
122
|
+
"""
|
|
123
|
+
cache = Cache()
|
|
124
|
+
calls = {"n": 0}
|
|
125
|
+
|
|
126
|
+
@cache.cached(ttl=60)
|
|
127
|
+
async def busca(x):
|
|
128
|
+
calls["n"] += 1
|
|
129
|
+
return x * 2
|
|
130
|
+
|
|
131
|
+
async def cenario():
|
|
132
|
+
# Duas chamadas no mesmo event loop — é aqui que o bug aparecia.
|
|
133
|
+
primeira = await busca(21)
|
|
134
|
+
segunda = await busca(21)
|
|
135
|
+
terceira = await busca(10) # args diferentes -> recomputa
|
|
136
|
+
return primeira, segunda, terceira
|
|
137
|
+
|
|
138
|
+
primeira, segunda, terceira = asyncio.run(cenario())
|
|
139
|
+
|
|
140
|
+
assert primeira == 42
|
|
141
|
+
assert segunda == 42
|
|
142
|
+
assert terceira == 20
|
|
143
|
+
assert calls["n"] == 2 # busca(21) rodou uma vez só
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_decorator_async_keeps_coroutine_function():
|
|
147
|
+
"""O wrapper de uma `async def` continua sendo uma coroutine function."""
|
|
148
|
+
cache = Cache()
|
|
149
|
+
|
|
150
|
+
@cache.cached()
|
|
151
|
+
async def f():
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
assert asyncio.iscoroutinefunction(f)
|
|
155
|
+
assert f.__name__ == "f"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_decorator_async_caches_none_result():
|
|
159
|
+
cache = Cache()
|
|
160
|
+
calls = {"n": 0}
|
|
161
|
+
|
|
162
|
+
@cache.cached()
|
|
163
|
+
async def returns_none():
|
|
164
|
+
calls["n"] += 1
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
async def cenario():
|
|
168
|
+
return await returns_none(), await returns_none()
|
|
169
|
+
|
|
170
|
+
a, b = asyncio.run(cenario())
|
|
171
|
+
assert a is None
|
|
172
|
+
assert b is None
|
|
173
|
+
assert calls["n"] == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|