macrame-db 0.17.0

A Bitemporal Graph Ledger on libSQL · Embedded knowledge database
Documentation
name: Python

# The Python side of the project: the binding crate compiles, the extension
# builds and installs the way a user installs it, and the 353-test suite passes
# on every platform this repository claims to support.
#
# ## Why this does not call `ci.yml` first
#
# The plan (§10) sketched this file as `rust: uses ./.github/workflows/ci.yml`
# with everything else behind `needs: rust`. That shape belonged to a file that
# also built the wheels — "the crate must be green before the wheel is built".
# P5 moved the wheel matrix into `wheels.yml`, tag-triggered, and what is left
# here is a test job whose failure mode is independent of the Rust suite's.
#
# `workflow_call` does not deduplicate: gating here would re-run clippy, MSRV,
# the two-OS Rust matrix (each with its own three R15 retries) and the publish
# dry-run on every pull request that already ran them, and would hold the Python
# answer behind ~15 minutes of work it does not depend on. Two independent
# signals, reported separately, is strictly more information than one signal
# reported late.
#
# Where the two *must* be green together is before an upload, and that is what
# `workflow_call` above is for: `wheels.yml`'s publish job gates on this file,
# exactly as `release.yml` gates on `ci.yml`. See D-108.
#
# ## The gap this closes
#
# `bindings/python` is a workspace *member* but never a *default* member — the
# root package is itself a member, so Cargo scopes bare commands to it alone,
# which is the property `tests/packaging_tests.rs` pins and the reason `cargo
# publish` stayed a one-package operation (D-098). The cost of that choice, not
# noticed until now: **nothing in CI ever compiled the binding crate.** Until
# this file existed, a pull request could break `bindings/python/src/*.rs` and
# every check would go green, because the only thing that built it was
# `wheels.yml`, which runs on tags.

# Development lines are covered, and publication is not on this path (0.15.20,
# [D-262], review C-23).
#
# [D-234] found sixteen releases whose CI had never run, and set the policy
# "branches live under pull requests; push triggers stay main-only" — widening
# `push:` was rejected there because it double-runs a branch that has a pull
# request open. The policy is correct and it did not hold: it depends on a
# person remembering to open a draft pull request per line, and for `dev/0.16.0`
# nobody did, so nineteen more releases went unreplicated. That is D-234's own
# defect arriving through the remedy's one manual step. A trigger nobody has to
# remember is worth the duplicate runs, and the duplicate is bounded — it costs
# runner minutes while a pull request from a `dev/**` branch is open, and
# nothing else. No concurrency group cancels it, because cancelling a
# superseded run would trade a documented cost for an undocumented weakening of
# D-234's standing obligation: **a run must exist for the pushed SHA**, and a
# release commit pushed shortly before the next one would lose its evidence.
#
# **Nothing here can publish.** `release.yml` (crates.io) and `wheels.yml`
# (PyPI) trigger on `push: tags: ["v*"]` and `workflow_dispatch` only, and a
# branch push creates no tag. Their publish jobs are guarded a second time by
# `startsWith(github.ref, 'refs/tags/v') || inputs.publish == true`, which a
# `refs/heads/dev/**` ref fails. They reach this file through `workflow_call`,
# which is a separate entry point a push trigger cannot fire. Two layers, and
# widening the branch filter moves neither.
on:
  push:
    branches: [main, "dev/**"]
  pull_request:
  workflow_call: # so wheels.yml can gate an upload on exactly this

env:
  CARGO_TERM_COLOR: always

