Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,4 @@ marimo/_lsp/
__marimo__/

docs/site/*
.claude/settings.local.json
411 changes: 261 additions & 150 deletions pywry/docs/docs/components/chat/index.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pywry/docs/docs/components/modal/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ modal = Modal(
)

def on_dismissed(data, event_type, label):
print("User dismissed the confirmation dialog")
app.emit("pywry:set-content", {"id": "status", "text": "Action cancelled"}, label)

app.show(content, modals=[modal], callbacks={"app:confirm-dismissed": on_dismissed})
```
Expand Down
2 changes: 1 addition & 1 deletion pywry/docs/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ One API, three output targets — PyWry automatically selects the right one:
| **[Configuration](guides/configuration.md)** | TOML files, env vars, layered precedence |
| **[Hot Reload](guides/hot-reload.md)** | Live CSS/JS updates during development |
| **[Deploy Mode](guides/deploy-mode.md)** | Redis backend for horizontal scaling |
| **[Tauri Plugins](integrations/tauri-plugins.md)** | 19 bundled plugins — clipboard, notifications, HTTP, and more |
| **[Tauri Plugins](integrations/pytauri/tauri-plugins.md)** | 19 bundled plugins — clipboard, notifications, HTTP, and more |

## Platform Support

Expand Down
2 changes: 0 additions & 2 deletions pywry/docs/docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ app = PyWry()

def on_button_click(data, event_type, label):
"""Called when the button is clicked."""
print(f"Button clicked! Data: {data}")
# Update the page content
app.emit("pywry:set-content", {"id": "greeting", "text": "Button was clicked!"}, label)

html = """
Expand Down
117 changes: 71 additions & 46 deletions pywry/docs/docs/getting-started/why-pywry.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,106 @@
# Why PyWry

PyWry is an open-source rendering engine for building lightweight, cross-platform interfaces using Python. It solves a specific problem: **how to build beautiful, modern data applications in Python without being forced into an opinionated web framework or a heavy native GUI toolkit.**
PyWry is an open-source rendering engine for building cross-platform interfaces using Python. It solves a specific problem: **how to build modern data applications in Python without being forced into an opinionated web framework or a heavy native GUI toolkit.**

PyWry renders standard HTML, CSS, and JavaScript inside battle-tested OS webviews (WebView2 on Windows, WebKit on macOS/Linux). Your team can use web skills they already have — no proprietary widget toolkit to learn. If it works in a browser, it works in PyWry.
PyWry renders standard HTML, CSS, and JavaScript inside OS-native webviews (WebView2 on Windows, WebKit on macOS/Linux) via [PyTauri](https://pytauri.github.io/pytauri/). Your team can use web skills they already have — no proprietary widget toolkit to learn. If it works in a browser, it works in PyWry.

There are many ways to render web content from Python — Electron, Dash, Streamlit, NiceGUI, Gradio, Flet, or plain FastAPI. So why choose PyWry?
## Write Once, Render Anywhere

### The "Goldilocks" Framework
PyWry's defining feature is that the same code renders in three environments without modification:

Python developers often find themselves choosing between uncomfortable extremes:
| Environment | Transport | How It Renders |
|---|---|---|
| Desktop terminal | PyTauri subprocess | Native OS webview window |
| Jupyter / VS Code / Colab | Anywidget traitlets | Notebook cell widget (no server) |
| Headless / SSH / Deploy | FastAPI + WebSocket | Browser tab via IFrame |

- **Native GUI Toolkits (PyQt/Tkinter)**: Steep learning curves, custom styling systems, and they don't look modern without massive effort.
- **Web-to-Desktop (Electron)**: Forces Python developers into the JavaScript/Node.js ecosystem and ships with hundreds of megabytes of Chromium bloat.
- **Data Dashboards (Streamlit/Gradio)**: Excellent for rapid deployment in a browser, but highly opinionated, difficult to deeply customize, and hard to package as a true desktop executable.
A Plotly chart, an AG Grid table, a TradingView financial chart, or a full chat interface — all render identically across these three paths. The same `on()`/`emit()` event protocol works in every environment, so components you build are portable by default.

PyWry targets the sweet spot: **Write your logic in Python, build your UI with modern web technologies, and deploy it anywhere**—including as a native, lightweight executable.
This pipeline is designed for data teams: prototype in a Jupyter notebook, share as a browser-based FastAPI application, and package as a standalone desktop executable with `pywry[freeze]` — all from the same Python code.

### The Jupyter → Web → Desktop Pipeline
## Built-In Integrations

PyWry's most potent feature is its **"Build Once, Render Anywhere"** pipeline. Most frameworks support Web + Desktop, but PyWry is uniquely optimized for data science and full-stack environments.
PyWry ships with production-ready integrations that implement industry-standard interfaces where they exist, so your code stays portable and your skills transfer.

You can instantly render a Plotly chart or AgGrid table directly inside a **Jupyter Notebook** cell. When you're ready to share your work, you use the exact same code to deploy a browser-based FastAPI application. When you want to hand an internal tool to a business user, you use `pywry[freeze]` to compile that *same code* into a standalone `.exe` or `.app`—dropping the notebook or server entirely.
### Plotly Charts

### Lightweight Native Windows
Interactive charts with automatic dark/light theming, pre-wired click/hover/selection/zoom events, programmatic updates, custom mode bar buttons that fire Python callbacks, and per-theme template overrides. Accepts standard Plotly `Figure` objects and figure dicts — the same data format used across the Plotly ecosystem.

PyWry uses the **OS-native webview** (WebView2, WebKit) via [PyTauri](https://github.com/pytauri/pytauri) instead of bundling a full browser engine like Electron. This results in apps that add only a few megabytes of overhead and open in under a second. There's no server to spin up and no browser to launch.
### AG Grid Tables

### One API, three targets
Sortable, filterable, editable data tables with automatic DataFrame conversion, cell editing callbacks, row selection events, and pagination. Configures through standard AG Grid `ColDef` and `GridOptions` structures — the same column definitions and grid options documented in the [AG Grid docs](https://www.ag-grid.com/javascript-data-grid/) work directly in PyWry.

Write your interface once. PyWry automatically renders it in the right place without changing your code:
### TradingView Financial Charts

| Environment | Rendering Path |
|---|---|
| Desktop terminal | Native OS window via PyTauri |
| Jupyter / VS Code / Colab | anywidget or inline IFrame |
| Headless / SSH / Deploy | Browser tab via FastAPI + WebSocket |
Full [TradingView Lightweight Charts](https://tradingview.github.io/lightweight-charts/) integration supporting three data modes:

- **Static** — pass a DataFrame or list of OHLCV dicts for one-shot rendering
- **Datafeed** — implement the `DatafeedProvider` interface for async on-demand bar loading, symbol resolution, and real-time subscriptions (follows TradingView's [Datafeed API](https://www.tradingview.com/charting-library-docs/latest/connecting_data/Datafeed-API/) contract)
- **UDF** — `UDFAdapter` wraps any `DatafeedProvider` as a [Universal Data Feed](https://www.tradingview.com/charting-library-docs/latest/connecting_data/UDF/) HTTP endpoint compatible with TradingView's server-side data protocol

### Built for data workflows
Also includes drawing tools, technical indicators, persistent chart layouts (file or Redis storage), and session/timezone management.

PyWry comes with built-in integrations tailored for data workflows:
### AI Chat (ACP)

- **Plotly charts** with pre-wired event callbacks (click, select, hover, zoom).
- **AG Grid tables** with automatic DataFrame conversion and grid events.
- **Toolbar system** with 18 declarative Pydantic input components across 7 layout positions to easily add headers, sidebars, and overlays.
- **Two-way events** between Python and JavaScript, with no boilerplate.
The chat system implements the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) — an open standard for AI agent communication using JSON-RPC 2.0. This means:

- **Provider interface** follows ACP's session lifecycle: `initialize` → `new_session` → `prompt` → `cancel`
- **Session updates** use ACP's typed notification system: `agent_message`, `tool_call`, `plan`, `available_commands`, `config_option`, `current_mode`
- **Content blocks** use ACP's content model: `text`, `image`, `audio`, `resource`, `resource_link`
- **StdioProvider** connects to any ACP-compatible agent (Claude Code, Gemini CLI) over stdio JSON-RPC without writing adapter code

### Production-ready
Built-in providers for OpenAI, Anthropic, Magentic (100+ backends), [Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview) (LangChain's agent harness with filesystem tools, planning, and subagents), and user-supplied callables adapt their respective APIs to the ACP session interface. Rich inline artifacts (code, markdown, tables, Plotly charts, TradingView charts, images, JSON trees) render directly in the chat transcript.

PyWry scales from prototyping to multi-user deployments:
### MCP Server

- **Deploy Mode** with an optional Redis backend for horizontal scaling.
- **OAuth2** authentication system for both native and deploy modes with enterprise RBAC.
- **Security built-in**: Token authentication, CSRF protection, and CSP headers out of the box.
A [Model Context Protocol](https://modelcontextprotocol.io/) server built on [FastMCP](https://github.com/jlowin/fastmcp) with 25+ tools that lets AI coding agents create and control PyWry widgets, send chat messages, manage chart data, and build interactive dashboards programmatically. MCP is the standard protocol used by Claude Code, Cursor, Windsurf, and other AI coding tools for tool integration.

### Cross-platform
### Toolbar System

PyWry runs on Windows, macOS, and Linux. The same code produces native windows on all three platforms, notebook widgets in any Jupyter environment, and browser-based interfaces anywhere Python runs.
18 declarative Pydantic input components (buttons, selects, toggles, sliders, text inputs, date pickers, search bars, secret inputs, radio groups, tab groups, marquees, and more) across 7 layout positions, all with automatic event wiring.

## Lightweight Native Windows

## Why not something else
PyWry uses the OS-native webview via PyTauri instead of bundling a full browser engine like Electron. Apps add only a few megabytes of overhead and open in under a second. The PyTauri subprocess provides access to 19 Tauri plugins for native OS capabilities — clipboard, file dialogs, notifications, global shortcuts, system tray icons, and more.

| Alternative | Trade-off |
|---|---|
| **NiceGUI** | Server + browser required natively; highly capable but lacks the single-codebase Jupyter → Desktop executable pipeline of PyWry. |
| **Electron** | 150 MB+ runtime per app, requires Node.js/JavaScript context, difficult integration for native Python execution. |
| **Dash / Streamlit / Gradio** | Opinionated UIs, browser-only deployment, not easily packagable into offline standalone executables. |
| **Flet (Flutter/Python)** | Cannot use standard web libraries (React, Tailwind, AG Grid) as it relies entirely on Flutter's custom canvas rendering. |
| **PyQt / Tkinter / wxPython** | Proprietary widget toolkits, requires learning custom desktop layout engines, lacks web interactivity features. |
| **Plain FastAPI + HTML** | No native OS windows, no notebook support, requires manual WebSocket and event wiring. |
## Unified Event Protocol

All three rendering transports implement the same bidirectional event protocol:

- **Python → JavaScript**: `widget.emit("app:update", {"count": 42})` updates the UI
- **JavaScript → Python**: `pywry.emit("app:click", {x: 100})` fires a Python callback

This means every component — whether it's a Plotly chart, an AG Grid table, a TradingView chart, a chat panel, or a custom HTML element — uses the same `on()`/`emit()` pattern. Build a component once and it works in native windows, notebooks, and browser tabs.

## Production Ready

PyWry sits in a unique position: native-quality lightweight desktop rendering, interactive Jupyter notebook support, and browser deployment, all from one Python API.
PyWry scales from a single-user notebook to multi-user deployments:

- **Three state backends**: in-memory (ephemeral), SQLite with encryption at rest (local persistent), and Redis (multi-worker distributed) — the same interfaces, queries, and RBAC work on all three
- **SQLite audit trail**: tool call traces, generated artifacts, token usage stats, resource references, and skill activations persisted to an encrypted local database
- **Deploy Mode** with a Redis backend for horizontal scaling across multiple Uvicorn workers
- **OAuth2 authentication** with pluggable providers (Google, GitHub, Microsoft, generic OIDC) for both native and deploy modes
- **Role-based access control** with viewer/editor/admin roles enforced across all ACP chat operations, file system access, and terminal control
- **Security built-in**: per-widget token authentication, origin validation, CSP headers, secret input values never rendered in HTML, and SQLite databases encrypted at rest via SQLCipher

## Cross Platform

PyWry runs on Windows, macOS, and Linux. The same code produces native windows on all three platforms, notebook widgets in any Jupyter environment, and browser-based interfaces anywhere Python runs. The PyTauri binary ships as a vendored wheel — no Rust toolchain or system dependencies required.

## Why Not Something Else

| Alternative | What PyWry Adds |
|---|---|
| **Electron** | 150MB+ runtime, requires Node.js. PyWry uses the OS webview — a few MB, pure Python. |
| **Dash / Streamlit / Gradio** | Browser-only, opinionated layouts, no desktop executables. PyWry renders in notebooks, browsers, and native windows from one codebase. |
| **NiceGUI** | Server + browser required for native rendering. PyWry renders directly in the OS webview with no server for desktop mode. |
| **Flet** | Flutter canvas rendering — cannot use standard web libraries (Plotly, AG Grid, TradingView). PyWry renders any HTML/CSS/JS. |
| **PyQt / Tkinter** | Proprietary widget toolkits with custom layout engines. PyWry uses standard web technologies. |
| **Plain FastAPI** | No native windows, no notebook rendering, no event system, no component library. PyWry provides all of these. |

## Next steps
None of these alternatives offer the combination of native desktop rendering, Jupyter notebook widgets, browser deployment, integrated AI chat with ACP protocol support, TradingView financial charting, and MCP agent tooling — all from one Python API with one event protocol.

Ready to try it?
## Next Steps

- [**Installation**](installation.md) — Install PyWry and platform dependencies
- [**Quick Start**](quickstart.md) — Build your first interface in 5 minutes
Expand Down
5 changes: 3 additions & 2 deletions pywry/docs/docs/guides/app-show.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,11 @@ A dictionary mapping event names to Python callback functions. These are registe

```python
def on_click(data, event_type, label):
print(f"Clicked: {data}")
selected_point = data.get("points", [{}])[0]
app.emit("pywry:set-content", {"id": "info", "text": f"x={selected_point.get('x')}"}, label)

def on_save(data, event_type, label):
print("Saving...")
app.emit("pywry:download", {"filename": "data.json", "content": "{}"}, label)

app.show(html, callbacks={
"plotly:click": on_click,
Expand Down
4 changes: 2 additions & 2 deletions pywry/docs/docs/guides/browser-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ h2 = app.show("<h1>Table</h1>", label="table")
# Two browser tabs open:
# http://127.0.0.1:8765/widget/chart
# http://127.0.0.1:8765/widget/table
print(h1.url) # Full URL for the chart widget
print(h2.url) # Full URL for the table widget
chart_url = h1.url # e.g. http://127.0.0.1:8765/widget/chart
table_url = h2.url # e.g. http://127.0.0.1:8765/widget/table

app.block()
```
Expand Down
8 changes: 4 additions & 4 deletions pywry/docs/docs/guides/builder-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ The `builder_kwargs()` method returns a dict of only the non-default builder fie
from pywry.models import WindowConfig

config = WindowConfig(transparent=True, user_agent="test/1.0")
print(config.builder_kwargs())
# {'transparent': True, 'user_agent': 'test/1.0'}
kwargs = config.builder_kwargs()
# kwargs == {'transparent': True, 'user_agent': 'test/1.0'}

config2 = WindowConfig() # all defaults
print(config2.builder_kwargs())
# {}
kwargs2 = config2.builder_kwargs()
# kwargs2 == {} — only non-default values are included
```

This is used internally by the runtime to avoid sending unnecessary data over IPC.
5 changes: 3 additions & 2 deletions pywry/docs/docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ auto_start = true
websocket_require_token = true

[deploy]
state_backend = "memory" # or "redis"
state_backend = "memory" # "memory", "sqlite", or "redis"
sqlite_path = "~/.config/pywry/pywry.db"
redis_url = "redis://localhost:6379/0"
```

Expand Down Expand Up @@ -194,6 +195,6 @@ pywry init
## Next Steps

- **[Configuration Reference](../reference/config.md)** — Complete `PyWrySettings` API
- **[Tauri Plugins](../integrations/tauri-plugins.md)** — Enable clipboard, notifications, HTTP & more
- **[Tauri Plugins](../integrations/pytauri/tauri-plugins.md)** — Enable clipboard, notifications, HTTP & more
- **[Deploy Mode](deploy-mode.md)** — Production server configuration
- **[Browser Mode](browser-mode.md)** — Server settings for browser mode
9 changes: 5 additions & 4 deletions pywry/docs/docs/guides/deploy-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ Deploy mode is configured through environment variables (prefix `PYWRY_SERVER__`

| Setting | Default | Environment variable | Description |
|:---|:---|:---|:---|
| State backend | `memory` | `PYWRY_DEPLOY__STATE_BACKEND` | `memory` or `redis` |
| State backend | `memory` | `PYWRY_DEPLOY__STATE_BACKEND` | `memory`, `sqlite`, or `redis` |
| SQLite path | `~/.config/pywry/pywry.db` | `PYWRY_DEPLOY__SQLITE_PATH` | Database file path (when backend is `sqlite`) |
| Redis URL | `redis://localhost:6379/0` | `PYWRY_DEPLOY__REDIS_URL` | Redis connection string |
| Redis prefix | `pywry` | `PYWRY_DEPLOY__REDIS_PREFIX` | Key namespace in Redis |
| Redis pool size | `10` | `PYWRY_DEPLOY__REDIS_POOL_SIZE` | Connection pool size (1–100) |
Expand Down Expand Up @@ -174,9 +175,9 @@ Redis key structure: `{prefix}:widget:{widget_id}` (hash), `{prefix}:widgets:act
```python
from pywry.state import is_deploy_mode, get_state_backend, get_worker_id

print(f"Deploy mode: {is_deploy_mode()}")
print(f"Backend: {get_state_backend().value}") # "memory" or "redis"
print(f"Worker: {get_worker_id()}")
deploy_active = is_deploy_mode() # True when PYWRY_DEPLOY__ENABLED=true
backend = get_state_backend().value # "memory", "redis", or "sqlite"
worker_id = get_worker_id() # Unique per-process identifier
```

Deploy mode is active when any of these are true:
Expand Down
3 changes: 2 additions & 1 deletion pywry/docs/docs/guides/javascript-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ In Python, register a callback for the event:

```python
def on_save(data, event_type, label):
print(f"Saving ID {data['id']} from window {label}")
record_id = data["id"]
app.emit("pywry:set-content", {"id": "status", "text": f"Saved {record_id}"}, label)

handle = app.show(html, callbacks={"app:save": on_save})
```
Expand Down
Loading
Loading