Skip to content

Commit 6499123

Browse files
claudeZhiXiao-Lin
authored andcommitted
feat(python-sdk): pure-Python bootstrap on PyPI fetches native wheels from GH Releases
After v3.2.0 stopped publishing native wheels to PyPI (PR #44, GH Releases is the canonical host), `pip install a3s-code` was broken on every platform except cp310 macOS arm64 (the one wheel that snuck into PyPI before the 10 GB project cap kicked in). This commit restores `pip install a3s-code` by adding a tiny pure-Python bootstrap package — also named `a3s-code` — that on first `import a3s_code` downloads the matching native wheel for the current interpreter and platform, verifies its sha256 against the release manifest, extracts the compiled `_native` extension into a per-user cache, and registers it as `sys.modules["a3s_code._native"]`. Subsequent imports use the cache; cold start is one download per (version, platform, interpreter) triple. New package - sdk/python-bootstrap/ — setuptools-built pure-Python wheel (a3s_code-X.Y.Z-py3-none-any.whl) and matching sdist. - src/a3s_code/__init__.py — calls ensure_native_loaded() then `from ._native import *`. - src/a3s_code/_bootstrap.py — wheel-name resolver, downloader, sha256 verifier, extension loader. Knobs: A3S_CODE_CACHE_DIR, A3S_CODE_RELEASES_BASE_URL, A3S_CODE_SKIP_HASH_CHECK. - tests/test_bootstrap.py — 15 unit tests (filename resolution, cache dir, sha256 mismatch, idempotency, manifest fallback) plus one live download test gated on A3S_CODE_BOOTSTRAP_LIVE=1. - README documenting install model and overrides. Workflow - publish-python-bootstrap.yml — runs bootstrap unit tests, builds wheel + sdist, twine check, twine upload on tag push. - release.yml — adds the bootstrap publish job; runs after publish-python (which produces the GH Release the bootstrap points at) and gates github-release on both. Release plumbing - scripts/check_release_versions.sh now validates the bootstrap pyproject.toml and the runtime __version__ in _bootstrap.py — they must equal the core version so the bootstrap finds the matching GH Release tag. - release.sh bumps both in lockstep with the other version files. - README + sdk/python README rewritten to advertise pip install a3s-code again and explain the cache. Bumps core / Node SDK / Python SDK from 3.2.0 to 3.2.1 since the bootstrap is a user-facing addition. CHANGELOG [3.2.1] entry documents the new package + rationale. End-to-end verified locally: built the wheel, installed into a fresh venv, ran `import a3s_code` with __version__ patched to 3.2.0 so the bootstrap points at the real GH Release; Agent.create succeeded.
1 parent 1017051 commit 6499123

25 files changed

Lines changed: 1020 additions & 55 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Publish Python Bootstrap
2+
3+
# Builds and publishes the pure-Python `a3s-code` bootstrap shim to PyPI.
4+
# The shim is tiny (no native code) so it sails under PyPI's per-project
5+
# size cap. On first `import a3s_code` it downloads the matching native
6+
# wheel for the user's platform from this repo's GitHub Releases.
7+
8+
on:
9+
workflow_call:
10+
workflow_dispatch:
11+
12+
jobs:
13+
publish:
14+
name: Publish Python Bootstrap to PyPI
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
23+
- name: Install build dependencies
24+
run: python -m pip install --upgrade pip build twine
25+
26+
- name: Run bootstrap unit tests
27+
working-directory: sdk/python-bootstrap
28+
run: python -m unittest tests.test_bootstrap -v
29+
30+
- name: Build wheel + sdist
31+
run: python -m build --wheel --sdist sdk/python-bootstrap
32+
33+
- name: Twine check
34+
run: python -m twine check sdk/python-bootstrap/dist/*
35+
36+
- name: Publish to PyPI
37+
if: startsWith(github.ref, 'refs/tags/')
38+
env:
39+
TWINE_USERNAME: __token__
40+
TWINE_PASSWORD: ${{ secrets.PYPI_NATIVE_TOKEN }}
41+
run: python -m twine upload --skip-existing sdk/python-bootstrap/dist/*

.github/workflows/release.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,32 @@ jobs:
119119
secrets: inherit
120120

121121
# ───────────────────────────────────────────────
122-
# Build and publish Python native SDK (reusable)
122+
# Build native Python wheels and attach to GitHub Release (reusable).
123+
# No longer pushes to PyPI — the per-project size cap made that
124+
# impractical. GH Release is the canonical wheel host.
123125
# ───────────────────────────────────────────────
124126
publish-python:
125-
name: Python SDK
127+
name: Python SDK (native wheels → GH Release)
126128
needs: [ci, ci-windows]
127129
uses: ./.github/workflows/publish-python.yml
128130
secrets: inherit
129131

132+
# ───────────────────────────────────────────────
133+
# Publish the pure-Python bootstrap to PyPI so `pip install a3s-code`
134+
# still works. Needs the native wheels to be on GH Release first.
135+
# ───────────────────────────────────────────────
136+
publish-python-bootstrap:
137+
name: Python SDK (bootstrap → PyPI)
138+
needs: [publish-python]
139+
uses: ./.github/workflows/publish-python-bootstrap.yml
140+
secrets: inherit
141+
130142
# ───────────────────────────────────────────────
131143
# Create GitHub Release
132144
# ───────────────────────────────────────────────
133145
github-release:
134146
name: GitHub Release
135-
needs: [publish-crate, publish-node, publish-python]
147+
needs: [publish-crate, publish-node, publish-python, publish-python-bootstrap]
136148
runs-on: ubuntu-latest
137149
steps:
138150
- uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.2.1] - 2026-05-24
9+
10+
### Added
11+
12+
- Python SDK: small pure-Python **bootstrap** shim published to PyPI as
13+
`a3s-code`. On first `import a3s_code` it downloads the matching
14+
native wheel for the current interpreter/platform from this repo's
15+
GitHub Releases, verifies the wheel's sha256 against the release
16+
manifest, extracts the compiled `_native` extension into
17+
`~/.cache/a3s-code/<version>/`, and registers it as
18+
`sys.modules["a3s_code._native"]`. Subsequent imports use the cache.
19+
Source under `sdk/python-bootstrap/`.
20+
- Environment knobs: `A3S_CODE_CACHE_DIR`, `A3S_CODE_RELEASES_BASE_URL`,
21+
`A3S_CODE_SKIP_HASH_CHECK`.
22+
- 15 unit tests + 1 live download test gated on
23+
`A3S_CODE_BOOTSTRAP_LIVE=1`.
24+
- New workflow `publish-python-bootstrap.yml`, wired after
25+
`publish-python` in `release.yml`.
26+
- `scripts/check_release_versions.sh` now also validates the bootstrap
27+
package version and the runtime `__version__` literal.
28+
- `release.sh` now bumps the bootstrap version in lockstep with the
29+
core release.
30+
31+
### Fixed
32+
33+
- `pip install a3s-code` works again from v3.2.1, restored after v3.2.0
34+
could only push a single wheel to PyPI under the quota cap.
35+
836
## [3.2.0] - 2026-05-24
937

1038
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,22 @@ Full migration notes are in [CHANGELOG.md](./CHANGELOG.md).
8181
## Install
8282

8383
```bash
84+
# Python
85+
pip install a3s-code
86+
8487
# Node.js
8588
npm install @a3s-lab/code
86-
87-
# Python (from v3.2.0; pick the wheel for your interpreter/platform)
88-
pip install \
89-
https://github.com/AI45Lab/Code/releases/download/v3.2.0/a3s_code-3.2.0-cp312-cp312-manylinux_2_28_x86_64.whl
9089
```
9190

9291
Rust users can depend on `a3s-code-core`.
9392

94-
Python wheels are hosted on [GitHub Releases](https://github.com/AI45Lab/Code/releases)
95-
from v3.2.0 onwards — the project grew past PyPI's per-project storage
96-
quota for binary distributions. Each release ships a
97-
`python-native-manifest.json` with sha256 hashes for every wheel.
98-
Versions up to 3.1.0 remain installable via `pip install a3s-code==3.1.0`.
93+
From v3.2.1 onwards the PyPI `a3s-code` package is a small pure-Python
94+
bootstrap. On first `import a3s_code` it downloads the matching native
95+
wheel from [GitHub Releases](https://github.com/AI45Lab/Code/releases),
96+
verifies the wheel's sha256 against the release manifest, and caches the
97+
compiled extension under `~/.cache/a3s-code/<version>/`. Subsequent
98+
imports use the cache. The split exists because the full native-wheel
99+
matrix grew past PyPI's per-project storage cap.
99100

100101
---
101102

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "3.2.0"
3+
version = "3.2.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

release.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ echo " Updating sdk/python/pyproject.toml..."
101101
sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" sdk/python/pyproject.toml
102102
rm -f sdk/python/pyproject.toml.bak
103103

104+
# Update Python bootstrap shim (pyproject.toml + runtime __version__).
105+
# Must stay in lockstep with core so the bootstrap fetches the matching
106+
# native wheel from GH Releases on first import.
107+
echo " Updating sdk/python-bootstrap/pyproject.toml..."
108+
sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" sdk/python-bootstrap/pyproject.toml
109+
rm -f sdk/python-bootstrap/pyproject.toml.bak
110+
111+
echo " Updating sdk/python-bootstrap/src/a3s_code/_bootstrap.py..."
112+
sed -i.bak "s/^__version__ = \".*\"/__version__ = \"${VERSION}\"/" sdk/python-bootstrap/src/a3s_code/_bootstrap.py
113+
rm -f sdk/python-bootstrap/src/a3s_code/_bootstrap.py.bak
114+
104115
echo " Updating Node package lockfiles..."
105116
update_node_lockfile sdk/node/package-lock.json
106117
update_node_lockfile sdk/node/examples/package-lock.json

scripts/check_release_versions.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ def check_node_lockfile(path):
9797
check_equal(f"{path} package {key or '<root>'} optionalDependency {name}", value)
9898
9999
100+
def check_bootstrap_runtime_version(path):
101+
match = re.search(r'^__version__\s*=\s*"([^"]+)"', read(path), re.MULTILINE)
102+
if not match:
103+
fail(f"{path}: missing __version__ literal")
104+
return
105+
check_equal(f"{path} __version__", match.group(1))
106+
107+
100108
if not expected:
101109
expected = first_manifest_version("core/Cargo.toml") or ""
102110
@@ -110,6 +118,8 @@ check_core_dependency("sdk/node/Cargo.toml")
110118
check_core_dependency("sdk/python/Cargo.toml")
111119
check_package_json("sdk/node/package.json")
112120
check_pyproject("sdk/python/pyproject.toml")
121+
check_pyproject("sdk/python-bootstrap/pyproject.toml")
122+
check_bootstrap_runtime_version("sdk/python-bootstrap/src/a3s_code/_bootstrap.py")
113123
check_cargo_lock("Cargo.lock")
114124
check_node_lockfile("sdk/node/package-lock.json")
115125
check_node_lockfile("sdk/node/examples/package-lock.json")

sdk/node/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "3.2.0"
3+
version = "3.2.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -11,7 +11,7 @@ description = "A3S Code Node.js bindings - Native addon via napi-rs"
1111
crate-type = ["cdylib"]
1212

1313
[dependencies]
14-
a3s-code-core = { version = "3.2.0", path = "../../core", features = ["ahp", "s3"] }
14+
a3s-code-core = { version = "3.2.1", path = "../../core", features = ["ahp", "s3"] }
1515
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1616
napi-derive = "2"
1717
tokio = { version = "1.35", features = ["full"] }

sdk/node/examples/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)