rust-py-cache 0.2.2__tar.gz → 0.3.0__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 (31) hide show
  1. rust_py_cache-0.3.0/.github/workflows/release.yml +389 -0
  2. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/.gitignore +3 -0
  3. rust_py_cache-0.3.0/CHANGELOG.md +180 -0
  4. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/Cargo.lock +1 -1
  5. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/Cargo.toml +2 -1
  6. rust_py_cache-0.3.0/LICENSE +21 -0
  7. rust_py_cache-0.3.0/Makefile +68 -0
  8. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/PKG-INFO +10 -5
  9. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/README.md +7 -2
  10. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/ROADMAP.md +8 -3
  11. rust_py_cache-0.3.0/clippy.toml +11 -0
  12. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/pyproject.toml +12 -4
  13. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/python/rust_py_cache/__init__.py +1 -1
  14. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/cache.rs +119 -13
  15. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/entry.rs +1 -1
  16. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/lib.rs +1 -6
  17. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/tests/test_eviction.py +15 -0
  18. rust_py_cache-0.2.2/.github/workflows/ci.yml +0 -47
  19. rust_py_cache-0.2.2/.github/workflows/release.yml +0 -232
  20. rust_py_cache-0.2.2/CHANGELOG.md +0 -78
  21. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/PUBLISHING.md +0 -0
  22. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/examples/django_example.py +0 -0
  23. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/examples/fastapi_example.py +0 -0
  24. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/python/rust_py_cache/decorators.py +0 -0
  25. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/stats.rs +0 -0
  26. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/sweeper.rs +0 -0
  27. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/src/ttl.rs +0 -0
  28. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/tests/test_cache_basic.py +0 -0
  29. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/tests/test_delete.py +0 -0
  30. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/tests/test_features.py +0 -0
  31. {rust_py_cache-0.2.2 → rust_py_cache-0.3.0}/tests/test_set_get.py +0 -0
