Skip to content

Commit 8181fea

Browse files
committed
Rework Python bindings
1 parent b760081 commit 8181fea

9 files changed

Lines changed: 1440 additions & 181 deletions

File tree

CLAUDE.md

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -864,17 +864,18 @@ When running in Positron IDE, the extension provides enhanced functionality:
864864

865865
### 8. Python Bindings (`ggsql-python/`)
866866

867-
**Responsibility**: Python bindings for ggsql, enabling Python users to render Altair charts using ggsql's VISUALISE syntax.
867+
**Responsibility**: Python bindings for ggsql, enabling Python users to create visualizations using ggsql's VISUALISE syntax.
868868

869869
**Features**:
870870

871871
- PyO3-based Rust bindings compiled to a native Python extension
872+
- Two-stage API mirroring the Rust API: `prepare()``render()`
873+
- DuckDB reader with DataFrame registration
874+
- Custom Python reader support: any object with `execute(sql) -> DataFrame` method
872875
- Works with any narwhals-compatible DataFrame (polars, pandas, etc.)
873876
- LazyFrames are collected automatically
874-
- Returns native `altair.Chart` objects for easy display and customization
875-
- Two-stage API: `prepare()``render()`
876-
- DuckDB reader with DataFrame registration
877-
- Query introspection (SQL, layer queries, stat queries)
877+
- Returns native `altair.Chart` objects via `render_altair()` convenience function
878+
- Query validation and introspection (SQL, layer queries, stat queries)
878879

879880
**Installation**:
880881

@@ -902,30 +903,105 @@ prepared = ggsql.prepare(
902903
reader
903904
)
904905

905-
# Inspect
906+
# Inspect metadata
906907
print(f"Rows: {prepared.metadata()['rows']}")
908+
print(f"Columns: {prepared.metadata()['columns']}")
907909
print(f"SQL: {prepared.sql()}")
908910

