Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/py-e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Test - Python E2E (Playwright)

on:
workflow_dispatch:
push:
branches: ["main", "rc-*"]
paths:
- 'pkg-py/**'
- 'pyproject.toml'
- '.github/workflows/py-e2e-test.yml'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'pkg-py/**'
- 'pyproject.toml'
- '.github/workflows/py-e2e-test.yml'

permissions:
contents: read

jobs:
e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 30
environment: pypi

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v3

- name: Set up Python 3.12
run: uv python install 3.12

- name: Install the project
run: uv sync --python 3.12 --all-extras --all-groups

- name: Install Playwright browsers
run: make py-e2e-setup

- name: Run E2E tests
run: make py-e2e-tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
14 changes: 4 additions & 10 deletions .github/workflows/py-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
config:
- { python-version: "3.10", test_google: false, test_azure: false }
- { python-version: "3.11", test_google: false, test_azure: false }
- { python-version: "3.12", test_google: true, test_azure: true }
- { python-version: "3.13", test_google: false, test_azure: false }
- { python-version: "3.14", test_google: false, test_azure: false }
fail-fast: false
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4

- name: 🚀 Install uv
uses: astral-sh/setup-uv@v3

- name: 🐍 Set up Python ${{ matrix.config.python-version }}
run: uv python install ${{matrix.config.python-version }}
- name: 🐍 Set up Python ${{ matrix.python-version }}
run: uv python install ${{matrix.python-version }}

- name: 📦 Install the project
run: uv sync --python ${{matrix.config.python-version }} --all-extras --all-groups
run: uv sync --python ${{matrix.python-version }} --all-extras --all-groups

- name: 🧪 Check tests
run: make py-check-tests
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,10 @@ renv.lock
# Claude
.claude/settings.local.json

# Planning documents (local only)
docs/plans/

# Playwright MCP
.playwright-mcp/