@@ -0,0 +1,389 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # release.yml canônico dos `rust_py_*` — P3-1
3
+ #
4
+ # Um arquivo só por projeto: lint, teste, build da matriz, ensaio no TestPyPI e
5
+ # publicação. Substitui o par `ci.yml` + `release.yml` que `cache` e
6
+ # `rate_limit` mantinham, e que tinha um problema silencioso — ver "Por que um
7
+ # workflow só", abaixo.
8
+ #
9
+ # Gerado a partir de `rust_py_template/.github/workflows/release.yml` em
10
+ # 12/08/2026 — o `cache` é o primeiro projeto a adotar o formato unificado.
11
+ # Correção feita aqui deve voltar para o template, senão a divergência
12
+ # recomeça pelo mesmo caminho de sempre.
13
+ #
14
+ # ⚠️ NÃO RENOMEAR ESTE ARQUIVO. O trusted publisher cadastrado no PyPI amarra
15
+ # `release.yml`; renomear quebra a publicação até o cadastro ser refeito.
16
+ #
17
+ # Substituiu o par `ci.yml` + `release.yml` que o `cache` mantinha.
18
+ #
19
+ # ── POR QUE UM WORKFLOW SÓ ───────────────────────────────────────────────────
20
+ # `cache` e `rate_limit` tinham dois arquivos, e a divisão escondia uma
21
+ # inversão: o `ci.yml` deles rodava a suíte RICA (multi-OS, Python 3.10–3.13)
22
+ # em push/PR sem barrar nada, enquanto o `release.yml` barrava a publicação com
23
+ # uma suíte MAGRA (ubuntu, 3.12). Ou seja, o portão do PyPI era o teste mais
24
+ # fraco dos dois. `needs` não atravessa workflow — é a lição do P1-1, aqui numa
25
+ # segunda forma.
26
+ #
27
+ # Este arquivo resolve juntando: a suíte rica é a que barra o release.
28
+ # ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ name: CI / Release
31
+
32
+ on:
33
+ push:
34
+ branches: [main]
35
+ tags: ["v*"] # `v*`, nunca `*` — ver nota no job `release`
36
+ pull_request:
37
+ workflow_dispatch: {}
38
+
39
+ permissions:
40
+ contents: read
41
+
42
+ env:
43
+ PACKAGE_NAME: rust-py-cache
44
+ # 2 manylinux + 2 musllinux + 1 windows + 2 macos. O `monitor` soma 4 do
45
+ # PyPy (11); o `scheduler`, com a matriz longa, chega a 15.
46
+ MIN_WHEELS: "7"
47
+
48
+ jobs:
49
+ # ── PORTÃO 1: lint ─────────────────────────────────────────────────────────
50
+ # Barra o release junto com o `test`. Nos repositórios de workflow único, um
51
+ # `lint` vermelho ao lado de uma publicação bem-sucedida seria ilegível — a
52
+ # leitura tem que ser inequívoca: vermelho significa que não publicou.
53
+ lint:
54
+ name: fmt + clippy
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ # Versão fixa, não "stable". Com `-D warnings`, um clippy mais novo no
59
+ # runner reprova código que ninguém tocou, e a CI fica vermelha por conta
60
+ # do calendário. Subir este número vira uma mudança deliberada.
61
+ - uses: dtolnay/rust-toolchain@1.88.0
62
+ with:
63
+ components: rustfmt, clippy
64
+ - uses: Swatinem/rust-cache@v2
65
+ - name: cargo fmt
66
+ run: cargo fmt --all -- --check
67
+ # clippy só checa tipos, não linka, então --all-features é seguro aqui —
68
+ # diferente do `cargo test`, que precisa de --no-default-features.
69
+ - name: cargo clippy
70
+ run: cargo clippy --all-targets --all-features -- -D warnings
71
+
72
+ # ── PORTÃO 2: teste ────────────────────────────────────────────────────────
73
+ # Nada é construído e nada chega ao PyPI antes disto passar. Publicação no
74
+ # PyPI é irreversível: um wheel ruim não pode ser substituído, só yankado.
75
+ test:
76
+ name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
77
+ runs-on: ${{ matrix.os }}
78
+ strategy:
79
+ fail-fast: false
80
+ # A matriz veio do `ci.yml` do `cache` e do `CI.yml` do `rate_limit`, que
81
+ # tinham a cobertura mais larga dos cinco — e que, por viverem em
82
+ # workflow separado, não barravam nada. Agora barram.
83
+ #
84
+ # O eixo de SO é o do `cache`, reduzido: ele rodava macOS em três versões
85
+ # de Python, o que dobrava a matriz para provar a mesma coisa. Uma versão
86
+ # em macOS basta para pegar o que ubuntu não pega (link da extensão,
87
+ # caminho de arquivo, comportamento de thread). O eixo de Python inteiro
88
+ # fica no ubuntu, que é onde ele importa.
89
+ matrix:
90
+ os: [ubuntu-latest]
91
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
92
+ include:
93
+ - os: macos-latest
94
+ python-version: "3.12"
95
+ steps:
96
+ - uses: actions/checkout@v4
97
+ - uses: dtolnay/rust-toolchain@stable
98
+ - uses: actions/setup-python@v5
99
+ with:
100
+ python-version: ${{ matrix.python-version }}
101
+ - uses: Swatinem/rust-cache@v2
102
+
103
+ # --no-default-features desliga `extension-module`, que manda o linker
104
+ # deixar os símbolos da libpython indefinidos. Correto para a wheel, mas
105
+ # produz um binário de teste que não linka no Linux.
106
+ #
107
+ # ⚠️ O `rate_limit` é o único sem seção `[features]` no Cargo.toml — lá
108
+ # o `extension-module` vem só de `[tool.maturin].features`, e a flag é
109
+ # inócua. Alinhar o Cargo.toml dele é item pendente.
110
+ - name: Rust unit tests
111
+ # Uma vez por SO basta: o core não muda com a versão do Python.
112
+ if: matrix.python-version == '3.12'
113
+ env:
114
+ PYO3_PYTHON: python
115
+ run: cargo test --no-default-features --lib
116
+
117
+ # `pip install` aciona o backend maturin, compila a extensão e a instala
118
+ # no Python do runner. Ao contrário de `maturin develop`, não exige
119
+ # virtualenv — armadilha que custou um release do `monitor` (P1-9).
120
+ # Instalar não-editável faz os testes exercitarem o artefato que embarca.
121
+ - name: Build, install the extension and run the Python tests
122
+ run: |
123
+ python -m pip install --upgrade pip
124
+ python -m pip install ".[dev]"
125
+ python -m pytest -q
126
+
127
+ # ── BUILD ──────────────────────────────────────────────────────────────────
128
+ # Guardados por tag OU disparo manual: um push comum não precisa pagar a
129
+ # matriz inteira, mas o ensaio no TestPyPI precisa dos artefatos.
130
+ #
131
+ # `fail-fast: false` em todas as matrizes: sem isso, um alvo que cai CANCELA
132
+ # os irmãos. Em 12/08/2026 o build x86 do `scheduler` quebrou num 403 do
133
+ # vault.centos.org e levou junto o diagnóstico de armv7/s390x/ppc64le.
134
+ #
135
+ # Nenhum job de build passa `--find-interpreter`: com `abi3-py310` ele não
136
+ # produz nada a mais. Verificado — o `audit` publicava 7 wheels, todos abi3,
137
+ # com a flag ligada. É flag morto e confunde quem lê.
138
+ linux:
139
+ runs-on: ubuntu-latest
140
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
141
+ needs: [lint, test]
142
+ strategy:
143
+ fail-fast: false
144
+ matrix:
145
+ target: [x86_64, aarch64]
146
+ steps:
147
+ - uses: actions/checkout@v4
148
+ - uses: PyO3/maturin-action@v1
149
+ with:
150
+ target: ${{ matrix.target }}
151
+ args: --release --out dist
152
+ # `auto` constrói dentro do container manylinux, para o wheel
153
+ # declarar um piso de glibc portável em vez do glibc do runner.
154
+ manylinux: auto
155
+ - uses: actions/upload-artifact@v4
156
+ with:
157
+ name: wheels-linux-${{ matrix.target }}
158
+ path: dist
159
+
160
+ musllinux:
161
+ runs-on: ubuntu-latest
162
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
163
+ needs: [lint, test]
164
+ strategy:
165
+ fail-fast: false
166
+ matrix:
167
+ target: [x86_64, aarch64]
168
+ steps:
169
+ - uses: actions/checkout@v4
170
+ - uses: PyO3/maturin-action@v1
171
+ with:
172
+ target: ${{ matrix.target }}
173
+ args: --release --out dist
174
+ manylinux: musllinux_1_2
175
+ - uses: actions/upload-artifact@v4
176
+ with:
177
+ name: wheels-musllinux-${{ matrix.target }}
178
+ path: dist
179
+
180
+ windows:
181
+ runs-on: windows-latest
182
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
183
+ needs: [lint, test]
184
+ steps:
185
+ - uses: actions/checkout@v4
186
+ - uses: actions/setup-python@v5
187
+ with:
188
+ architecture: x64
189
+ - uses: PyO3/maturin-action@v1
190
+ with:
191
+ target: x64
192
+ args: --release --out dist
193
+ - uses: actions/upload-artifact@v4
194
+ with:
195
+ name: wheels-windows-x64
196
+ path: dist
197
+
198
+ macos:
199
+ runs-on: macos-latest
200
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
201
+ needs: [lint, test]
202
+ strategy:
203
+ fail-fast: false
204
+ matrix:
205
+ target: [x86_64, aarch64]
206
+ steps:
207
+ - uses: actions/checkout@v4
208
+ - uses: PyO3/maturin-action@v1
209
+ with:
210
+ target: ${{ matrix.target }}
211
+ args: --release --out dist
212
+ - uses: actions/upload-artifact@v4
213
+ with:
214
+ name: wheels-macos-${{ matrix.target }}
215
+ path: dist
216
+
217
+ sdist:
218
+ runs-on: ubuntu-latest
219
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
220
+ needs: [lint, test]
221
+ steps:
222
+ - uses: actions/checkout@v4
223
+ - uses: PyO3/maturin-action@v1
224
+ with:
225
+ command: sdist
226
+ args: --out dist
227
+ - uses: actions/upload-artifact@v4
228
+ with:
229
+ name: wheels-sdist
230
+ path: dist
231
+
232
+ # ── ENSAIO ─────────────────────────────────────────────────────────────────
233
+ # Valida a troca OIDC inteira sem tocar no índice de produção. É onde um
234
+ # publisher mal cadastrado deve ser descoberto — no PyPI, um nome de arquivo
235
+ # publicado nunca pode ser reusado.
236
+ #
237
+ # ⚠️ Precisa do seu próprio publisher, cadastrado no TestPyPI (conta separada
238
+ # do PyPI), com environment `testpypi`.
239
+ #
240
+ # Sem verificação pós-upload de propósito: o que se valida aqui é a troca de
241
+ # token, e o TestPyPI expira distribuições — reprovar por um arquivo que o
242
+ # índice de teste descartou seria alarme falso.
243
+ publish-testpypi:
244
+ name: Publish to TestPyPI (ensaio)
245
+ runs-on: ubuntu-latest
246
+ if: github.event_name == 'workflow_dispatch'
247
+ needs: [lint, test, linux, musllinux, windows, macos, sdist]
248
+ environment: testpypi
249
+ permissions:
250
+ id-token: write
251
+ steps:
252
+ - uses: actions/download-artifact@v4
253
+ with:
254
+ pattern: wheels-*
255
+ path: dist
256
+ merge-multiple: true
257
+ - uses: pypa/gh-action-pypi-publish@release/v1
258
+ with:
259
+ repository-url: https://test.pypi.org/legacy/
260
+ packages-dir: dist
261
+ skip-existing: true
262
+
263
+ # ── PUBLICAÇÃO ─────────────────────────────────────────────────────────────
264
+ release:
265
+ name: Publish to PyPI
266
+ runs-on: ubuntu-latest
267
+ # SÓ TAG. Sem a cláusula de `workflow_dispatch` que os jobs de build têm —
268
+ # a assimetria é a proteção: disparo manual vai ao TestPyPI, só a tag chega
269
+ # ao índice de verdade.
270
+ #
271
+ # ⚠️ Isto depende do filtro `tags: ["v*"]` lá em cima. O `scheduler` usava
272
+ # `'*'`, e como esta guarda só checa `refs/tags/`, QUALQUER tag empurrada
273
+ # teria publicado. Corrigido em 12/08/2026.
274
+ if: startsWith(github.ref, 'refs/tags/')
275
+ needs: [lint, test, linux, musllinux, windows, macos, sdist]
276
+
277
+ # Trusted publishing (OIDC): sem `secrets.PYPI_API_TOKEN`. O GitHub emite
278
+ # um JWT curto afirmando repositório, workflow e ref, e o PyPI o troca por
279
+ # um token de upload de poucos minutos, restrito a este projeto.
280
+ #
281
+ # ⚠️ Os três valores precisam bater EXATAMENTE com o publisher cadastrado
282
+ # em PyPI → Manage → Publishing:
283
+ # owner/repo .... <owner>/<repo>
284
+ # workflow ...... release.yml (o NOME DO ARQUIVO, não o `name:` acima)
285
+ # environment ... pypi
286
+ # Errar qualquer um dá `invalid-publisher`, que é falha segura: acontece na
287
+ # troca do token, depois de construir tudo e antes de qualquer arquivo sair
288
+ # do runner.
289
+ environment: pypi
290
+ permissions:
291
+ id-token: write
292
+
293
+ steps:
294
+ - uses: actions/download-artifact@v4
295
+ with:
296
+ pattern: wheels-*
297
+ path: dist
298
+ merge-multiple: true
299
+ - uses: actions/setup-python@v5
300
+ with:
301
+ python-version: "3.12"
302
+ - name: Install the verification dependency
303
+ run: python -m pip install --upgrade requests
304
+
305
+ # Conferência antes de qualquer coisa sair do runner: publicar um
306
+ # conjunto incompleto é pior que não publicar, porque o PyPI nunca deixa
307
+ # reusar um nome de arquivo — uma versão meio-publicada só pode ser
308
+ # completada, nunca substituída. Aconteceu de verdade: `monitor` 0.4.0
309
+ # ficou com 37 de 45 arquivos e sem sdist (P1-9).
310
+ - name: Verify the artifact set is complete
311
+ run: |
312
+ set -euo pipefail
313
+ ls -la dist/
314
+ test -n "$(find dist -name '*.tar.gz' -print -quit)" \
315
+ || { echo "::error::sdist ausente — abortando antes de publicar"; exit 1; }
316
+ wheels=$(find dist -name '*.whl' | wc -l)
317
+ echo "wheels: $wheels (mínimo ${MIN_WHEELS})"
318
+ test "$wheels" -ge "${MIN_WHEELS}" \
319
+ || { echo "::error::só $wheels wheels; esperado >= ${MIN_WHEELS}"; exit 1; }
320
+
321
+ # O PyPI devolve 500 transitório com frequência suficiente para importar,
322
+ # e o upload é arquivo por arquivo: uma falha no meio deixa a versão
323
+ # publicada pela metade. `skip-existing` torna a segunda tentativa segura
324
+ # — sem ele, ela morre nos arquivos que já subiram.
325
+ #
326
+ # Dois passos explícitos em vez de um wrapper de retry porque o trusted
327
+ # publishing exige a action oficial, que não dá para acionar por linha de
328
+ # comando de dentro do `nick-fields/retry`.
329
+ #
330
+ # ⚠️ Se a primeira tentativa falhar por `invalid-publisher`, a segunda vai
331
+ # falhar igual — é erro de configuração, não transitório. Custa 45s de
332
+ # espera inútil; melhoria conhecida e ainda não feita.
333
+ - name: Publish to PyPI (attempt 1)
334
+ id: publish1
335
+ continue-on-error: true
336
+ uses: pypa/gh-action-pypi-publish@release/v1
337
+ with:
338
+ packages-dir: dist
339
+ skip-existing: true
340
+
341
+ - name: Wait before retrying
342
+ if: steps.publish1.outcome == 'failure'
343
+ run: sleep 45
344
+
345
+ - name: Publish to PyPI (attempt 2)
346
+ if: steps.publish1.outcome == 'failure'
347
+ uses: pypa/gh-action-pypi-publish@release/v1
348
+ with:
349
+ packages-dir: dist
350
+ skip-existing: true
351
+
352
+ # Prova que a release está completa, em vez de confiar num código de
353
+ # saída que uma publicação parcial também produz.
354
+ - name: Verify every file reached PyPI
355
+ run: |
356
+ set -euo pipefail
357
+ python - <<'PY'
358
+ import os, sys, glob, time, requests
359
+ package = os.environ["PACKAGE_NAME"]
360
+ version = os.environ["GITHUB_REF_NAME"].lstrip("v")
361
+ # Só .whl e .tar.gz: os únicos tipos que o PyPI hospeda. O
362
+ # `pypa/gh-action-pypi-publish` deixa um .publish.attestation por
363
+ # distribuição em dist/ DEPOIS de publicar (atestados PEP 740, que
364
+ # sobem junto do arquivo e não como arquivo próprio). Um glob amplo
365
+ # os conta como "faltando" e reprova uma release que deu certo — foi
366
+ # o alarme falso da rate_limit 0.2.1 (P1-9).
367
+ local = {
368
+ os.path.basename(p)
369
+ for pattern in ("dist/*.whl", "dist/*.tar.gz")
370
+ for p in glob.glob(pattern)
371
+ }
372
+ url = f"https://pypi.org/pypi/{package}/{version}/json"
373
+ for attempt in range(6): # a API JSON do PyPI é cacheada
374
+ response = requests.get(url, timeout=30)
375
+ if response.status_code == 200:
376
+ remote = {f["filename"] for f in response.json()["urls"]}
377
+ missing = local - remote
378
+ if not missing:
379
+ print(f"OK: {len(remote)} arquivos publicados na {version}")
380
+ sys.exit(0)
381
+ else:
382
+ missing = local
383
+ print(f"tentativa {attempt + 1}: faltam {len(missing)}")
384
+ time.sleep(20)
385
+ print("::error::faltaram arquivos no PyPI:")
386
+ for name in sorted(missing):
387
+ print(f" - {name}")
388
+ sys.exit(1)
389
+ PY
@@ -5,3 +5,6 @@ __pycache__/
5
5
  dist/
