rust-py-audit 0.3.2__tar.gz → 0.3.3__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 (50) hide show
  1. rust_py_audit-0.3.3/.github/workflows/release.yml +328 -0
  2. rust_py_audit-0.3.3/CHANGELOG.md +160 -0
  3. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/Cargo.lock +1 -1
  4. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/Cargo.toml +1 -1
  5. rust_py_audit-0.3.3/Makefile +68 -0
  6. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/PKG-INFO +2 -3
  7. rust_py_audit-0.3.3/clippy.toml +11 -0
  8. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/pyproject.toml +9 -4
  9. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/python/rust_py_audit/__init__.py +1 -1
  10. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/audit_logger.rs +30 -11
  11. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/errors.rs +1 -3
  12. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/event.rs +2 -1
  13. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/hash.rs +2 -2
  14. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/immutablelog_client.rs +88 -35
  15. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/retry.rs +24 -7
  16. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/storage.rs +31 -10
  17. rust_py_audit-0.3.2/.github/workflows/release.yml +0 -208
  18. rust_py_audit-0.3.2/CHANGELOG.md +0 -50
  19. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/.gitignore +0 -0
  20. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/LICENSE +0 -0
  21. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/README.md +0 -0
  22. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/ROADMAP.md +0 -0
  23. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/__init__.py +0 -0
  24. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/basic_usage.py +0 -0
  25. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/django_example/__init__.py +0 -0
  26. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/django_example/settings.py +0 -0
  27. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/django_example/urls.py +0 -0
  28. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/django_example/views.py +0 -0
  29. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/fastapi_app.py +0 -0
  30. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/examples/immutablelog_basic.py +0 -0
  31. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/python/rust_py_audit/django.py +0 -0
  32. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/python/rust_py_audit/fastapi.py +0 -0
  33. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/immutablelog_config.rs +0 -0
  34. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/immutablelog_receipt.rs +0 -0
  35. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/lib.rs +0 -0
  36. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/pyconv.rs +0 -0
  37. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/src/verifier.rs +0 -0
  38. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/conftest.py +0 -0
  39. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_audit_logger.py +0 -0
  40. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_concurrency.py +0 -0
  41. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_django_immutablelog.py +0 -0
  42. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_django_middleware.py +0 -0
  43. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_fastapi_immutablelog.py +0 -0
  44. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_fastapi_middleware.py +0 -0
  45. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_flush_pending.py +0 -0
  46. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_hello.py +0 -0
  47. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_immutablelog_hybrid.py +0 -0
  48. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_immutablelog_remote.py +0 -0
  49. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_metadata_bigint.py +0 -0
  50. {rust_py_audit-0.3.2 → rust_py_audit-0.3.3}/tests/test_verify.py +0 -0
