Skip to content

Commit 816e375

Browse files
committed
Add PyPI wheel CI/CD and switch to Apache-2.0
1 parent 3d7312b commit 816e375

7 files changed

Lines changed: 609 additions & 28 deletions

File tree

.github/workflows/build.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Build Wheels
2+
3+
on:
4+
workflow_dispatch: # Manual trigger only to save CI minutes
5+
6+
jobs:
7+
build_wheels:
8+
name: Build wheels on ${{ matrix.os }}
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
include:
14+
# Linux x86_64
15+
- os: ubuntu-latest
16+
cibw_archs: "x86_64"
17+
18+
# macOS Intel (x86_64)
19+
- os: macos-15-intel
20+
cibw_archs: "x86_64"
21+
22+
# macOS Apple Silicon (arm64)
23+
- os: macos-15
24+
cibw_archs: "arm64"
25+
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- name: Build wheels
30+
uses: pypa/cibuildwheel@v2.22
31+
env:
32+
CIBW_ARCHS: ${{ matrix.cibw_archs }}
33+
34+
- name: Upload wheels
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: wheels-${{ matrix.os }}-${{ matrix.cibw_archs }}
38+
path: ./wheelhouse/*.whl
39+
retention-days: 7
40+
41+
build_sdist:
42+
name: Build source distribution
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: "3.12"
51+
52+
- name: Install build dependencies
53+
run: |
54+
python -m pip install --upgrade pip
55+
pip install build
56+
57+
- name: Build sdist
58+
run: python -m build --sdist
59+
60+
- name: Upload sdist
61+
uses: actions/upload-artifact@v4
62+
with:
63+
name: sdist
64+
path: dist/*.tar.gz
65+
retention-days: 7
66+
67+
verify_wheels:
68+
name: Verify built wheels
69+
needs: [build_wheels, build_sdist]
70+
runs-on: ubuntu-latest
71+
steps:
72+
- uses: actions/download-artifact@v4
73+
with:
74+
path: dist
75+
merge-multiple: true
76+
77+
- name: List artifacts
78+
run: |
79+
ls -lh dist/
80+
echo "Total wheels: $(ls -1 dist/*.whl 2>/dev/null | wc -l)"
81+
echo "Total sdist: $(ls -1 dist/*.tar.gz 2>/dev/null | wc -l)"

.github/workflows/publish.yml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # Trigger on version tags like v0.1.0, v1.0.0, etc.
7+
workflow_dispatch:
8+
inputs:
9+
dry_run:
10+
description: 'Dry run (skip actual upload)'
11+
required: false
12+
default: false
13+
type: boolean
14+
15+
jobs:
16+
build_wheels:
17+
name: Build wheels on ${{ matrix.os }}
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
include:
23+
# Linux x86_64
24+
- os: ubuntu-latest
25+
cibw_archs: "x86_64"
26+
27+
# macOS Intel (x86_64)
28+
- os: macos-15-intel
29+
cibw_archs: "x86_64"
30+
31+
# macOS Apple Silicon (arm64)
32+
- os: macos-15
33+
cibw_archs: "arm64"
34+
35+
steps:
36+
- uses: actions/checkout@v4
37+
38+
- name: Build wheels
39+
uses: pypa/cibuildwheel@v2.22
40+
env:
41+
CIBW_ARCHS: ${{ matrix.cibw_archs }}
42+
43+
- name: Upload wheels
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: wheels-${{ matrix.os }}-${{ matrix.cibw_archs }}
47+
path: ./wheelhouse/*.whl
48+
retention-days: 1
49+
50+
build_sdist:
51+
name: Build source distribution
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v4
55+
56+
- name: Set up Python
57+
uses: actions/setup-python@v5
58+
with:
59+
python-version: "3.12"
60+
61+
- name: Install build dependencies
62+
run: |
63+
python -m pip install --upgrade pip
64+
pip install build
65+
66+
- name: Build sdist
67+
run: python -m build --sdist
68+
69+
- name: Upload sdist
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: sdist
73+
path: dist/*.tar.gz
74+
retention-days: 1
75+
76+
publish:
77+
name: Publish to PyPI
78+
needs: [build_wheels, build_sdist]
79+
runs-on: ubuntu-latest
80+
environment:
81+
name: pypi
82+
url: https://pypi.org/project/kh57/
83+
84+
permissions:
85+
id-token: write # IMPORTANT: Needed for Trusted Publisher (OIDC)
86+
contents: read
87+
88+
steps:
89+
- name: Download all artifacts
90+
uses: actions/download-artifact@v4
91+
with:
92+
path: dist
93+
merge-multiple: true
94+
95+
- name: List distributions
96+
run: |
97+
ls -lh dist/
98+
echo "Total wheels: $(ls -1 dist/*.whl 2>/dev/null | wc -l)"
99+
echo "Total sdist: $(ls -1 dist/*.tar.gz 2>/dev/null | wc -l)"
100+
101+
- name: Publish to PyPI
102+
if: ${{ !inputs.dry_run }}
103+
uses: pypa/gh-action-pypi-publish@release/v1
104+
with:
105+
skip-existing: true
106+
verbose: true
107+
108+
- name: Dry run mode
109+
if: ${{ inputs.dry_run }}
110+
run: |
111+
echo "DRY RUN MODE - Skipping PyPI upload"
112+
echo "Wheels that would be uploaded:"
113+
ls -lh dist/

.github/workflows/test-pypi.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Test PyPI Package
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Version to test (e.g., 0.1.0). Leave empty for latest.'
8+
required: false
9+
default: ''
10+
11+
jobs:
12+
smoke_test:
13+
name: Smoke test on ${{ matrix.os }} (Python ${{ matrix.python-version }})
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, macos-latest]
19+
python-version: ['3.12', '3.13']
20+
21+
steps:
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
27+
- name: Install package from PyPI
28+
run: |
29+
python -m pip install --upgrade pip
30+
if [ -z "${{ inputs.version }}" ]; then
31+
echo "Installing latest version from PyPI..."
32+
pip install kh57
33+
else
34+
echo "Installing version ${{ inputs.version }} from PyPI..."
35+
pip install kh57==${{ inputs.version }}
36+
fi
37+
38+
- name: Show installed version
39+
run: |
40+
python -c "import kh57; print('kh57 imported successfully')"
41+
pip show kh57
42+
43+
- name: Smoke test - core primitives
44+
run: |
45+
python << 'EOF'
46+
import random
47+
import kh57
48+
49+
# 1. uniform_hash is deterministic
50+
h1 = kh57.uniform_hash(12345)
51+
h2 = kh57.uniform_hash(12345)
52+
assert h1 == h2, f"uniform_hash not deterministic"
53+
assert isinstance(h1, int)
54+
print(f" uniform_hash(12345) ok")
55+
56+
# 2. kh57 encode/recover round-trip
57+
for key in [0, 1, 42, 12345, (1 << 57) - 1]:
58+
encoded = kh57.kh57(key)
59+
recovered = kh57.recover(encoded)
60+
assert recovered == key, f"kh57/recover broken for {key}: got {recovered}"
61+
print(f" kh57/recover round-trip ok")
62+
63+
# 3. sampling from a MemBackend
64+
backend = kh57.MemBackend()
65+
for i in range(1000):
66+
# store under kh57-encoded 8-byte big-endian keys, as the docstring specifies
67+
backend.put(kh57.kh57(i).to_bytes(8, "big"), f"v{i}".encode())
68+
69+
rng_a = random.Random(42)
70+
rng_b = random.Random(42)
71+
samples_a = kh57.sample(backend, n=10, begin=0, end=1000, rng=rng_a)
72+
samples_b = kh57.sample(backend, n=10, begin=0, end=1000, rng=rng_b)
73+
assert set(samples_a) == set(samples_b), "sampling not deterministic with same seeded rng"
74+
assert len(samples_a) == 10, f"expected 10 samples, got {len(samples_a)}"
75+
# every sample must fall in the queried range
76+
for k, _v in samples_a:
77+
assert 0 <= k < 1000, f"sample key {k} out of range"
78+
print(f" sampling deterministic + in-range: {len(samples_a)} samples")
79+
80+
print("All smoke tests passed!")
81+
EOF

0 commit comments

Comments
 (0)