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