tommyslate 0.4.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 (98) hide show
  1. tommyslate-0.4.0/.github/workflows/ci.yml +107 -0
  2. tommyslate-0.4.0/.github/workflows/safe-slate-release.yml +597 -0
  3. tommyslate-0.4.0/.gitignore +11 -0
  4. tommyslate-0.4.0/CHANGELOG.md +233 -0
  5. tommyslate-0.4.0/CITATION.cff +14 -0
  6. tommyslate-0.4.0/Cargo.lock +1342 -0
  7. tommyslate-0.4.0/Cargo.toml +39 -0
  8. tommyslate-0.4.0/Dockerfile.sidecar +16 -0
  9. tommyslate-0.4.0/LICENSE +201 -0
  10. tommyslate-0.4.0/LICENSING.md +16 -0
  11. tommyslate-0.4.0/PKG-INFO +1480 -0
  12. tommyslate-0.4.0/PYPI_READINESS.md +60 -0
  13. tommyslate-0.4.0/README.md +1454 -0
  14. tommyslate-0.4.0/RELEASE_CHECKLIST.md +60 -0
  15. tommyslate-0.4.0/SECURITY.md +15 -0
  16. tommyslate-0.4.0/SOFTWARE_PROVENANCE.md +32 -0
  17. tommyslate-0.4.0/benchmarks/benchmark_candidate_pool.py +254 -0
  18. tommyslate-0.4.0/benchmarks/benchmark_guarded_rerank.py +256 -0
  19. tommyslate-0.4.0/build.rs +13 -0
  20. tommyslate-0.4.0/docs/api-reference.md +77 -0
  21. tommyslate-0.4.0/docs/compatibility.md +46 -0
  22. tommyslate-0.4.0/docs/quickstart.md +85 -0
  23. tommyslate-0.4.0/docs/research-integration.md +91 -0
  24. tommyslate-0.4.0/docs/research-lineage.json +223 -0
  25. tommyslate-0.4.0/docs/sidecar.md +28 -0
  26. tommyslate-0.4.0/external-inputs.template.json +104 -0
  27. tommyslate-0.4.0/library.json +41 -0
  28. tommyslate-0.4.0/proto/safe_slate_reranker.proto +55 -0
  29. tommyslate-0.4.0/pyproject.toml +61 -0
  30. tommyslate-0.4.0/python/safe_slate/__init__.py +291 -0
  31. tommyslate-0.4.0/python/safe_slate/_core.pyi +205 -0
  32. tommyslate-0.4.0/python/safe_slate/calibration.py +99 -0
  33. tommyslate-0.4.0/python/safe_slate/contract.py +574 -0
  34. tommyslate-0.4.0/python/safe_slate/estimation.py +2189 -0
  35. tommyslate-0.4.0/python/safe_slate/evidence.py +695 -0
  36. tommyslate-0.4.0/python/safe_slate/external_intake.py +322 -0
  37. tommyslate-0.4.0/python/safe_slate/feedback.py +250 -0
  38. tommyslate-0.4.0/python/safe_slate/gates.py +159 -0
  39. tommyslate-0.4.0/python/safe_slate/input_manifest.py +669 -0
  40. tommyslate-0.4.0/python/safe_slate/log_io.py +341 -0
  41. tommyslate-0.4.0/python/safe_slate/online.py +505 -0
  42. tommyslate-0.4.0/python/safe_slate/ope.py +1278 -0
  43. tommyslate-0.4.0/python/safe_slate/population_positivity.py +505 -0
  44. tommyslate-0.4.0/python/safe_slate/production.py +904 -0
  45. tommyslate-0.4.0/python/safe_slate/production_intake.py +287 -0
  46. tommyslate-0.4.0/python/safe_slate/py.typed +1 -0
  47. tommyslate-0.4.0/python/safe_slate/runner.py +469 -0
  48. tommyslate-0.4.0/python/safe_slate/service_trace.py +703 -0
  49. tommyslate-0.4.0/python/safe_slate/shadow.py +559 -0
  50. tommyslate-0.4.0/python/safe_slate/shadow_contract.py +305 -0
  51. tommyslate-0.4.0/python/safe_slate/shadow_evidence.py +715 -0
  52. tommyslate-0.4.0/python/safe_slate/uncertainty_binding.py +299 -0
  53. tommyslate-0.4.0/python/safe_slate/uncertainty_utils.py +107 -0
  54. tommyslate-0.4.0/schemas/model_bundle.schema.json +52 -0
  55. tommyslate-0.4.0/schemas/production_grpc_envelope.schema.json +72 -0
  56. tommyslate-0.4.0/schemas/service_trace_completion.schema.json +109 -0
  57. tommyslate-0.4.0/scripts/check_coverage.py +198 -0
  58. tommyslate-0.4.0/scripts/verify_release_artifacts.py +217 -0
  59. tommyslate-0.4.0/src/bin/safe_slate_sidecar.rs +29 -0
  60. tommyslate-0.4.0/src/constraints.rs +235 -0
  61. tommyslate-0.4.0/src/lib.rs +1798 -0
  62. tommyslate-0.4.0/src/oracle.rs +964 -0
  63. tommyslate-0.4.0/src/sidecar.rs +1156 -0
  64. tommyslate-0.4.0/src/uncertainty.rs +275 -0
  65. tommyslate-0.4.0/tests/test_api.py +223 -0
  66. tommyslate-0.4.0/tests/test_benchmark_cli.py +94 -0
  67. tommyslate-0.4.0/tests/test_calibration.py +101 -0
  68. tommyslate-0.4.0/tests/test_candidate_pool_benchmark.py +116 -0
  69. tommyslate-0.4.0/tests/test_contract.py +486 -0
  70. tommyslate-0.4.0/tests/test_coverage_gate.py +172 -0
  71. tommyslate-0.4.0/tests/test_distribution_migration.py +45 -0
  72. tommyslate-0.4.0/tests/test_estimation.py +1302 -0
  73. tommyslate-0.4.0/tests/test_evidence.py +1101 -0
  74. tommyslate-0.4.0/tests/test_external_intake.py +594 -0
  75. tommyslate-0.4.0/tests/test_feedback.py +252 -0
  76. tommyslate-0.4.0/tests/test_gates.py +134 -0
  77. tommyslate-0.4.0/tests/test_input_manifest.py +584 -0
  78. tommyslate-0.4.0/tests/test_log_io.py +208 -0
  79. tommyslate-0.4.0/tests/test_manifest_bound_ope.py +676 -0
  80. tommyslate-0.4.0/tests/test_matrix_digest.py +61 -0
  81. tommyslate-0.4.0/tests/test_native_error_contracts.py +251 -0
  82. tommyslate-0.4.0/tests/test_online.py +275 -0
  83. tommyslate-0.4.0/tests/test_ope.py +492 -0
  84. tommyslate-0.4.0/tests/test_population_positivity.py +308 -0
  85. tommyslate-0.4.0/tests/test_production.py +491 -0
  86. tommyslate-0.4.0/tests/test_production_intake.py +215 -0
  87. tommyslate-0.4.0/tests/test_public_contract.py +163 -0
  88. tommyslate-0.4.0/tests/test_release_artifacts.py +105 -0
  89. tommyslate-0.4.0/tests/test_replacements.py +260 -0
  90. tommyslate-0.4.0/tests/test_research_lineage.py +145 -0
  91. tommyslate-0.4.0/tests/test_runner.py +342 -0
  92. tommyslate-0.4.0/tests/test_service_trace.py +752 -0
  93. tommyslate-0.4.0/tests/test_shadow.py +400 -0
  94. tommyslate-0.4.0/tests/test_shadow_contract.py +141 -0
  95. tommyslate-0.4.0/tests/test_shadow_evidence.py +867 -0
  96. tommyslate-0.4.0/tests/test_uncertainty_binding.py +464 -0
  97. tommyslate-0.4.0/tests/test_uncertainty_utils.py +42 -0
  98. tommyslate-0.4.0/uv.lock +302 -0