909911
# Render to Vega-Lite JSON
910912
writer = ggsql.VegaLiteWriter()
911913
json_output = prepared.render(writer)
912914
```
913915

916+
**Convenience Function** (`render_altair`):
917+
918+
For quick visualizations without explicit reader setup:
919+
920+
```python
921+
import ggsql
922+
import polars as pl
923+
924+
df = pl.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})
925+
926+
# Render DataFrame to Altair chart in one call
927+
chart = ggsql.render_altair(df, "VISUALISE x, y DRAW point")
928+
chart.display() # In Jupyter
929+
```
930+
931+
**Query Validation**:
932+
933+
```python
934+
# Validate syntax without execution
935+
validated = ggsql.validate(
936+
"SELECT x, y FROM data VISUALISE x, y DRAW point"
937+
)
938+
print(f"Valid: {validated.valid()}")
939+
print(f"Has VISUALISE: {validated.has_visual()}")
940+
print(f"SQL portion: {validated.sql()}")
941+
print(f"Errors: {validated.errors()}")
942+
```
943+
914944
**Classes**:
915945

916-
| Class | Description |
917-
| -------------------------- | ---------------------------- |
918-
| `DuckDBReader(connection)` | Database reader |
919-
| `VegaLiteWriter()` | Vega-Lite JSON output writer |
920-
| `Validated` | Result of `validate()` |
946+
| Class | Description |
947+
| -------------------------- | -------------------------------------------- |
948+
| `DuckDBReader(connection)` | Database reader with DataFrame registration |
949+
| `VegaLiteWriter()` | Vega-Lite JSON output writer |
950+
| `Validated` | Result of `validate()` with query inspection |
951+
| `Prepared` | Result of `prepare()`, ready for rendering |
921952

922953
**Functions**:
923954

924-
| Function | Description |
925-
| ------------------------ | ------------------------------------------------ |
926-
| `validate(query)` | Syntax/semantic validation with query inspection |
927-
| `prepare(query, reader)` | Full preparation pipeline |
928-
| `render_altair(df, viz)` | Render DataFrame to Altair chart |
955+
| Function | Description |
956+
| ------------------------ | ------------------------------------------------- |
957+
| `validate(query)` | Syntax/semantic validation with query inspection |
958+
| `prepare(query, reader)` | Full preparation (reader can be native or custom) |
959+
| `render_altair(df, viz)` | Convenience: render DataFrame to Altair chart |
960+
961+
**Prepared Object Methods**:
962+
963+
| Method | Description |
964+
| ---------------- | -------------------------------------------- |
965+
| `render(writer)` | Generate Vega-Lite JSON |
966+
| `metadata()` | Get rows, columns, layer_count |
967+
| `sql()` | Get the SQL portion |
968+
| `visual()` | Get the VISUALISE portion |
969+
| `layer_count()` | Number of DRAW layers |
970+
| `data()` | Get the main DataFrame |
971+
| `layer_data(i)` | Get layer-specific DataFrame (if filtered) |
972+
| `stat_data(i)` | Get stat transform DataFrame (if applicable) |
973+
| `layer_sql(i)` | Get layer filter SQL (if applicable) |
974+
| `stat_sql(i)` | Get stat transform SQL (if applicable) |
975+
| `warnings()` | Get validation warnings |
976+
977+
**Custom Python Readers**:
978+
979+
Any Python object with an `execute(sql: str) -> polars.DataFrame` method can be used as a reader:
980+
981+
```python
982+
import ggsql
983+
import polars as pl
984+
985+
class MyReader:
986+
"""Custom reader that returns static data."""
987+
988+
def execute(self, sql: str) -> pl.DataFrame:
989+
return pl.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})
990+
991+
# Use custom reader with prepare()
992+
reader = MyReader()
993+
prepared = ggsql.prepare(
994+
"SELECT * FROM data VISUALISE x, y DRAW point",
995+
reader
996+
)
997+
```
998+
999+
Optional methods for custom readers:
1000+
1001+
- `supports_register() -> bool` - Return `True` if registration is supported
1002+
- `register(name: str, df: polars.DataFrame) -> None` - Register a DataFrame as a table
1003+
1004+
Native readers (e.g., `DuckDBReader`) use an optimized fast path, while custom Python readers are automatically bridged via IPC serialization.
9291005

9301006
**Dependencies**:
9311007

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ THEME minimal
3030
- ✅ REST API server (`ggsql-rest`) with CORS support
3131
- ✅ Jupyter kernel (`ggsql-jupyter`) with inline Vega-Lite visualizations
3232
- ✅ VS Code extension (`ggsql-vscode`) with syntax highlighting and Positron IDE integration
33+
- ✅ Python bindings (`ggsql-python`) with Altair chart output
3334

3435
**Planned:**
3536

@@ -93,7 +94,9 @@ ggsql/
9394
9495
├── ggsql-jupyter/ # Jupyter kernel
9596
96-
└── ggsql-vscode/ # VS Code extension
97+
├── ggsql-vscode/ # VS Code extension
98+
99+
└── ggsql-python/ # Python bindings
97100
```
98101

99102
## Development Workflow
@@ -297,6 +300,44 @@ When running in Positron IDE, the extension provides additional features:
297300
- **Language runtime registration** for executing ggsql queries directly within Positron
298301
- **Plot pane integration** - visualizations are automatically routed to Positron's Plots pane
299302

303+
## Python Bindings
304+
305+
The `ggsql-python` package provides Python bindings for using ggsql with DataFrames.
306+
307+
### Installation
308+
309+
```bash
310+
cd ggsql-python
311+
pip install maturin
312+
maturin develop
313+
```
314+
315+
### Usage
316+
317+
```python
318+
import ggsql
319+
import polars as pl
320+
321+
# Simple usage with render_altair
322+
df = pl.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})
323+
chart = ggsql.render_altair(df, "VISUALISE x, y DRAW point")
324+
chart.display()
325+
326+
# Two-stage API for full control
327+
reader = ggsql.DuckDBReader("duckdb://memory")
328+
reader.register("data", df)
329+
330+
prepared = ggsql.prepare(
331+
"SELECT * FROM data VISUALISE x, y DRAW point",
332+
reader
333+
)
334+
335+
writer = ggsql.VegaLiteWriter()
336+
json_output = prepared.render(writer)
337+
```
338+
339+
See the [ggsql-python README](ggsql-python/README.md) for complete API documentation.
340+
300341
## CLI
301342

302343
### Installation

ggsql-python/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
1212
[dependencies]
1313
pyo3 = { version = "0.26", features = ["extension-module"] }
1414
polars = { workspace = true, features = ["ipc"] }
15-
ggsql = { path = "../src", default-features = false, features = ["vegalite"] }
15+
ggsql = { path = "../src", default-features = false, features = ["duckdb", "vegalite"] }
1616

1717
[features]
1818
default = []

0 commit comments

Comments
 (0)