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.
Files changed (26) hide show
  1. rust_py_cache-0.2.1/CHANGELOG.md +36 -0
  2. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/Cargo.lock +1 -1
  3. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/Cargo.toml +1 -1
  4. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/PKG-INFO +1 -1
  5. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/pyproject.toml +1 -1
  6. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/python/rust_py_cache/__init__.py +1 -1
  7. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/python/rust_py_cache/decorators.py +44 -16
  8. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_features.py +61 -0
  9. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.github/workflows/ci.yml +0 -0
  10. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.github/workflows/release.yml +0 -0
  11. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/.gitignore +0 -0
  12. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/PUBLISHING.md +0 -0
  13. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/README.md +0 -0
  14. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/ROADMAP.md +0 -0
  15. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/examples/django_example.py +0 -0
  16. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/examples/fastapi_example.py +0 -0
  17. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/cache.rs +0 -0
  18. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/entry.rs +0 -0
  19. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/lib.rs +0 -0
  20. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/stats.rs +0 -0
  21. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/sweeper.rs +0 -0
  22. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/src/ttl.rs +0 -0
  23. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_cache_basic.py +0 -0
  24. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_delete.py +0 -0
  25. {rust_py_cache-0.2.0 → rust_py_cache-0.2.1}/tests/test_eviction.py +0 -0
  26. {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`.
@@ -202,7 +202,7 @@ dependencies = [
202
202
 
203
203
  [[package]]
204
204
  name = "rust_py_cache"
205
- version = "0.2.0"
205
+ version = "0.2.1"
206
206
  dependencies = [
207
207
  "dashmap",
208
208
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rust_py_cache"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rust-py-cache
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rust-py-cache"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "An ultra-fast local cache for Python, powered by Rust."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,4 +19,4 @@ class Cache(_RustCache):
19
19
 
20
20
 
21
21
  __all__ = ["Cache", "hello"]
22
- __version__ = "0.2.0"
22
+ __version__ = "0.2.1"
@@ -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
- @functools.wraps(func)
44
- def wrapper(*args, **kwargs):
45
- if key is None:
46
- cache_key = _default_key(func, args, kwargs)
47
- elif callable(key):
48
- cache_key = key(*args, **kwargs)
49
- else:
50
- cache_key = key
51
-
52
- hit = self.get(cache_key, _MISSING)
53
- if hit is not _MISSING:
54
- return hit
55
-
56
- result = func(*args, **kwargs)
57
- self.set(cache_key, result, ttl=ttl)
58
- return result
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