/.luarc.json
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,7 @@ The package has deprecated the old functional API (`querychat_init()`, `querycha
7. Always pay attention to your working directory when running commands, especially when working in a sub-package.
8. When planning, talk through all function and argument names, file names and locations.
9. Additional, context-specific instructions can be found in `.claude/`.

### Python Naming Conventions

- **Do not use `_` prefixes for names inside private modules.** Files like `_state.py`, `_streamlit.py`, etc. are already private (indicated by the `_` prefix on the filename). Functions and classes defined inside these modules should use regular names without a leading underscore. For example, use `format_query_error` not `_format_query_error` inside `_state.py`.
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,19 @@ py-check-tox: ## [py] Run python 3.9 - 3.12 checks with tox
py-check-tests: ## [py] Run python tests
@echo ""
@echo "🧪 Running tests with pytest"
uv run pytest
uv run pytest pkg-py/tests --ignore=pkg-py/tests/playwright

.PHONY: py-e2e-setup
py-e2e-setup: ## [py] Install Playwright browsers for e2e tests
@echo ""
@echo "🎭 Installing Playwright browsers"
uv run playwright install chromium

.PHONY: py-e2e-tests
py-e2e-tests: ## [py] Run Playwright e2e tests (requires OPENAI_API_KEY)
@echo ""
@echo "🧪 Running Playwright e2e tests"
uv run pytest pkg-py/tests/playwright -v -n auto --reruns 2 --reruns-delay 5

.PHONY: py-check-types
py-check-types: ## [py] Run python type checks
Expand Down Expand Up @@ -217,7 +229,7 @@ py-update-dist: ## [py] Update shinychat web assets

.PHONY: help
help: ## Show help messages for make targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \
printf "\033[32m%-18s\033[0m", $$1; \
if ($$2 ~ /^\[docs\]/) { \
printf "\033[37m[docs]\033[0m%s\n", substr($$2, 7); \
Expand Down
7 changes: 7 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### New features

* Added support for Gradio, Dash, and Streamlit web frameworks in addition to Shiny. Import from the new submodules:
* `from querychat.gradio import QueryChat`
* `from querychat.dash import QueryChat`
* `from querychat.streamlit import QueryChat`

Each framework's `QueryChat` provides `.app()` for quick standalone apps and `.ui()` for custom layouts. Install framework dependencies with pip extras: `pip install querychat[gradio]`, `pip install querychat[dash]`, or `pip install querychat[streamlit]`.

## [0.4.0] - 2026-01-14

Expand Down
16 changes: 16 additions & 0 deletions pkg-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ Install the latest stable release [from PyPI](https://pypi.org/project/querychat
pip install querychat
```

### Web Framework Extras

querychat supports Gradio, Dash, and Streamlit. Install with the extras you need:

```bash
pip install "querychat[gradio]"
pip install "querychat[dash]"
pip install "querychat[streamlit]"
```

Or install directly from GitHub:

```bash
pip install "querychat[gradio] @ git+https://github.com/posit-dev/querychat"
```

## Quick start

The main entry point is the [`QueryChat` class](https://posit-dev.github.io/querychat/py/reference/QueryChat.html). It requires a [data source](https://posit-dev.github.io/querychat/py/data-sources.html) (e.g., pandas, polars, etc) and a name for the data.
Expand Down
2 changes: 2 additions & 0 deletions pkg-py/docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ CHANGELOG.md
_sidebar-python.yml
api/
reference/

**/*.quarto_ipynb
119 changes: 119 additions & 0 deletions pkg-py/docs/_examples/dash-complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import dash_bootstrap_components as dbc
import plotly.express as px
from querychat.dash import QueryChat
from querychat.data import titanic
from querychat.types import AppStateDict

from dash import Dash, Input, Output, dcc, html

qc = QueryChat(titanic(), "titanic")

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container(
[
html.H1("Titanic Dataset Explorer", className="my-3"),
dbc.Row(
[
dbc.Col(qc.ui(), width=4),
dbc.Col(
[
html.H3(id="data-title", className="mb-3"),
dbc.Row(
[
dbc.Col(
dbc.Card(
[
dbc.CardHeader("Passengers"),
dbc.CardBody(
html.H4(id="metric-passengers")
),
]
)
),
dbc.Col(
dbc.Card(
[
dbc.CardHeader("Survivors"),
dbc.CardBody(
html.H4(id="metric-survivors")
),
]
)
),
dbc.Col(
dbc.Card(
[
dbc.CardHeader("Survival Rate"),
dbc.CardBody(html.H4(id="metric-rate")),
]
)
),
],
className="mb-3",
),
dbc.Row(
[
dbc.Col(dcc.Graph(id="age-chart")),
dbc.Col(dcc.Graph(id="class-chart")),
]
),
],
width=8,
),
]
),
],
fluid=True,
)

qc.init_app(app)


@app.callback(
[
Output("data-title", "children"),
Output("metric-passengers", "children"),
Output("metric-survivors", "children"),
Output("metric-rate", "children"),
Output("age-chart", "figure"),
Output("class-chart", "figure"),
],
Input(qc.store_id, "data"),
)
def update_all(state: AppStateDict):
df = qc.df(state).to_pandas()
title = qc.title(state) or "All Data"

# Metrics
n_passengers = len(df)
n_survivors = int(df["survived"].sum())
survival_rate = f"{df['survived'].mean():.1%}"

# Charts
fig1 = px.histogram(
df, x="age", color="survived", title="Age Distribution by Survival"
)
fig2 = px.bar(
df.groupby("pclass")["survived"].mean().reset_index(),
x="pclass",
y="survived",
title="Survival by Class",
)

return (
title,
n_passengers,
n_survivors,
survival_rate,
fig1,
fig2,
)


if __name__ == "__main__":
import os

port = int(os.environ.get("DASH_PORT", "8050"))
debug = os.environ.get("DASH_DEBUG", "true").lower() == "true"
app.run(debug=debug, port=port)
64 changes: 64 additions & 0 deletions pkg-py/docs/_examples/dash-custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import dash_ag_grid as dag
import dash_bootstrap_components as dbc
from querychat.dash import QueryChat
from querychat.data import titanic
from querychat.types import AppStateDict

from dash import Dash, Input, Output, html

qc = QueryChat(titanic(), "titanic")

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container(
[
dbc.Row(
[
# Left column: Chat
dbc.Col(qc.ui(), width=4),
# Right column: Data display
dbc.Col(
[
html.H3(id="data-title"),
dag.AgGrid(
id="data-table",
className="ag-theme-balham",
defaultColDef={"filter": True, "sortable": True},
dashGridOptions={"pagination": True, "paginationPageSize": 10},
columnSize="responsiveSizeToFit",
),
html.Pre(id="sql-display"),
],
width=8,
),
]
)
],
fluid=True,
)

# Register querychat's internal callbacks
qc.init_app(app)


# Add your own callbacks using qc.store_id
@app.callback(
[
Output("data-title", "children"),
Output("data-table", "rowData"),
Output("data-table", "columnDefs"),
Output("sql-display", "children"),
],
Input(qc.store_id, "data"),
)
def update_display(state: AppStateDict):
df = qc.df(state).to_pandas()
sql = qc.sql(state) or f"SELECT * FROM {qc.data_source.table_name}"
title = qc.title(state) or "All Data"

columns = [{"field": c} for c in df.columns]
return title, df.to_dict("records"), columns, sql


if __name__ == "__main__":
app.run(debug=True)
39 changes: 39 additions & 0 deletions pkg-py/docs/_examples/dash-viz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import dash_bootstrap_components as dbc
import plotly.express as px
from querychat.dash import QueryChat
from querychat.data import titanic
from querychat.types import AppStateDict

from dash import Dash, Input, Output, dcc

qc = QueryChat(titanic(), "titanic")

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container([
dbc.Row([
dbc.Col(qc.ui(), width=4),
dbc.Col([
dcc.Graph(id="age-histogram"),
dcc.Graph(id="class-survival"),
], width=8),
])
], fluid=True)

qc.init_app(app)

@app.callback(
[Output("age-histogram", "figure"), Output("class-survival", "figure")],
Input(qc.store_id, "data"),
)
def update_charts(state: AppStateDict):
df = qc.df(state).to_pandas()
fig1 = px.histogram(df, x="age", color="survived", title="Age Distribution")
fig2 = px.bar(
df.groupby("pclass")["survived"].mean().reset_index(),
x="pclass", y="survived", title="Survival by Class"
)
return fig1, fig2

if __name__ == "__main__":
app.run(debug=True)
Loading