diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index d91bb058b..048e70707 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -7,6 +7,7 @@ on: pull_request: paths: - .github/workflows/python-publish.yml + - ci/check_wheel.py workflow_dispatch: inputs: mode: @@ -18,12 +19,13 @@ on: - dry_run - release +permissions: + contents: read + jobs: - publish: + build-linux: runs-on: ubuntu-latest - permissions: - id-token: write - contents: read + timeout-minutes: 60 env: CARGO_TERM_COLOR: always RUSTFLAGS: "-C debuginfo=1" @@ -64,13 +66,83 @@ jobs: - name: Upload distributions uses: actions/upload-artifact@v4 with: - name: python-dist + name: python-dist-linux path: python/dist + if-no-files-found: error - - name: Publish to PyPI - if: > - (github.event_name == 'release' && github.event.action == 'released') || - (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'release') - working-directory: python + build-macos: + # Same native Apple Silicon runner used by pylance. + runs-on: warp-macos-14-arm64-6x + timeout-minutes: 60 + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: "0" + MACOSX_DEPLOYMENT_TARGET: "11.0" + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install system dependencies + run: brew install protobuf + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Build Apple Silicon wheel + # Match pylance's native macOS build action. + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1 + with: + command: build + rust-toolchain: stable + args: --release --strip --target aarch64-apple-darwin --interpreter python --out dist + working-directory: python + + - name: Test installed wheel without Rust + shell: bash run: | - uv publish --trusted-publishing always + # Match the test-data writer to the Rust reader; constrain CI only. + UV_BIN="$(command -v uv)" + for version in 3.11 3.12; do + environment="$RUNNER_TEMP/wheel-test-$version" + "$UV_BIN" venv --python "$version" "$environment" + PATH="$environment/bin:/usr/bin:/bin" "$UV_BIN" pip install \ + --python "$environment/bin/python" --only-binary=:all: \ + python/dist/*.whl "pylance>=1.0.0,<2" + PATH="$environment/bin:/usr/bin:/bin" "$environment/bin/python" \ + -I ci/check_wheel.py + done + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-dist-macos-aarch64 + path: python/dist/*.whl + if-no-files-found: error + + publish: + needs: [build-linux, build-macos] + if: > + (github.event_name == 'release' && github.event.action == 'released') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'release') + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + pattern: python-dist-* + path: dist + merge-multiple: true + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Publish to PyPI + run: uv publish --trusted-publishing always diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index ee5c92d85..70f3fbe52 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -54,7 +54,8 @@ jobs: source .venv/bin/activate uv pip install maturin[patchelf] # Install test dependencies only (skip editable install to avoid double-building Rust) - uv pip install pytest pyarrow pandas ruff + # Match test-data writers to the Rust Lance 1.x reader. + uv pip install pytest pyarrow pandas ruff "pylance>=1.0.0,<2" - name: Build Python extension working-directory: python run: | diff --git a/ci/check_wheel.py b/ci/check_wheel.py new file mode 100644 index 000000000..1c7460c17 --- /dev/null +++ b/ci/check_wheel.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright The Lance Authors + +"""Exercise the installed binary wheel outside the source tree, without Rust.""" + +import importlib.metadata +import shutil +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +import pyarrow as pa +from lance import write_dataset +from lance_graph import CypherQuery, DirNamespace, GraphConfig, SqlQuery, _internal + + +def main(): + assert sys.prefix != sys.base_prefix, "Use a clean virtual environment" + assert shutil.which("cargo") is None, "Remove Cargo from the smoke-test PATH" + assert shutil.which("rustc") is None, "Remove rustc from the smoke-test PATH" + extension = Path(_internal.__file__).resolve() + assert extension.is_relative_to(Path(sys.prefix).resolve()), extension + assert extension.suffix == ".so", "Expected the installed native extension" + + people = pa.table({"id": [1, 2, 3], "name": ["Alice", "Bob", "Carol"]}) + knows = pa.table({"source": [1, 2], "target": [2, 3]}) + config = ( + GraphConfig.builder() + .with_node_label("Person", "id") + .with_relationship("KNOWS", "source", "target") + .build() + ) + query = CypherQuery( + "MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) " + "WHERE a.id = 1 RETURN b.name AS friend, c.name AS friend_of_friend" + ).with_config(config) + expected = [{"friend": "Bob", "friend_of_friend": "Carol"}] + assert query.execute({"Person": people, "KNOWS": knows}).to_pylist() == expected + with TemporaryDirectory() as root: + write_dataset(people, Path(root) / "Person.lance") + write_dataset(knows, Path(root) / "KNOWS.lance") + assert query.execute_with_namespace(DirNamespace(root)).to_pylist() == expected + assert SqlQuery("SELECT COUNT(*) AS count FROM people").execute( + {"people": people} + ).to_pylist() == [{"count": 3}] + print( + f"Installed lance-graph {importlib.metadata.version('lance-graph')} wheel " + f"passed Cypher and SQL checks on Python {sys.version.split()[0]} without Rust" + ) + + +if __name__ == "__main__": + main() diff --git a/python/README.md b/python/README.md index 520d8f279..b4d2cc87a 100644 --- a/python/README.md +++ b/python/README.md @@ -15,6 +15,26 @@ Lance Graph combines a Rust-powered Cypher query engine with Python APIs for: pip install lance-graph ``` +The Python release workflow builds Linux x86-64 and native Apple Silicon wheels. +Installing a version with a wheel for your platform does not require Rust: + +```bash +pip install --only-binary=:all: lance-graph +``` + +The binary-only option fails if that version has no compatible wheel, rather than +compiling during installation. Earlier releases may not include the Apple Silicon +wheel; check the selected release's files on PyPI. Rust and maturin are needed +only when building from source. + +Release-workflow pull requests and manual `dry_run` builds produce downloadable +workflow artifacts without publishing. Release runs publish the Linux distributions +and the tested Apple Silicon wheel together after both builds succeed. The macOS wheel is tested +in clean Python 3.11 and 3.12 environments with Rust absent from PATH, using actual +in-memory and disk-backed Cypher traversal and SQL execution from the installed +extension. CI uses a Lance 1.x test-data writer to match the embedded Rust reader; +this test constraint does not change the package runtime requirements. + ## Quick Start ### 1. Simple Cypher Query