@@ -0,0 +1,107 @@
1
+ name: Verify library
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ env:
12
+ PYTHON_VERSION: "3.15"
13
+ RUST_VERSION: "1.99.0"
14
+
15
+ jobs:
16
+ verify:
17
+ runs-on: ubuntu-24.04
18
+ steps:
19
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20
+ with:
21
+ persist-credentials: false
22
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
23
+ with:
24
+ python-version: ${{ env.PYTHON_VERSION }}
25
+ - name: Install fixed toolchains
26
+ run: |
27
+ rustup toolchain install "$RUST_VERSION" --profile minimal --component clippy
28
+ python -m pip install --disable-pip-version-check uv==0.13.1
29
+ - name: Install locked dependencies and native extension
30
+ run: |
31
+ uv sync --frozen
32
+ uv run maturin develop --release
33
+ - name: Verify Python API and types
34
+ run: |
35
+ uv run pytest
36
+ uv run mypy python/safe_slate --check-untyped-defs
37
+ uv run python -m mypy.stubtest safe_slate._core
38
+ uv run python -m compileall -q python/safe_slate scripts tests
39
+ - name: Verify Rust backend
40
+ run: |
41
+ cargo +"$RUST_VERSION" test --locked --all-targets
42
+ cargo +"$RUST_VERSION" clippy --locked --all-targets -- -D warnings
43
+ uv run python -c "import safe_slate._core as core; assert not hasattr(core, '_run_coverage_suite')"
44
+ - name: Verify sidecar container definition
45
+ run: docker build --check -f Dockerfile.sidecar .
46
+ - name: Verify local distributions
47
+ run: |
48
+ uv run maturin build --release --locked --compatibility pypi -i python3.15 --out dist
49
+ uv run maturin sdist --out dist
50
+ uvx --from twine==6.2.0 twine check dist/*
51
+ uv venv "$RUNNER_TEMP/wheel-check" --python python3.15
52
+ uv pip install --python "$RUNNER_TEMP/wheel-check/bin/python" --no-deps dist/*.whl
53
+ "$RUNNER_TEMP/wheel-check/bin/python" -c "import safe_slate._core as core; assert not hasattr(core, '_run_coverage_suite')"
54
+
55
+ coverage:
56
+ runs-on: ubuntu-24.04
57
+ env:
58
+ COVERAGE_FILE: coverage/.coverage
59
+ RUSTUP_TOOLCHAIN: "1.99.0"
60
+ steps:
61
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
62
+ with:
63
+ persist-credentials: false
64
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
65
+ with:
66
+ python-version: ${{ env.PYTHON_VERSION }}
67
+ - name: Install fixed coverage toolchains
68
+ run: |
69
+ rustup toolchain install "$RUST_VERSION" --profile minimal --component clippy
70
+ rustup component add llvm-tools-preview --toolchain "$RUST_VERSION"
71
+ python -m pip install --disable-pip-version-check uv==0.13.1
72
+ cargo +"$RUST_VERSION" install cargo-llvm-cov --version 0.9.1 --locked
73
+ uv sync --frozen --no-install-project
74
+ - name: Collect Python and Rust coverage
75
+ shell: bash
76
+ run: |
77
+ set -euo pipefail
78
+ mkdir -p coverage
79
+ cargo llvm-cov clean --workspace
80
+ eval "$(cargo llvm-cov show-env --sh)"
81
+ uv run maturin develop
82
+ uv run coverage run -m pytest
83
+ uv run python -c "import safe_slate._core as core; core._run_coverage_suite()"
84
+ uv run coverage report --show-missing | tee coverage/python.txt
85
+ uv run coverage json -o coverage/python.json
86
+ uv run coverage xml -o coverage/python.xml
87
+ uv run coverage html -d coverage/python-html
88
+ cargo llvm-cov report --show-missing-lines 2>&1 | tee coverage/rust.txt
89
+ if grep -q "mismatched data" coverage/rust.txt; then
90
+ echo "Rust coverage profiles are incompatible" >&2
91
+ exit 1
92
+ fi
93
+ cargo llvm-cov report --json --summary-only --output-path coverage/rust.json
94
+ cargo llvm-cov report --html --output-dir coverage/rust-html
95
+ - name: Enforce coverage contract
96
+ run: >-
97
+ uv run python scripts/check_coverage.py
98
+ --python-json coverage/python.json
99
+ --rust-json coverage/rust.json
100
+ - name: Upload coverage reports
101
+ if: always()
102
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
103
+ with:
104
+ name: coverage-reports
105
+ path: coverage
106
+ if-no-files-found: warn
107
+ retention-days: 14
@@ -0,0 +1,597 @@
1
+ name: Qualify and publish TommySlate
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ source_sha:
7
+ description: Exact 40-character source commit SHA to build
8
+ required: true
9
+ type: string
10
+ publish_testpypi:
11
+ description: Publish the verified artifacts to TestPyPI
12
+ required: true
13
+ default: false
14
+ type: boolean
15
+ publish_pypi:
16
+ description: Publish the same TestPyPI-verified artifacts to production PyPI
17
+ required: true
18
+ default: false
19
+ type: boolean
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ env:
25
+ PYTHON_VERSION: "3.15"
26
+ RUST_VERSION: "1.99.0"
27
+ MATURIN_VERSION: "v1.16.0"
28
+ PACKAGE_NAME: "tommyslate"
29
+ PACKAGE_VERSION: "0.4.0"
30
+
31
+ jobs:
32
+ validate-source:
33
+ runs-on: ubuntu-24.04
34
+ steps:
35
+ - name: Validate source SHA format
36
+ env:
37
+ SOURCE_SHA: ${{ inputs.source_sha }}
38
+ PUBLISH_TESTPYPI: ${{ inputs.publish_testpypi }}
39
+ PUBLISH_PYPI: ${{ inputs.publish_pypi }}
40
+ run: |
41
+ if [[ ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
42
+ echo "source_sha must be a lowercase 40-character commit SHA" >&2
43
+ exit 1
44
+ fi
45
+ if [[ "$PUBLISH_PYPI" == "true" && "$PUBLISH_TESTPYPI" != "true" ]]; then
46
+ echo "production PyPI publication requires the same run to publish and verify TestPyPI first" >&2
47
+ exit 1
48
+ fi
49
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
50
+ with:
51
+ ref: ${{ inputs.source_sha }}
52
+ fetch-depth: 1
53
+ persist-credentials: false
54
+ - name: Confirm source and package contract
55
+ env:
56
+ SOURCE_SHA: ${{ inputs.source_sha }}
57
+ run: |
58
+ test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
59
+ python3 - <<'PY'
60
+ import tomllib
61
+ import os
62
+ from pathlib import Path
63
+
64
+ pyproject = tomllib.loads(Path("pyproject.toml").read_text())
65
+ cargo = tomllib.loads(Path("Cargo.toml").read_text())
66
+ expected = os.environ["PACKAGE_VERSION"]
67
+ assert pyproject["project"]["version"] == expected
68
+ assert cargo["package"]["version"] == expected
69
+ assert pyproject["project"]["name"] == os.environ["PACKAGE_NAME"]
70
+ assert cargo["package"]["name"] == os.environ["PACKAGE_NAME"]
71
+ assert pyproject["tool"]["maturin"]["module-name"] == "safe_slate._core"
72
+ assert pyproject["project"]["license"] == "Apache-2.0"
73
+ assert cargo["package"]["license"] == "Apache-2.0"
74
+ PY
75
+
76
+ build-wheels:
77
+ name: Build and smoke ${{ matrix.platform.name }}
78
+ needs: validate-source
79
+ strategy:
80
+ fail-fast: false
81
+ matrix:
82
+ platform:
83
+ - name: linux-x86_64
84
+ os: ubuntu-24.04
85
+ target: x86_64-unknown-linux-gnu
86
+ manylinux: "2_28"
87
+ deployment-target: ""
88
+ - name: linux-aarch64
89
+ os: ubuntu-24.04-arm
90
+ target: aarch64-unknown-linux-gnu
91
+ manylinux: "2_28"
92
+ deployment-target: ""
93
+ - name: macos-x86_64
94
+ os: macos-15-intel
95
+ target: x86_64-apple-darwin
96
+ manylinux: ""
97
+ deployment-target: "11.0"
98
+ - name: macos-arm64
99
+ os: macos-15
100
+ target: aarch64-apple-darwin
101
+ manylinux: ""
102
+ deployment-target: "11.0"
103
+ - name: windows-x86_64
104
+ os: windows-2025
105
+ target: x86_64-pc-windows-msvc
106
+ manylinux: ""
107
+ deployment-target: ""
108
+ runs-on: ${{ matrix.platform.os }}
109
+ env:
110
+ MACOSX_DEPLOYMENT_TARGET: ${{ matrix.platform.deployment-target }}
111
+ steps:
112
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
113
+ with:
114
+ ref: ${{ inputs.source_sha }}
115
+ fetch-depth: 1
116
+ persist-credentials: false
117
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
118
+ with:
119
+ python-version: ${{ env.PYTHON_VERSION }}
120
+ - name: Build wheel
121
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
122
+ with:
123
+ command: build
124
+ maturin-version: ${{ env.MATURIN_VERSION }}
125
+ rust-toolchain: ${{ env.RUST_VERSION }}
126
+ target: ${{ matrix.platform.target }}
127
+ manylinux: ${{ matrix.platform.manylinux }}
128
+ args: >-
129
+ --release --locked --compatibility pypi
130
+ -i python3.15 --out dist
131
+ - name: Install and smoke-test local wheel
132
+ shell: bash
133
+ run: |
134
+ python - <<'PY'
135
+ import os
136
+ from pathlib import Path
137
+ import subprocess
138
+ import tempfile
139
+ import venv
140
+
141
+ wheels = list(Path("dist").resolve().glob("*.whl"))
142
+ assert len(wheels) == 1, wheels
143
+ environment = Path(tempfile.mkdtemp(prefix="tommyslate-wheel-"))
144
+ venv.EnvBuilder(with_pip=True).create(environment)
145
+ python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
146
+ subprocess.run(
147
+ [python, "-m", "pip", "install", "--no-index", str(wheels[0])],
148
+ check=True,
149
+ )
150
+ subprocess.run([python, "-m", "pip", "check"], check=True)
151
+ smoke = """
152
+ from safe_slate import optimize_slate
153
+ kwargs = dict(
154
+ rewards=[2.0, 1.0],
155
+ safety_means=[1.0, 1.0],
156
+ safety_variances=[0.0, 0.0],
157
+ max_items=1,
158
+ )
159
+ first = optimize_slate(**kwargs)
160
+ second = optimize_slate(**kwargs)
161
+ assert first.selected_indices == second.selected_indices == [0]
162
+ assert first.rechecked and second.rechecked
163
+ """
164
+ subprocess.run([python, "-c", smoke], cwd=environment, check=True)
165
+ PY
166
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
167
+ with:
168
+ name: wheel-${{ matrix.platform.name }}
169
+ path: dist/*.whl
170
+ if-no-files-found: error
171
+ retention-days: 14
172
+
173
+ build-sdist:
174
+ runs-on: ubuntu-24.04
175
+ needs: validate-source
176
+ steps:
177
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
178
+ with:
179
+ ref: ${{ inputs.source_sha }}
180
+ fetch-depth: 1
181
+ persist-credentials: false
182
+ - name: Build sdist
183
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
184
+ with:
185
+ command: sdist
186
+ maturin-version: ${{ env.MATURIN_VERSION }}
187
+ rust-toolchain: ${{ env.RUST_VERSION }}
188
+ args: --out dist
189
+ - name: Confirm locked Rust graph
190
+ run: cargo +${{ env.RUST_VERSION }} metadata --locked --format-version 1 > /dev/null
191
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
192
+ with:
193
+ name: sdist
194
+ path: dist/*.tar.gz
195
+ if-no-files-found: error
196
+ retention-days: 14
197
+
198
+ aggregate:
199
+ name: Verify complete release set
200
+ needs: [build-wheels, build-sdist]
201
+ runs-on: ubuntu-24.04
202
+ steps:
203
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
204
+ with:
205
+ ref: ${{ inputs.source_sha }}
206
+ fetch-depth: 1
207
+ persist-credentials: false
208
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
209
+ with:
210
+ python-version: ${{ env.PYTHON_VERSION }}
211
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
212
+ with:
213
+ pattern: wheel-*
214
+ path: incoming
215
+ merge-multiple: true
216
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
217
+ with:
218
+ name: sdist
219
+ path: incoming
220
+ - name: Check metadata and generate immutable inventory
221
+ env:
222
+ SOURCE_SHA: ${{ inputs.source_sha }}
223
+ run: |
224
+ python -m pip install --disable-pip-version-check twine==6.2.0
225
+ python -m twine check incoming/*
226
+ mkdir -p release/packages
227
+ cp incoming/*.whl incoming/*.tar.gz release/packages/
228
+ python scripts/verify_release_artifacts.py \
229
+ incoming \
230
+ --source-sha "$SOURCE_SHA" \
231
+ --version "$PACKAGE_VERSION" \
232
+ --output-dir release
233
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
234
+ with:
235
+ name: qualified-release-${{ inputs.source_sha }}
236
+ path: release
237
+ if-no-files-found: error
238
+ retention-days: 14
239
+
240
+ testpypi-preflight:
241
+ name: Check TestPyPI retry safety
242
+ if: inputs.publish_testpypi == true
243
+ needs: aggregate
244
+ runs-on: ubuntu-24.04
245
+ steps:
246
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
247
+ with:
248
+ name: qualified-release-${{ inputs.source_sha }}
249
+ path: release
250
+ - name: Reject conflicting existing files
251
+ run: |
252
+ python - <<'PY'
253
+ import json
254
+ import os
255
+ from pathlib import Path
256
+ from urllib.error import HTTPError
257
+ from urllib.request import urlopen
258
+
259
+ manifest = json.loads(Path("release/release-manifest.json").read_text())
260
+ expected = {item["filename"]: item["sha256"] for item in manifest["files"]}
261
+ version = os.environ["PACKAGE_VERSION"]
262
+ project = os.environ["PACKAGE_NAME"]
263
+ url = f"https://test.pypi.org/pypi/{project}/{version}/json"
264
+ try:
265
+ with urlopen(url, timeout=30) as response:
266
+ payload = json.load(response)
267
+ except HTTPError as error:
268
+ if error.code != 404:
269
+ raise
270
+ payload = {"urls": []}
271
+ for item in payload["urls"]:
272
+ filename = item["filename"]
273
+ remote_hash = item["digests"]["sha256"]
274
+ if filename not in expected or expected[filename] != remote_hash:
275
+ raise SystemExit(f"conflicting TestPyPI file: {filename}")
276
+ print(f"preflight accepted {len(payload['urls'])} existing file(s)")
277
+ PY
278
+
279
+ publish-testpypi:
280
+ name: Publish to TestPyPI with OIDC
281
+ if: inputs.publish_testpypi == true
282
+ needs: testpypi-preflight
283
+ runs-on: ubuntu-24.04
284
+ environment:
285
+ name: testpypi
286
+ url: https://test.pypi.org/p/tommyslate/
287
+ permissions:
288
+ contents: read
289
+ id-token: write
290
+ steps:
291
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
292
+ with:
293
+ name: qualified-release-${{ inputs.source_sha }}
294
+ path: release
295
+ - name: Publish immutable packages
296
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
297
+ with:
298
+ repository-url: https://test.pypi.org/legacy/
299
+ packages-dir: release/packages
300
+ skip-existing: true
301
+ verify-metadata: true
302
+ print-hash: true
303
+ attestations: true
304
+
305
+ verify-testpypi:
306
+ name: Match TestPyPI hashes
307
+ if: inputs.publish_testpypi == true
308
+ needs: publish-testpypi
309
+ runs-on: ubuntu-24.04
310
+ steps:
311
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
312
+ with:
313
+ name: qualified-release-${{ inputs.source_sha }}
314
+ path: release
315
+ - name: Compare remote files with manifest
316
+ run: |
317
+ python - <<'PY'
318
+ import json
319
+ import os
320
+ from pathlib import Path
321
+ import time
322
+ from urllib.error import HTTPError
323
+ from urllib.request import urlopen
324
+
325
+ manifest = json.loads(Path("release/release-manifest.json").read_text())
326
+ expected = {item["filename"]: item["sha256"] for item in manifest["files"]}
327
+ version = os.environ["PACKAGE_VERSION"]
328
+ project = os.environ["PACKAGE_NAME"]
329
+ url = f"https://test.pypi.org/pypi/{project}/{version}/json"
330
+ remote = None
331
+ for attempt in range(12):
332
+ try:
333
+ with urlopen(url, timeout=30) as response:
334
+ payload = json.load(response)
335
+ remote = {
336
+ item["filename"]: item["digests"]["sha256"]
337
+ for item in payload["urls"]
338
+ }
339
+ except HTTPError as error:
340
+ if error.code != 404:
341
+ raise
342
+ if remote == expected:
343
+ break
344
+ if attempt < 11:
345
+ time.sleep(10)
346
+ if remote != expected:
347
+ raise SystemExit(
348
+ f"TestPyPI files differ from manifest: expected={expected}, remote={remote}"
349
+ )
350
+ print("TestPyPI filenames and SHA-256 values match the release manifest")
351
+ PY
352
+
353
+ smoke-test-testpypi:
354
+ name: Install from TestPyPI on ${{ matrix.platform.name }}
355
+ if: inputs.publish_testpypi == true
356
+ needs: verify-testpypi
357
+ strategy:
358
+ fail-fast: false
359
+ matrix:
360
+ platform:
361
+ - name: linux-x86_64
362
+ os: ubuntu-24.04
363
+ - name: linux-aarch64
364
+ os: ubuntu-24.04-arm
365
+ - name: macos-x86_64
366
+ os: macos-15-intel
367
+ - name: macos-arm64
368
+ os: macos-15
369
+ - name: windows-x86_64
370
+ os: windows-2025
371
+ runs-on: ${{ matrix.platform.os }}
372
+ steps:
373
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
374
+ with:
375
+ python-version: ${{ env.PYTHON_VERSION }}
376
+ - name: Install exact TestPyPI version and smoke-test
377
+ shell: bash
378
+ run: |
379
+ python - <<'PY'
380
+ import os
381
+ from pathlib import Path
382
+ import subprocess
383
+ import tempfile
384
+ import venv
385
+
386
+ environment = Path(tempfile.mkdtemp(prefix="tommyslate-testpypi-"))
387
+ venv.EnvBuilder(with_pip=True).create(environment)
388
+ python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
389
+ subprocess.run(
390
+ [
391
+ python,
392
+ "-m",
393
+ "pip",
394
+ "install",
395
+ "--index-url",
396
+ "https://test.pypi.org/simple/",
397
+ "--no-deps",
398
+ f"{os.environ['PACKAGE_NAME']}=={os.environ['PACKAGE_VERSION']}",
399
+ ],
400
+ check=True,
401
+ )
402
+ subprocess.run([python, "-m", "pip", "check"], check=True)
403
+ smoke = """
404
+ from safe_slate import optimize_slate
405
+ kwargs = dict(
406
+ rewards=[2.0, 1.0],
407
+ safety_means=[1.0, 1.0],
408
+ safety_variances=[0.0, 0.0],
409
+ max_items=1,
410
+ )
411
+ first = optimize_slate(**kwargs)
412
+ second = optimize_slate(**kwargs)
413
+ assert first.selected_indices == second.selected_indices == [0]
414
+ assert first.rechecked and second.rechecked
415
+ """
416
+ subprocess.run([python, "-c", smoke], cwd=environment, check=True)
417
+ PY
418
+
419
+ pypi-preflight:
420
+ name: Check production PyPI retry safety
421
+ if: inputs.publish_pypi == true
422
+ needs: smoke-test-testpypi
423
+ runs-on: ubuntu-24.04
424
+ steps:
425
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
426
+ with:
427
+ name: qualified-release-${{ inputs.source_sha }}
428
+ path: release
429
+ - name: Reject conflicting existing production files
430
+ run: |
431
+ python - <<'PY'
432
+ import json
433
+ import os
434
+ from pathlib import Path
435
+ from urllib.error import HTTPError
436
+ from urllib.request import urlopen
437
+
438
+ manifest = json.loads(Path("release/release-manifest.json").read_text())
439
+ expected = {item["filename"]: item["sha256"] for item in manifest["files"]}
440
+ version = os.environ["PACKAGE_VERSION"]
441
+ project = os.environ["PACKAGE_NAME"]
442
+ url = f"https://pypi.org/pypi/{project}/{version}/json"
443
+ try:
444
+ with urlopen(url, timeout=30) as response:
445
+ payload = json.load(response)
446
+ except HTTPError as error:
447
+ if error.code != 404:
448
+ raise
449
+ payload = {"urls": []}
450
+ for item in payload["urls"]:
451
+ filename = item["filename"]
452
+ remote_hash = item["digests"]["sha256"]
453
+ if filename not in expected or expected[filename] != remote_hash:
454
+ raise SystemExit(f"conflicting production PyPI file: {filename}")
455
+ print(f"production preflight accepted {len(payload['urls'])} existing file(s)")
456
+ PY
457
+
458
+ publish-pypi:
459
+ name: Publish to production PyPI with OIDC
460
+ if: inputs.publish_pypi == true
461
+ needs: pypi-preflight
462
+ runs-on: ubuntu-24.04
463
+ environment:
464
+ name: pypi
465
+ url: https://pypi.org/p/tommyslate/
466
+ permissions:
467
+ contents: read
468
+ id-token: write
469
+ steps:
470
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
471
+ with:
472
+ name: qualified-release-${{ inputs.source_sha }}
473
+ path: release
474
+ - name: Publish immutable production packages
475
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
476
+ with:
477
+ packages-dir: release/packages
478
+ skip-existing: true
479
+ verify-metadata: true
480
+ print-hash: true
481
+ attestations: true
482
+
483
+ verify-pypi:
484
+ name: Match production PyPI hashes
485
+ if: inputs.publish_pypi == true
486
+ needs: publish-pypi
487
+ runs-on: ubuntu-24.04
488
+ steps:
489
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
490
+ with:
491
+ name: qualified-release-${{ inputs.source_sha }}
492
+ path: release
493
+ - name: Compare production files with manifest
494
+ run: |
495
+ python - <<'PY'
496
+ import json
497
+ import os
498
+ from pathlib import Path
499
+ import time
500
+ from urllib.error import HTTPError
501
+ from urllib.request import urlopen
502
+
503
+ manifest = json.loads(Path("release/release-manifest.json").read_text())
504
+ expected = {item["filename"]: item["sha256"] for item in manifest["files"]}
505
+ version = os.environ["PACKAGE_VERSION"]
506
+ project = os.environ["PACKAGE_NAME"]
507
+ url = f"https://pypi.org/pypi/{project}/{version}/json"
508
+ remote = None
509
+ for attempt in range(12):
510
+ try:
511
+ with urlopen(url, timeout=30) as response:
512
+ payload = json.load(response)
513
+ remote = {
514
+ item["filename"]: item["digests"]["sha256"]
515
+ for item in payload["urls"]
516
+ }
517
+ except HTTPError as error:
518
+ if error.code != 404:
519
+ raise
520
+ if remote == expected:
521
+ break
522
+ if attempt < 11:
523
+ time.sleep(10)
524
+ if remote != expected:
525
+ raise SystemExit(
526
+ f"production PyPI files differ from manifest: expected={expected}, remote={remote}"
527
+ )
528
+ print("production PyPI filenames and SHA-256 values match the release manifest")
529
+ PY
530
+
531
+ smoke-test-pypi:
532
+ name: Install from production PyPI on ${{ matrix.platform.name }}
533
+ if: inputs.publish_pypi == true
534
+ needs: verify-pypi
535
+ strategy:
536
+ fail-fast: false
537
+ matrix:
538
+ platform:
539
+ - name: linux-x86_64
540
+ os: ubuntu-24.04
541
+ - name: linux-aarch64
542
+ os: ubuntu-24.04-arm
543
+ - name: macos-x86_64
544
+ os: macos-15-intel
545
+ - name: macos-arm64
546
+ os: macos-15
547
+ - name: windows-x86_64
548
+ os: windows-2025
549
+ runs-on: ${{ matrix.platform.os }}
550
+ steps:
551
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
552
+ with:
553
+ python-version: ${{ env.PYTHON_VERSION }}
554
+ - name: Install exact production version and smoke-test
555
+ shell: bash
556
+ run: |
557
+ python - <<'PY'
558
+ import os
559
+ from pathlib import Path
560
+ import subprocess
561
+ import tempfile
562
+ import venv
563
+
564
+ environment = Path(tempfile.mkdtemp(prefix="tommyslate-pypi-"))
565
+ venv.EnvBuilder(with_pip=True).create(environment)
566
+ python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
567
+ subprocess.run(
568
+ [
569
+ python,
570
+ "-m",
571
+ "pip",
572
+ "install",
573
+ "--index-url",
574
+ "https://pypi.org/simple/",
575
+ "--retries",
576
+ "10",
577
+ "--no-deps",
578
+ f"{os.environ['PACKAGE_NAME']}=={os.environ['PACKAGE_VERSION']}",
579
+ ],
580
+ check=True,
581
+ )
582
+ subprocess.run([python, "-m", "pip", "check"], check=True)
583
+ smoke = """
584
+ from safe_slate import optimize_slate
585
+ kwargs = dict(
586
+ rewards=[2.0, 1.0],
587
+ safety_means=[1.0, 1.0],
588
+ safety_variances=[0.0, 0.0],
589
+ max_items=1,
590
+ )
591
+ first = optimize_slate(**kwargs)
592
+ second = optimize_slate(**kwargs)
593
+ assert first.selected_indices == second.selected_indices == [0]
594
+ assert first.rechecked and second.rechecked
595
+ """
596
+ subprocess.run([python, "-c", smoke], cwd=environment, check=True)
597
+ PY