rust-py-scheduler 0.2.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.
- rust_py_scheduler-0.2.3/.github/workflows/release.yml +387 -0
- rust_py_scheduler-0.2.3/.gitignore +7 -0
- rust_py_scheduler-0.2.3/CHANGELOG.md +218 -0
- rust_py_scheduler-0.2.3/Cargo.lock +445 -0
- rust_py_scheduler-0.2.3/Cargo.toml +27 -0
- rust_py_scheduler-0.2.3/LICENSE +21 -0
- rust_py_scheduler-0.2.3/Makefile +68 -0
- rust_py_scheduler-0.2.3/PKG-INFO +422 -0
- rust_py_scheduler-0.2.3/README.md +387 -0
- rust_py_scheduler-0.2.3/clippy.toml +11 -0
- rust_py_scheduler-0.2.3/examples/basic_usage.py +56 -0
- rust_py_scheduler-0.2.3/examples/celery_example.py +66 -0
- rust_py_scheduler-0.2.3/examples/django_apps.py +39 -0
- rust_py_scheduler-0.2.3/examples/fastapi_example.py +32 -0
- rust_py_scheduler-0.2.3/pyproject.toml +45 -0
- rust_py_scheduler-0.2.3/python/rust_py_scheduler/__init__.py +6 -0
- rust_py_scheduler-0.2.3/python/rust_py_scheduler/django.py +82 -0
- rust_py_scheduler-0.2.3/python/rust_py_scheduler/fastapi.py +67 -0
- rust_py_scheduler-0.2.3/src/backoff.rs +143 -0
- rust_py_scheduler-0.2.3/src/cron.rs +294 -0
- rust_py_scheduler-0.2.3/src/errors.rs +38 -0
- rust_py_scheduler-0.2.3/src/executor.rs +246 -0
- rust_py_scheduler-0.2.3/src/interval.rs +82 -0
- rust_py_scheduler-0.2.3/src/job.rs +200 -0
- rust_py_scheduler-0.2.3/src/lib.rs +19 -0
- rust_py_scheduler-0.2.3/src/registry.rs +817 -0
- rust_py_scheduler-0.2.3/src/scheduler.rs +305 -0
- rust_py_scheduler-0.2.3/src/time_utils.rs +45 -0
- rust_py_scheduler-0.2.3/tests/test_background.py +76 -0
- rust_py_scheduler-0.2.3/tests/test_concurrency.py +212 -0
- rust_py_scheduler-0.2.3/tests/test_cron.py +127 -0
- rust_py_scheduler-0.2.3/tests/test_django.py +89 -0
- rust_py_scheduler-0.2.3/tests/test_errors.py +85 -0
- rust_py_scheduler-0.2.3/tests/test_fastapi.py +82 -0
- rust_py_scheduler-0.2.3/tests/test_interval.py +93 -0
- rust_py_scheduler-0.2.3/tests/test_jobs.py +62 -0
- rust_py_scheduler-0.2.3/tests/test_list_jobs.py +70 -0
- rust_py_scheduler-0.2.3/tests/test_remove_job.py +62 -0
- rust_py_scheduler-0.2.3/tests/test_retry.py +198 -0
- rust_py_scheduler-0.2.3/tests/test_scheduler.py +62 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Base structure autogenerated by maturin v1.14.0
|
|
2
|
+
# (maturin generate-ci github), then extended with a `test` job that runs the
|
|
3
|
+
# Rust and Python suites on every push/PR. Every wheel build — and the
|
|
4
|
+
# release — depends on `test`, so a change that breaks the tests never gets
|
|
5
|
+
# packaged or published.
|
|
6
|
+
name: CI / Release
|
|
7
|
+
|
|
8
|
+
# ⚠️ O nome deste arquivo é parte da configuração de publicação. O trusted
|
|
9
|
+
# publisher cadastrado no PyPI amarra `release.yml`; renomear aqui quebra a
|
|
10
|
+
# release até o cadastro ser refeito. Era `CI.yml` até 12/08/2026.
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches:
|
|
15
|
+
- main
|
|
16
|
+
- master
|
|
17
|
+
tags:
|
|
18
|
+
# `v*`, não `*`. Com o filtro aberto, QUALQUER tag empurrada disparava o
|
|
19
|
+
# job de release — que só checa `startsWith(github.ref, 'refs/tags/')` —
|
|
20
|
+
# e uma tag de anotação qualquer teria ido ao PyPI. Os outros quatro
|
|
21
|
+
# projetos sempre usaram `v*`.
|
|
22
|
+
- 'v*'
|
|
23
|
+
pull_request:
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
# O lint faz parte do portão de release, não de um workflow paralelo.
|
|
31
|
+
# Nos repositórios de workflow único, um `lint` vermelho ao lado de uma
|
|
32
|
+
# publicação bem-sucedida seria ruído difícil de ler — a leitura passa a ser
|
|
33
|
+
# inequívoca: vermelho significa que não publicou.
|
|
34
|
+
#
|
|
35
|
+
# A toolchain é fixa, não "stable": com `-D warnings`, um clippy mais novo
|
|
36
|
+
# no runner reprova código que ninguém tocou, e a CI ficaria vermelha por
|
|
37
|
+
# conta do calendário. Subir este número vira uma mudança deliberada.
|
|
38
|
+
lint:
|
|
39
|
+
name: fmt + clippy
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
- uses: dtolnay/rust-toolchain@1.88.0
|
|
44
|
+
with:
|
|
45
|
+
components: rustfmt, clippy
|
|
46
|
+
- uses: Swatinem/rust-cache@v2
|
|
47
|
+
- name: cargo fmt
|
|
48
|
+
run: cargo fmt --all -- --check
|
|
49
|
+
# clippy só checa tipos, não linka, então --all-features é seguro aqui
|
|
50
|
+
# (diferente do `cargo test`, que precisa de --no-default-features).
|
|
51
|
+
- name: cargo clippy
|
|
52
|
+
run: cargo clippy --all-targets --all-features -- -D warnings
|
|
53
|
+
|
|
54
|
+
test:
|
|
55
|
+
name: Test (Rust + Python)
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v6
|
|
59
|
+
- uses: actions/setup-python@v6
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.x"
|
|
62
|
+
- name: Install Rust toolchain
|
|
63
|
+
uses: dtolnay/rust-toolchain@stable
|
|
64
|
+
- name: Rust unit tests
|
|
65
|
+
# --no-default-features drops the extension-module feature so the test
|
|
66
|
+
# binary can link against libpython on the runner.
|
|
67
|
+
env:
|
|
68
|
+
PYO3_PYTHON: python
|
|
69
|
+
run: cargo test --no-default-features --lib
|
|
70
|
+
- name: Build, install the extension, and run Python tests
|
|
71
|
+
# `pip install` drives the maturin build backend, so it compiles the
|
|
72
|
+
# Rust extension and installs it (plus the dev extras) into the
|
|
73
|
+
# runner's Python — no virtualenv needed, unlike `maturin develop`.
|
|
74
|
+
run: |
|
|
75
|
+
python -m pip install --upgrade pip
|
|
76
|
+
python -m pip install ".[dev]"
|
|
77
|
+
python -m pytest
|
|
78
|
+
|
|
79
|
+
linux:
|
|
80
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
81
|
+
needs: [test, lint]
|
|
82
|
+
strategy:
|
|
83
|
+
# Sem isto, um alvo que cai CANCELA os irmãos. Foi o que escondeu o
|
|
84
|
+
# estado de armv7/s390x/ppc64le quando o build x86 quebrou no 403 do
|
|
85
|
+
# vault.centos.org, em 12/08/2026: uma falha apagou o diagnóstico
|
|
86
|
+
# das outras cinco.
|
|
87
|
+
fail-fast: false
|
|
88
|
+
matrix:
|
|
89
|
+
platform:
|
|
90
|
+
- runner: ubuntu-22.04
|
|
91
|
+
target: x86_64
|
|
92
|
+
- runner: ubuntu-22.04
|
|
93
|
+
target: x86
|
|
94
|
+
- runner: ubuntu-22.04
|
|
95
|
+
target: aarch64
|
|
96
|
+
- runner: ubuntu-22.04
|
|
97
|
+
target: armv7
|
|
98
|
+
- runner: ubuntu-22.04
|
|
99
|
+
target: s390x
|
|
100
|
+
- runner: ubuntu-22.04
|
|
101
|
+
target: ppc64le
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v6
|
|
104
|
+
- uses: actions/setup-python@v6
|
|
105
|
+
with:
|
|
106
|
+
python-version: "3.x"
|
|
107
|
+
- name: Build wheels
|
|
108
|
+
uses: PyO3/maturin-action@v1
|
|
109
|
+
with:
|
|
110
|
+
target: ${{ matrix.platform.target }}
|
|
111
|
+
args: --release --out dist --find-interpreter
|
|
112
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
113
|
+
manylinux: auto
|
|
114
|
+
- name: Upload wheels
|
|
115
|
+
uses: actions/upload-artifact@v6
|
|
116
|
+
with:
|
|
117
|
+
name: wheels-linux-${{ matrix.platform.target }}
|
|
118
|
+
path: dist
|
|
119
|
+
|
|
120
|
+
musllinux:
|
|
121
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
122
|
+
needs: [test, lint]
|
|
123
|
+
strategy:
|
|
124
|
+
# Uma falha não pode apagar o diagnóstico das outras.
|
|
125
|
+
fail-fast: false
|
|
126
|
+
matrix:
|
|
127
|
+
platform:
|
|
128
|
+
- runner: ubuntu-22.04
|
|
129
|
+
target: x86_64
|
|
130
|
+
- runner: ubuntu-22.04
|
|
131
|
+
target: x86
|
|
132
|
+
- runner: ubuntu-22.04
|
|
133
|
+
target: aarch64
|
|
134
|
+
- runner: ubuntu-22.04
|
|
135
|
+
target: armv7
|
|
136
|
+
steps:
|
|
137
|
+
- uses: actions/checkout@v6
|
|
138
|
+
- uses: actions/setup-python@v6
|
|
139
|
+
with:
|
|
140
|
+
python-version: "3.x"
|
|
141
|
+
- name: Build wheels
|
|
142
|
+
uses: PyO3/maturin-action@v1
|
|
143
|
+
with:
|
|
144
|
+
target: ${{ matrix.platform.target }}
|
|
145
|
+
args: --release --out dist --find-interpreter
|
|
146
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
147
|
+
manylinux: musllinux_1_2
|
|
148
|
+
- name: Upload wheels
|
|
149
|
+
uses: actions/upload-artifact@v6
|
|
150
|
+
with:
|
|
151
|
+
name: wheels-musllinux-${{ matrix.platform.target }}
|
|
152
|
+
path: dist
|
|
153
|
+
|
|
154
|
+
windows:
|
|
155
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
156
|
+
needs: [test, lint]
|
|
157
|
+
strategy:
|
|
158
|
+
# Uma falha não pode apagar o diagnóstico das outras.
|
|
159
|
+
fail-fast: false
|
|
160
|
+
matrix:
|
|
161
|
+
platform:
|
|
162
|
+
- runner: windows-latest
|
|
163
|
+
target: x64
|
|
164
|
+
python_arch: x64
|
|
165
|
+
- runner: windows-latest
|
|
166
|
+
target: x86
|
|
167
|
+
python_arch: x86
|
|
168
|
+
- runner: windows-11-arm
|
|
169
|
+
target: aarch64
|
|
170
|
+
python_arch: arm64
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/checkout@v6
|
|
173
|
+
- uses: actions/setup-python@v6
|
|
174
|
+
with:
|
|
175
|
+
python-version: "3.13"
|
|
176
|
+
architecture: ${{ matrix.platform.python_arch }}
|
|
177
|
+
- name: Build wheels
|
|
178
|
+
uses: PyO3/maturin-action@v1
|
|
179
|
+
with:
|
|
180
|
+
target: ${{ matrix.platform.target }}
|
|
181
|
+
args: --release --out dist --find-interpreter
|
|
182
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
183
|
+
- name: Upload wheels
|
|
184
|
+
uses: actions/upload-artifact@v6
|
|
185
|
+
with:
|
|
186
|
+
name: wheels-windows-${{ matrix.platform.target }}
|
|
187
|
+
path: dist
|
|
188
|
+
|
|
189
|
+
macos:
|
|
190
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
191
|
+
needs: [test, lint]
|
|
192
|
+
strategy:
|
|
193
|
+
# Uma falha não pode apagar o diagnóstico das outras.
|
|
194
|
+
fail-fast: false
|
|
195
|
+
matrix:
|
|
196
|
+
platform:
|
|
197
|
+
- runner: macos-15-intel
|
|
198
|
+
target: x86_64
|
|
199
|
+
- runner: macos-latest
|
|
200
|
+
target: aarch64
|
|
201
|
+
steps:
|
|
202
|
+
- uses: actions/checkout@v6
|
|
203
|
+
- uses: actions/setup-python@v6
|
|
204
|
+
with:
|
|
205
|
+
python-version: "3.x"
|
|
206
|
+
- name: Build wheels
|
|
207
|
+
uses: PyO3/maturin-action@v1
|
|
208
|
+
with:
|
|
209
|
+
target: ${{ matrix.platform.target }}
|
|
210
|
+
args: --release --out dist --find-interpreter
|
|
211
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
212
|
+
- name: Upload wheels
|
|
213
|
+
uses: actions/upload-artifact@v6
|
|
214
|
+
with:
|
|
215
|
+
name: wheels-macos-${{ matrix.platform.target }}
|
|
216
|
+
path: dist
|
|
217
|
+
|
|
218
|
+
sdist:
|
|
219
|
+
runs-on: ubuntu-latest
|
|
220
|
+
needs: [test, lint]
|
|
221
|
+
steps:
|
|
222
|
+
- uses: actions/checkout@v6
|
|
223
|
+
- name: Build sdist
|
|
224
|
+
uses: PyO3/maturin-action@v1
|
|
225
|
+
with:
|
|
226
|
+
command: sdist
|
|
227
|
+
args: --out dist
|
|
228
|
+
- name: Upload sdist
|
|
229
|
+
uses: actions/upload-artifact@v6
|
|
230
|
+
with:
|
|
231
|
+
name: wheels-sdist
|
|
232
|
+
path: dist
|
|
233
|
+
|
|
234
|
+
# Ensaio da troca OIDC, sem tocar no índice de produção. Descobre um
|
|
235
|
+
# publisher mal cadastrado num lugar onde nada é irreversível — no PyPI, um
|
|
236
|
+
# nome de arquivo publicado nunca pode ser reusado.
|
|
237
|
+
#
|
|
238
|
+
# ⚠️ Precisa do seu próprio publisher, cadastrado no TestPyPI (conta separada
|
|
239
|
+
# do PyPI), com owner/repo, workflow `release.yml` e environment `testpypi`.
|
|
240
|
+
publish-testpypi:
|
|
241
|
+
name: Publish to TestPyPI (ensaio)
|
|
242
|
+
runs-on: ubuntu-latest
|
|
243
|
+
if: github.event_name == 'workflow_dispatch'
|
|
244
|
+
needs: [lint, test, linux, musllinux, windows, macos, sdist]
|
|
245
|
+
environment: testpypi
|
|
246
|
+
permissions:
|
|
247
|
+
id-token: write
|
|
248
|
+
steps:
|
|
249
|
+
- uses: actions/download-artifact@v7
|
|
250
|
+
with:
|
|
251
|
+
pattern: wheels-*
|
|
252
|
+
path: dist
|
|
253
|
+
merge-multiple: true
|
|
254
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
255
|
+
with:
|
|
256
|
+
repository-url: https://test.pypi.org/legacy/
|
|
257
|
+
packages-dir: dist
|
|
258
|
+
skip-existing: true
|
|
259
|
+
|
|
260
|
+
release:
|
|
261
|
+
name: Publish to PyPI
|
|
262
|
+
runs-on: ubuntu-latest
|
|
263
|
+
# Só tag. Antes este job também rodava em `workflow_dispatch` porque o
|
|
264
|
+
# atestado de artefato vivia aqui; o ensaio manual virou o job acima, então
|
|
265
|
+
# a guarda pôde ficar estrita — e os `if:` que cada passo repetia sumiram.
|
|
266
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
267
|
+
needs: [lint, test, linux, musllinux, windows, macos, sdist]
|
|
268
|
+
|
|
269
|
+
# Trusted publishing (OIDC): o `UV_PUBLISH_TOKEN` deixou de existir. O
|
|
270
|
+
# GitHub emite um JWT curto afirmando repositório, workflow e ref, e o PyPI
|
|
271
|
+
# o troca por um token de upload de poucos minutos, restrito a este projeto.
|
|
272
|
+
# Era o último dos cinco publicando com segredo de longa duração.
|
|
273
|
+
#
|
|
274
|
+
# ⚠️ Os três valores precisam bater EXATAMENTE com o publisher cadastrado
|
|
275
|
+
# em PyPI → Manage → Publishing:
|
|
276
|
+
# owner/repo .... robertolima-dev/rust-py-scheduler
|
|
277
|
+
# workflow ...... release.yml (renomeado de CI.yml em 12/08/2026)
|
|
278
|
+
# environment ... pypi
|
|
279
|
+
environment: pypi
|
|
280
|
+
permissions:
|
|
281
|
+
# Troca OIDC do trusted publishing e assinatura do atestado.
|
|
282
|
+
id-token: write
|
|
283
|
+
contents: write
|
|
284
|
+
attestations: write
|
|
285
|
+
|
|
286
|
+
steps:
|
|
287
|
+
# `dist/` com merge-multiple, no lugar dos diretórios `wheels-*/` que a
|
|
288
|
+
# forma antiga produzia — é o layout que os outros quatro projetos usam,
|
|
289
|
+
# e o que `packages-dir` da action oficial espera.
|
|
290
|
+
- uses: actions/download-artifact@v7
|
|
291
|
+
with:
|
|
292
|
+
pattern: wheels-*
|
|
293
|
+
path: dist
|
|
294
|
+
merge-multiple: true
|
|
295
|
+
|
|
296
|
+
# Atestado do Actions (sigstore), independente dos atestados PEP 740 que
|
|
297
|
+
# o publicador gera depois — sistemas diferentes, ambos úteis.
|
|
298
|
+
- name: Generate artifact attestation
|
|
299
|
+
uses: actions/attest@v4
|
|
300
|
+
with:
|
|
301
|
+
subject-path: 'dist/*'
|
|
302
|
+
|
|
303
|
+
- uses: actions/setup-python@v6
|
|
304
|
+
with:
|
|
305
|
+
python-version: "3.12"
|
|
306
|
+
- name: Install the verification dependency
|
|
307
|
+
run: python -m pip install --upgrade requests
|
|
308
|
+
|
|
309
|
+
# A sanity check before anything leaves the runner: publishing an
|
|
310
|
+
# incomplete set is worse than not publishing, because PyPI never lets a
|
|
311
|
+
# filename be reused — a half-uploaded version can only be completed,
|
|
312
|
+
# never replaced.
|
|
313
|
+
- name: Verify the artifact set is complete
|
|
314
|
+
run: |
|
|
315
|
+
set -euo pipefail
|
|
316
|
+
ls -la dist/
|
|
317
|
+
test -n "$(find dist -name '*.tar.gz' -print -quit)" \
|
|
318
|
+
|| { echo "::error::sdist ausente — abortando antes de publicar"; exit 1; }
|
|
319
|
+
wheels=$(find dist -name '*.whl' | wc -l)
|
|
320
|
+
echo "wheels: $wheels"
|
|
321
|
+
test "$wheels" -ge 15 \
|
|
322
|
+
|| { echo "::error::só $wheels wheels; esperado >= 15 (6 linux, 4 musl, 3 win, 2 macos)"; exit 1; }
|
|
323
|
+
|
|
324
|
+
# O PyPI devolve 500 transitório com frequência suficiente para importar,
|
|
325
|
+
# e o upload é arquivo por arquivo: uma falha no meio deixa a versão
|
|
326
|
+
# publicada pela metade. `skip-existing` é o que torna a segunda
|
|
327
|
+
# tentativa segura — faz o papel que o `--check-url` do `uv publish`
|
|
328
|
+
# fazia aqui antes.
|
|
329
|
+
#
|
|
330
|
+
# São dois passos explícitos em vez de um wrapper de retry porque o
|
|
331
|
+
# trusted publishing exige a action oficial, e ela não dá para acionar
|
|
332
|
+
# por linha de comando de dentro do `nick-fields/retry`.
|
|
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
|
+
# Proves the release is actually complete, rather than trusting an exit
|
|
353
|
+
# code that a partial upload can still produce.
|
|
354
|
+
- name: Verify every file reached PyPI
|
|
355
|
+
run: |
|
|
356
|
+
set -euo pipefail
|
|
357
|
+
python - <<'PY'
|
|
358
|
+
import os, sys, glob, time, requests
|
|
359
|
+
version = os.environ["GITHUB_REF_NAME"].lstrip("v")
|
|
360
|
+
# Só .whl e .tar.gz: são os únicos tipos de arquivo que o PyPI hospeda.
|
|
361
|
+
# `pypa/gh-action-pypi-publish` deixa um .publish.attestation por
|
|
362
|
+
# distribuição em dist/ depois de publicar (atestados PEP 740, que
|
|
363
|
+
# sobem junto do arquivo, não como arquivo próprio). Um glob amplo os
|
|
364
|
+
# conta como "faltando no PyPI" e reprova uma release que deu certo.
|
|
365
|
+
local = {
|
|
366
|
+
os.path.basename(p)
|
|
367
|
+
for pattern in ("dist/*.whl", "dist/*.tar.gz")
|
|
368
|
+
for p in glob.glob(pattern)
|
|
369
|
+
}
|
|
370
|
+
url = f"https://pypi.org/pypi/rust-py-scheduler/{version}/json"
|
|
371
|
+
for attempt in range(6): # PyPI's JSON API is cached
|
|
372
|
+
response = requests.get(url, timeout=30)
|
|
373
|
+
if response.status_code == 200:
|
|
374
|
+
remote = {f["filename"] for f in response.json()["urls"]}
|
|
375
|
+
missing = local - remote
|
|
376
|
+
if not missing:
|
|
377
|
+
print(f"OK: {len(remote)} arquivos publicados na {version}")
|
|
378
|
+
sys.exit(0)
|
|
379
|
+
else:
|
|
380
|
+
missing = local
|
|
381
|
+
print(f"tentativa {attempt + 1}: faltam {len(missing)}")
|
|
382
|
+
time.sleep(20)
|
|
383
|
+
print("::error::faltaram arquivos no PyPI:")
|
|
384
|
+
for name in sorted(missing):
|
|
385
|
+
print(f" - {name}")
|
|
386
|
+
sys.exit(1)
|
|
387
|
+
PY
|
|
@@ -0,0 +1,218 @@
|
|
|
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.3] — 2026-08-12
|
|
9
|
+
|
|
10
|
+
Migração para trusted publishing. Nada muda no comportamento da biblioteca.
|
|
11
|
+
Era o último dos cinco `rust_py_*` publicando com segredo de longa duração.
|
|
12
|
+
|
|
13
|
+
### Alterado
|
|
14
|
+
|
|
15
|
+
- **Publicação por trusted publishing (OIDC)**, no lugar de
|
|
16
|
+
`secrets.PYPI_API_TOKEN` + `uv publish`. O GitHub emite um JWT curto
|
|
17
|
+
afirmando repositório, workflow e ref, e o PyPI o troca por um token de
|
|
18
|
+
upload válido por poucos minutos e restrito a este projeto. O upload passou
|
|
19
|
+
a ser dois `pypa/gh-action-pypi-publish` explícitos com
|
|
20
|
+
`skip-existing: true`, que faz o papel que o `--check-url` do `uv publish`
|
|
21
|
+
fazia: tornar a segunda tentativa segura.
|
|
22
|
+
|
|
23
|
+
Diferente do `rust_py_monitor`, aqui **não havia bug a consertar** — o
|
|
24
|
+
`uv publish` já transmitia o metadado de licença corretamente, e a 0.2.2 saiu
|
|
25
|
+
com `license_expression: MIT`. Esta migração é higiene de segredo e
|
|
26
|
+
convergência de forma.
|
|
27
|
+
|
|
28
|
+
⚠️ **Depende de cadastro no PyPI**, com os valores batendo exatamente:
|
|
29
|
+
`robertolima-dev` / `rust-py-scheduler` / `release.yml` / `pypi`. Sem o
|
|
30
|
+
publisher, o job falha na troca do token — depois de construir tudo e antes
|
|
31
|
+
de qualquer arquivo sair do runner.
|
|
32
|
+
|
|
33
|
+
- **`CI.yml` renomeado para `release.yml`.** O trusted publisher cadastrado no
|
|
34
|
+
PyPI amarra o nome do arquivo de workflow, então o rename precisou vir
|
|
35
|
+
**antes** do cadastro — renomear depois obrigaria a cadastrar de novo. Alinha
|
|
36
|
+
com os outros quatro projetos e é pré-requisito do `release.yml` único do
|
|
37
|
+
template (**P3-1**).
|
|
38
|
+
|
|
39
|
+
- **Artefatos passam por `dist/`** (`merge-multiple: true`) no lugar dos
|
|
40
|
+
diretórios `wheels-*/`. É o layout que `packages-dir` espera e o que os
|
|
41
|
+
outros quatro já usam. Os dois passos de verificação e o atestado foram
|
|
42
|
+
ajustados para o novo caminho.
|
|
43
|
+
|
|
44
|
+
- **O job de release ficou restrito a tag.** Antes ele também rodava em
|
|
45
|
+
`workflow_dispatch`, porque o atestado de artefato vivia nele; o ensaio
|
|
46
|
+
manual virou um job próprio (`publish-testpypi`, apontando para o TestPyPI),
|
|
47
|
+
então a guarda pôde ficar estrita e os `if:` repetidos em cada passo sumiram.
|
|
48
|
+
|
|
49
|
+
### Corrigido
|
|
50
|
+
|
|
51
|
+
- **Filtro de tag fechado para `v*`.** Era `'*'`, ou seja, **qualquer** tag
|
|
52
|
+
empurrada disparava o workflow — e o job de release só checava
|
|
53
|
+
`startsWith(github.ref, 'refs/tags/')`, que é verdadeiro para qualquer tag.
|
|
54
|
+
Uma tag de anotação, de rascunho ou de qualquer outra finalidade teria ido
|
|
55
|
+
ao PyPI. Os outros quatro projetos sempre usaram `v*`.
|
|
56
|
+
|
|
57
|
+
- **`fail-fast: false` nas quatro matrizes de build.** O padrão do GitHub
|
|
58
|
+
cancela os alvos irmãos quando um cai. Em 12/08/2026 o build `x86` quebrou
|
|
59
|
+
num 403 do `vault.centos.org` (CentOS 7 EOL, instabilidade de mirror) e
|
|
60
|
+
levou junto o diagnóstico de `armv7`, `s390x` e `ppc64le` — não dava para
|
|
61
|
+
saber se tinham falhado ou apenas sido interrompidos. Uma falha não pode
|
|
62
|
+
apagar a informação das outras.
|
|
63
|
+
|
|
64
|
+
## [0.2.2] - 2026-08-12
|
|
65
|
+
|
|
66
|
+
Correção do passo de verificação pós-upload, mais o trabalho de licença e lint
|
|
67
|
+
de 12/08/2026 (**P1-4** e **P1-6**).
|
|
68
|
+
|
|
69
|
+
### Adicionado
|
|
70
|
+
|
|
71
|
+
- **`LICENSE`** (MIT). O projeto declarava MIT no `Cargo.toml` e no
|
|
72
|
+
`pyproject.toml` desde sempre, mas o texto não existia no repositório
|
|
73
|
+
(**P1-4**). Era o único dos cinco cujo metadado no PyPI já estava certo
|
|
74
|
+
(`license_expression: MIT`, graças à maturin 1.14 que ele já exigia) e ainda
|
|
75
|
+
assim sem o arquivo — metadado e texto são coisas separadas.
|
|
76
|
+
- **Job `lint`** (`cargo fmt --check` + `cargo clippy -D warnings`) no
|
|
77
|
+
`CI.yml`, com `needs: [test, lint]` nos jobs de build e no de publicação —
|
|
78
|
+
o lint barra a release, igual ao `test`. Toolchain fixa em `1.88.0`.
|
|
79
|
+
- **`clippy.toml` e `Makefile`** compartilhados com os outros quatro
|
|
80
|
+
`rust_py_*`. O `clippy.toml` sobe `too-many-arguments-threshold` para 10, e
|
|
81
|
+
é o que silencia os dois `too_many_arguments` deste projeto: `add_job()` leva
|
|
82
|
+
8 parâmetros porque a API Python leva 8 (`retry_backoff`, `timeout`,
|
|
83
|
+
`max_instances` e afins). Reagrupar em struct pioraria a API que o usuário vê.
|
|
84
|
+
|
|
85
|
+
### Corrigido
|
|
86
|
+
|
|
87
|
+
- **Formatação.** `cargo fmt --all` aplicado (12 blocos divergentes). Os 55
|
|
88
|
+
testes Rust seguem passando.
|
|
89
|
+
|
|
90
|
+
- **Declaração de licença no formato do PEP 639.** Entrou
|
|
91
|
+
`license-files = ["LICENSE"]` e o classifier `License :: OSI Approved` saiu,
|
|
92
|
+
por ser mutuamente exclusivo com `License-Expression` no Metadata 2.4.
|
|
93
|
+
|
|
94
|
+
### Corrigido — pipeline
|
|
95
|
+
|
|
96
|
+
- **Falso negativo na verificação pós-publicação.** O passo comparava
|
|
97
|
+
`glob("wheels-*/*")` com o que estava no PyPI, o que conta qualquer arquivo
|
|
98
|
+
que apareça nos diretórios de artefato — não só distribuições. O passo agora
|
|
99
|
+
só considera `*.whl` e `*.tar.gz`, os únicos tipos de arquivo que o PyPI
|
|
100
|
+
hospeda. Foi o `rust_py_rate_limit` que expôs o problema: lá o publicador
|
|
101
|
+
deixa um `.publish.attestation` por distribuição, e a verificação reprovou
|
|
102
|
+
uma release que tinha subido inteira.
|
|
103
|
+
|
|
104
|
+
**Não vai ao PyPI sozinha:** a 0.2.0 está lá completa e a 0.2.1/0.2.2 só mexem
|
|
105
|
+
no pipeline. Os números ficam reservados e saem com a próxima mudança que valha
|
|
106
|
+
uma tag.
|
|
107
|
+
|
|
108
|
+
## [0.2.1] — não publicada
|
|
109
|
+
|
|
110
|
+
Versão de infraestrutura: nada muda no comportamento da biblioteca.
|
|
111
|
+
|
|
112
|
+
**Não foi ao PyPI de propósito.** A 0.2.0 está completa lá (ver a nota abaixo)
|
|
113
|
+
e esta versão só mexe no pipeline de publicação, então uma release dela seria
|
|
114
|
+
ruído para quem usa a biblioteca — as melhorias já valem no próximo release,
|
|
115
|
+
que herda o workflow. O número fica reservado aqui e sai junto com a próxima
|
|
116
|
+
mudança que valha uma tag.
|
|
117
|
+
|
|
118
|
+
### Corrigido
|
|
119
|
+
|
|
120
|
+
- **O upload aguenta o 500 do PyPI.** A publicação da 0.2.0 morreu num
|
|
121
|
+
`500 Internal Server Error` do `upload.pypi.org` no meio da fila, deixando
|
|
122
|
+
11 dos 16 arquivos publicados — faltaram a wheel musllinux x86_64, as três
|
|
123
|
+
de Windows e o sdist. O passo de publicação agora tem retry (4 tentativas).
|
|
124
|
+
- **O retry ficou seguro.** O `uv publish` só pula arquivos que consegue
|
|
125
|
+
confirmar que já estão no índice; sem `--check-url` uma segunda tentativa
|
|
126
|
+
podia falhar de vez nos arquivos que já tinham subido. O parâmetro foi
|
|
127
|
+
acrescentado.
|
|
128
|
+
- **Conferência dos dois lados.** O job de release valida o conjunto de
|
|
129
|
+
artefatos **antes** de publicar (aborta se faltar o sdist ou se houver menos
|
|
130
|
+
wheels que o esperado) e confere contra a API do PyPI **depois**, listando
|
|
131
|
+
nome por nome o que não chegou.
|
|
132
|
+
|
|
133
|
+
### Nota sobre a 0.2.0
|
|
134
|
+
|
|
135
|
+
A 0.2.0 chegou ao PyPI com 11 dos 16 arquivos por causa do erro descrito
|
|
136
|
+
acima, e **foi completada por um re-run em 11/08/2026** — hoje tem os 16
|
|
137
|
+
arquivos, com sdist. Ou seja, a 0.2.0 é válida e é a versão a usar; esta 0.2.1
|
|
138
|
+
não muda uma linha da biblioteca, só o pipeline que a publica.
|
|
139
|
+
|
|
140
|
+
## [0.2.0] - 2026-08-11
|
|
141
|
+
|
|
142
|
+
Quatro correções de P0 do BACKLOG2, todas no mesmo release porque as três
|
|
143
|
+
últimas mexem no mesmo caminho de execução.
|
|
144
|
+
|
|
145
|
+
### Corrigido
|
|
146
|
+
|
|
147
|
+
- **Deriva acumulada no intervalo (P0-3).** O reagendamento usava
|
|
148
|
+
`Instant::now()` depois de o callback terminar, então o período real era
|
|
149
|
+
`intervalo + duração do callback`: um `every("60s")` com callback de 5s
|
|
150
|
+
rodava a cada 65s e perdia ~1h de execuções por dia. Agora o próximo
|
|
151
|
+
deadline é ancorado no deadline que acabou de disparar, mantendo a cadência
|
|
152
|
+
na grade `t0 + n * intervalo`.
|
|
153
|
+
- **Retry em rajada (P0-4a).** As tentativas disparavam coladas, sem espera —
|
|
154
|
+
`max_retries=5` contra uma API fora do ar virava 6 chamadas em sequência,
|
|
155
|
+
martelando o serviço que já estava caído. Agora há backoff exponencial com
|
|
156
|
+
jitter entre as tentativas.
|
|
157
|
+
- **Um job pendurado congelava o scheduler (P0-4b).** Os callbacks rodavam em
|
|
158
|
+
sequência dentro do loop de agendamento, que é single-thread: um callback
|
|
159
|
+
travado parava *todos* os outros jobs. Agora existe um pool de workers e o
|
|
160
|
+
loop só despacha.
|
|
161
|
+
- **Mutex envenenado derrubava tudo em cascata (P0-5).** Havia 14
|
|
162
|
+
`.expect("... mutex poisoned")` em caminho de produção. Bastava uma thread
|
|
163
|
+
entrar em pânico segurando o lock para toda chamada seguinte (`add_job`,
|
|
164
|
+
`remove_job`, `list_jobs`, o próprio loop) entrar em pânico, permanentemente.
|
|
165
|
+
Agora todos recuperam com `unwrap_or_else(|p| p.into_inner())`.
|
|
166
|
+
|
|
167
|
+
### Adicionado
|
|
168
|
+
|
|
169
|
+
- `Scheduler(max_workers=4)` — tamanho do pool de workers.
|
|
170
|
+
- `every(...)` e `cron(...)` aceitam:
|
|
171
|
+
- `retry_backoff=0.5` — base do backoff exponencial entre tentativas. A
|
|
172
|
+
espera dobra a cada tentativa, é limitada a 30s e recebe jitter. `0`
|
|
173
|
+
restaura o comportamento anterior (imediato).
|
|
174
|
+
- `timeout=None` — segundos após os quais o scheduler para de esperar por
|
|
175
|
+
uma execução.
|
|
176
|
+
- `max_instances=1` — quantas execuções do mesmo job podem se sobrepor.
|
|
177
|
+
- **Política de misfire.** Quando um job fica para trás em um ou mais ciclos
|
|
178
|
+
inteiros, ele pula para o próximo deadline futuro e roda uma vez — em vez de
|
|
179
|
+
disparar de uma vez todas as execuções perdidas.
|
|
180
|
+
- **Coalescing.** Com `max_instances` saturado, o tick que vence é descartado e
|
|
181
|
+
contado em `coalesced_count`, em vez de enfileirar execuções que o job nunca
|
|
182
|
+
vai alcançar.
|
|
183
|
+
- `list_jobs()` ganhou as chaves `retry_backoff`, `timeout`, `max_instances`,
|
|
184
|
+
`running`, `coalesced_count` e `timeout_count`.
|
|
185
|
+
|
|
186
|
+
### Alterado — atenção ao atualizar
|
|
187
|
+
|
|
188
|
+
- **O padrão de retry mudou.** Antes as tentativas eram imediatas; agora
|
|
189
|
+
esperam `retry_backoff` (0,5s de base) entre elas. Código que dependia de
|
|
190
|
+
`max_retries` resolver dentro do mesmo tick precisa ou de mais tempo, ou de
|
|
191
|
+
`retry_backoff=0`.
|
|
192
|
+
- **Callbacks agora rodam em threads de worker**, não na thread que chamou
|
|
193
|
+
`run()`. Callback que dependia de estado preso a uma thread específica
|
|
194
|
+
(`threading.local`, por exemplo) precisa ser revisto. Dois callbacks do mesmo
|
|
195
|
+
job não se sobrepõem por padrão (`max_instances=1`), mas callbacks de jobs
|
|
196
|
+
diferentes agora podem rodar ao mesmo tempo.
|
|
197
|
+
- `Scheduler()` continua funcionando sem argumento; `max_workers` tem padrão.
|
|
198
|
+
|
|
199
|
+
### Limitação conhecida
|
|
200
|
+
|
|
201
|
+
- **`timeout` não mata o callback.** Não existe forma segura de interromper
|
|
202
|
+
código Python arbitrário a partir de outra thread. O que o timeout faz é
|
|
203
|
+
limitar a exposição do *scheduler*: marca a execução como estourada, devolve
|
|
204
|
+
a vaga de `max_instances` e para de contabilizá-la. A thread do worker segue
|
|
205
|
+
presa dentro do Python até o callback retornar por conta própria — ou seja,
|
|
206
|
+
um job que pendura para sempre custa um worker para sempre. Por isso
|
|
207
|
+
`shutdown()` pode bloquear esperando um callback assim.
|
|
208
|
+
|
|
209
|
+
## [0.1.5]
|
|
210
|
+
|
|
211
|
+
### Adicionado
|
|
212
|
+
|
|
213
|
+
- `Scheduler` com `every()` / `cron()`, nas formas de chamada direta e de
|
|
214
|
+
decorator.
|
|
215
|
+
- Expressões cron de 5 campos, avaliadas no fuso local.
|
|
216
|
+
- `list_jobs()`, `remove_job()`, `run()`, `start_background()`, `shutdown()`.
|
|
217
|
+
- `max_retries` por job.
|
|
218
|
+
- Integrações para FastAPI e Django.
|