jobs:
  # `-p macrame-py` explicitly. Every other clippy invocation in this repository
  # is scoped to the root package by Cargo's own default, so this one has to
  # name the crate or it lints the same code `ci.yml` already linted.
  #
  # Deliberately **without** `--features extension-module`: that feature is
  # turned on by `pyproject.toml` for maturin's build only, and the binding
  # manifest states the crate must build without it. A check that silently
  # required it would make that statement untestable.
  lint:
    name: clippy (binding crate)
    runs-on: ubuntu-latest
    # clippy on the binding crate; max observed 0.6 min.
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - name: clippy
        run: cargo clippy -p macrame-py --all-targets
        env:
          RUSTFLAGS: -D warnings

      # `tests_py/test_stubs.py` compares the stub's *names* against the
      # extension in both directions. It cannot see a wrong **type** — a stub
      # that says `-> int` where the runtime answers a `datetime` passes every
      # one of those checks. A type checker is what reads the annotations, so
      # one runs here, over the stub and the package that re-exports it.
      #
      # No Rust toolchain needed and no extension built: `--strict` on a `.pyi`
      # is a pure text analysis. Measured clean at 0.7.0, so this is a gate that
      # passes rather than a backlog.
      - uses: actions/setup-python@v6
        with:
          python-version: "3.13"
      - name: mypy (the stub, and the package that re-exports it)
        run: |
          python -m pip install --upgrade pip mypy
          python -m mypy --strict python/macrame

  test:
    name: suite (${{ matrix.os }}, py${{ matrix.python }})
    runs-on: ${{ matrix.os }}
    # the extension build and the suite on four legs; max observed 4.0 min.
    timeout-minutes: 30
    strategy:
      # One platform failing must not hide the others: R15 is platform-sensitive
      # and the whole point of a three-OS matrix is knowing which ones it hit.
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            python: "3.13"
          # The floor `pyproject.toml` declares. `requires-python = ">=3.10"` is
          # a claim pip enforces against users and nothing enforces against us.
          - os: ubuntu-latest
            python: "3.10"
          - os: windows-latest
            python: "3.13"
          # macOS has never run this suite. `wheels.yml` builds a universal2
          # wheel and smoke-tests it with six lines; this is the first time the
          # ledger's Python surface is exercised on Apple silicon at all.
          - os: macos-latest
            python: "3.13"

    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python }}
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      # `pip install .` rather than `maturin develop`: it goes through the PEP
      # 517 backend, which means it reads `[tool.maturin]` and therefore builds
      # with `--features extension-module` and in release, i.e. it is the same
      # path a user takes. `maturin develop` would need a virtualenv here and
      # would test a build configuration nobody installs.
      - name: install the extension the way a user installs it
        run: |
          python -m pip install --upgrade pip
          python -m pip install .
          python -m pip install pytest

      # **Not bare pytest** (D-107). R15 is an intermittent libSQL access
      # violation on concurrent open, reproduced through this binding at 2 in 12
      # runs, and it kills the interpreter rather than raising. `run_suite.py`
      # distinguishes the four ways this suite can end — CRASH, FAILED,
      # INCOMPLETE, TEARDOWN — retries only the first, three times, and reports
      # the rest red on the first attempt. A green summary is not sufficient
      # here and neither is a zero exit; it checks them against each other.
      - name: python suite (through the R15 gate)
        run: python tests_py/run_suite.py

  # abi3 is what turns a 4-platform × 5-version matrix into 4 builds, and the
  # claim underneath it — "one wheel serves CPython 3.10 through whatever ships
  # next" — is not tested by any job above, because each of those builds and
  # runs on a single interpreter.
  #
  # So: build once on the floor, install that same artifact into an interpreter
  # it has never seen, and run the whole suite through it. If pyo3's abi3
  # feature were ever dropped, every other job here would still pass and the
  # wheel matrix would quietly become wrong by a factor of five.
  abi3:
    name: one wheel, two interpreters
    runs-on: ubuntu-latest
    # one wheel built once and imported on two interpreters; max observed 1.3 min.
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        id: floor
        with:
          python-version: "3.10"
      - uses: actions/setup-python@v6
        id: newest
        with:
          python-version: "3.13"
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      # Debug, deliberately: this job asks what the wheel is *tagged* and
      # whether it imports, not how fast it runs. `wheels.yml` builds the
      # release artifact.
      - name: build one wheel, on 3.10
        shell: bash
        run: |
          "${{ steps.floor.outputs.python-path }}" -m pip install --upgrade pip maturin
          "${{ steps.floor.outputs.python-path }}" -m maturin build --out dist

      - name: the tag names the ABI, not the interpreter that built it
        shell: bash
        run: |
          ls -la dist
          if ! ls dist/*-abi3-*.whl >/dev/null 2>&1; then
            echo "::error::no abi3 wheel — pyo3's abi3-py310 feature is off, and"
            echo "::error::the wheel matrix in wheels.yml is now wrong per Python version"
            exit 1
          fi

      - name: run the whole suite on 3.13, against the wheel built by 3.10
        shell: bash
        run: |
          "${{ steps.newest.outputs.python-path }}" -m pip install --upgrade pip pytest
          "${{ steps.newest.outputs.python-path }}" -m pip install --no-index --find-links dist macrame-db
          "${{ steps.newest.outputs.python-path }}" tests_py/run_suite.py