diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index a46bdbbdd1..0000000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -if [[ ! -f $(which buf) ]]; then - echo "Could not find 'buf' executable. Ensure that it is installed in your path." - exit 2 -fi - -changed_files=($(git diff --cached --name-only --diff-filter=ACM | grep '^protos/' | grep '**.proto$')) - -if [[ -n "$changed_files" ]]; then - echo "running buf lint protos" - buf lint protos || { - echo -e "ERROR! protos linting failed. Please fix the errors and recommit.\n" - exit 1 - } - echo "running buf format protos" - buf format protos -w --exit-code || { - echo -e "ERROR! protos file were reformatted. Please stage the changes and recommit.\n" - exit 1 - } -fi - -exit 0 diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index 8c11acbb87..0000000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -# Store the root directory of the repository -REPO_ROOT="$(git rev-parse --show-toplevel)" -GITHOOKS_DIR="$REPO_ROOT/.githooks" - -# Track failures -FAILED=0 - -echo "========================================" -echo " Pre-Push Checks" -echo "========================================" -echo - -# Check for changes in Python files -# Compare against the remote branch being pushed to -python_changed_files=($(git diff --name-only --diff-filter=ACM @{upstream}... | grep '^python/' || true)) - -echo "→ Python files changed:" -if [[ -n "$python_changed_files" ]]; then - echo " ${python_changed_files[*]}" - echo - echo " [1/4] Formatting and linting..." - bash "$GITHOOKS_DIR/pre-push-python/fmt-lint.sh" || FAILED=1 - - echo " [2/4] Stub generation..." - bash "$GITHOOKS_DIR/pre-push-python/stubs.sh" || FAILED=1 - - echo " [3/4] Extras validation..." - bash "$GITHOOKS_DIR/pre-push-python/extras.sh" || FAILED=1 - - echo " [4/4] Lockfile check..." - bash "$GITHOOKS_DIR/pre-push-python/lock.sh" || FAILED=1 - echo -else - echo " (none)" - echo -fi - -# Check for changes in Rust bindings files -bindings_changed_files=($(git diff --name-only --diff-filter=ACM @{upstream}... | grep '^rust/crates/sift_stream_bindings/src/' || true)) - -echo "→ Rust binding files changed:" -if [[ -n "$bindings_changed_files" ]]; then - echo " ${bindings_changed_files[*]}" - echo - echo " [1/1] Stub generation..." - bash "$GITHOOKS_DIR/pre-push-rust/stubs.sh" || FAILED=1 - echo -else - echo " (none)" - echo -fi - -if [[ $FAILED -eq 1 ]]; then - echo "========================================" - echo " ✗ Some checks failed" - echo "========================================" - exit 1 -else - echo "========================================" - echo " ✓ All checks passed" - echo "========================================" -fi - diff --git a/.githooks/pre-push-python/extras.sh b/.githooks/pre-push-python/extras.sh deleted file mode 100755 index 522f1deba7..0000000000 --- a/.githooks/pre-push-python/extras.sh +++ /dev/null @@ -1,37 +0,0 @@ -# ensure generated pyproject.toml extras are up-to-date - -# Clear git env vars set by the parent hook so git commands resolve the work tree normally -unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX - -# Store the root directory of the repository -REPO_ROOT="$(git rev-parse --show-toplevel)" -PYTHON_DIR="$REPO_ROOT/python" -PYPROJECT_FILE="$PYTHON_DIR/pyproject.toml" - -# Function to check if pyproject.toml has changed -check_extras_changes() { - local changed_files=$(git status --porcelain "$PYPROJECT_FILE" || true) - - if [ -n "$changed_files" ]; then - echo " ❌ ERROR: Generated extras are not up-to-date:" - echo "$changed_files" | sed 's/^/ /' - echo " Please commit these changes before pushing." - exit 1 - fi -} - -# Function to generate Python extras -generate_python_extras() { - echo " → Generating extras..." - cd "$PYTHON_DIR" - - # Idempotent: fast on a warm cache, recreates .venv on a cold checkout. - uv sync --extra dev-all --quiet - - bash ./scripts/dev gen-extras - check_extras_changes -} - -generate_python_extras - -echo " ✓ Extras are up-to-date" diff --git a/.githooks/pre-push-python/fmt-lint.sh b/.githooks/pre-push-python/fmt-lint.sh deleted file mode 100644 index 0eea389f81..0000000000 --- a/.githooks/pre-push-python/fmt-lint.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Clear git env vars set by the parent hook so git commands resolve the work tree normally -unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX - -# Store the root directory of the repository -REPO_ROOT="$(git rev-parse --show-toplevel)" -PYTHON_DIR="$REPO_ROOT/python" - -# Change to Python directory -cd "$PYTHON_DIR" - -# Ensure the dev environment is in place. Idempotent: fast on a warm -# cache, recreates .venv on a cold checkout. Without this, a fresh -# clone's first push fails with "ruff: command not found". -uv sync --extra dev-all --quiet - -# Run ruff format (formatter) -echo " → Running ruff format..." -bash ./scripts/dev fmt - -# Run ruff check with --fix (linter) -echo " → Running ruff check --fix..." -bash ./scripts/dev lint-fix - -# Check if any files were modified by formatting/linting -cd "$REPO_ROOT" -changed_files=$(git status --porcelain python/lib/sift_client/ | grep -E '\.py$' || true) - -if [ -n "$changed_files" ]; then - echo "" - echo " ❌ ERROR: Formatting/linting made changes:" - echo "$changed_files" | sed 's/^/ /' - echo "" - echo " Please commit these changes before pushing." - exit 1 -fi - -echo " ✓ Formatting and linting passed" diff --git a/.githooks/pre-push-python/lock.sh b/.githooks/pre-push-python/lock.sh deleted file mode 100755 index 371fa227d7..0000000000 --- a/.githooks/pre-push-python/lock.sh +++ /dev/null @@ -1,18 +0,0 @@ -# ensure uv.lock is up-to-date with pyproject.toml - -# Clear git env vars set by the parent hook so git commands resolve the work tree normally -unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX - -REPO_ROOT="$(git rev-parse --show-toplevel)" -PYTHON_DIR="$REPO_ROOT/python" - -echo " → Checking uv.lock..." -cd "$PYTHON_DIR" - -if ! uv lock --check; then - echo " ❌ ERROR: uv.lock is out of date with pyproject.toml." - echo " Run 'uv lock' and commit the result before pushing." - exit 1 -fi - -echo " ✓ uv.lock is up-to-date" diff --git a/.githooks/pre-push-python/stubs.sh b/.githooks/pre-push-python/stubs.sh deleted file mode 100644 index a5f37785d1..0000000000 --- a/.githooks/pre-push-python/stubs.sh +++ /dev/null @@ -1,38 +0,0 @@ -# ensure generated python stubs are up-to-date, from sync clients - -# Clear git env vars set by the parent hook so git commands resolve the work tree normally -unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX - -# Store the root directory of the repository -REPO_ROOT="$(git rev-parse --show-toplevel)" -PYTHON_DIR="$REPO_ROOT/python" -STUBS_DIR="$PYTHON_DIR/lib/sift_client/resources/sync_stubs" - -# Function to check if generated stub files have changed -check_stub_changes() { - local target_path="$1" - local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true) - - if [ -n "$changed_files" ]; then - echo " ❌ ERROR: Generated stubs are not up-to-date:" - echo "$changed_files" | sed 's/^/ /' - echo " Please commit these changes before pushing." - exit 1 - fi -} - -# Function to generate Python stubs -generate_python_stubs() { - echo " → Generating stubs..." - cd "$PYTHON_DIR" - - # Idempotent: fast on a warm cache, recreates .venv on a cold checkout. - uv sync --extra dev-all --quiet - - bash ./scripts/dev gen-stubs - check_stub_changes "$STUBS_DIR" -} - -generate_python_stubs - -echo " ✓ Stubs are up-to-date" \ No newline at end of file diff --git a/.githooks/pre-push-rust/stubs.sh b/.githooks/pre-push-rust/stubs.sh deleted file mode 100644 index 980750c9d3..0000000000 --- a/.githooks/pre-push-rust/stubs.sh +++ /dev/null @@ -1,33 +0,0 @@ -# ensure generated python stubs are up-to-date, from sift_stream_bindings - -# Store the root directory of the repository -REPO_ROOT="$(git rev-parse --show-toplevel)" -BINDINGS_DIR="$REPO_ROOT/rust/crates/sift_stream_bindings" - -# Function to check if generated stub files have changed -check_stub_changes() { - local target_path="$1" - local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true) - - if [ -n "$changed_files" ]; then - echo " ❌ ERROR: Generated stubs are not up-to-date:" - echo "$changed_files" | sed 's/^/ /' - echo " Please commit these changes before pushing." - exit 1 - fi -} - -# Function to generate bindings stubs -generate_bindings_stubs() { - echo " → Generating stubs..." - cd "$BINDINGS_DIR" - cargo run --bin stub_gen - - # The stub file is generated in the bindings directory - local stub_file="$BINDINGS_DIR/sift_stream_bindings.pyi" - check_stub_changes "$stub_file" -} - -generate_bindings_stubs - -echo " ✓ Stubs are up-to-date" \ No newline at end of file diff --git a/.github/workflows/python_ci.yaml b/.github/workflows/python_ci.yaml index 4e2c766fd4..33eabbca04 100644 --- a/.github/workflows/python_ci.yaml +++ b/.github/workflows/python_ci.yaml @@ -54,6 +54,9 @@ jobs: with: enable-cache: true + - name: Set up just + uses: extractions/setup-just@v4 + - name: Verify lockfile is up to date run: uv lock --check @@ -61,30 +64,30 @@ jobs: run: uv sync --extra dev-all - name: Lint - run: uv run ruff check + run: just lint - name: Format - run: uv run ruff format --check + run: just fmt-check - name: MyPy - run: uv run mypy lib + run: just mypy # Re-run mypy with --platform=win32 so typeshed evaluates platform-gated # stubs (e.g. fcntl) as if we were on Windows. Catches imports that # would only fail at runtime on Windows. - name: MyPy (Windows platform) - run: uv run mypy --platform=win32 lib + run: just mypy-win - name: Pyright - run: uv run pyright lib + run: just pyright - name: Check Stubs Generation working-directory: . - run: bash .githooks/pre-push-python/stubs.sh + run: just python::stubs-check - name: Check Extras Generation working-directory: . - run: bash .githooks/pre-push-python/extras.sh + run: just python::extras-check - name: Sync Stubs Mypy working-directory: python/lib @@ -110,7 +113,7 @@ jobs: # code that runs on 3.8). Ceiling testing is rarely useful in # practice — Python's deprecation cycle is long and the project's # stdlib usage is conservative — so it stays available locally as - # `./scripts/dev test-ceiling` rather than running on every PR. + # `just test-ceiling` rather than running on every PR. # The full 3.8-3.14 install matrix lives in `python_build.yaml`. python-version: ["3.8"] steps: @@ -124,11 +127,16 @@ jobs: with: enable-cache: true + - name: Set up just + uses: extractions/setup-just@v4 + - name: Install dependencies (Python ${{ matrix.python-version }}) run: uv sync --python ${{ matrix.python-version }} --extra dev-all + # The venv is synced to the matrix Python above, so `just test` + # (uv run pytest) collects against that interpreter. - name: Pytest Unit Tests (Python ${{ matrix.python-version }}) - run: uv run --python ${{ matrix.python-version }} pytest -m "not integration" + run: just test # Disabling integration tests that interact with Sift until a better solution is implemented # - name: Pytest Integration Tests diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 1339c09dcf..83847220bc 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -65,17 +65,23 @@ jobs: with: mdbook-version: "0.4.40" + - name: Set up just + uses: extractions/setup-just@v4 + + # These call the same recipes developers run locally (rust/justfile), so + # CI and local stay in lockstep. The toolchain install above provides + # cargo/clippy/rustfmt; just only wraps the commands. - name: Run cargo check - run: cargo check --all-features + run: just rust::check - name: Run cargo clippy - run: cargo clippy --all-features + run: just rust::clippy - name: Run cargo fmt - run: cargo fmt --check --all + run: just rust::fmt-check - name: Run cargo test - run: cargo test --all-features + run: just rust::test - name: Run sift-stream-bindings lint & test shell: bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..e41228cde1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,105 @@ +# Local git hooks, managed by pre-commit. Install with `just install-hooks` +# (sets up both the commit and push stages). +# +# Two layers, fast -> thorough: +# - commit stage: file hygiene, ruff lint+format, and buf lint/format for protos. +# - push stage: the full `just python::check` gate plus the generated-artifact, +# lockfile, and rust-binding-stub checks. +# +# Python tooling runs through `just` recipes (which invoke `uv run` inside +# python/) so hooks use the exact tool versions pinned in the project env -- +# no second, drifting ruff/mypy. That also handles the monorepo layout: uv and +# the tools must run from python/, not the repo root. +default_install_hook_types: [pre-commit, pre-push] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-yaml + # --unsafe checks YAML syntax without resolving custom tags; mkdocs.yml uses `!ENV`. + args: [--unsafe] + - id: check-toml + - id: check-merge-conflict + - id: check-added-large-files + + - repo: local + hooks: + # --- commit stage (fast): ruff + protos --- + - id: ruff-check + name: ruff check --fix + entry: just python::lint-fix + language: system + files: ^python/.*\.pyi?$ + pass_filenames: false + require_serial: true + - id: ruff-format + name: ruff format + entry: just python::fmt + language: system + files: ^python/.*\.pyi?$ + pass_filenames: false + require_serial: true + - id: buf-lint + name: buf lint protos + entry: buf lint protos + language: system + files: ^protos/.*\.proto$ + pass_filenames: false + - id: buf-format + name: buf format protos + entry: buf format protos -w --exit-code + language: system + files: ^protos/.*\.proto$ + pass_filenames: false + - id: rust-fmt + name: cargo fmt --check + entry: just rust::fmt-check + language: system + files: ^rust/.*\.rs$ + pass_filenames: false + - id: go-fmt + name: gofmt + entry: just go::fmt-check + language: system + files: ^go/.*\.go$ + pass_filenames: false + + # --- push stage (thorough) --- + - id: python-check + name: just python::check (lint + types) + entry: just python::check + language: system + files: ^python/ + pass_filenames: false + stages: [pre-push] + - id: python-stubs-check + name: sync stubs up to date + entry: just python::stubs-check + language: system + files: ^python/ + pass_filenames: false + stages: [pre-push] + - id: python-extras-check + name: extras up to date + entry: just python::extras-check + language: system + files: ^python/ + pass_filenames: false + stages: [pre-push] + - id: python-lock-check + name: uv.lock up to date + entry: just python::lock-check + language: system + files: ^python/ + pass_filenames: false + stages: [pre-push] + - id: bindings-stubs-check + name: rust binding stubs up to date + entry: just rust::bindings-stubs-check + language: system + files: ^rust/crates/sift_stream_bindings/src/ + pass_filenames: false + stages: [pre-push] diff --git a/go/justfile b/go/justfile new file mode 100644 index 0000000000..928a6d7d71 --- /dev/null +++ b/go/justfile @@ -0,0 +1,41 @@ +# Go module tasks. Run from the repo root as `just go::`. +# Recipes run in go/ (where go.mod lives), matching the go_ci workflow. + +# Format all Go files +fmt: + gofmt -w . + +# Check formatting without writing (used by the pre-commit hook) +fmt-check: + #!/usr/bin/env bash + set -uo pipefail + unformatted=$(gofmt -l .) + if [[ -n "$unformatted" ]]; then + echo "ERROR: these files need gofmt:" + echo "$unformatted" | sed 's/^/ /' + exit 1 + fi + +# Verify module dependencies +mod-verify: + go mod verify + +# Build all packages +build: + go build -v ./... + +# Run go vet +vet: + go vet ./... + +# Run staticcheck +staticcheck: + go run honnef.co/go/tools/cmd/staticcheck@latest ./... + +# Run golangci-lint (must be installed) +lint: + golangci-lint run + +# Run the test suite +test: + go test ./... diff --git a/justfile b/justfile new file mode 100644 index 0000000000..cae97bded5 --- /dev/null +++ b/justfile @@ -0,0 +1,39 @@ +# Repo-wide developer tasks. Language-specific tasks live in each language's +# own justfile, exposed here as modules, e.g. `just python::test`. +# +# Requires `just` (https://github.com/casey/just). Install with one of: +# uv tool install rust-just # via uv +# cargo install just # via cargo +# brew install just # via Homebrew +# +# Git hooks are managed by pre-commit (.pre-commit-config.yaml); install them +# with `just install-hooks`. +# +# Run `just` (or `just --list`) to see all recipes. + +mod python +mod rust +mod go + +# List available recipes +default: + @just --list + +# Install the pre-commit git hooks (commit + push stages) +install-hooks: + # Clear any core.hooksPath left by the old .githooks setup; pre-commit + # refuses to install while it is set. + git config --unset-all core.hooksPath || true + cd python && uv run pre-commit install --install-hooks --config ../.pre-commit-config.yaml + +# Generate code from protobuf for all languages, or one: `just gen python` +gen *lang: + bash scripts/gen.sh {{lang}} + +# Serve the CLI mdBook docs locally +serve-cli-docs: + mdbook serve ./rust/crates/sift_cli/assets/docs -d ./rust/crates/sift_cli/assets/docs/book-dev + +# Build the CLI mdBook docs +build-cli-docs: + mdbook build ./rust/crates/sift_cli/assets/docs -d ./rust/crates/sift_cli/assets/docs/book-dev diff --git a/makefile b/makefile deleted file mode 100644 index 1e4d2f1ddc..0000000000 --- a/makefile +++ /dev/null @@ -1,34 +0,0 @@ -SHELL := /bin/zsh - -.PHONY: gen -gen: - bash scripts/gen.sh - -.PHONY: gen-go -gen-go: - bash scripts/gen.sh go - -.PHONY: gen-python -gen-python: - bash scripts/gen.sh python - -.PHONY: gen-rust -gen-rust: - bash scripts/gen.sh rust - -.PHONY: sanitize -sanitize: - echo "Error: sanitize has been removed from this repo. Please see Confluence on proto updating." - exit 1 - -.PHONY: git-hooks -git-hooks: - git config core.hooksPath .githooks - -.PHONY: serve-cli-docs -serve-cli-docs: - mdbook serve ./rust/crates/sift_cli/assets/docs -d ./rust/crates/sift_cli/assets/docs/book-dev - -.PHONY: build-cli-docs -build-cli-docs: - mdbook build ./rust/crates/sift_cli/assets/docs -d ./rust/crates/sift_cli/assets/docs/book-dev diff --git a/python/justfile b/python/justfile new file mode 100644 index 0000000000..02be7c4d36 --- /dev/null +++ b/python/justfile @@ -0,0 +1,164 @@ +# Development tasks for the sift-stack-py package. +# +# Requires `just` (https://github.com/casey/just) and `uv` +# (https://docs.astral.sh/uv/#installation). Install just with one of: +# uv tool install rust-just # via uv +# cargo install just # via cargo +# brew install just # via Homebrew +# +# Run `just` (or `just --list`) to see all recipes. Every recipe runs against +# the project's uv-managed environment. Bootstrap a fresh checkout with +# `just bootstrap`. + +# List available recipes +default: + @just --list + +# Create .venv and install all dev dependencies via uv +bootstrap: + # Sweep any stale venv from the legacy pip-based setup so editor configs + # don't keep pointing at it. + rm -rf venv build lib/sift_stack_py.egg-info + uv sync --extra dev-all + +# Re-install project dependencies +pip-install: + uv sync --extra dev-all + +# Run 'ruff format' +fmt: + uv run ruff format + +# Run 'ruff format --check' (no writes; used by CI) +fmt-check: + uv run ruff format --check + +# Run 'ruff check' +lint: + uv run ruff check + +# Run 'ruff check --fix' +lint-fix: + uv run ruff check --fix + +# Run 'mypy lib' for static analysis +mypy: + uv run mypy lib + +# Run 'mypy --platform=win32 lib' (Windows compat check) +mypy-win: + # Re-run mypy with --platform=win32 so typeshed evaluates platform-gated + # stubs (e.g. fcntl) as if we were on Windows. Catches imports that would + # only fail at runtime on Windows. + uv run mypy --platform=win32 lib + +# Run 'pyright lib' for type checking +pyright: + uv run pyright lib + +# (internal) Ensure the dev-all environment is present. A bare `uv sync` prunes +# these extras, which would make the checks below fail with "command not found". +_sync-dev: + uv sync --extra dev-all --quiet + +# Run lint-fix, fmt, mypy, mypy-win, pyright sequentially +check: _sync-dev lint-fix fmt mypy mypy-win pyright + +# Execute non-integration tests against the project's Python +test: + uv run pytest -m "not integration" + +# Execute non-integration tests against the floor Python (3.8) +test-floor: + # uv auto-downloads a managed Python 3.8 on first run. --no-project + # --with-editable '.[dev-all]' resolves the project + dev-all extra ad hoc + # under 3.8, installing the project editable so pytest collects from the + # working tree. + uv run --python 3.8 --no-project --with-editable '.[dev-all]' pytest -m "not integration" + +# Execute non-integration tests against the ceiling Python (3.14) +test-ceiling: + # Symmetric to test-floor: runs against the highest officially supported + # Python version (per the `Programming Language :: Python :: 3.14` + # classifier in pyproject.toml). Catches forward-compat issues. + uv run --python 3.14 --no-project --with-editable '.[dev-all]' pytest -m "not integration" + +# Run the pytest-plugin test subset against pytest 7/8/9 +test-pytest-compat: + #!/usr/bin/env bash + set -euo pipefail + # Each cell pairs a pytest line with a compatible Python and pytest-randomly + # pin. Fork support keeps the grpc the plugin imports from aborting in the + # parent when runpytest_subprocess forks (mirrors the CI job env). + export GRPC_ENABLE_FORK_SUPPORT=1 + for cell in "3.8 ~=7.4 3.15.0" "3.9 ~=8.4 3.16.0" "3.10 ~=9.0 4.1.0"; do + read -r py spec randomly <<< "$cell" + echo "=== Python $py, pytest $spec, pytest-randomly $randomly ===" + uv run --python "$py" --no-project --with-editable '.' \ + --with "pytest$spec" \ + --with "pytest-randomly==$randomly" \ + pytest lib/sift_client/_tests/pytest_plugin -m "plugin_compat" + done + +# Execute integration tests +test-integration: + uv run pytest -m "integration" + +# Execute all tests +test-all: + uv run pytest + +# Generate pyi stubs for sift_client synchronous wrappers +gen-stubs: + cd lib && uv run python sift_client/_internal/gen_pyi.py sift_client/resources/sync_stubs + uv run ruff format ./lib/sift_client/resources/sync_stubs/*.pyi -q + uv run ruff check ./lib/sift_client/resources/sync_stubs/*.pyi --fix --unsafe-fixes -q + +# Run stubtest (mypy) on the generated pyi stubs +mypy-stubs: + cd lib && uv run stubtest sift_client.resources.sync_stubs + +# Generate [project.optional-dependencies] +gen-extras: + cd scripts && uv run python generate_extras.py + +# Boot up the documentation server +docs: + uv run mkdocs serve + +# Build docs +docs-build: + uv run mkdocs build + +# --- Pre-push hook checks (verify committed output is up to date) --- + +# Verify the generated sync stubs are up to date +stubs-check: + #!/usr/bin/env bash + set -uo pipefail + uv sync --extra dev-all --quiet + just gen-stubs + changed=$(git status --porcelain lib/sift_client/resources/sync_stubs | grep -E '\.pyi$' || true) + if [[ -n "$changed" ]]; then + echo "ERROR: generated stubs are not up to date:" + echo "$changed" | sed 's/^/ /' + echo "Run 'just gen-stubs' and commit the result before pushing." + exit 1 + fi + +# Verify [project.optional-dependencies] is up to date +extras-check: + #!/usr/bin/env bash + set -uo pipefail + uv sync --extra dev-all --quiet + just gen-extras + changed=$(git status --porcelain pyproject.toml || true) + if [[ -n "$changed" ]]; then + echo "ERROR: extras are not up to date (pyproject.toml changed)." + echo "Run 'just gen-extras' and commit the result before pushing." + exit 1 + fi + +# Verify uv.lock is in sync with pyproject.toml +lock-check: + uv lock --check diff --git a/python/lib/sift_client/_tests/README.md b/python/lib/sift_client/_tests/README.md index f5f6360f3d..10693d8765 100644 --- a/python/lib/sift_client/_tests/README.md +++ b/python/lib/sift_client/_tests/README.md @@ -10,4 +10,4 @@ a. Ensure that your local Sift instance contains data for the asset NostromoLV426 b. If it doesn't them export data for NostromoLV426 from development 4. Run tests - a. Run tests using /python/scripts/dev {test, test-integration, test-all} + a. From /python, run tests using `just {test, test-integration, test-all}` diff --git a/python/pyproject.toml b/python/pyproject.toml index dfe94c0430..51556a5138 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -81,6 +81,7 @@ data-review = [ dev = [ 'grpcio-testing~=1.13', 'mypy==1.10.0', + "pre-commit~=4.0 ; python_version >= '3.9'", 'pyright==1.1.386', 'pytest-asyncio==0.23.7', 'pytest-benchmark==4.0.0', @@ -100,6 +101,7 @@ dev-all = [ 'npTDMS~=1.9', 'pdoc==14.5.0', 'polars~=1.8', + "pre-commit~=4.0 ; python_version >= '3.9'", 'pyOpenSSL<24.0.0', 'pyarrow>=17.0.0', 'pyright==1.1.386', @@ -118,6 +120,7 @@ dev-all = [ development = [ 'grpcio-testing~=1.13', 'mypy==1.10.0', + "pre-commit~=4.0 ; python_version >= '3.9'", 'pyright==1.1.386', 'pytest-asyncio==0.23.7', 'pytest-benchmark==4.0.0', @@ -155,6 +158,7 @@ docs-build = [ 'npTDMS~=1.9', 'pdoc==14.5.0', 'polars~=1.8', + "pre-commit~=4.0 ; python_version >= '3.9'", 'pyOpenSSL<24.0.0', 'pyarrow>=17.0.0', 'pyright==1.1.386', @@ -215,7 +219,11 @@ development = [ # which needs 3.10+) so randomization is active on the 3.8 CI test job too. "pytest-randomly==3.15.0", "ruff~=0.12.10", - "tomlkit~=0.13.3" + "tomlkit~=0.13.3", + # Drives the git hooks (.pre-commit-config.yaml). pre-commit 4.x needs + # Python 3.9+, so it is skipped on the 3.8 floor; hooks are used on 3.9+ + # dev machines and CI invokes the underlying just recipes directly. + "pre-commit~=4.0 ; python_version >= '3.9'" ] build = ["pdoc==14.5.0", "build==1.2.1"] docs = [ diff --git a/python/scripts/dev b/python/scripts/dev deleted file mode 100755 index bffabf771b..0000000000 --- a/python/scripts/dev +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env bash - -usage() { - cat<' directly." - exit 1 - ;; - "") - echo "No subcommand provided" - usage - exit 1 - ;; - *) - echo "Invalid subcommand: $1" - usage - exit 1 - ;; -esac - -# Leave the script's exit code as the subcommand's. A trailing `exit 0` here -# silently masked ruff / mypy / pytest failures from the pre-push hook. diff --git a/python/uv.lock b/python/uv.lock index 7a0c68645b..09900b149e 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -372,6 +372,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -647,6 +656,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + [[package]] name = "eval-type-backport" version = "0.3.1" @@ -1145,6 +1163,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, ] +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -1413,7 +1440,7 @@ name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "traitlets", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } @@ -1887,7 +1914,7 @@ version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep", marker = "python_full_version >= '3.10'" }, - { name = "platformdirs", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyyaml", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } @@ -2699,10 +2726,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, @@ -2835,6 +2883,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/ad/b33c3022a394f3eb55c3310597cec615412a8a33880055eee191d154a628/polars_runtime_32-1.40.1-cp310-abi3-win_arm64.whl", hash = "sha256:b5cbfaf6b085b420b4bfcbe24e8f665076d1cccfdb80c0484c02a023ce205537", size = 45822104, upload-time = "2026-04-22T19:14:54.192Z" }, ] +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv", marker = "python_full_version >= '3.9'" }, + { name = "identify", marker = "python_full_version >= '3.9'" }, + { name = "nodeenv", marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, + { name = "virtualenv", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -3650,6 +3714,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/81/58c70036dffeccb7fe7d79d6260c69f7a28272bbd3909c29a01ea9422744/python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3", size = 72212, upload-time = "2026-07-08T23:06:50.691Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe", size = 34181, upload-time = "2026-07-08T23:06:49.402Z" }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -4418,6 +4497,7 @@ dev = [ { name = "grpcio-testing", version = "1.70.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "grpcio-testing", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mypy" }, + { name = "pre-commit", marker = "python_full_version >= '3.9'" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -4442,6 +4522,7 @@ dev-all = [ { name = "polars", version = "1.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "polars", version = "1.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "polars", version = "1.40.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pre-commit", marker = "python_full_version >= '3.9'" }, { name = "pyarrow", version = "17.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "pyarrow", version = "24.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -4464,6 +4545,7 @@ development = [ { name = "grpcio-testing", version = "1.70.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "grpcio-testing", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mypy" }, + { name = "pre-commit", marker = "python_full_version >= '3.9'" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -4506,6 +4588,7 @@ docs-build = [ { name = "polars", version = "1.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "polars", version = "1.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "polars", version = "1.40.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pre-commit", marker = "python_full_version >= '3.9'" }, { name = "pyarrow", version = "17.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "pyarrow", version = "24.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -4621,6 +4704,10 @@ requires-dist = [ { name = "polars", marker = "extra == 'docs-build'", specifier = "~=1.8" }, { name = "polars", marker = "extra == 'file-imports'", specifier = "~=1.8" }, { name = "polars", marker = "extra == 'hdf5'", specifier = "~=1.8" }, + { name = "pre-commit", marker = "python_full_version >= '3.9' and extra == 'dev'", specifier = "~=4.0" }, + { name = "pre-commit", marker = "python_full_version >= '3.9' and extra == 'dev-all'", specifier = "~=4.0" }, + { name = "pre-commit", marker = "python_full_version >= '3.9' and extra == 'development'", specifier = "~=4.0" }, + { name = "pre-commit", marker = "python_full_version >= '3.9' and extra == 'docs-build'", specifier = "~=4.0" }, { name = "protobuf", specifier = ">=5.0" }, { name = "protoc-gen-openapiv2", specifier = ">=0.0.1" }, { name = "pyarrow", marker = "extra == 'all'", specifier = ">=17.0.0" }, @@ -5177,6 +5264,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] +[[package]] +name = "virtualenv" +version = "21.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib", marker = "python_full_version >= '3.9'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-discovery", marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/d9/b477fddb68840b570af8b22afe9b035cbc277b5fb7b33dea390617a8b10f/virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128", size = 5526620, upload-time = "2026-07-10T19:33:53.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392, upload-time = "2026-07-10T19:33:51.629Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" diff --git a/rust/justfile b/rust/justfile new file mode 100644 index 0000000000..aea0838595 --- /dev/null +++ b/rust/justfile @@ -0,0 +1,36 @@ +# Rust workspace tasks. Run from the repo root as `just rust::`. +# The Cargo workspace root is the repo root; cargo resolves it from here. + +# Format all crates +fmt: + cargo fmt --all + +# Check formatting without writing (used by the pre-commit hook) +fmt-check: + cargo fmt --check --all + +# Type-check without producing artifacts +check: + cargo check --all-features + +# Lint with clippy +clippy: + cargo clippy --all-features + +# Run the test suite +test: + cargo test --all-features + +# Verify sift_stream_bindings' generated .pyi stub is up to date (pre-push hook) +bindings-stubs-check: + #!/usr/bin/env bash + set -uo pipefail + cd crates/sift_stream_bindings + cargo run --bin stub_gen + changed=$(git status --porcelain sift_stream_bindings.pyi | grep -E '\.pyi$' || true) + if [[ -n "$changed" ]]; then + echo "ERROR: generated stubs are not up to date:" + echo "$changed" | sed 's/^/ /' + echo "Commit these changes before pushing." + exit 1 + fi