Skip to content

Commit 177e000

Browse files
authored
Merge pull request #74 from cpsievert/feature/python-e2e-tests
feat(python): add Python bindings for ggsql
2 parents 3ab1ba2 + d0bb9fe commit 177e000

15 files changed

Lines changed: 817 additions & 28 deletions

File tree

.github/workflows/R-CMD-check.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ jobs:
2020
sudo docker image prune --all --force
2121
sudo docker builder prune -a
2222
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '20'
27+
28+
- name: Install tree-sitter-cli
29+
run: npm install -g tree-sitter-cli
30+
2331
- name: Install Rust
2432
uses: dtolnay/rust-toolchain@stable
2533

.github/workflows/build.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ jobs:
2121
sudo docker image prune --all --force
2222
sudo docker builder prune -a
2323
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: '20'
28+
29+
- name: Install tree-sitter-cli
30+
run: npm install -g tree-sitter-cli
31+
2432
- name: Install Rust
2533
uses: dtolnay/rust-toolchain@stable
2634

@@ -39,7 +47,7 @@ jobs:
3947

4048
- name: Run Rust tests
4149
if: success()
42-
run: cargo test
50+
run: cargo test --lib --bins
4351

4452
- name: Run Rust formatting check
4553
if: success()

.github/workflows/publish.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ jobs:
2222
sudo docker image prune --all --force
2323
sudo docker builder prune -a
2424
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: '20'
29+
30+
- name: Install tree-sitter-cli
31+
run: npm install -g tree-sitter-cli
32+
2533
- name: Install Rust
2634
uses: dtolnay/rust-toolchain@stable
2735

.github/workflows/python.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Python
2+
3+
on:
4+
push:
5+
paths: ['ggsql-python/**', '.github/workflows/python.yml']
6+
pull_request:
7+
paths: ['ggsql-python/**', '.github/workflows/python.yml']
8+
9+
jobs:
10+
test:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, macos-latest, windows-latest]
15+
python: ['3.10', '3.11', '3.12', '3.13']
16+
runs-on: ${{ matrix.os }}
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python }}
23+
24+
- uses: actions/setup-node@v4
25+
with:
26+
node-version: '20'
27+
28+
- name: Install tree-sitter-cli
29+
run: npm install -g tree-sitter-cli
30+
31+
- name: Install Rust
32+
uses: dtolnay/rust-toolchain@stable
33+
34+
- name: Rust cache
35+
uses: Swatinem/rust-cache@v2
36+
with:
37+
workspaces: ggsql-python
38+
shared-key: ${{ matrix.os }}-python
39+
40+
- name: Build wheel
41+
uses: PyO3/maturin-action@v1
42+
with:
43+
working-directory: ggsql-python
44+
command: build
45+
args: --release
46+
sccache: true
47+
48+
- name: Install wheel and test dependencies
49+
shell: bash
50+
run: pip install --find-links target/wheels/ ggsql[test]
51+
52+
- name: Run tests
53+
shell: bash
54+
run: pytest ggsql-python/tests/ -v
55+
56+
lint:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- uses: actions/checkout@v4
60+
61+
- uses: actions/setup-node@v4
62+
with:
63+
node-version: '20'
64+
65+
- name: Install tree-sitter-cli
66+
run: npm install -g tree-sitter-cli
67+
68+
- name: Check Rust formatting
69+
run: cargo fmt --package ggsql-python -- --check
70+
71+
- name: Clippy
72+
run: cargo clippy --package ggsql-python -- -D warnings

CLAUDE.md

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,63 @@ When running in Positron IDE, the extension provides enhanced functionality:
790790

791791
---
792792

