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.
- rust_py_cache-0.2.2/.github/workflows/ci.yml +47 -0
- rust_py_cache-0.2.2/.github/workflows/release.yml +232 -0
- rust_py_cache-0.2.2/CHANGELOG.md +78 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/Cargo.lock +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/Cargo.toml +20 -5
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/PKG-INFO +4 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/pyproject.toml +7 -3
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/python/rust_py_cache/__init__.py +1 -1
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/python/rust_py_cache/decorators.py +44 -16
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_features.py +61 -0
- rust_py_cache-0.2.0/.github/workflows/ci.yml +0 -77
- rust_py_cache-0.2.0/.github/workflows/release.yml +0 -57
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/.gitignore +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/PUBLISHING.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/README.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/ROADMAP.md +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/examples/django_example.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/examples/fastapi_example.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/cache.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/entry.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/lib.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/stats.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/sweeper.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/src/ttl.rs +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_cache_basic.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_delete.py +0 -0
- {rust_py_cache-0.2.0 → rust_py_cache-0.2.2}/tests/test_eviction.py +0 -0
- {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`.
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rust_py_cache"
|
|
3
|
-
version = "0.2.
|
|
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
|
-
# -
|
|
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
|
-
#
|
|
18
|
-
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
33
|
-
|
|
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.
|
|
@@ -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
|
|
@@ -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
|
|
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
|