-
Notifications
You must be signed in to change notification settings - Fork 28
Add python bindings for bonsai #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 19 commits
411c97c
1256636
c50f103
13cd7fd
b58e318
a1523fe
7c9ab29
04c5f28
6c5a673
b44f2ee
f79836b
fc7fd40
0203c6c
38c5322
2ddc0c3
81e5505
1fcca8f
6f50ffe
bf07d92
7328a58
7d3ff05
c3deb8d
41259b1
596ec76
0baad28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| name: Python wheels | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| tags: ["py-v*", "py-test-*"] | ||
| pull_request: | ||
| branches: [main] | ||
| workflow_dispatch: # manual ad-hoc builds from any branch | ||
|
|
||
| concurrency: | ||
| # Tag pushes get their own group so publishes never get cancelled. | ||
| group: >- | ||
| ${{ github.workflow }}-${{ github.ref }}-${{ startsWith(github.ref, 'refs/tags/') && 'publish' || 'branch' }} | ||
| cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} | ||
|
|
||
| env: | ||
| CARGO_TERM_COLOR: always | ||
|
|
||
| jobs: | ||
| build: | ||
| name: Build wheel (${{ matrix.target.label }}) | ||
| runs-on: ${{ matrix.target.runner }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| target: | ||
| # `manylinux: "2_28"` makes maturin-action run the build inside the | ||
| # official PyPA manylinux_2_28 container (Rocky Linux 8 / glibc 2.28). | ||
| # Without this, the build runs on the host (Ubuntu glibc 2.39) and | ||
| # produces a wheel that fails the auditwheel manylinux_2_28 check. | ||
| - label: linux-x86_64 | ||
| runner: ubuntu-latest | ||
| target: x86_64-unknown-linux-gnu | ||
| manylinux: "2_28" | ||
| - label: macos-universal2 | ||
| runner: macos-latest | ||
| target: universal2-apple-darwin | ||
| manylinux: "auto" | ||
| - label: windows-x86_64 | ||
| runner: windows-latest | ||
| target: x86_64-pc-windows-msvc | ||
| manylinux: "auto" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.10" # abi3 — any 3.10+ works for building | ||
|
|
||
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: ${{ matrix.target.label == 'macos-universal2' && 'x86_64-apple-darwin,aarch64-apple-darwin' || '' }} | ||
|
|
||
| - uses: Swatinem/rust-cache@v2 | ||
| with: | ||
| workspaces: bonsai-py | ||
| key: ${{ matrix.target.label }} | ||
|
|
||
| - name: Tag/version guard (tag pushes only) | ||
| if: startsWith(github.ref, 'refs/tags/') | ||
| shell: bash | ||
| run: | | ||
| tag="${GITHUB_REF#refs/tags/}" | ||
| version="${tag#py-v}" | ||
| version="${version#py-test-}" | ||
| cargo_version=$(grep -m1 '^version' bonsai-py/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') | ||
| if [ "$version" != "$cargo_version" ]; then | ||
| echo "::error::Tag version '$version' does not match Cargo.toml version '$cargo_version'." | ||
| exit 1 | ||
| fi | ||
|
|
||
| - uses: PyO3/maturin-action@v1 | ||
| with: | ||
| working-directory: bonsai-py | ||
| command: build | ||
| target: ${{ matrix.target.target }} | ||
| manylinux: ${{ matrix.target.manylinux }} | ||
| args: --release --out dist --strip | ||
|
|
||
| - name: Verify wheel (Linux/macOS only — Windows venv quirks) | ||
| if: matrix.target.runner != 'windows-latest' | ||
| shell: bash | ||
| run: | | ||
| python -m venv .venv-test | ||
| source .venv-test/bin/activate | ||
| pip install --upgrade pip | ||
| pip install pytest pytest-timeout mypy | ||
| pip install bonsai-py/dist/*.whl | ||
| pytest bonsai-py/tests/ | ||
| python bonsai-py/scripts/verify.py | ||
|
|
||
| - name: Wheel size sanity (Linux/macOS only) | ||
| if: matrix.target.runner != 'windows-latest' | ||
| shell: bash | ||
| run: | | ||
| size=$(stat -c%s bonsai-py/dist/*.whl 2>/dev/null || stat -f%z bonsai-py/dist/*.whl) | ||
| ceiling=$((5 * 1024 * 1024)) | ||
| if [ "$size" -gt "$ceiling" ]; then | ||
| echo "::error::Wheel size $size exceeds 5MB ceiling." | ||
| exit 1 | ||
| fi | ||
| echo "Wheel size: $size bytes (under 5MB ceiling)." | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: wheel-${{ matrix.target.label }} | ||
| path: bonsai-py/dist/*.whl | ||
| retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 90 || 14 }} | ||
|
|
||
| sdist: | ||
| name: Build sdist | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.10" | ||
|
kmolan marked this conversation as resolved.
|
||
| - uses: PyO3/maturin-action@v1 | ||
| with: | ||
| working-directory: bonsai-py | ||
| command: sdist | ||
| args: --out dist | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: sdist | ||
| path: bonsai-py/dist/*.tar.gz | ||
| retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 90 || 14 }} | ||
|
|
||
| verify-sdist: | ||
| name: Verify sdist builds from source | ||
| needs: sdist | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.10" | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| name: sdist | ||
| path: dist | ||
| - name: Install + smoke-test from sdist | ||
| run: | | ||
| python -m venv .venv-sdist | ||
| source .venv-sdist/bin/activate | ||
| pip install --upgrade pip | ||
| pip install --no-binary :all: dist/*.tar.gz | ||
| python -c "import bonsai_py; print(bonsai_py.__version__)" | ||
|
|
||
| publish: | ||
| name: Publish to PyPI / TestPyPI | ||
| needs: [build, sdist, verify-sdist] | ||
| if: startsWith(github.ref, 'refs/tags/py-v') || startsWith(github.ref, 'refs/tags/py-test-') | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: ${{ startsWith(github.ref, 'refs/tags/py-test-') && 'testpypi' || 'pypi' }} | ||
| permissions: | ||
| id-token: write # OIDC for Trusted Publishing | ||
| steps: | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| path: dist | ||
| pattern: wheel-* | ||
| merge-multiple: true | ||
|
|
||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| name: sdist | ||
| path: dist | ||
|
|
||
| - name: List artifacts to publish | ||
| run: ls -la dist/ | ||
|
|
||
| - uses: pypa/gh-action-pypi-publish@release/v1 | ||
| with: | ||
| repository-url: >- | ||
| ${{ startsWith(github.ref, 'refs/tags/py-test-') && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} | ||
| packages-dir: dist | ||
| skip-existing: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| [workspace] | ||
| resolver = "2" | ||
| members = ["bonsai", "examples"] | ||
| members = ["bonsai", "examples", "bonsai-py"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| [package] | ||
| name = "bonsai-py" | ||
| version = "0.12.0" | ||
| edition = "2021" | ||
| rust-version = "1.80.0" | ||
| description = "Python bindings for the bonsai-bt behavior tree library" | ||
| license = "MIT" | ||
| authors = ["Kristoffer Solberg Rakstad <kristoffer.solberg@cognite.com>"] | ||
| repository = "https://github.com/sollimann/bonsai.git" | ||
| homepage = "https://github.com/sollimann/bonsai" | ||
| publish = false | ||
|
|
||
| [lib] | ||
| name = "bonsai_py" | ||
| crate-type = ["cdylib", "rlib"] | ||
|
|
||
| [[bin]] | ||
| name = "stub_gen" | ||
| path = "src/bin/stub_gen.rs" | ||
|
|
||
| [dependencies] | ||
| # Note: `extension-module` is enabled by maturin via pyproject.toml's | ||
| # `[tool.maturin].features` setting. Keeping it out of the default feature | ||
| # list lets `cargo run --bin stub_gen` link libpython for the regular binary | ||
| # path; maturin still activates it for the wheel build. | ||
| pyo3 = { version = "0.28", features = ["abi3-py310"] } | ||
| bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] } | ||
| pyo3-stub-gen = "0.22.3" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # bonsai-py - Python bindings | ||
|
|
||
| Python bindings for the [bonsai-bt](https://github.com/sollimann/bonsai) | ||
| behavior-tree library. | ||
|
|
||
| ## Installation (dev) | ||
|
|
||
| ```bash | ||
| python -m venv .venv | ||
| source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 | ||
| pip install maturin | ||
| cd bonsai-py | ||
| maturin develop | ||
| python -c "import bonsai_py; print(bonsai_py.__version__)" | ||
| ``` | ||
|
|
||
| ## License | ||
|
|
||
| MIT - see [LICENSE](../LICENSE). |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||
| # bonsai-py examples | ||||
|
|
||||
| Pure-Python examples mirroring `examples/` in the Rust workspace. Each example is a single self-contained `.py` file. | ||||
|
Comment on lines
+1
to
+3
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we done in a separate PR, but I think it would be nice to have a python example where we serialize and deserialise the BT with json. ref Line 189 in 341b7a7
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created #66 |
||||
|
|
||||
| ## Prerequisites | ||||
|
|
||||
| Create and activate a Python venv (one-time), then build & install the extension: | ||||
|
|
||||
| ```bash | ||||
| # 1. Create a venv (only needed the first time) | ||||
| python3 -m venv .venv | ||||
|
|
||||
| # 2. Activate it (every new shell) | ||||
| source .venv/bin/activate # macOS / Linux / WSL | ||||
| # .\.venv\Scripts\Activate.ps1 # Windows PowerShell | ||||
|
|
||||
| # 3. Install build deps + build the extension into the venv | ||||
| pip install maturin | ||||
| cd bonsai-py && maturin develop --release && cd .. | ||||
| ``` | ||||
|
|
||||
| After that, just `source .venv/bin/activate` + `python bonsai-py/examples/<name>.py` in any new shell. | ||||
|
|
||||
| ## Examples (6) | ||||
|
|
||||
| ### [simple_npc_ai.py](simple_npc_ai.py) — console NPC | ||||
| NPC runs and shoots until action points are exhausted, then rests and dies. Demonstrates `WhileAll`, blackboard mutation via `@dataclass`, structural-`match` callback. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/simple_npc_ai.py | ||||
| ``` | ||||
|
|
||||
| ### [race_timeout.py](race_timeout.py) — `Race` between work and timeout | ||||
| A simulated long-running job (random 200–1200 ms on a `threading.Thread`) races a 600 ms timeout. The callback polls the work's `queue.Queue` non-blockingly. Demonstrates `Race`, asyncio main loop + threading worker, the unsendable-BT constraint. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/race_timeout.py | ||||
| ``` | ||||
|
|
||||
| ### [graphviz_demo.py](graphviz_demo.py) — tree visualization | ||||
| Builds an attack-drone tree (mix of plain-string and `@dataclass(frozen=True)` payload actions) and prints the graphviz DOT representation. Paste the output into <https://dreampuf.github.io/GraphvizOnline/> to render it. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/graphviz_demo.py | ||||
| python bonsai-py/examples/graphviz_demo.py > tree.dot | ||||
| ``` | ||||
|
|
||||
| ### [visualizer_smoke.py](visualizer_smoke.py) — live web visualizer | ||||
| Drives a deliberately rich 27-node tree at ~400 ms/tick with a 5-step status rotation and per-leaf phase offset; the browser shows continuous color animation. Demonstrates `BT.with_telemetry(port)`, `reset_bt()`, and every major factory. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/visualizer_smoke.py | ||||
| ``` | ||||
|
|
||||
| Then open <http://127.0.0.1:8910/> in a browser. `Ctrl-C` to stop. | ||||
|
|
||||
| ### [boids_console.py](boids_console.py) — shared BT across N agents | ||||
| Builds **one** `Behavior` tree and binds it to 10 independent `BT` instances (each with its own `Boid` dataclass blackboard). Updates positions every tick for 30 frames. Demonstrates the shared-subtree pattern, real-time-loop dt, `WhenAll` for parallel updates. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/boids_console.py | ||||
| ``` | ||||
|
kmolan marked this conversation as resolved.
|
||||
|
|
||||
| ### [async_drone.py](async_drone.py) — multi-job mission | ||||
| Drone mission: takeoff → check battery → fly (or fall back to land) → land → repeat. Each long-running step runs on a background thread; the BT polls per-job queues. Prints the tree's `graphviz()` at the start, then runs the mission for ~8 seconds. | ||||
|
|
||||
| ```bash | ||||
| python bonsai-py/examples/async_drone.py | ||||
| ``` | ||||
|
kmolan marked this conversation as resolved.
Outdated
|
||||
|
|
||||
| Demonstrates `Select` for prioritized fallback, multi-job orchestration via per-job channels, asyncio + threading. | ||||
Uh oh!
There was an error while loading. Please reload this page.