6
6
  *.egg-info/
7
7
  .pytest_cache/
8
+
9
+ # macOS
10
+ .DS_Store
@@ -0,0 +1,180 @@
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.3.0] — não publicada
9
+
10
+ Evicção LRU com custo amortizado O(1) (**P2-2**). Inclui também tudo o que
11
+ estava na 0.2.4, que nunca chegou a ser publicada.
12
+
13
+ ### Alterado
14
+
15
+ - **A evicção LRU passou a remover um lote, não uma entrada.** Até a 0.2.x,
16
+ achar a entrada menos recentemente usada varria o `DashMap` inteiro — e isso
17
+ acontecia a **cada** `set` de chave nova com o cache cheio, que é o regime
18
+ normal de um cache cheio sob escrita. Agora cada varredura remove
19
+ `max_size / 32` entradas (no mínimo 1), e a próxima só vem depois de outras
20
+ ~n/32 inserções. Medido com `max_size=100_000`: **~620 µs → ~1,6 µs** por
21
+ `set` com o cache cheio (com 10 000: ~70 µs → ~1,5 µs). O custo deixou de
22
+ crescer com o tamanho do cache.
23
+ - A seleção acha o corte com `select_nth_unstable` sobre os carimbos de acesso
24
+ (sem clonar chaves nem ordenar) e remove com `remove_if`, que recheca o
25
+ carimbo: uma chave lida por outra thread no meio da varredura fica.
26
+ - Um `Mutex` cobre **só** o caminho de evicção, para várias threads que acham o
27
+ cache cheio ao mesmo tempo não varrerem em paralelo. `get`/`set` comuns não o
28
+ tocam.
29
+
30
+ ### Compatibilidade
31
+
32
+ - Nenhuma API mudou. O que muda de observável, e por isso o minor sobe: logo
33
+ depois de uma evicção o cache fica ~3% abaixo de `max_size` (antes ficava em
34
+ `max_size - 1`), e `stats()["evicted"]` cresce em lotes. Com `max_size < 64`
35
+ o lote é de 1 entrada e o comportamento é idêntico ao anterior.
36
+ - Amostragem estilo Redis, que o backlog sugeria, foi descartada: exigiria
37
+ acesso aleatório às entradas, que o `DashMap` só expõe pela `raw-api` (tabela
38
+ interna do hashbrown, iteração `unsafe`).
39
+
40
+ ## [0.2.4] — não publicada
41
+
42
+ Adoção do `release.yml` unificado do template (**P3-1**). Nada muda no
43
+ comportamento da biblioteca — mas muda, e bastante, o que precisa passar antes
44
+ de uma versão chegar ao PyPI.
45
+
46
+ ### Corrigido
47
+
48
+ - **O release era barrado pela suíte mais fraca das duas.** O projeto tinha
49
+ dois workflows, e a divisão escondia uma inversão:
50
+
51
+ | | rodava | barrava o release? |
52
+ | --- | --- | --- |
53
+ | `ci.yml` | ubuntu + macOS × Python 3.10/3.12/3.13 | **não** |
54
+ | `release.yml` | ubuntu × Python 3.12 | sim |
55
+
56
+ Ou seja, a suíte boa não protegia nada e o portão do PyPI era a magra.
57
+ `needs` não atravessa workflow — a mesma lição do P1-1, numa segunda forma.
58
+ Os dois arquivos ainda mantinham o job `lint` duplicado.
59
+
60
+ Agora é um workflow só, e quem barra a publicação é
61
+ **ubuntu × Python 3.10/3.11/3.12/3.13 + macOS × 3.12**.
62
+
63
+ ⚠️ O eixo de macOS foi reduzido de três versões de Python para uma. Rodar as
64
+ três dobrava a matriz para provar a mesma coisa; uma basta para pegar o que
65
+ o ubuntu não pega (link da extensão, caminhos, threads).
66
+
67
+ ### Adicionado
68
+
69
+ - **Ensaio no TestPyPI** (`publish-testpypi`, por `workflow_dispatch`), que o
70
+ projeto não tinha. Valida a troca de token OIDC sem tocar no índice de
71
+ produção. ⚠️ Exige um publisher próprio cadastrado no TestPyPI.
72
+ - **`fail-fast: false`** nas matrizes: um alvo que cai deixava de cancelar os
73
+ irmãos e apagar o diagnóstico deles.
74
+
75
+ ### Removido
76
+
77
+ - **`.github/workflows/ci.yml`**, fundido no `release.yml`.
78
+
79
+ ## [0.2.3] - 2026-08-12
80
+
81
+ Versão de empacotamento e qualidade: nada muda no comportamento da biblioteca.
82
+
83
+ ### Adicionado
84
+
85
+ - **`LICENSE`** (MIT). O projeto declarava MIT no `pyproject.toml` desde sempre,
86
+ mas o texto da licença não existia no repositório nem na wheel (**P1-4**).
87
+ - **`clippy.toml` e `Makefile`** compartilhados com os outros quatro `rust_py_*`,
88
+ e um job `lint` (`cargo fmt --check` + `cargo clippy -D warnings`) no `ci.yml`
89
+ e no `release.yml` — este barrando a publicação, como o `test` já barrava
90
+ (**P1-6**). O `cargo clippy` já estava limpo aqui; foi o único dos cinco.
91
+
92
+ ### Corrigido
93
+
94
+ - **Metadado de licença ausente no PyPI.** O `pyproject.toml` usava
95
+ `license = { text = "MIT" }`, forma legada que o Metadata 2.4 depreca. Passou
96
+ para a do PEP 639 (`license = "MIT"` + `license-files = ["LICENSE"]`), e o
97
+ classifier `License :: OSI Approved` saiu — ele é mutuamente exclusivo com
98
+ `License-Expression`. A 0.2.2 no PyPI mostra `license: MIT` no campo legado e
99
+ `license_expression: null`; a partir daqui é o contrário, que é o correto.
100
+
101
+ - **Piso da maturin subiu para 1.14** no `[build-system]`. Era `>=1.7`, e
102
+ versões anteriores não emitem `License-Expression` — foi essa, e não a forma
103
+ no `pyproject`, a causa raiz do metadado faltando.
104
+
105
+ ### Declarado como Cargo
106
+
107
+ - `Cargo.toml` não tinha campo `license` nenhum. Agora tem `license = "MIT"`,
108
+ como os outros quatro.
109
+
110
+ ## [0.2.2] - 2026-08-11
111
+
112
+ Release de infraestrutura: nada muda no comportamento da biblioteca, mas muda
113
+ quem consegue instalá-la sem compilar.
114
+
115
+ ### Corrigido
116
+
117
+ - **A publicação não roda mais sem teste.** O `ci.yml` roda em push/PR, mas é
118
+ um workflow **separado**: um push de tag ia direto para build e publicação
119
+ sem depender dele. O `release.yml` agora tem o seu próprio job `test` e todos
120
+ os demais declaram `needs: test`. O job `publish` também ganhou
121
+ `if: startsWith(github.ref, 'refs/tags/')` — sem essa guarda, um
122
+ `workflow_dispatch` a partir de um branch publicaria no PyPI.
123
+ (BACKLOG2 P1-1)
124
+ - **Cobertura de plataformas.** A 0.2.1 publicou 3 wheels — uma por runner
125
+ (ubuntu, macos, windows) — e a de Linux herdava a glibc da máquina de build
126
+ em vez de passar pelo container manylinux. Caíam no sdist, ou seja,
127
+ precisavam de toolchain Rust: Linux aarch64, Alpine/musl e Mac Intel. Agora
128
+ são 7 wheels (2 manylinux, 2 musllinux, 1 Windows, 2 macOS) mais o sdist.
129
+ - **O upload aguenta o 500 do PyPI.** O upload é arquivo por arquivo e sem
130
+ transação — uma falha no meio deixa a versão publicada pela metade, e o PyPI
131
+ nunca permite reusar um nome de arquivo. Agora há `skip-existing` (que é o
132
+ que torna a segunda tentativa segura), uma segunda tentativa explícita,
133
+ conferência do conjunto de artefatos **antes** de publicar e validação contra
134
+ a API do PyPI **depois**. (BACKLOG2 P1-9)
135
+
136
+ ### Alterado
137
+
138
+ - `crate-type` ganhou `rlib` além de `cdylib`, e `extension-module` virou
139
+ feature opcional ligada por padrão. Assim
140
+ `cargo test --no-default-features --lib` produz um binário de teste que linka
141
+ no Linux, em vez de depender da tolerância do linker do macOS. É o mesmo
142
+ arranjo dos outros quatro projetos. (BACKLOG2 P1-8)
143
+ - O `ci.yml` deixou de criar um virtualenv à mão para rodar `maturin develop`;
144
+ agora usa `pip install ".[dev]"`, que aciona o mesmo backend maturin sem
145
+ exigir virtualenv.
146
+
147
+ ### Adicionado
148
+
149
+ - Extra `[project.optional-dependencies] dev`. Este era o único dos cinco
150
+ projetos sem um. (BACKLOG2 P2-9)
151
+
152
+ ## [0.2.1] - 2026-08-11
153
+
154
+ ### Corrigido
155
+
156
+ - `@cache.cached` sobre função `async def` guardava no cache o objeto coroutine
157
+ em vez do resultado do `await`, quebrando o decorator já na primeira chamada
158
+ com `TypeError: cannot pickle 'coroutine' object`. O decorator agora detecta
159
+ `inspect.iscoroutinefunction` e gera um wrapper `async` que faz o `await`
160
+ antes do `set`. (BACKLOG2 P0-2)
161
+
162
+ ## [0.2.0]
163
+
164
+ ### Adicionado
165
+
166
+ - Expiração em background via `Sweeper` (thread dedicada com `Weak<T>`,
167
+ encerramento limpo no `Drop`).
168
+ - Evicção por `max_size` com política LRU.
169
+ - Exemplos para FastAPI e Django.
170
+
171
+ ## [0.1.x]
172
+
173
+ ### Adicionado
174
+
175
+ - `Cache` com `set` / `get(default=...)` / `delete` / `exists` / `keys` /
176
+ `len` / `clear` / `cleanup_expired` / `stats`.
177
+ - TTL por entrada com expiração preguiçosa.
178
+ - `stats()` cumulativo baseado em `AtomicU64`.
179
+ - Decorator de memoização `@cache.cached(ttl=..., key=...)`.
180
+ - 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.2"
205
+ version = "0.3.0"
206
206
  dependencies = [
207
207
  "dashmap",
208
208
  "pyo3",
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "rust_py_cache"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
+ license = "MIT"
5
6
  readme = "README.md"
6
7
 
7
8
  # Biblioteca de extensão para Python:
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Lima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ # Makefile compartilhado dos `rust_py_*`.
2
+ #
3
+ # Existe para que a CI e a pessoa rodem exatamente o mesmo comando. Quando o
4
+ # lint da CI diverge do lint local, quem perde é sempre quem abriu o PR.
5
+ #
6
+ # A toolchain NÃO é fixada por `rust-toolchain.toml` de propósito: esse arquivo
7
+ # vale para toda invocação do cargo, inclusive a dos containers manylinux do
8
+ # release, e um download de toolchain no meio de uma publicação no PyPI é risco
9
+ # sem contrapartida. A fixação vive no job de lint da CI, que é onde
10
+ # `-D warnings` realmente morde. Localmente, use uma stable recente.
11
+
12
+ PYTHON ?= python3
13
+
14
+ # Fixa qual Python o pyo3 usa na compilação. Sem isto ele pega o primeiro do
15
+ # PATH, que pode não ser o mesmo do `make dev` — e aí o binário de teste linka
16
+ # contra uma libpython e o pytest roda contra outra.
17
+ export PYO3_PYTHON ?= $(PYTHON)
18
+
19
+ # No macOS o binário de `cargo test` carrega a libpython em runtime, e o dyld
20
+ # não a encontra sozinho: o erro é `Library not loaded: @rpath/libpython3.X.dylib`,
21
+ # em SIGABRT, depois de compilar com sucesso — parece falha de teste e não é.
22
+ # Os caminhos padrão entram junto porque definir DYLD_FALLBACK_LIBRARY_PATH
23
+ # *substitui* a lista default do dyld em vez de acrescentar a ela (senão o
24
+ # erro seguinte é libiconv.2.dylib).
25
+ PY_LIBDIR := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR") or "")' 2>/dev/null)
26
+ export DYLD_FALLBACK_LIBRARY_PATH := $(PY_LIBDIR):/opt/homebrew/lib:/usr/local/lib:/usr/lib
27
+
28
+ .PHONY: help dev test lint fmt clippy test-rust test-python clean
29
+
30
+ help:
31
+ @echo "make dev instala o pacote em modo editável com os extras de dev"
32
+ @echo "make test cargo test + pytest"
33
+ @echo "make lint cargo fmt --check + cargo clippy -D warnings"
34
+ @echo "make fmt aplica a formatação (escreve nos arquivos)"
35
+
36
+ # `pip install` aciona o backend maturin, compila a extensão e instala.
37
+ # Ao contrário de `maturin develop`, não exige um virtualenv ativo — que é
38
+ # exatamente a armadilha que custou um release do monitor (ver P1-9).
39
+ dev:
40
+ $(PYTHON) -m pip install --upgrade pip
41
+ $(PYTHON) -m pip install -e ".[dev]"
42
+
43
+ test: test-rust test-python
44
+
45
+ # --no-default-features desliga `extension-module`, que impede o binário de
46
+ # teste de linkar contra a libpython no Linux. --lib porque os testes de
47
+ # integração precisam do módulo nativo, e esses ficam do lado Python.
48
+ test-rust:
49
+ cargo test --no-default-features --lib
50
+
51
+ test-python:
52
+ pytest -q
53
+
54
+ lint: fmt-check clippy
55
+
56
+ .PHONY: fmt-check
57
+ fmt-check:
58
+ cargo fmt --all -- --check
59
+
60
+ clippy:
61
+ cargo clippy --all-targets --all-features -- -D warnings
62
+
63
+ fmt:
64
+ cargo fmt --all
65
+
66
+ clean:
67
+ cargo clean
68
+ rm -rf dist build *.egg-info .pytest_cache