@@ -0,0 +1,328 @@
1
+ name: CI / Release
2
+
3
+ # Push e PR rodam `lint` + `test` e param por aí. Os jobs de build são
4
+ # liberados por uma tag `v*` ou por um `workflow_dispatch` manual — o segundo
5
+ # existe para o ensaio no TestPyPI, abaixo.
6
+ #
7
+ # **Só a tag publica no PyPI de produção.** O job `release` é o único guardado
8
+ # apenas por `startsWith(github.ref, 'refs/tags/')`, sem a cláusula de
9
+ # dispatch: um disparo manual constrói tudo e vai para o TestPyPI, nunca para
10
+ # o índice de verdade. Publicação no PyPI não se desfaz, então essa assimetria
11
+ # é proposital.
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ tags:
16
+ - "v*"
17
+ pull_request:
18
+ workflow_dispatch: {}
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ # O lint faz parte do portão de release, não de um workflow paralelo.
25
+ # Nos repositórios de workflow único, um `lint` vermelho ao lado de uma
26
+ # publicação bem-sucedida seria ruído difícil de ler — a leitura passa a ser
27
+ # inequívoca: vermelho significa que não publicou.
28
+ #
29
+ # A toolchain é fixa, não "stable": com `-D warnings`, um clippy mais novo
30
+ # no runner reprova código que ninguém tocou, e a CI ficaria vermelha por
31
+ # conta do calendário. Subir este número vira uma mudança deliberada.
32
+ lint:
33
+ name: fmt + clippy
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: dtolnay/rust-toolchain@1.88.0
38
+ with:
39
+ components: rustfmt, clippy
40
+ - uses: Swatinem/rust-cache@v2
41
+ - name: cargo fmt
42
+ run: cargo fmt --all -- --check
43
+ # clippy só checa tipos, não linka, então --all-features é seguro aqui
44
+ # (diferente do `cargo test`, que precisa de --no-default-features).
45
+ - name: cargo clippy
46
+ run: cargo clippy --all-targets --all-features -- -D warnings
47
+
48
+ # Nothing gets built, and nothing reaches PyPI, until this passes. Publishing
49
+ # to PyPI is irreversible: a bad wheel cannot be replaced, only yanked.
50
+ test:
51
+ name: Test (Rust + Python)
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ - uses: dtolnay/rust-toolchain@stable
56
+ - uses: actions/setup-python@v5
57
+ with:
58
+ python-version: "3.12"
59
+
60
+ # --no-default-features drops extension-module, which tells the linker to
61
+ # leave libpython symbols undefined. That is correct for the wheel but
62
+ # produces a test binary that cannot link on Linux.
63
+ - name: Rust unit tests
64
+ env:
65
+ PYO3_PYTHON: python
66
+ run: cargo test --no-default-features --lib
67
+
68
+ - name: Build, install the extension and run the Python tests
69
+ # `pip install` drives the maturin build backend, so it compiles the
70
+ # Rust extension and installs it (plus the dev extras) into the
71
+ # runner's Python — no virtualenv needed, unlike `maturin develop`,
72
+ # which aborts when it cannot find one.
73
+ run: |
74
+ python -m pip install --upgrade pip
75
+ python -m pip install ".[dev]"
76
+ python -m pytest
77
+
78
+ linux:
79
+ runs-on: ubuntu-latest
80
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
81
+ needs: [test, lint]
82
+ strategy:
83
+ matrix:
84
+ target: [x86_64, aarch64]
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+ - uses: PyO3/maturin-action@v1
88
+ env:
89
+ # `ring` (puxado por `rustls`, via `reqwest`) falha ao cross-compilar
90
+ # sua assembly pregenerada para aarch64 dentro do container manylinux
91
+ # do maturin-action: o cross-gcc não define `__ARM_ARCH` por padrão
92
+ # nesse ambiente, e o preprocessor de `ring` exige essa macro
93
+ # (erro: "ARM assembler must define __ARM_ARCH"). Forçar a macro via
94
+ # CFLAGS é o workaround padrão para esse problema conhecido — não
95
+ # afeta o build x86_64 (a env var é específica do target aarch64).
96
+ CFLAGS_aarch64_unknown_linux_gnu: -D__ARM_ARCH=8
97
+ with:
98
+ target: ${{ matrix.target }}
99
+ args: --release --out dist --find-interpreter
100
+ manylinux: auto
101
+ - uses: actions/upload-artifact@v4
102
+ with:
103
+ name: wheels-linux-${{ matrix.target }}
104
+ path: dist
105
+
106
+ musllinux:
107
+ runs-on: ubuntu-latest
108
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
109
+ needs: [test, lint]
110
+ strategy:
111
+ matrix:
112
+ target: [x86_64, aarch64]
113
+ steps:
114
+ - uses: actions/checkout@v4
115
+ - uses: PyO3/maturin-action@v1
116
+ with:
117
+ target: ${{ matrix.target }}
118
+ args: --release --out dist --find-interpreter
119
+ manylinux: musllinux_1_2
120
+ - uses: actions/upload-artifact@v4
121
+ with:
122
+ name: wheels-musllinux-${{ matrix.target }}
123
+ path: dist
124
+
125
+ windows:
126
+ runs-on: windows-latest
127
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
128
+ needs: [test, lint]
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - uses: actions/setup-python@v5
132
+ with:
133
+ architecture: x64
134
+ - uses: PyO3/maturin-action@v1
135
+ with:
136
+ target: x64
137
+ args: --release --out dist --find-interpreter
138
+ - uses: actions/upload-artifact@v4
139
+ with:
140
+ name: wheels-windows-x64
141
+ path: dist
142
+
143
+ macos:
144
+ runs-on: macos-latest
145
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
146
+ needs: [test, lint]
147
+ strategy:
148
+ matrix:
149
+ target: [x86_64, aarch64]
150
+ steps:
151
+ - uses: actions/checkout@v4
152
+ - uses: PyO3/maturin-action@v1
153
+ with:
154
+ target: ${{ matrix.target }}
155
+ args: --release --out dist --find-interpreter
156
+ - uses: actions/upload-artifact@v4
157
+ with:
158
+ name: wheels-macos-${{ matrix.target }}
159
+ path: dist
160
+
161
+ sdist:
162
+ runs-on: ubuntu-latest
163
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
164
+ needs: [test, lint]
165
+ steps:
166
+ - uses: actions/checkout@v4
167
+ - uses: PyO3/maturin-action@v1
168
+ with:
169
+ command: sdist
170
+ args: --out dist
171
+ - uses: actions/upload-artifact@v4
172
+ with:
173
+ name: wheels-sdist
174
+ path: dist
175
+
176
+ # Ensaio da troca OIDC, sem tocar no índice de produção. É o que permite
177
+ # descobrir um publisher mal cadastrado num lugar onde nada é irreversível —
178
+ # no PyPI, um nome de arquivo publicado nunca pode ser reusado.
179
+ #
180
+ # ⚠️ Precisa do seu próprio publisher, cadastrado no TestPyPI (que é uma
181
+ # conta separada do PyPI), com owner/repo, workflow `release.yml` e
182
+ # environment `testpypi`.
183
+ #
184
+ # Não há verificação pós-upload aqui de propósito: o que se está validando é
185
+ # a troca de token, e o TestPyPI expira distribuições — reprovar por um
186
+ # arquivo que o índice de teste descartou seria alarme falso.
187
+ publish-testpypi:
188
+ name: Publish to TestPyPI (ensaio)
189
+ runs-on: ubuntu-latest
190
+ if: github.event_name == 'workflow_dispatch'
191
+ needs: [lint, test, linux, musllinux, windows, macos, sdist]
192
+ environment: testpypi
193
+ permissions:
194
+ id-token: write
195
+ steps:
196
+ - uses: actions/download-artifact@v4
197
+ with:
198
+ pattern: wheels-*
199
+ path: dist
200
+ merge-multiple: true
201
+ - uses: pypa/gh-action-pypi-publish@release/v1
202
+ with:
203
+ repository-url: https://test.pypi.org/legacy/
204
+ packages-dir: dist
205
+ skip-existing: true
206
+
207
+ release:
208
+ name: Publish to PyPI
209
+ runs-on: ubuntu-latest
210
+ if: startsWith(github.ref, 'refs/tags/')
211
+ needs: [lint, test, linux, musllinux, windows, macos, sdist]
212
+
213
+ # Trusted publishing (OIDC). Não existe mais `secrets.PYPI_API_TOKEN` aqui:
214
+ # o GitHub emite um JWT curto afirmando repositório, workflow e ref, e o
215
+ # PyPI o troca por um token de upload válido por poucos minutos e só para
216
+ # este projeto. Some o segredo de longa duração que, se vazasse, publicaria
217
+ # até alguém revogá-lo à mão.
218
+ #
219
+ # ⚠️ Os três valores abaixo precisam bater EXATAMENTE com o publisher
220
+ # cadastrado em PyPI → Manage → Publishing:
221
+ # owner/repo .... robertolima-dev/rust-py-audit
222
+ # workflow ...... release.yml (o nome deste arquivo)
223
+ # environment ... pypi
224
+ # Divergência em qualquer um deles é rejeitada na troca do token.
225
+ environment: pypi
226
+ permissions:
227
+ id-token: write
228
+
229
+ steps:
230
+ - uses: actions/download-artifact@v4
231
+ with:
232
+ pattern: wheels-*
233
+ path: dist
234
+ merge-multiple: true
235
+ - uses: actions/setup-python@v5
236
+ with:
237
+ python-version: "3.12"
238
+ # `requests` é só para a verificação pós-upload, mais abaixo. A maturin
239
+ # não é mais instalada aqui: quem publica agora é a action oficial do
240
+ # PyPA, que é a que sabe fazer a troca OIDC.
241
+ - name: Install the verification dependency
242
+ run: python -m pip install --upgrade requests
243
+
244
+ # A sanity check before anything leaves the runner: publishing an
245
+ # incomplete set is worse than not publishing, because PyPI never lets a
246
+ # filename be reused — a half-uploaded version can only be completed,
247
+ # never replaced.
248
+ - name: Verify the artifact set is complete
249
+ run: |
250
+ set -euo pipefail
251
+ ls -la dist/
252
+ test -n "$(find dist -name '*.tar.gz' -print -quit)" \
253
+ || { echo "::error::sdist ausente — abortando antes de publicar"; exit 1; }
254
+ wheels=$(find dist -name '*.whl' | wc -l)
255
+ echo "wheels: $wheels"
256
+ test "$wheels" -ge 7 \
257
+ || { echo "::error::só $wheels wheels; esperado >= 7 (2 linux, 2 musl, 1 win, 2 macos)"; exit 1; }
258
+
259
+ # O PyPI devolve 500 transitório com frequência suficiente para importar,
260
+ # e o upload é arquivo por arquivo: uma falha no meio deixa a versão
261
+ # publicada pela metade. `skip-existing` é o que torna a segunda
262
+ # tentativa segura — sem ele, ela morre nos arquivos que já subiram.
263
+ #
264
+ # São dois passos explícitos em vez de um wrapper de retry porque o
265
+ # trusted publishing exige a action oficial, e ela não dá para acionar
266
+ # por linha de comando de dentro do `nick-fields/retry`. Mesma forma que
267
+ # o `rust_py_cache` e o `rust_py_rate_limit` já rodam.
268
+ - name: Publish to PyPI (attempt 1)
269
+ id: publish1
270
+ continue-on-error: true
271
+ uses: pypa/gh-action-pypi-publish@release/v1
272
+ with:
273
+ packages-dir: dist
274
+ skip-existing: true
275
+
276
+ - name: Wait before retrying
277
+ if: steps.publish1.outcome == 'failure'
278
+ run: sleep 45
279
+
280
+ - name: Publish to PyPI (attempt 2)
281
+ if: steps.publish1.outcome == 'failure'
282
+ uses: pypa/gh-action-pypi-publish@release/v1
283
+ with:
284
+ packages-dir: dist
285
+ skip-existing: true
286
+
287
+ # Proves the release is actually complete, rather than trusting an exit
288
+ # code that a partial upload can still produce.
289
+ - name: Verify every file reached PyPI
290
+ run: |
291
+ set -euo pipefail
292
+ python - <<'PY'
293
+ import os, sys, glob, time, requests
294
+ version = os.environ["GITHUB_REF_NAME"].lstrip("v")
295
+ # Só .whl e .tar.gz: são os únicos tipos de arquivo que o PyPI hospeda.
296
+ # `pypa/gh-action-pypi-publish` deixa um .publish.attestation por
297
+ # distribuição em dist/ depois de publicar (atestados PEP 740, que
298
+ # sobem junto do arquivo, não como arquivo próprio). Um glob amplo os
299
+ # conta como "faltando no PyPI" e reprova uma release que deu certo.
300
+ #
301
+ # ⚠️ Isto deixou de ser precaução e passou a valer aqui: com trusted
302
+ # publishing a action gera os atestados, coisa que o `maturin upload`
303
+ # não fazia. O recorte veio do alarme falso da rate_limit 0.2.1
304
+ # (P1-9) e é o que impede esta migração de reprovar o primeiro
305
+ # release por um motivo inventado.
306
+ local = {
307
+ os.path.basename(p)
308
+ for pattern in ("dist/*.whl", "dist/*.tar.gz")
309
+ for p in glob.glob(pattern)
310
+ }
311
+ url = f"https://pypi.org/pypi/rust-py-audit/{version}/json"
312
+ for attempt in range(6): # PyPI's JSON API is cached
313
+ response = requests.get(url, timeout=30)
314
+ if response.status_code == 200:
315
+ remote = {f["filename"] for f in response.json()["urls"]}
316
+ missing = local - remote
317
+ if not missing:
318
+ print(f"OK: {len(remote)} arquivos publicados na {version}")
319
+ sys.exit(0)
320
+ else:
321
+ missing = local
322
+ print(f"tentativa {attempt + 1}: faltam {len(missing)}")
323
+ time.sleep(20)
324
+ print("::error::faltaram arquivos no PyPI:")
325
+ for name in sorted(missing):
326
+ print(f" - {name}")
327
+ sys.exit(1)
328
+ PY
@@ -0,0 +1,160 @@
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.3] — não publicada
9
+
10
+ Correção do passo de verificação pós-upload introduzido na 0.3.2, mais o
11
+ trabalho de licença e lint de 12/08/2026 (**P1-4** e **P1-6**).
12
+
13
+ ### Alterado
14
+
15
+ - **Publicação migrada para trusted publishing (OIDC).** O `release.yml` não
16
+ usa mais `secrets.PYPI_API_TOKEN`: o GitHub emite um JWT curto afirmando
17
+ repositório, workflow e ref, e o PyPI o troca por um token de upload válido
18
+ por poucos minutos e restrito a este projeto. Acaba o segredo de longa
19
+ duração que, se vazasse, publicaria até ser revogado à mão.
20
+
21
+ O passo de upload deixou de ser `maturin upload` dentro de um
22
+ `nick-fields/retry` e passou a ser dois `pypa/gh-action-pypi-publish`
23
+ explícitos (`continue-on-error` na primeira, `if: outcome == 'failure'` na
24
+ segunda) — a action oficial é a que faz a troca OIDC, e não dá para acioná-la
25
+ por linha de comando de dentro do wrapper de retry. `skip-existing: true`
26
+ mantém a segunda tentativa idempotente. É a mesma forma que `rust_py_cache` e
27
+ `rust_py_rate_limit` já rodam.
28
+
29
+ ⚠️ **Depende de configuração no PyPI**, e os valores têm que bater exatamente:
30
+ `robertolima-dev/rust-py-audit`, workflow `release.yml`, environment `pypi`.
31
+ O modo de falha é seguro — sem o publisher cadastrado, o job falha na troca
32
+ do token, depois de construir tudo e antes de qualquer arquivo sair do
33
+ runner. Nada é publicado pela metade.
34
+
35
+ 💡 A partir daqui a release passa a gerar **atestados PEP 740**, que o
36
+ `maturin upload` não gerava. A verificação pós-upload já estava preparada:
37
+ ela só conta `*.whl` e `*.tar.gz` desde a 0.3.2, recorte que nasceu do alarme
38
+ falso da `rate_limit` 0.2.1. Sem ele, este primeiro release por OIDC seria
39
+ reprovado por um motivo inventado.
40
+
41
+ - **Ensaio no TestPyPI por `workflow_dispatch`.** Job `publish-testpypi` novo,
42
+ na forma que o `rust_py_rate_limit` já tem: um disparo manual constrói a
43
+ matriz inteira e publica no índice de teste, para validar a troca de token
44
+ num lugar onde nada é irreversível.
45
+
46
+ Para isso, os cinco jobs de build passaram de
47
+ `if: startsWith(github.ref, 'refs/tags/')` para
48
+ `... || github.event_name == 'workflow_dispatch'` — sem essa mudança um
49
+ disparo manual não construiria wheel nenhuma e o ensaio ficaria sem o que
50
+ publicar.
51
+
52
+ ⚠️ **O job `release` deliberadamente NÃO ganhou a cláusula de dispatch.** A
53
+ assimetria é a proteção: disparo manual vai ao TestPyPI, só a tag chega ao
54
+ PyPI de produção.
55
+
56
+ ### Adicionado
57
+
58
+ - **Job `lint`** (`cargo fmt --check` + `cargo clippy -D warnings`) no
59
+ `release.yml`, com `needs: [test, lint]` em todos os jobs de build e no de
60
+ publicação — o lint barra a release, igual ao `test`. A toolchain é fixa
61
+ (`dtolnay/rust-toolchain@1.88.0`) e não `stable`: com `-D warnings`, um
62
+ clippy mais novo no runner reprovaria código que ninguém tocou.
63
+ - **`clippy.toml` e `Makefile`** compartilhados com os outros quatro
64
+ `rust_py_*`. O `clippy.toml` sobe `too-many-arguments-threshold` para 10:
65
+ os construtores PyO3 espelham a assinatura Python, e reagrupar em struct
66
+ para agradar o lint pioraria a API que o usuário vê.
67
+
68
+ ### Corrigido
69
+
70
+ - **Metadado de licença ausente no PyPI.** A 0.3.2 está publicada com
71
+ `license_expression: null` e `license: ""` — ou seja, sem metadado de
72
+ licença nenhum, só o classifier. A causa raiz não era a forma no
73
+ `pyproject.toml` (que já usava `license = "MIT"`), e sim a **maturin velha**:
74
+ versões anteriores à 1.14 não emitem `License-Expression`. O piso do
75
+ `[build-system]` subiu de `>=1.5` para `>=1.14`, entrou
76
+ `license-files = ["LICENSE"]` e o classifier `License :: OSI Approved` saiu,
77
+ por ser mutuamente exclusivo com `License-Expression` no Metadata 2.4.
78
+ Verificado: o `dist-info` gerado agora traz `License-Expression: MIT` e o
79
+ texto da licença em `licenses/LICENSE`.
80
+
81
+ - **Formatação.** `cargo fmt --all` aplicado — eram 38 blocos divergentes, de
82
+ longe o maior dos cinco. Mudança puramente de formatação, sem efeito em
83
+ comportamento; os 50 testes Rust seguem passando.
84
+
85
+ ### Conhecido
86
+
87
+ - `src/audit_logger.rs` ganhou um `#![allow(clippy::useless_conversion)]` de
88
+ módulo. **Não é código a corrigir:** o lint aponta o *tipo de retorno* de
89
+ `log`, `verify` e `flush_pending`, mas a conversão está no código que
90
+ `#[pymethods]` gera — a macro do pyo3 0.22. `#[allow]` no bloco `impl` e na
91
+ função foram testados e não silenciam, porque o código gerado fica fora do
92
+ `impl`. `cache` (0.24), `monitor` (0.25) e `scheduler` (0.29) não emitem o
93
+ aviso, o que confirma ser artefato da versão. O `allow` sai com o **P3-2**.
94
+
95
+ ### Corrigido — pipeline (0.3.2)
96
+
97
+ - **Falso negativo na verificação pós-publicação.** O passo comparava
98
+ `glob("dist/*")` com o que estava no PyPI. Nada quebrou aqui — o
99
+ `maturin upload` não deixa arquivo extra em `dist/` —, mas o glob amplo é
100
+ frágil: qualquer arquivo novo no diretório reprovaria uma release que deu
101
+ certo, que foi exatamente o que aconteceu no `rust_py_rate_limit`. O passo
102
+ agora só considera `*.whl` e `*.tar.gz`, os únicos tipos de arquivo que o
103
+ PyPI hospeda.
104
+
105
+ - **Os testes passam a rodar em push e PR.** O `release.yml` é o único workflow
106
+ destes repositórios e só disparava em tag, então o job `test` só rodava na
107
+ hora do release — protegia a publicação, mas só avisava do problema no pior
108
+ momento. Agora o workflow também dispara em `push` para `main` e em
109
+ `pull_request`, e os jobs de build e o de publicação ganharam
110
+ `if: startsWith(github.ref, 'refs/tags/')`. Essa guarda corrige um risco que
111
+ existia antes: o job de release **não tinha `if:`**, então um
112
+ `workflow_dispatch` manual a partir de um branch teria publicado no PyPI.
113
+
114
+ **Não vai ao PyPI sozinha:** a 0.3.2 está lá completa e esta versão só
115
+ mexe no pipeline. O número fica reservado e sai com a próxima mudança que
116
+ valha uma tag.
117
+
118
+ ## [0.3.2] - 2026-08-11
119
+
120
+ Release de infraestrutura: nada muda no comportamento da biblioteca.
121
+
122
+ ### Corrigido
123
+
124
+ - **A publicação não roda mais sem teste.** O `release.yml` era o único
125
+ workflow do repositório e só tinha jobs de build: `git push --tags`
126
+ publicava no PyPI sem nenhum `cargo test` nem `pytest`. Agora existe um job
127
+ `test` e todos os jobs de wheel, o de sdist e o de release declaram
128
+ `needs: test`. (BACKLOG2 P1-1)
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. O passo de publicação agora tem
132
+ retry (4 tentativas), continua usando `--skip-existing` para ser idempotente,
133
+ confere o conjunto de artefatos **antes** de subir qualquer coisa (aborta se
134
+ faltar o sdist) e valida contra a API do PyPI **depois**, listando nome por
135
+ nome o que não chegou. (BACKLOG2 P1-9)
136
+
137
+ ### Adicionado
138
+
139
+ - Este `CHANGELOG.md`. (BACKLOG2 P1-5)
140
+
141
+ ## [0.3.1] e anteriores
142
+
143
+ Sem changelog retroativo. O histórico do que entrou em cada versão está no
144
+ `ROADMAP.md`, e o das mudanças no `git log`.
145
+
146
+ Resumo do que o projeto entrega hoje:
147
+
148
+ - `AuditLogger` com log append-only em JSONL e cadeia de hashes encadeada.
149
+ - `verify()` para detectar adulteração da cadeia.
150
+ - Integração com o ImmutableLog nos modos remoto e híbrido.
151
+ - Middlewares para FastAPI e Django.
152
+ - Wheels `abi3-py310` — uma wheel por plataforma, válida da 3.10 em diante.
153
+
154
+ ### Limitação conhecida (não corrigida nesta versão)
155
+
156
+ ⚠️ **Múltiplos processos escrevendo no mesmo arquivo quebram a cadeia.** Cada
157
+ processo mantém seu próprio `last_hash` em memória e não há `flock` no append,
158
+ então dois workers gunicorn apontando para o mesmo JSONL produzem uma cadeia
159
+ inconsistente — e `verify()` acusa corrupção. O modo de falha é silencioso até
160
+ alguém rodar `verify()`. Ver **P2-4** no `BACKLOG2.md`.
@@ -1597,7 +1597,7 @@ dependencies = [
1597
1597
 
1598
1598
  [[package]]
1599
1599
  name = "rust_py_audit"
1600
- version = "0.3.2"
1600
+ version = "0.3.3"
1601
1601
  dependencies = [
1602
1602
  "httpmock",
1603
1603
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rust_py_audit"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  edition = "2021"
5
5
  description = "Auditoria de eventos com cadeia de hashes (SHA-256) para apps Python, core em Rust via PyO3."
6
6
  license = "MIT"
@@ -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
@@ -1,9 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rust-py-audit
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
- Classifier: License :: OSI Approved :: MIT License
7
6
  Classifier: Programming Language :: Rust
8
7
  Classifier: Programming Language :: Python :: 3
9
8
  Classifier: Programming Language :: Python :: 3.10
@@ -18,7 +17,7 @@ Requires-Dist: httpx>=0.24 ; extra == 'dev'
18
17
  Requires-Dist: fastapi>=0.100.0 ; extra == 'dev'
19
18
  Requires-Dist: starlette>=0.27.0 ; extra == 'dev'
20
19
  Requires-Dist: django>=4.0 ; extra == 'dev'
21
- Requires-Dist: maturin>=1.5 ; extra == 'dev'
20
+ Requires-Dist: maturin>=1.14 ; extra == 'dev'
22
21
  Requires-Dist: django>=4.0 ; extra == 'django'
23
22
  Requires-Dist: fastapi>=0.100.0 ; extra == 'fastapi'
24
23
  Requires-Dist: starlette>=0.27.0 ; extra == 'fastapi'
@@ -0,0 +1,11 @@
1
+ # Configuração compartilhada do clippy para os cinco `rust_py_*`.
2
+ #
3
+ # O padrão do clippy para `too_many_arguments` é 7. Estes projetos expõem
4
+ # construtores PyO3 que espelham a assinatura Python — `add_job()` do
5
+ # scheduler leva 8 parâmetros porque a API pública leva 8 (retry_backoff,
6
+ # timeout, max_instances e afins). Reagrupar em struct para agradar o lint
7
+ # pioraria a API do lado Python, que é a que o usuário vê.
8
+ #
9
+ # O teto sobe para 10 em vez de o lint ser desligado: acima de 10 o aviso
10
+ # volta a ser informação útil.
11
+ too-many-arguments-threshold = 10
@@ -1,13 +1,19 @@
1
1
  [build-system]
2
- requires = ["maturin>=1.5,<2.0"]
2
+ # Piso 1.14 de propósito: versões anteriores não emitem o `License-Expression`
3
+ # do PEP 639, e a 0.3.2 foi ao PyPI sem metadado de licença nenhum (ver P1-4).
4
+ requires = ["maturin>=1.14,<2.0"]
3
5
  build-backend = "maturin"
4
6
 
5
7
  [project]
6
8
  name = "rust-py-audit"
7
- version = "0.3.2"
9
+ version = "0.3.3"
8
10
  description = "Fast, tamper-evident event auditing (hash chain) for Python apps, with a Rust core."
9
11
  readme = "README.md"
12
+ # Forma do PEP 639: expressão SPDX + arquivos de licença declarados. O
13
+ # classifier "License :: OSI Approved" saiu — é mutuamente exclusivo com
14
+ # License-Expression no Metadata 2.4.
10
15
  license = "MIT"
16
+ license-files = ["LICENSE"]
11
17
  authors = [
12
18
  { name = "Roberto Lima", email = "robertolima.izphera@gmail.com" }
13
19
  ]
@@ -15,7 +21,6 @@ keywords = ["audit", "audit-log", "security", "django", "fastapi", "rust", "inte
15
21
  classifiers = [
16
22
  "Development Status :: 3 - Alpha",
17
23
  "Intended Audience :: Developers",
18
- "License :: OSI Approved :: MIT License",
19
24
  "Programming Language :: Rust",
20
25
  "Programming Language :: Python :: 3",
21
26
  "Programming Language :: Python :: 3.10",
@@ -40,7 +45,7 @@ dev = [
40
45
  "fastapi>=0.100.0",
41
46
  "starlette>=0.27.0",
42
47
  "django>=4.0",
43
- "maturin>=1.5",
48
+ "maturin>=1.14",
44
49
  ]
45
50
 
46
51
  [project.urls]
@@ -8,5 +8,5 @@ que existe um módulo nativo por trás.
8
8
 
9
9
  from ._rust_py_audit import AuditLogger, hello
10
10
 
11
- __version__ = "0.3.2"
11
+ __version__ = "0.3.3"
12
12
  __all__ = ["AuditLogger", "hello"]