rust-py-cache 0.2.0__tar.gz → 0.2.2__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 (28) hide show
  1. rust_py_cache-0.2.2/.github/workflows/ci.yml +47 -0
  2. rust_py_cache-0.2.2/.github/workflows/release.yml +232 -0
  3. rust_py_cache-0.2.2/CHANGELOG.md +78 -0
  4. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/Cargo.lock +1 -1
  5. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/Cargo.toml +20 -5
  6. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/PKG-INFO +4 -1
  7. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/pyproject.toml +7 -3
  8. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/python/rust_py_cache/__init__.py +1 -1
  9. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/python/rust_py_cache/decorators.py +44 -16
  10. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_features.py +61 -0
  11. rust_py_cache-0.2.0/.github/workflows/ci.yml +0 -77
  12. rust_py_cache-0.2.0/.github/workflows/release.yml +0 -57
  13. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/.gitignore +0 -0
  14. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/PUBLISHING.md +0 -0
  15. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/README.md +0 -0
  16. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/ROADMAP.md +0 -0
  17. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/examples/django_example.py +0 -0
  18. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/examples/fastapi_example.py +0 -0
  19. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/cache.rs +0 -0
  20. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/entry.rs +0 -0
  21. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/lib.rs +0 -0
  22. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/stats.rs +0 -0
  23. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/sweeper.rs +0 -0
  24. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/ttl.rs +0 -0
  25. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_cache_basic.py +0 -0
  26. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_delete.py +0 -0
  27. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_eviction.py +0 -0
  28. {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_set_get.py +0 -0
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ name: cargo test + pytest
12
+ runs-on: ${{ matrix.os }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest, macos-latest]
17
+ python-version: ["3.10", "3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Set up Rust
27
+ uses: dtolnay/rust-toolchain@stable
28
+
29
+ - name: Cache cargo
30
+ uses: Swatinem/rust-cache@v2
31
+
32
+ # --no-default-features desliga extension-module, que manda o linker
33
+ # deixar os símbolos da libpython indefinidos. Isso é correto para a
34
+ # wheel, mas produz um binário de teste que não linka no Linux.
35
+ - name: cargo test
36
+ env:
37
+ PYO3_PYTHON: python
38
+ run: cargo test --no-default-features --lib
39
+
40
+ # `pip install` aciona o backend maturin, que compila a extensão e a
41
+ # instala no Python do runner — sem exigir virtualenv, ao contrário do
42
+ # `maturin develop`, que aborta quando não encontra um.
43
+ - name: Build, install the extension and run the Python tests
44
+ run: |
45
+ python -m pip install --upgrade pip
46
+ python -m pip install ".[dev]"
47
+ python -m pytest -q
@@ -0,0 +1,232 @@
1
+ name: Release
2
+
3
+ # Dispara ao publicar uma tag de versão (ex.: v0.1.0). Constrói as wheels para
4
+ # todas as plataformas + sdist e publica no PyPI via Trusted Publishing (OIDC),
5
+ # sem precisar guardar um token no repositório.
6
+ #
7
+ # O `ci.yml` roda em push/PR, mas é um workflow *separado*: um push de tag ia
8
+ # direto para build e publicação sem depender dele. O job `test` abaixo é o que
9
+ # torna a publicação condicionada a uma suíte verde — publicar no PyPI é
10
+ # irreversível, então não pode acontecer sobre código não verificado.
11
+ on:
12
+ push:
13
+ tags: ["v*"]
14
+ workflow_dispatch: {}
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+ name: Test (Rust + Python)
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: dtolnay/rust-toolchain@stable
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+ - name: cargo test
30
+ env:
31
+ PYO3_PYTHON: python
32
+ run: cargo test --no-default-features --lib
33
+ - name: Build, install the extension and run the Python tests
34
+ run: |
35
+ python -m pip install --upgrade pip
36
+ python -m pip install ".[dev]"
37
+ python -m pytest -q
38
+
39
+ # A matriz abaixo substitui um job `build` que rodava em ubuntu/macos/windows
40
+ # com `maturin build` puro. Aquilo publicava 3 wheels e o de Linux herdava a
41
+ # glibc do runner, em vez de passar pelo container manylinux. Quem estava em
42
+ # aarch64, em musl ou em Mac Intel caía no sdist — que exige toolchain Rust.
43
+ linux:
44
+ runs-on: ubuntu-latest
45
+ if: startsWith(github.ref, 'refs/tags/')
46
+ needs: test
47
+ strategy:
48
+ matrix:
49
+ target: [x86_64, aarch64]
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - uses: PyO3/maturin-action@v1
53
+ with:
54
+ target: ${{ matrix.target }}
55
+ args: --release --out dist
56
+ # `auto` constrói dentro do container manylinux, então a wheel
57
+ # declara um piso de glibc portável em vez do da máquina de build.
58
+ manylinux: auto
59
+ - uses: actions/upload-artifact@v4
60
+ with:
61
+ name: wheels-linux-${{ matrix.target }}
62
+ path: dist
63
+
64
+ musllinux:
65
+ runs-on: ubuntu-latest
66
+ if: startsWith(github.ref, 'refs/tags/')
67
+ needs: test
68
+ strategy:
69
+ matrix:
70
+ target: [x86_64, aarch64]
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: PyO3/maturin-action@v1
74
+ with:
75
+ target: ${{ matrix.target }}
76
+ args: --release --out dist
77
+ manylinux: musllinux_1_2
78
+ - uses: actions/upload-artifact@v4
79
+ with:
80
+ name: wheels-musllinux-${{ matrix.target }}
81
+ path: dist
82
+
83
+ windows:
84
+ runs-on: windows-latest
85
+ if: startsWith(github.ref, 'refs/tags/')
86
+ needs: test
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ - uses: actions/setup-python@v5
90
+ with:
91
+ architecture: x64
92
+ - uses: PyO3/maturin-action@v1
93
+ with:
94
+ target: x64
95
+ args: --release --out dist
96
+ - uses: actions/upload-artifact@v4
97
+ with:
98
+ name: wheels-windows-x64
99
+ path: dist
100
+
101
+ macos:
102
+ runs-on: macos-latest
103
+ if: startsWith(github.ref, 'refs/tags/')
104
+ needs: test
105
+ strategy:
106
+ matrix:
107
+ target: [x86_64, aarch64]
108
+ steps:
109
+ - uses: actions/checkout@v4
110
+ - uses: PyO3/maturin-action@v1
111
+ with:
112
+ target: ${{ matrix.target }}
113
+ args: --release --out dist
114
+ - uses: actions/upload-artifact@v4
115
+ with:
116
+ name: wheels-macos-${{ matrix.target }}
117
+ path: dist
118
+
119
+ sdist:
120
+ runs-on: ubuntu-latest
121
+ if: startsWith(github.ref, 'refs/tags/')
122
+ needs: test
123
+ steps:
124
+ - uses: actions/checkout@v4
125
+ - uses: PyO3/maturin-action@v1
126
+ with:
127
+ command: sdist
128
+ args: --out dist
129
+ - uses: actions/upload-artifact@v4
130
+ with:
131
+ name: wheels-sdist
132
+ path: dist
133
+
134
+ publish:
135
+ name: publish to PyPI
136
+ # Guarda além do gatilho: sem ela, um `workflow_dispatch` a partir de um
137
+ # branch publicaria no PyPI.
138
+ if: startsWith(github.ref, 'refs/tags/')
139
+ needs: [test, linux, musllinux, windows, macos, sdist]
140
+ runs-on: ubuntu-latest
141
+ environment: pypi
142
+ permissions:
143
+ id-token: write # necessário para o Trusted Publishing (OIDC)
144
+ steps:
145
+ - uses: actions/download-artifact@v4
146
+ with:
147
+ pattern: wheels-*
148
+ merge-multiple: true
149
+ path: dist
150
+ - uses: actions/setup-python@v5
151
+ with:
152
+ python-version: "3.12"
153
+
154
+ # Conferência antes de qualquer coisa sair do runner: publicar um
155
+ # conjunto incompleto é pior que não publicar, porque o PyPI nunca deixa
156
+ # reusar um nome de arquivo — uma versão meio-publicada só pode ser
157
+ # completada, nunca substituída.
158
+ - name: Verify the artifact set is complete
159
+ run: |
160
+ set -euo pipefail
161
+ ls -la dist/
162
+ test -n "$(find dist -name '*.tar.gz' -print -quit)" \
163
+ || { echo "::error::sdist ausente — abortando antes de publicar"; exit 1; }
164
+ wheels=$(find dist -name '*.whl' | wc -l)
165
+ echo "wheels: $wheels"
166
+ test "$wheels" -ge 7 \
167
+ || { echo "::error::só $wheels wheels; esperado >= 7 (2 linux, 2 musl, 1 win, 2 macos)"; exit 1; }
168
+
169
+ # O PyPI devolve 500 transitório com frequência suficiente para importar,
170
+ # e o upload é arquivo por arquivo: uma falha no meio deixa a versão
171
+ # publicada pela metade. `skip-existing` é o que torna a segunda
172
+ # tentativa segura — sem ele, ela morre nos arquivos que já subiram.
173
+ #
174
+ # São dois passos explícitos em vez de um wrapper de retry porque o
175
+ # trusted publishing (OIDC) exige a action oficial, que não dá para
176
+ # acionar por linha de comando.
177
+ - name: Publish to PyPI (attempt 1)
178
+ id: publish1
179
+ continue-on-error: true
180
+ uses: pypa/gh-action-pypi-publish@release/v1
181
+ with:
182
+ packages-dir: dist
183
+ skip-existing: true
184
+
185
+ - name: Wait before retrying
186
+ if: steps.publish1.outcome == 'failure'
187
+ run: sleep 45
188
+
189
+ - name: Publish to PyPI (attempt 2)
190
+ if: steps.publish1.outcome == 'failure'
191
+ uses: pypa/gh-action-pypi-publish@release/v1
192
+ with:
193
+ packages-dir: dist
194
+ skip-existing: true
195
+
196
+ # Prova que a release está completa, em vez de confiar num código de
197
+ # saída que uma publicação parcial também produz.
198
+ - name: Verify every file reached PyPI
199
+ run: |
200
+ set -euo pipefail
201
+ python -m pip install --quiet requests
202
+ python - <<'PY'
203
+ import os, sys, glob, time, requests
204
+ version = os.environ["GITHUB_REF_NAME"].lstrip("v")
205
+ # Só .whl e .tar.gz: são os únicos tipos de arquivo que o PyPI hospeda.
206
+ # `pypa/gh-action-pypi-publish` deixa um .publish.attestation por
207
+ # distribuição em dist/ depois de publicar (atestados PEP 740, que
208
+ # sobem junto do arquivo, não como arquivo próprio). Um glob amplo os
209
+ # conta como "faltando no PyPI" e reprova uma release que deu certo.
210
+ local = {
211
+ os.path.basename(p)
212
+ for pattern in ("dist/*.whl", "dist/*.tar.gz")
213
+ for p in glob.glob(pattern)
214
+ }
215
+ url = f"https://pypi.org/pypi/rust-py-cache/{version}/json"
216
+ for attempt in range(6): # a API JSON do PyPI é cacheada
217
+ response = requests.get(url, timeout=30)
218
+ if response.status_code == 200:
219
+ remote = {f["filename"] for f in response.json()["urls"]}
220
+ missing = local - remote
221
+ if not missing:
222
+ print(f"OK: {len(remote)} arquivos publicados na {version}")
223
+ sys.exit(0)
224
+ else:
225
+ missing = local
226
+ print(f"tentativa {attempt + 1}: faltam {len(missing)}")
227
+ time.sleep(20)
228
+ print("::error::faltaram arquivos no PyPI:")
229
+ for name in sorted(missing):
230
+ print(f" - {name}")
231
+ sys.exit(1)
232
+ PY
@@ -0,0 +1,78 @@
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.2] - 2026-08-11
9
+
10
+ Release de infraestrutura: nada muda no comportamento da biblioteca, mas muda
11
+ quem consegue instalá-la sem compilar.
12
+
13
+ ### Corrigido
14
+
15
+ - **A publicação não roda mais sem teste.** O `ci.yml` roda em push/PR, mas é
16
+ um workflow **separado**: um push de tag ia direto para build e publicação
17
+ sem depender dele. O `release.yml` agora tem o seu próprio job `test` e todos
18
+ os demais declaram `needs: test`. O job `publish` também ganhou
19
+ `if: startsWith(github.ref, 'refs/tags/')` — sem essa guarda, um
20
+ `workflow_dispatch` a partir de um branch publicaria no PyPI.
21
+ (BACKLOG2 P1-1)
22
+ - **Cobertura de plataformas.** A 0.2.1 publicou 3 wheels — uma por runner
23
+ (ubuntu, macos, windows) — e a de Linux herdava a glibc da máquina de build
24
+ em vez de passar pelo container manylinux. Caíam no sdist, ou seja,
25
+ precisavam de toolchain Rust: Linux aarch64, Alpine/musl e Mac Intel. Agora
26
+ são 7 wheels (2 manylinux, 2 musllinux, 1 Windows, 2 macOS) mais o sdist.
27
+ - **O upload aguenta o 500 do PyPI.** O upload é arquivo por arquivo e sem
28
+ transação — uma falha no meio deixa a versão publicada pela metade, e o PyPI
29
+ nunca permite reusar um nome de arquivo. Agora há `skip-existing` (que é o
30
+ que torna a segunda tentativa segura), uma segunda tentativa explícita,
31
+ conferência do conjunto de artefatos **antes** de publicar e validação contra
32
+ a API do PyPI **depois**. (BACKLOG2 P1-9)
33
+
34
+ ### Alterado
35
+
36
+ - `crate-type` ganhou `rlib` além de `cdylib`, e `extension-module` virou
37
+ feature opcional ligada por padrão. Assim
38
+ `cargo test --no-default-features --lib` produz um binário de teste que linka
39
+ no Linux, em vez de depender da tolerância do linker do macOS. É o mesmo
40
+ arranjo dos outros quatro projetos. (BACKLOG2 P1-8)
41
+ - O `ci.yml` deixou de criar um virtualenv à mão para rodar `maturin develop`;
42
+ agora usa `pip install ".[dev]"`, que aciona o mesmo backend maturin sem
43
+ exigir virtualenv.
44
+
45
+ ### Adicionado
46
+
47
+ - Extra `[project.optional-dependencies] dev`. Este era o único dos cinco
48
+ projetos sem um. (BACKLOG2 P2-9)
49
+
50
+ ## [0.2.1] - 2026-08-11
51
+
52
+ ### Corrigido
53
+
54
+ - `@cache.cached` sobre função `async def` guardava no cache o objeto coroutine
55
+ em vez do resultado do `await`, quebrando o decorator já na primeira chamada
56
+ com `TypeError: cannot pickle 'coroutine' object`. O decorator agora detecta
57
+ `inspect.iscoroutinefunction` e gera um wrapper `async` que faz o `await`
58
+ antes do `set`. (BACKLOG2 P0-2)
59
+
60
+ ## [0.2.0]
61
+
62
+ ### Adicionado
63
+
64
+ - Expiração em background via `Sweeper` (thread dedicada com `Weak<T>`,
65
+ encerramento limpo no `Drop`).
66
+ - Evicção por `max_size` com política LRU.
67
+ - Exemplos para FastAPI e Django.
68
+
69
+ ## [0.1.x]
70
+
71
+ ### Adicionado
72
+
73
+ - `Cache` com `set` / `get(default=...)` / `delete` / `exists` / `keys` /
74
+ `len` / `clear` / `cleanup_expired` / `stats`.
75
+ - TTL por entrada com expiração preguiçosa.
76
+ - `stats()` cumulativo baseado em `AtomicU64`.
77
+ - Decorator de memoização `@cache.cached(ttl=..., key=...)`.
78
+ - 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.2"
206
206
  dependencies = [
207
207
  "dashmap",
208
208
  "pyo3",
@@ -1,21 +1,29 @@
1
1
  [package]
2
2
  name = "rust_py_cache"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
7
7
  # Biblioteca de extensão para Python:
8
- # - crate-type = ["cdylib"] -> gera um .so/.dylib carregável pelo Python
8
+ # - cdylib -> gera o .so/.dylib carregável pelo Python
9
+ # - rlib -> alvo de biblioteca normal, que o `cargo test` usa para montar
10
+ # o harness de teste
9
11
  # - name = "_rust_py_cache" -> nome do módulo nativo (importado pelo __init__.py)
10
12
  [lib]
11
13
  name = "_rust_py_cache"
12
- crate-type = ["cdylib"]
14
+ crate-type = ["cdylib", "rlib"]
13
15
 
14
16
  [dependencies]
15
17
  # abi3-py310: compila contra a Stable ABI do CPython 3.10+, gerando uma única
16
18
  # wheel compatível com 3.10, 3.11, 3.12, 3.13...
17
- # extension-module: não linka o binário do Python (o interpretador resolve em runtime).
18
- pyo3 = { version = "0.24", features = ["abi3-py310", "extension-module"] }
19
+ #
20
+ # extension-module NÃO fica aqui: ele manda o linker deixar os símbolos da
21
+ # libpython indefinidos, o que é correto para a wheel mas frágil para o binário
22
+ # de teste no Linux (o macOS tolera com -undefined dynamic_lookup). Vive em
23
+ # [features] no fim do arquivo, ligado por padrão, para que
24
+ # `cargo test --no-default-features` possa desligá-lo. É o mesmo arranjo dos
25
+ # outros quatro projetos.
26
+ pyo3 = { version = "0.24", features = ["abi3-py310"] }
19
27
 
20
28
  # DashMap: HashMap concorrente (lock por shard). Permite get/set de várias threads
21
29
  # Python sem um Mutex global no caminho crítico. Usado pelo RustCache (Etapa 4).
@@ -30,3 +38,10 @@ thiserror = "2"
30
38
  # serde_json = "1" # serializer JSON
31
39
  # No MVP o TTL usa std::time::SystemTime (epoch em ms), então o crate `time`
32
40
  # também não é necessário ainda.
41
+
42
+ [features]
43
+ # maturin constrói com as features default (a wheel ganha extension-module);
44
+ # a CI roda `cargo test --no-default-features --lib` para obter um binário de
45
+ # teste que consegue linkar contra a libpython.
46
+ extension-module = ["pyo3/extension-module"]
47
+ default = ["extension-module"]
@@ -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.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -8,6 +8,9 @@ Classifier: License :: OSI Approved :: MIT License
8
8
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
9
9
  Classifier: Topic :: Database
10
10
  Classifier: Topic :: System :: Distributed Computing
11
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
12
+ Requires-Dist: maturin>=1.7 ; extra == 'dev'
13
+ Provides-Extra: dev
11
14
  Summary: An ultra-fast local cache for Python, powered by Rust.
12
15
  Keywords: cache,rust,pyo3,ttl,performance
13
16
  Author-email: Roberto Lima <robertolima.izphera@gmail.com>
@@ -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.2"
8
8
  description = "An ultra-fast local cache for Python, powered by Rust."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,6 +21,9 @@ classifiers = [
21
21
  "Topic :: System :: Distributed Computing",
22
22
  ]
23
23
 
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=7.0", "maturin>=1.7"]
26
+
24
27
  [project.urls]
25
28
  Homepage = "https://github.com/robertolima/rust-py-cache"
26
29
 
@@ -29,5 +32,6 @@ Homepage = "https://github.com/robertolima/rust-py-cache"
29
32
  python-source = "python"
30
33
  # Compila o .so como submódulo nativo dentro do pacote Python.
31
34
  module-name = "rust_py_cache._rust_py_cache"
32
- # Gera wheel abi3 (uma wheel para 3.10+).
33
- features = ["pyo3/extension-module"]
35
+ # extension-module vem das features default do crate (ver Cargo.toml), então
36
+ # não precisa ser repetido aqui — e repetir reativaria a feature mesmo num
37
+ # build que pediu --no-default-features de propósito.
@@ -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.2"
@@ -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
@@ -1,77 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
-
8
- jobs:
9
- test:
10
- name: cargo test + pytest
11
- runs-on: ${{ matrix.os }}
12
- strategy:
13
- fail-fast: false
14
- matrix:
15
- os: [ubuntu-latest, macos-latest]
16
- python-version: ["3.10", "3.12", "3.13"]
17
- steps:
18
- - uses: actions/checkout@v4
19
-
20
- - name: Set up Python ${{ matrix.python-version }}
21
- uses: actions/setup-python@v5
22
- with:
23
- python-version: ${{ matrix.python-version }}
24
-
25
- - name: Set up Rust
26
- uses: dtolnay/rust-toolchain@stable
27
-
28
- - name: Cache cargo
29
- uses: Swatinem/rust-cache@v2
30
-
31
- # Testes do core Rust (puro, sem Python).
32
- - name: cargo test
33
- run: cargo test --verbose
34
-
35
- # maturin develop exige um virtualenv ativo. O setup-python não cria um,
36
- # então criamos o .venv e o exportamos para os próximos steps via
37
- # GITHUB_ENV/GITHUB_PATH (cada step roda num shell novo).
38
- - name: Create virtualenv
39
- run: |
40
- python -m venv .venv
41
- echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
42
- echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
43
-
44
- - name: Install Python deps
45
- run: |
46
- python -m pip install --upgrade pip
47
- pip install maturin pytest
48
-
49
- # Compila a extensão e instala no .venv.
50
- - name: maturin develop
51
- run: maturin develop
52
-
53
- - name: pytest
54
- run: pytest -q
55
-
56
- build-wheels:
57
- name: build wheels (abi3)
58
- runs-on: ${{ matrix.os }}
59
- needs: test
60
- strategy:
61
- matrix:
62
- os: [ubuntu-latest, macos-latest, windows-latest]
63
- steps:
64
- - uses: actions/checkout@v4
65
- - uses: actions/setup-python@v5
66
- with:
67
- python-version: "3.12"
68
- - name: Build wheels
69
- uses: PyO3/maturin-action@v1
70
- with:
71
- command: build
72
- args: --release --out dist
73
- - name: Upload artifacts
74
- uses: actions/upload-artifact@v4
75
- with:
76
- name: wheels-${{ matrix.os }}
77
- path: dist
@@ -1,57 +0,0 @@
1
- name: Release
2
-
3
- # Dispara ao publicar uma tag de versão (ex.: v0.1.0). Constrói as wheels para
4
- # todas as plataformas + sdist e publica no PyPI via Trusted Publishing (OIDC),
5
- # sem precisar guardar um token no repositório.
6
- on:
7
- push:
8
- tags: ["v*"]
9
-
10
- permissions:
11
- contents: read
12
-
13
- jobs:
14
- build:
15
- name: build (${{ matrix.os }})
16
- runs-on: ${{ matrix.os }}
17
- strategy:
18
- matrix:
19
- os: [ubuntu-latest, macos-latest, windows-latest]
20
- steps:
21
- - uses: actions/checkout@v4
22
- - uses: actions/setup-python@v5
23
- with:
24
- python-version: "3.12"
25
- - name: Build wheels
26
- uses: PyO3/maturin-action@v1
27
- with:
28
- command: build
29
- args: --release --out dist
30
- - name: Build sdist
31
- if: matrix.os == 'ubuntu-latest'
32
- uses: PyO3/maturin-action@v1
33
- with:
34
- command: sdist
35
- args: --out dist
36
- - uses: actions/upload-artifact@v4
37
- with:
38
- name: dist-${{ matrix.os }}
39
- path: dist
40
-
41
- publish:
42
- name: publish to PyPI
43
- needs: build
44
- runs-on: ubuntu-latest
45
- environment: pypi
46
- permissions:
47
- id-token: write # necessário para o Trusted Publishing (OIDC)
48
- steps:
49
- - uses: actions/download-artifact@v4
50
- with:
51
- pattern: dist-*
52
- merge-multiple: true
53
- path: dist
54
- - name: Publish
55
- uses: pypa/gh-action-pypi-publish@release/v1
56
- with:
57
- packages-dir: dist
File without changes
File without changes
File without changes
File without changes
File without changes