793+
### 8. Python Bindings (`ggsql-python/`)
794+
795+
**Responsibility**: Python bindings for ggsql, enabling Python users to render Altair charts using ggsql's VISUALISE syntax.
796+
797+
**Features**:
798+
799+
- PyO3-based Rust bindings compiled to a native Python extension
800+
- Works with any narwhals-compatible DataFrame (polars, pandas, etc.)
801+
- LazyFrames are collected automatically
802+
- Returns native `altair.Chart` objects for easy display and customization
803+
- Query splitting to separate SQL from VISUALISE portions
804+
805+
**Installation**:
806+
807+
```bash
808+
# From source (requires Rust toolchain and maturin)
809+
cd ggsql-python
810+
pip install maturin
811+
maturin develop
812+
```
813+
814+
**API**:
815+
816+
```python
817+
import ggsql
818+
import polars as pl
819+
820+
# Split a ggSQL query into SQL and VISUALISE portions
821+
sql, viz = ggsql.split_query("""
822+
SELECT date, revenue FROM sales
823+
VISUALISE date AS x, revenue AS y
824+
DRAW line
825+
""")
826+
827+
# Execute SQL and render to Altair chart
828+
df = pl.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})
829+
chart = ggsql.render_altair(df, "VISUALISE x, y DRAW point")
830+
831+
# Display or save
832+
chart.display() # In Jupyter
833+
chart.save("chart.html")
834+
```
835+
836+
**Functions**:
837+
838+
- `split_query(query: str) -> tuple[str, str]` - Split ggSQL query into SQL and VISUALISE portions
839+
- `render_altair(df, viz, **kwargs) -> altair.Chart` - Render DataFrame with VISUALISE spec to Altair chart
840+
841+
**Dependencies**:
842+
843+
- Python >= 3.10
844+
- altair >= 5.0
845+
- narwhals >= 2.15
846+
- polars >= 1.0
847+
848+
---
849+
793850
## Feature Flags and Build Configuration
794851

795852
ggsql uses Cargo feature flags to enable optional functionality and reduce binary size.
@@ -822,9 +879,9 @@ ggsql uses Cargo feature flags to enable optional functionality and reduce binar
822879
- Includes: `axum`, `tokio`, `tower-http`, `tracing`, `duckdb`, `vegalite`
823880
- Required for building `ggsql-rest` server
824881

825-
**Future features**:
882+
**Python bindings**:
826883

827-
- `python` - Python bindings via PyO3 (planned)
884+
- `ggsql-python` - Python bindings via PyO3 (separate crate, not a feature flag)
828885

829886
### Building with Custom Features
830887

@@ -850,7 +907,7 @@ cargo build --all-features
850907
- `postgres``postgres` crate (future)
851908
- `sqlite``rusqlite` crate (future)
852909
- `rest-api``axum`, `tokio`, `tower-http`, `tracing`, `tracing-subscriber`
853-
- `python``pyo3` crate (future)
910+
- `ggsql-python``pyo3`, `narwhals`, `altair` (separate workspace crate)
854911

855912
---
856913

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
[workspace]
22
members = [
3+
"tree-sitter-ggsql",
4+
"src",
5+
"ggsql-jupyter",
6+
"ggsql-python"
7+
]
8+
# ggsql-python is excluded from default builds because it's a PyO3 extension
9+
# that requires Python dev headers. Build it separately with maturin.
10+
default-members = [
311
"tree-sitter-ggsql",
412
"src",
513
"ggsql-jupyter"
@@ -21,7 +29,7 @@ tree-sitter = "0.25"
2129
csscolorparser = "0.8.1"
2230

2331
# Data processing
24-
polars = { version = "0.52", features = ["lazy", "sql"] }
32+
polars = { version = "0.52", features = ["lazy", "sql", "ipc"] }
2533

2634
# Readers
2735
duckdb = { version = "1.1", features = ["bundled"] }

ggsql-python/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode/
2+
3+
uv.lock

ggsql-python/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "ggsql-python"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT"
6+
description = "Python bindings for ggsql"
7+
8+
[lib]
9+
name = "_ggsql"
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
pyo3 = { version = "0.26", features = ["extension-module"] }
14+
polars = { workspace = true, features = ["ipc"] }
15+
ggsql = { path = "../src", default-features = false, features = ["vegalite"] }
16+
17+
[features]
18+
default = []

0 commit comments

Comments
 (0)