# Technical Documentation: h2oai/wave

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/h2oai/wave)

### Overview (README.md)

## H2O Wave

<img src="assets/brand/wave-type-yellow.png" alt="Wave logo">

<p>
  <img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/h2o-wave?color=FBE52B">
  <img alt="License" src="https://img.shields.io/github/license/h2oai/wave?color=yellow">
  <img alt="PyPI" src="https://img.shields.io/pypi/v/h2o-wave?label=current-version">
  <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/h2o_wave">
  <a href="https://discord.gg/V8GZFAy3WM"><img src="https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord" alt="discord chat"></a>
  <img alt="Contributor covenant" src="https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg">
</p>

###

**New!** _[R Language API](https://wave.h2o.ai/blog/r-api-preview) Build and control Wave dashboards using R!_

H2O Wave is a software stack for building beautiful, low-latency, realtime, browser-based applications and dashboards entirely in Python/R without using HTML, Javascript, or CSS.

H2O Wave excels at capturing information from multiple sources and broadcasting them live over the web, letting you build and deploy realtime analytics with dramatically less effort.

- [Download](https://github.com/h2oai/wave/releases) / [How to install](https://wave.h2o.ai/docs/installation/)
- [Documentation](https://wave.h2o.ai/): [Getting Started](https://wave.h2o.ai/docs/getting-started/) / [Widgets](https://wave.h2o.ai/docs/widgets/overview) / [Gallery and Examples](https://wave.h2o.ai/docs/examples/) / [Python API](https://wave.h2o.ai/docs/api/index/)
- [Developing](https://github.com/h2oai/wave/blob/main/.github/CONTRIBUTING.md#development-setup)
- [Discuss / Support / Q&A / Ideas](https://github.com/h2oai/wave/discussions)
- [Report a problem](https://github.com/h2oai/wave/issues/new/choose)
- [Blog](https://wave.h2o.ai/blog/) / [@h2o_wave](https://twitter.com/h2o_wave)

### Live demos

- [Wave Tour](https://huggingface.co/spaces/h2oai/wave-tour) - 200+ interactive examples.
- [Wave University](https://huggingface.co/spaces/h2oai/wave-university) - Learn Wave basics interactively.
- [Wave Theme Generator](https://huggingface.co/spaces/h2oai/theme-generator) - Create your color theme.

### Responsive Layouts















Code: <https://github.com/h2oai/wave/tree/main/py/demo>

### Themes





(Coming soon!)



### 80+ components, 250+ examples

More at <https://wave.h2o.ai/docs/examples>





### License

H2O Wave is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for more information.

---

### Getting Started (`website/docs/getting-started.md`)

---
title: Introduction
---

H2O Wave lets you build and deploy amazing, realtime analytics with dramatically less effort.

## What is H2O Wave?

H2O Wave is a software stack for building beautiful, low-latency, realtime, browser-based applications and dashboards entirely in Python without using HTML, Javascript, or CSS.

It excels at capturing data, visualizations, and graphics from multiple sources and broadcasting them live over the web.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/getting-started__dashboard.mp4').default} type='video/mp4'/></video>

## What can I do with it?

H2O Wave gives your Python programs the ability to push content to connected clients as it happens, in realtime. In other words, it lets your program display up-to-date information without asking your users to hit their browser's reload button.

You can use H2O Wave for:

- Dashboards and visualizations for live monitoring.
- Live information displays: news, tickers, health, or performance data.
- Apps that require instant notifications, updates, events, or alerts.
- Apps that involve messaging: chat, bots, etc.
- Collaborative apps: whiteboards, sharing, etc.

You can also use H2O Wave when you find yourself reaching for a web application framework - it can handle regular (non-realtime) apps just fine.

## How do I use it?

H2O Wave's mental model is simple, but powerful[^1]:

1. Your instance holds a collection of pages.
2. To change a page, simply grab a reference to a page, change it, and save it.

That's it. Your changes are now visible to everyone.

This simplicity makes it fast, fun, and easy to rapidly build and deploy interactive applications without having to reason about client/server, HTTP request/response, browser quirks, or front-end development.

The API is concise and elegant, freeing you to create in broad strokes, tackling high level ideas first and polishing up the details later.

## Show me some code

Here's a bean counter. Clicking the button increments the bean count:

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/getting-started__beans.mp4').default} type='video/mp4'/></video>

And here's how it's written:

```py {9-10,17}
from h2o_wave import Q, main, app, ui

bean_count = 0

@app('/counter')
async def serve(q: Q):
    global bean_count
    # Was the 'increment' button clicked?
    if q.args.increment:
        bean_count += 1

    # Display a form on the page
    q.page['beans'] = ui.form_card(
        box='1 1 1 2',
        items=[
            ui.text_xl('Beans!'),
            ui.button(name='increment', label=f'{bean_count} beans'),
        ],
    )
    
    # Save the page
    await q.page.save()
```

## What's included?

The SDK ships batteries-included with a wide variety of user interface widgets and charts. You also get to use your favorite Python libraries seamlessly - anything that works in a Jupyter notebook works in H2O Wave - including Altair, Bokeh, H2O, Keras, Matplotlib, Plotly, PyTorch, Seaborn, TensorFlow, Vega-lite, and others. H2O Wave lets you use and broadcast results from all of these libraries, in realtime.

H2O Wave is not only a programming toolkit but a programmable content server as well: it can capture, retain, and relay information efficiently in realtime. The content server (or *Wave server*) is written in Go, a ~10MB static executable without runtime dependencies[^2]. It currently ships with a Python language driver, but is language-agnostic (an R language driver is under development).

The Wave server stores all the content and acts as a hub between your user's web browser and your apps. Therefore, it must be up and running before you launch Wave apps. Typically, you only need one Wave server to serve several apps.

```
                                      +---------+
                       +------------->+ app1.py |
                       |              +---------+
                       |
+---------+       +----+-----+        +---------+
| Browser +------>+  Server  +------->+ app2.py |
+---------+       +----+-----+        +---------+
                       |
                       |              +---------+
                       +------------->+ app3.py |
                                      +---------+

```

## In Summary

H2O Wave is rapid application development for a more... civilized age[^3].

Also, this page was mostly hyperbole, so let's download it and take it for a spin, shall we.

[^1]: The model parallels [retained mode](https://en.wikipedia.org/wiki/Retained_mode) graphics, with compositing performed on an remote server.

[^2]: Runs anywhere Go executables run, which is [almost everywhere](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63).

[^3]: Hat tip to [xkcd](https://xkcd.com/297/).

## Interactive learning

If you don't feel like going through the docs because you are more of a hands-on person, you can try our Wave University app.

Within your activated virtual environment, run:

```sh
wave learn
```

The command requires `h2o_wave_university` to be installed. To do that, run:

```sh
pip install h2o_wave_university
```

:::tip
Wave University is a standalone package that can be pip-installed and run independently. After installation, you will be able to launch it via `wave-university` command within your python environment.
:::

---

### Apps (`website/docs/apps.md`)

---
title: Wave Apps
---

A Wave app is the primary mechanism to publish interactive web content in Wave.

A Wave app can publish content and handle user interactions, unlike a [Wave script](scripts.md), which can publish content but not handle user interactions.

## Structure

Here is the skeleton of a Wave app:

```py title="app.py"
# Note: Main must be imported even though it is not used.
from h2o_wave import main, app, Q, ui

@app('/foo')
async def serve(q: Q):
    # Modify the page
    q.page['qux'] = ui.some_card()

    # Save the page
    await q.page.save()
```

An app typically imports four symbols from `h2o_wave`:

- `main`: An [ASGI](https://asgi.readthedocs.io/en/latest/)-compatible function. See [Deployment](deployment.md).
- `app`: A decorator for your query handler (or request handler).
- `Q`: A class that represents the query sent to your query handler.
- `ui`: The module containing the API for drawing UI elements.

`@app()` has one required argument - the route your app is interested in (in this case `/foo`). Whenever a user performs any action at `/foo` - access the page, reload it, click a button, access a menu, enter text, and so on - the query handler `serve()` is called. The details about what action was performed, and who  performed the action, are available in the argument passed to `serve()`, the *query context* `q` (of type [Q](api/server#q)).

Wave apps are run using the `wave run` command, which accepts the name of the Python module in which `main` is imported from `h2o_wave`.

:::warning
Do not remove imported `main` despite linters saying it's not used. It is used for marking the entrypoint file of your app.
:::

If your app is contained in `app.py`, run it like this:

```shell
wave run app
```

Or,

```shell
wave run app.py
```

If your app is contained in `path/to/app.py`, run it like this:

```shell
wave run path.to.app
```

Or,

```shell
wave run path/to/app.py
```

When run, the app starts an event loop listening for user interaction events, and announces itself to the Wave server. The Wave server then starts routing any user actions happening at `/foo` to your app.

## Runtime context

The query context `q` carries the following attributes:

- `route`: The route at which the action was performed (in this case, `/foo`).
- `page`: A reference to the current [page](pages.md) (in this case, the page hosted at `/foo`).
- `site`: A reference to the page's parent site, from which you can grab references to other pages.
- `args`: The *event arguments*, a dictionary-like object containing information about the user action.
- `app`, `user`, `client`: Dictionary-like objects holding [server-side state](state.md), at the app-level, the user-level and the client-level, respectively. Here, 'client' refers to the browser tab.
- `username`: The username of the user who performed the action.
- `mode`: The [app's realtime mode](realtime.md), one of `unicast`, `multicast`, or `broadcast`.

## Lifecycle

To perform actions at application startup and shutdown, pass `on_startup` and `on_shutdown` functions to `@app`, like this:

```py
from h2o_wave import main, app, Q, ui

def on_startup():
    print('App started!')

def on_shutdown():
    print('App stopped!')

@app('/foo', on_startup=on_startup, on_shutdown=on_shutdown)
async def serve(q: Q):
    pass
```

The `on_startup` and `on_shutdown` functions can also be `async`.

---

### Architecture (`website/docs/architecture.md`)

---
title: Architecture
---

H2O Wave is a software stack for building beautiful, low-latency, realtime, browser-based applications and dashboards entirely in Python without using HTML, Javascript or CSS.

It excels at capturing data, visualizations, and graphics from multiple sources, and broadcasting them live over the web.

The stack is engineered from the ground-up for low-latency, realtime information display, and is usable on its own (not only a programming framework, but a programmable content server).

## Overview

The Wave runtime operates over three tiers:

- **A content server.** The Wave server, a ~10MB static binary executable that runs anywhere[^1].
- **A language driver.** The [h2o-wave PyPI package](https://pypi.org/project/h2o-wave/) used by Wave scripts and apps.
- **A browser-based client.** The user interface and components.

```
+---------+                        +---------+
| Browser +---------+      +------>+ app1.py |
+---------+         |      |       +---------+
                    v      |
+---------+       +-+------+-+     +---------+
| Browser +------>+  Server  +---->+ app2.py |
+---------+       +-+------+-+     +---------+
                    ^      |
+---------+         |      |       +---------+
| Browser +---------+      +------>+ app3.py |
+---------+                        +---------+
```

The Wave server has three main functions:

- Store site content.
- Transmit content changes to browsers.
- Transmit browser events to apps.

In other words, the Wave server is comparable to an in-memory realtime database, an HTTP web server and a proxy server, all rolled into one, with browsers (*clients*) downstream, and Wave apps (or scripts) upstream.

The language driver (the [h2o-wave PyPI package](https://pypi.org/project/h2o-wave/)) provides the ability to manage content on the Wave server. It's similar in function to a database driver, but unlike typical database drivers (which use SQL as a protocol), the Wave driver provides an API closely integrated with the Python language that feels natural and idiomatic in practice.

The browser-based client's job is to render content on the user interface, and transmit user actions in the form of *events* back to the Wave server.

## How does it work?

The Wave server stores all content in a page cache called a *site*. A site is a collection of [pages](pages.md). Each page has an address, called its *route*. A page is composed of [cards](/docs/widgets/overview). A card holds content, and any tabular data associated with the content, called [data buffers](buffers.md).

When a browser is pointed to a route, it pulls a copy of the page, creates a *replica* locally, and renders the content on the user interface.

The language driver (the `h2o-wave` PyPI package) maintains an illusion that server-side content is available locally. Local updates to pages and cards are transmitted in the form of *operations* to the server. The server applies those updates to the master copy of the content. If any browser is currently displaying that content, the server forwards updates to the browser, causing the browser to update its replica and re-render its user interface.

```
   Python
+------------+
|  app.py    |
|     +      |
|     |      |
|     v      |
| +---+----+ |
+-+ Driver +-+
  +---+----+
      |
      |Operations
      |
      v
+-----+------+
|   Server   |
|            |
|  +------+  |
|  | Page |  |
|  +------+  |
|            |
+-----+------+
      |
      |Replication
      |
+-------------+
| +---------+ |
| | Replica | |
| +---------+ |
|             |
|     UI      |
+-------------+
    Browser
```

The language driver can be used by two kinds of Python programs: *Wave apps* and *Wave scripts*.

- [Wave apps](apps.md) are interactive programs that can update content and respond to user actions.
- [Wave scripts](scripts.md) are simpler, non-interactive (batch) programs: they can update content, but not respond to user actions.

Wave apps are ASGI servers under the hood. When a Wave app is launched, it announces its existence to the Wave server, and the Wave server establishes a *relay* with the Wave app. When a browser tries to connect to an app, the Wave server acts as a hub, relaying information back and forth between the browser and the app.

## How is it different?

The Wave server retains content. This is an important concept to understand, and the primary reason why Wave is different from a typical web framework. A Wave script can update content and exit, and the Wave server will happily continue serving that content. In other words, no Python process needs to be around if a new user arrives after you script has exited.

Different parts of the same page can be updated by different scripts running on different devices. Also, all content is live (or reactive) all the time: browsers always display up-to-date content without the need to reload.

[^1]: Linux, Windows, Darwin, BSD, Solaris, Android on amd64, arm, 386, ppc, mips; [almost everywhere](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63).

---

### Arguments (`website/docs/arguments.md`)

---
title: Query Arguments
---

When a user interacts with [components](/docs/widgets/overview) on a page - like typing in text, making choices, clicking buttons, and so on - that information is available to your app in the form of *query arguments*.

The query arguments can be read from `q.args`, a read-only dictionary-like object passed to your `@app()` handler:

```py {5-7}
from h2o_wave import Q, main, app

@app('/foo')
async def serve(q: Q):
    print(q.args.foo)
    print(q.args.bar)
    print(q.args.qux)
```

:::tip
`q.args` is an [Expando](api/core#Expando) instance, which means it behaves both like a dictionary and an object: `q.args['foo']` is the same as `q.args.foo`. `q.args.foo` is easier to read.
:::

## Interpreting arguments

The table below summarizes how to interpret inputs from various components.

| Component | If the component is named `foo`, the value of `q.args.foo` is... |
|---|---|
| `ui.button()` | `value` if provided, else `True`. |
| `ui.breadcrumb()` | `True` if clicked. |
| `ui.checkbox()` | `True` if checked, `False` if unchecked, `None` if indeterminate. |
| `ui.checklist()` | A list of names of all the selected choices (a list of strings). |
| `ui.choice_group()` | The name of the selected choice (a string). |
| `ui.color_picker()` | The selected color (a string). |
| `ui.combobox()` | Either the name of the selected choice or the value typed in (a string). |
| `ui.command()` | `value` if provided, else `True`. |
| `ui.date_picker()` | The selected date in `YYYY-MM-DD` format (a string). |
| `ui.dropdown()` | If multi-valued, a list of names of all the selected choices (a list of strings), otherwise the name of the selected choice (a string).  |
| `ui.expander()` | `True` if expanded, else `False`. |
| `ui.facepile()` | `value` if provided, else `True`. |
| `ui.file_upload()` | A list of paths to the uploaded files (a list of strings). |
| `ui.nav_item()` | `True` if clicked. |
| `ui.persona()` | `True` if clicked. |
| `ui.picker()` | A list of names of all the selected choices (a list of strings). |
| `ui.range_slider()` | `[min, max]` (a list of two numbers). |
| `ui.slider()` | The selected value (a number). |
| `ui.spinbox()` | The selected value (a number). |
| `ui.table()` | A list of names of all the selected rows (a list of strings). |
| `ui.tabs()` | The name of the active tab (a string). |
| `ui.textbox()` | The value typed in (a string). |
| `ui.text_annotator()` | List of lists containing parts of text with accompanying tag (if not annotated, tag is `''`). |
| `ui.toggle()` | `True` if checked, `False` if unchecked. |

## App-wide global loading spinner

Wave assumes that every user interaction (every time `q.args` or `q.events` is submitted to the app) needs to result in a UI update. The reason is proper UX - you should always inform your user what's going on, give them feedback. With this assumption, we try to make the UI unclickable (to prevent double submission) until server responds with the newest changes.

You should always handle:

* Button clicks.
* Actions that result in sending events (e.g. plots, dismissing dialogs).
* Every input into components having `trigger=True` set.

There might be very specific cases when you would want to get around this behavior. The solution is to update any card, even non-existent so simply specifying `q.page['non-existent'].items = []` will result in loading spinner going away. However, you should almost always default to proper UX, thus update the UI properly and inform your user about what's currently happening within the app.

## Handling interactivity

A common pattern for inspecting query arguments and determining the appropriate response is a simple `if/elif/else` conditional.

If `add_to_cart`, `empty_cart`, `place_order` and `display_products` are all names of buttons somewhere in the user interface, then you can structure your `serve()` function something like this:

```py {17-24}
from h2o_wave import Q, main, app

async def add_to_cart(q: Q):
    pass

async def empty_cart(q: Q):
    pass

async def place_order(q: Q):
    pass

async def display_products(q: Q):
    pass

@app('/hole_foods')
async def serve(q: Q):
    if q.args.add_to_cart:
        await add_to_cart(q)
    elif q.args.empty_cart:
        await empty_cart(q)
    elif q.args.place_order:
        await place_order(q)
    else: # Display products
        await display_products(q)

```

If this feels too repetitive, you can use `on` and `run_on` to remove some of the boilerplate:

```py {3,7,11,15,21}
from h2o_wave import Q, main, app, on, run_on

@on('add_to_cart')
async def add_to_cart(q: Q):
    pass

@on('empty_cart')
async def empty_cart(q: Q):
    pass

@on('place_order')
async def place_order(q: Q):
    pass

@on('display_products')
async def display_products(q: Q):
    pass

@app('/hole_foods')
async def serve(q: Q):
    await run_on(q)

```

In the above example, the `@on('add_to_cart')` is read as "when `q.arg['add_to_cart']` is `True` (or truthy), then invoke the function the `@on()` is applied to" - in this case, `add_to_cart()`.

If the name of the function is the same as the name of the query argument, then the name can be elided. This simplifies the above example to:

```py {3,7,11,15}
from h2o_wave import Q, main, app, on, run_on

@on()
async def add_to_cart(q: Q):
    pass

@on()
async def empty_cart(q: Q):
    pass

@on()
async def place_order(q: Q):
    pass

@on()
async def display_products(q: Q):
    pass

@app('/hole_foods')
async def serve(q: Q):
    await run_on(q)

```

:::tip
`@on()` also supports pattern matching. See [Routing](routing.md) for more information.
:::

---

### Background (`website/docs/background.md`)

---
title: Background Tasks
---

Wave apps are servers based on [asyncio](https://docs.python.org/3/library/asyncio.html), Python's library for Asynchronous I/O, and lets you develop and deploy high-performance applications.

Your `@app` query handler is invoked every time a user performs some action in your app's UI - access the page, reload it, click a button, access a menu, enter text, and so on. Performing blocking operations in your handler will hang your app's server and make your app's UI appear unresponsive until the blocking operation completes.

In some cases, blocking calls can be pushed to the background to improve concurrency. To achieve this, the Wave API provides two lightweight wrappers over [asyncio.run_in_executor()](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor): `q.run()` and `q.exec()`.

Here is an example of a function that blocks:

```python {1,4}
import time

def blocking_function(seconds) -> str:
    time.sleep(seconds)  # Blocks!
    return f'Done!'
```

To call the above function from an app, don't do this:

```python {4}
@app('/demo')
async def serve(q: Q):
    # ...
    message = blocking_function(42)
    # ...
```

Instead, do this:

```python {4}
@app('/demo')
async def serve(q: Q):
    # ...
    message = await q.run(blocking_function, 42)
    # ...
```

`q.run()` runs the blocking function in the background, in-process.

Depending on your use case, you might want to use a separate process pool or thread pool from Python's [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) library, like this:

```python {1,6-7}
import concurrent.futures

@app('/demo')
async def serve(q: Q):
    # ...
    with concurrent.futures.ThreadPoolExecutor() as pool:
        message = await q.exec(pool, blocking_function, 42)
    # ...
```

`q.exec()` accepts a custom process pool or thread pool to run the blocking function.

```python {1,6-7}
import concurrent.futures

@app('/demo')
async def serve(q: Q):
    # ...
    with concurrent.futures.ProcessPoolExecutor() as pool:
        message = await q.exec(pool, blocking_function, seconds)
    # ...
```

:::tip
Apps that make calls to external services or APIs are better off replacing blocking HTTP clients like [requests](https://requests.readthedocs.io/en/master/) with non-blocking clients like [HTTPX](https://www.python-httpx.org/async/).
:::

To update UI from within a background job, see [this example](/docs/examples/background-progress/) or a detailed [blog post](https://medium.com/@unusualcode/background-jobs-in-wave-or-how-not-to-kill-your-ui-ae1fed95693a).

---

### Backup (`website/docs/backup.md`)

---
title: Backup and Recovery
---

:::caution
This feature is experimental. Do not rely on this in production!
:::

The Wave server logs all content changes to `stderr`. The changes are written in a format that can be read back in. This means that you can replay the log from beginning to end to recover the server's state (content, pages, everything). The log is, literally, a change log.

To capture the log, redirect `stderr` to a file when you launch the server:

```shell
./waved 2> backup.log
```

To recover state, feed the log file back in the next time you launch the server:

```shell 
./waved -init backup.log
```

To recover state and continue capturing the log, use:

```shell 
./waved -init backup.log 2> other.log
```

If you end up with a big log file, you can compact it like this:

```shell 
./waved -compact big.log 2> small.log
```

---

### Browser Testing (`website/docs/browser-testing.md`)

---
title: Browser Testing
---


:::caution Deprecated
Wave native testing tools are no longer supported. Use modern alternatives like [Playwright](https://playwright.dev/python/).
:::

Wave supports authoring functional tests in Python for the [Cypress](https://www.cypress.io/) test framework. This feature lets you automate browser-based point-and-click tests for your app.

## Installation

#### Step 1: Install Node.js

Install a recent version of [Node.js](https://nodejs.org/en/).

#### Step 2: Set up Cypress

Using your terminal, go to your Wave installation's `test` directory and install Cypress

```
cd $HOME/wave/test
npm install
```

## Writing a test

See the [Wizard](#wizard) example to understand how to author tests for your interactive app. Specifically, note how the `@cypress` attribute is used. Refer to the [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html) to learn how to author assertions.

```py
from h2o_wave import cypress

@cypress('Walk through the wizard')
def test_wizard(cy):
    cy.visit('/demo')
    cy.locate('step1').click()
    cy.locate('text').should('contain.text', 'What is your name?')
    cy.locate('nickname').clear().type('Fred')
    cy.locate('step2').click()
    cy.locate('text').should('contain.text', 'Hi Fred! How do you feel right now?')
    cy.locate('feeling').clear().type('quirky')
    cy.locate('step3').click()
    cy.locate('text').should('contain.text', 'What a coincidence, Fred! I feel quirky too!')

```

:::tip
To escape Cypress function names that overlap with Python's reserved keywords, prefix the name with an underscore `_`. For example, use `cy._as()` instead of `cy.as()`.
:::

## Running your test

#### Step 1: Start the Cypress test runner

```
cd $HOME/wave/test
./node_modules/.bin/cypress open
```

#### Step 2: Start the Wave server and your app as usual

```
./waved
```

```
(venv) $ wave run --no-reload examples.wizard
```

#### Step 3: Translate your Python tests to Javascript

To translate your Python tests to Javascript, execute the Python module or file containing your tests like this:

```
CYPRESS_INTEGRATION_TEST_DIR=$HOME/wave/test/cypress/integration ./venv/bin/python examples/wizard.py
```

The `CYPRESS_INTEGRATION_TEST_DIR` environment variable indicates where the Wave SDK should write translated files to. This must be set to the `cypress/integration` directory.

Alternatively, you can set the `CYPRESS_INTEGRATION_TEST_DIR` environment variable in your shell (or IDE) to simplify running your test file:

```
export CYPRESS_INTEGRATION_TEST_DIR=$HOME/wave/test/cypress/integration
./venv/bin/python examples/wizard.py
```

#### Step 4: Run your tests

At this point, you should find all your tests displayed in the Cypress UI. Simply click on a test to run it. Happy testing!

---

### Buffers (`website/docs/buffers.md`)

---
title: Data Buffers
---

*Data buffers* are in-memory data structures designed to hold a card's tabular data.

Data buffers make it convenient to separate data (what is displayed) from presentation (how it is displayed). You can declare a card once, and update its underlying data buffer multiple times. A card can hold zero or more data buffers.

## Cards and buffers

Data buffers are tabular data structures containing columns and rows. Different cards utilize data buffers in different ways. For example, the plot in the [ui.small_series_stat_card()](api/ui#small_series_stat_card) uses a data buffer called `plot_data` to hold time series data.



```py {5-7}
card = ui.small_series_stat_card(
    box=f'1 1 1 1',
    title='CPU',
    value='10%',
    plot_data=data('time usage', -15),
    plot_category='time',
    plot_value='usage',
))
```

In the above snippet, `data('time usage', -15)` defines a placeholder for a table with two columns (`time` and `usage`) and 15 rows (we'll get to why the `15` is negative shortly), and the card's `plot_category` and `plot_value` point to the two columns `time` and `usage`, respectively.

Appending *rows* (*tuples* or *records*) to the data buffer make the card plot those rows.

```py {2}
while True:
    card.plot_data[-1] = [get_time(), get_usage()]
    time.sleep(1)
```

The `while` loop above does something like this:

```py
card.plot_data[-1] = ['2020-10-05T02:10:20Z', 42.5]
card.plot_data[-1] = ['2020-10-05T02:10:21Z', 43.1]
card.plot_data[-1] = ['2020-10-05T02:10:22Z', 39.2]
card.plot_data[-1] = ['2020-10-05T02:10:23Z', 38.7]
```

## Types of buffers

There are three types of data buffers:

- **Array buffers** hold tabular data with a fixed number of rows, and allow random access to rows using 0-based integers as keys.
- **Cyclic buffers** also hold tabular data with a fixed number of rows, but do not allow random access to rows. They can only be appended to. Rows are first-in first-out (FIFO), and adding rows beyond its capacity overwrites the oldest rows.
- **Map buffers** (or dictionary buffers) hold tabular data with a variable number of rows, and allow random access to rows using string keys.
- **List buffers** (or dynamic array buffers) hold tabular data with a variable number of rows, allow random access to rows using 0-based integers as keys (negative indexing supported), and allow appending via `+=` operator.

:::caution
Map buffers are convenient to use, but use more memory on the server compared to array buffers and cyclic buffers, so use them sparingly.
:::

## The data function

Use the `data()` function to declare a data buffer. The Wave server uses this declaration to allocate memory for the data buffer.

`data()` takes multiple arguments:

- `fields`: The names of columns, in order; a space-separated string or a list or a tuple (`'foo bar'` or `['foo', 'bar']` or `('foo', 'bar')`.
- `size`: The number of rows to allocate.
  - A positive row count creates an array buffer.
  - A negative row count creates a cyclic buffer.
  - A zero row count (or `None`) creates a map buffer.
- `rows`: A `dict` or `list` of rows to initialize the data buffer with. Each row can be a list or tuple.
  - For array or cyclic buffers, pass a list of rows.
  - For map buffers, pass a dict with strings as keys and rows as values.
- `columns`: A `list` of columns to initialize the data buffer with. All columns must be of the same length. The columns are automatically transposed to `rows`.
- `pack`: `True` to pack (compress) the provided rows or columns, use less memory on the server-side, and improve performance. Set `pack=True` if you intend to never make any changes to the data buffer once created. Defaults to `False`.
- `t`: Buffer type. One of 'list', 'map', 'cyclic' or 'fixed'. Overrides the buffer type inferred from the size.

## Buffers in practice

Declare a 5-row buffer with columns `donut` and `price`.

```py
# Array buffer
b = data(fields='donut price', size=5)

# Cyclic buffer
b = data(fields='donut price', size=-5)

# Map buffer
b = data(fields='donut price')

# List buffer
b = data(fields='donut price', t='list')
```

Declare and initialize a 5-row buffer with columns `donut` and `price`.

```py
# Array buffer
data(fields='donut price', size=5, rows=[
    ['cream', 3.99],
    ['custard', 2.99],
    ['cinnamon', 2.49],
    ['sprinkles', 2.49],
    ['sugar', 1.99],
])

# Cyclic buffer
data(fields='donut price', size=-5, rows=[
    ['cream', 3.99],
    ['custard', 2.99],
    ['cinnamon', 2.49],
    ['sprinkles', 2.49],
    ['sugar', 1.99],
])

# Map buffer
data(fields='donut price', rows=dict(
    crm=['cream', 3.99],
    cst=['custard', 2.99],
    cin=['cinnamon', 2.49],
    spr=['sprinkles', 2.49],
    sug=['sugar', 1.99],
))

# List buffer
data(fields='donut price', t='list', rows=[ 
    ['cream', 3.99],
    ['custard', 2.99],
    ['cinnamon', 2.49],
    ['sprinkles', 2.49],
    ['sugar', 1.99],
 ])
```

A buffer can only be modified via card. To get a reference to the buffer:

```py
page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Line',
    data=data(fields='donut price', size=5, rows=[
        ['cream', 3.99],
        ['custard', 2.99],
        ['cinnamon', 2.49],
        ['sprinkles', 2.49],
        ['sugar', 1.99],
    ])
    plot=ui.plot([ui.mark(type='line', x_scale='time', x='=year', y='=value', y_min=0)])
)
b = page['example'].data
```

Modify a buffer row:

```py
# Array buffer
b[2] = ['cinnamon', 2.99]

# Cyclic buffer
# Appends to the end, shifts all elements by 1,
# dropping the first one as it would be the 6th element.
b[-1] = ['fruit', 2.99]

# Map buffer (the following two forms are equivalent)
b['cin'] = ['cinnamon', 2.99]
b.cin = ['cinnamon', 2.99]

# List buffer
b[2] += ['fruit', 2.99]
b[-2] += ['fruit', 2.99] # Modify second index from the end.
```

Modify a buffer value:

```py
# Array and list buffer (the following two forms are equivalent).
b[2]['price'] = 2.99
b[2].price = 2.99

# Map buffer (the following three forms are equivalent).
b['cin']['price'] = 2.99
b['cin'].price = 2.99
b.cin.price = 2.99

# List buffer (the following two forms are equivalent)
b[2]['price'] = 2.99 # third donut on menu now costs 2.99
b[2].price = 2.99

b[-1]['price'] = 2.99 # last donut from end on menu now costs 2.99
b[-1].price = 2.99
```

## Packed buffers

The following is true for Wave scripts only. Unicast apps do not store data buffers on server unless started with `-editable` option.

If you intend to create tabular data once and never change individual rows or values, it is better to avoid allocating memory on the server by using a *packed buffer*. Packed buffers use less memory on the server and improve performance. To create a packed buffer, use `data(..., pack=True)`. Note that `size` is not required, and is ignored if provided.

```py
b = data(fields='donut price', rows=[
    ['cream', 3.99],
    ['custard', 2.99],
    ['cinnamon', 2.49],
    ['sprinkles', 2.49],
    ['sugar', 1.99],
], pack=True)
```

```py
b = data(fields='donut price', columns=[
    ['cream', 'custard', 'cinnamon', 'sprinkles', 'sugar'],
    [3.99 , 2.99, 2.49, 2.49, 1.99],
], pack=True)
```

---

### Cli (`website/docs/cli.md`)

---
title: Command Line Interface (CLI)
---

The CLI is accessible using the `wave` command, available once you `pip install h2o-wave`.

To view a list of sub-commands, simply run `wave`:

```sh
wave
```

```sh
Usage: wave [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  fetch  Download examples and related files to ./wave.
  init   Initial scaffolding for your Wave project.
  learn  Run interactive learning app - Wave university.
  run    Run an app.
  share  Share your locally running app with the world.
```

Get help on a sub-command:

```sh
wave <command> --help
```

## wave run

`wave run` runs an app.

Run app.py with auto reload:

```sh
wave run app
```

Run path/to/app.py with auto reload:

```sh
wave run path.to.app
```

Run path/to/app.py without auto reload:

```sh
wave run --no-reload path.to.app
```

Do not start `waved` automatically:

```sh
wave run app --no-autostart
```

## wave init

Pick a starter app template

## wave fetch

Download examples and `waved`.

```sh
wave fetch
```

```sh
Usage: wave fetch [OPTIONS]

  Download examples and related files to ./wave folder.

  $ wave fetch

Options:
  --platform [linux|windows|darwin] Operating system type.
  --arch [amd64|arm64]              Processor architecture type.
  --help                            Show this message and exit.
```

## wave learn

Run interactive learning app - Wave University. (Requires `h2o-wave-university` package to be installed)

```sh
wave learn
```

## wave share

Expose locally running Wave app to the public internet. Stop sharing by hitting `CTRL+C`.

```sh
wave share
```

```sh
Usage: wave share [OPTIONS]

  Share your locally running app with the world.

  $ wave share

Options:
  --port INTEGER         Port your app is running on (defaults to 10101).
  --subdomain TEXT       Subdomain to use. If not available, a random one is generated.
  --remote-host TEXT     Remote host to use (defaults to h2oai.app).
  --remote-port INTEGER  Remote port to use (defaults to 443).
  --open                 Open the shared app in your browser automatically.
  --help                 Show this message and exit.
```

---

### Color Theming (`website/docs/color-theming.md`)

---
title: Colors
---

The general idea of Wave is that (almost) all the colors are handled for you meaning you do not have much control over the individual colors for components usually. If you want to tweak all the colors at once, you can reach out for themes.

## Themes

Wave ships with prebuilt themes that are ready to be used. These include:

* `default`
* `neon`
* `h2o-dark`
* `benext`
* `ember`
* `fuchasia`
* `kiwi`
* `lighting`
* `monokai`
* `nature`
* `nord`
* `oceanic`
* `one-dark-pro`
* `solarized`
* `winter-is-coming`

Setting a theme is very simple:

```py
q.page['meta'] = ui.meta_card(box='', theme='h2o-dark')
```

### Custom user-defined themes

Having only predefined color palettes keeps you from matching your Wave app to your specific color brand, so we have added custom, user-defined palettes.

When using custom colors, first a new `ui.theme` needs to be registered within the `themes` attribute in `ui.meta_card`. It takes the name of the theme and 4 main colors as arguments:

* `primary` - Accent color for primary buttons, checkboxes etc.
* `text` - Default text color.
* `card` - Card background color.
* `page` - Base background color behind cards.

After the theme is successfully registered, you can use it in the same fashion as predefined themes, by setting `ui.meta_card`'s `theme` attribute.

```py
q.page['meta'] = ui.meta_card(
    box='',
    themes=[
        ui.theme(
            name='my-awesome-theme',
            primary='#13ebe7',
            text='#e8e1e1',
            card='#12123b',
            page='#070b1a',
        )
    ],
    theme='my-awesome-theme'
)
```

:::tip
You can register multiple themes, which might be handy for switching between the light / dark mode of your app.
:::

### Theme generator

Wave comes with a simple [Theme generator app](https://huggingface.co/spaces/h2oai/theme-generator) that allows you to pick colors and see how they work together instantly.

This Wave app provides:

* Immediate visual feedback.
* Color contrast warnings when a particular contrast ratio is less than [4.5](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).
* Quick theme copy-pasting.



We would love to see all the beautiful themes our community came up with so don't hesitate and post the screenshots into our [show and tell](https://github.com/h2oai/wave/discussions/categories/show-and-tell) Github Discussions section! The best ones might even be included as predefined themes within Wave itself in the future.

## Can individual colors be changed?

In general, the answer is no. Wave tries to be smart and do all the minor decisions for you, e.g. picking a color for primary/secondary buttons, checkboxes, etc. This way, a consistent UX is ensured and you can focus solely on the product itself rather than wasting time on nitpicking small stuff.

If full control over colors/typography etc. is needed, going with regular HTML/CSS/JS is probably a better choice.

However, some cards allow changing colors of particular parts, for example, icons or plots, but this is more of an exception rather than a strict rule. You can find available colors in the section below.

## Wave colors

In addition to arbitrary CSS colors, you can use the colors that Wave uses itself.

* Spectrum colors are the most colors common colors with a slightly edited saturation so that they match the current theme.
* Wave's most used colors (text, card and primary) and their variations with increasing transparency (tone).

| **Spectrum colors** | **Text tones** | **Card tones** | **Primary color tones** |
|:-------------------:|:--------------:|:--------------:|:------------------------:|
|        $amber       |      $text     |      $card     |         $primary         |
|        $azure       |     $text0     |     $card0     |         $primary0        |
|        $blue        |     $text1     |     $card1     |         $primary1        |
|        $brown       |     $text2     |     $card2     |         $primary2        |
|        $cyan        |     $text3     |     $card3     |         $primary3        |
|        $gray        |     $text4     |     $card4     |         $primary4        |
|        $green       |     $text5     |     $card5     |         $primary5        |
|       $indigo       |     $text6     |     $card6     |         $primary6        |
|        $lime        |     $text7     |     $card7     |         $primary7        |
|        $mint        |     $text8     |     $card8     |         $primary8        |
|       $orange       |     $text9     |     $card9     |         $primary9        |
|        $page        |                |                |                          |
|        $pink        |                |                |                          |
|       $purple       |                |                |                          |
|         $red        |                |                |                          |
|      $tangerine     |                |                |                          |
|        $teal        |                |                |                          |
|       $violet       |                |                |                          |
|       $yellow       |                |                |                          |

:::tip
All the above colors are just CSS variables with a `$` prefix replaced with `wave-` which means they can be also used within your custom CSS. E.g. red color would be `--wave-red`.
:::

---

### Configuration (`website/docs/configuration.md`)

---
title: Configuration
---

## Configuring the server

Wave allows starting Wave server in 2 ways:

- via `wave run` command - this automatically starts Wave server (waved) under the hood, useful for development.
- via Wave server binary (waved) - useful during deployment or when you need to run your Wave server on a different machine than your app.

Wave can be configured via configuration (`.env`) file, environment variables or command line arguments with the following priority: `cmd arg > env var > config > default`.




| ENV var or config (wave run or waved)  | CLI args (waved)                      | Description                                                                                                                                                                                                                                                                                                          |
| -------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| H2O_WAVE_ACCESS_KEY_ID                 | -access-key-id string                 | default API access key ID (default "access_key_id")                                                                                                                                                                                                                                                                  |
| H2O_WAVE_ACCESS_KEY_SECRET             | -access-key-secret string             | default API access key secret (default "access_key_secret")                                                                                                                                                                                                                                                          |
| H2O_WAVE_ACCESS_KEYCHAIN               | -access-keychain string               | path to file containing API access keys (default ".wave-keychain")                                                                                                                                                                                                                                                   |
|                                        | -compact string                       | compact AOF log                                                                                                                                                                                                                                                                                                      |
|                                        | -create-access-key                    | generate and add a new API access key ID and secret pair to the keychain                                                                                                                                                                                                                                             |
| H2O_WAVE_DATA_DIR                      | -data-dir string                      | directory to store site data (default "./data").                                                                                                                                                                                                                                                                     |
| H2O_WAVE_DEBUG [^1]                    | -debug                                | enable debug mode (profiling, inspection, etc.)                                                                                                                                                                                                                                                                      |
| H2O_WAVE_EDITABLE [^1]                 | -editable                             | allow users to edit web pages                                                                                                                                                                                                                                                                                        |
| H2O_WAVE_FORWARDED_HTTP_HEADERS        | -forwarded-http-headers string        | comma-separated list of case-insensitive HTTP header keys to forward to the Wave app from the browser WS connection. If not specified, defaults to '\*' - all headers are allowed. If set to an empty string, no headers are forwarded.                                                                              |
| H2O_WAVE_HTTP_HEADERS_FILE             | -http-headers-file string             | path to a MIME-formatted file containing additional HTTP headers to add to responses from the server                                                                                                                                                                                                                 |
| H2O_WAVE_INIT                          | -init string                          | initialize site content from AOF log                                                                                                                                                                                                                                                                                 |
| H2O_WAVE_LISTEN                        | -listen string                        | listen on this address (default ":10101")                                                                                                                                                                                                                                                                            |
|                                        | -list-access-keys                     | list all the access key IDs in the keychain                                                                                                                                                                                                                                                                          |
| H2O_WAVE_BASE_URL                      | -base-url string                      | the base URL (path prefix) to be used for resolving relative URLs (e.g. /foo/ or /foo/bar/, without the host (default "/")                                                                                                                                                                                           |
| H2O_WAVE_MAX_CACHE_REQUEST_SIZE        | -max-cache-request-size string        | maximum allowed size of HTTP requests to the server cache (e.g. 5M or 5MB or 5MiB) (default "5M")                                                                                                                                                                                                                    |
| H2O_WAVE_MAX_PROXY_REQUEST_SIZE        | -max-proxy-request-size string        | maximum allowed size of proxied HTTP requests (e.g. 5M or 5MB or 5MiB) (default "5M")                                                                                                                                                                                                                                |
| H2O_WAVE_MAX_PROXY_RESPONSE_SIZE       | -max-proxy-response-size string       | maximum allowed size of proxied HTTP responses (e.g. 5M or 5MB or 5MiB) (default "5M")                                                                                                                                                                                                                               |
| H2O_WAVE_MAX_REQUEST_SIZE              | -max-request-size string              | maximum allowed size of HTTP requests to the server (e.g. 5M or 5MB or 5MiB) (default "5M")                                                                                                                                                                                                                          |
| H2O_WAVE_NO_STORE [^1]                 | -no-store                             | disable storage (scripts and multicast/broadcast apps will not work)                                                                                                                                                                                                                                                 |
| H2O_WAVE_NO_LOG [^1]                   | -no-log                               | disable AOF logging (connect/disconnect and diagnostic logging messages are not disabled)                                                                                                                                                                                                                            |
| H2O_WAVE_OIDC_AUTH_URL_PARAMS          | -oidc-auth-url-params string          | additional URL parameters to pass during OIDC authorization, in the format "key:value", comma-separated, e.g. "foo:bar,qux:42"                                                                                                                                                                                       |
| H2O_WAVE_OIDC_CLIENT_ID                | -oidc-client-id string                | OIDC client ID                                                                                                                                                                                                                                                                                                       |
| H2O_WAVE_OIDC_CLIENT_SECRET            | -oidc-client-secret string            | OIDC client secret                                                                                                                                                                                                                                                                                                   |
| H2O_WAVE_OIDC_END_SESSION_URL          | -oidc-end-session-url string          | OIDC end session URL                                                                                                                                                                                                                                                                                                 |
| H2O_WAVE_OIDC_PROVIDER_URL             | -oidc-provider-url string             | OIDC provider URL                                                                                                                                                                                                                                                                                                    |
| H2O_WAVE_OIDC_REDIRECT_URL             | -oidc-redirect-url string             | OIDC redirect URL                                                                                                                                                                                                                                                                                                    |
| H2O_WAVE_OIDC_POST_LOGOUT_REDIRECT_URL | -oidc-post-logout-redirect-url string | OIDC post logout redirect URL                                                                                                                                                                                                                                                                                        |
| H2O_WAVE_OIDC_SCOPES                   | -oidc-scopes                          | OIDC scopes separated by comma (default "openid,profile")                                                                                                                                                                                                                                                            |
| H2O_WAVE_OIDC_SKIP_LOGIN [^1]          | -oidc-skip-login                      | don't show the built -in login form during OIDC authorization                                                                                                                                                                                                                                                        |
| H2O_WAVE_PRIVATE_DIR [^2]              | -private-dir value                    | additional directory to serve files from (authenticated users only), in the format "[url-path]@[filesystem-path]", e.g. "/public/files/@/some/local/path" will host /some/local/path/foo.txt at /public/files/foo.txt; multiple directory mappings allowed; paths need to be relative to Wave server binary location |
| H2O_WAVE_PUBLIC_DIR [^2]               | -public-dir value                     | additional directory to serve files from, in the format "[url-path]@[filesystem-path]", e.g. "/public/files/@/some/local/path" will host /some/local/path/foo.txt at /public/files/foo.txt; multiple directory mappings allowed; paths need to be relative to Wave server binary location                            |
| H2O_WAVE_PROXY [^1]                    | -proxy                                | enable HTTP proxy (for IDE / language server support only - not recommended for internet-facing websites)                                                                                                                                                                                                            |
|                                        | -remove-access-key string             | remove the specified API access key ID from the keychain                                                                                                                                                                                                                                                             |
| H2O_WAVE_SESSION_EXPIRY                | -session-expiry string                | session cookie lifetime duration (e.g. 1800s or 30m or 0.5h) (default "720h")                                                                                                                                                                                                                                        |
| H2O_WAVE_SESSION_INACTIVITY_TIMEOUT    | -session-inactivity-timeout string    | session inactivity timeout duration (e.g. 1800s or 30m or 0.5h) (default "30m")                                                                                                                                                                                                                                      |
| H2O_WAVE_TLS_CERT_FILE                 | -tls-cert-file string                 | path to certificate file (TLS only)                                                                                                                                                                                                                                                                                  |
| H2O_WAVE_TLS_KEY_FILE                  | -tls-key-file string                  | path to private key file (TLS only)                                                                                                                                                                                                                                                                                  |
| H2O_WAVE_NO_TLS_VERIFY [^1]            | -no-tls-verify                        | do not verify TLS certificates during external communication - DO NOT USE IN PRODUCTION                                                                                                                                                                                                                              |
|                                        | -version                              | print version and exit                                                                                                                                                                                                                                                                                               |
| H2O_WAVE_KEEP_APP_LIVE [^1]            | -keep-app-live                        | do not unregister unresponsive apps (default false)                                                                                                                                                                                                                                                                  |
| H2O_WAVE_WEB_DIR                       | -web-dir string                       | directory to serve web assets from (default "./www")                                                                                                                                                                                                                                                                 |
| H2O_WAVE_CONF                          | -conf string                          | path to a configuration file (default ".env")                                                                                                                                                                                                                                                                        |
| H2O_WAVE_PING_INTERVAL                 | -ping-interval string                 | how often should ping messages be sent (e.g. 60s or 1m or 0.1h) to keep the websocket connection alive (default "50s")                                                                                                                                                                                               |
| H2O_WAVE_RECONNECT_TIMEOUT             | -reconnect-timeout string             | Time to wait for reconnect before dropping the client (default "2s")                                                                                                                                                                                                                                                 |
| H2O_WAVE_ALLOWED_ORIGINS               | -allowed-origins string               | comma-separated list of allowed origins (e.g. http://foo.com) for websocket upgrades, use `*` to allow all origins                                                                                                                                                                                                   |

[^1]: `1`, `t`, `true` to enable; `0`, `f`, `false` to disable (case insensitive).

[^2]: Use OS-specific path list separator to specify multiple arguments - `:` for Linux/OSX and `;` for Windows. For example, `H2O_WAVE_PUBLIC_DIR=/images/@./files/images:/downloads/@./files/downloads`.

### File paths

All the configuration options that expect a path as value (public/private dir, data dir etc.) need the path provided to be either absolute or relative to a directory where `waved` is stored.

Those that do not start `waved` manually (but use `wave run` instead) and would like to use relative paths need to know that `waved` is stored in their 'virtualenv' (venv) folder (when installed via `pip`). If the `venv` folder is within the project root, then all the paths relative to your project dir need to be prepended with `../` (up one directory).

### Supported size units (case insensitive)

- Exabyte: `E` / `EB` / `EIB`.
- Petabyte: `P` / `PB` / `PIB`.
- Terabyte: `T` / `TB` / `TIB`.
- Gigabyte: `G` / `GB` / `GIB`.
- Megabyte: `M` / `MB` / `MIB`.
- Kilobyte: `K` / `KB` / `KIB`.
- Byte: `B`

### Suported time units

- Nanosecond: `ns`.
- Microsecond: `us` (or `µs`).
- Millisecond `ms`.
- Second: `s`.
- Minute: `m`.
- Hour: `h`.

### Public/Private dirs

When the wave server (waved) is started as part of the `wave run` command, the waved binary is located in the root of the currently active python virtual environment. Local paths need to be relative to this location. Another quick trick to find the waved location is to check `web-dir` during app startup which is located in the same directory as waved itself.

```sh {10}
wave run app --no-reload
2023/09/12 14:24:35 #
2023/09/12 14:24:35 # ┌────────────────┐ H2O Wave
2023/09/12 14:24:35 # │  ┐┌┐┐┌─┐┌ ┌┌─┐ │ 0.26.2 20230803101411
2023/09/12 14:24:35 # │  └┘└┘└─└└─┘└── │ © 2021 H2O.ai, Inc.
2023/09/12 14:24:35 # └────────────────┘
2023/09/12 14:24:35 # ┌──────────────────────────────────────┐
2023/09/12 14:24:35 # │  Running at http://localhost:10101/  │
2023/09/12 14:24:35 # └──────────────────────────────────────┘
2023/09/12 14:24:35 # {"address":":10101","base-url":"/","t":"listen","web-dir":"/path/to/venv/www"}
INFO:     Started server process [10429]
INFO:     Waiting for application startup.
2023/09/12 14:24:36 # {"host":"http://127.0.0.1:8000","route":"/","t":"app_add"}
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```

From the above, the root of the active virtual python environment is `/path/to/venv`. Another, more straightforward, option is to use absolute paths.

Wave serves whole directories as they are. This means that these directories are listable by default. If you wish to turn off this behavior, simply put an empty file called `index.html` into the folder you wish to not list.

### TLS verification

During development, you might want to test out TLS encryption, e.g. communication between Wave server and Keycloak. The easiest thing to do is to generate a self-signed certificate. However, Wave server verifies certificates for all communication by default, thus would throw an error for a self-signed one. **_FOR DEVELOPMENT PURPOSES ONLY_**, it's possible to turn off the check using either `H2O_WAVE_NO_TLS_VERIFY` environment variable or `no-tls-verify` parameter.

:::warning
**Disabling TLS verification is a security risk.** Make sure TLS is not disabled in production environments.
:::

## Configuring your app

Your Wave application is an ASGI server. When you run your app during development, the app server runs at <http://127.0.0.1:8000/> by default (localhost, port 8000), and assumes that your Wave server is running at <http://127.0.0.1:10101/> (localhost, port 10101). The `wave run` command automatically picks another available port if `8000` is not available.

The Wave server and apps communicate with each other using RPC over persistent HTTP connections.

For production deployments, you'll want to configure which port your app listens to, how it can access the Wave server, and how the Wave server can access your app.

You can use the following environment variables to configure your app's server's behavior:

### H2O_WAVE_APP_ADDRESS

The public host/port of the app server. Defaults to `http://127.0.0.1:8000`. Set this variable if you are running your Wave server and your app on different machines or containers.

### H2O_WAVE_APP_MODE

The [realtime sync mode](realtime.md) of the app server. One of `unicast` (default), `multicast`, or `broadcast`.

### H2O_WAVE_ADDRESS

The public host/port of the Wave server. Defaults to `http://127.0.0.1:10101`. Set this variable if you are running the Wave server on a remote machine or container. Another common usage for this var is changing the app port together with `H2O_WAVE_LISTEN`.

### H2O_WAVE_CONNECTION_TIMEOUT

The number of seconds to attempt to connect to the Wave server before giving up.

### H2O_WAVE_ACCESS_KEY_ID

The API access key ID to use when communicating with the Wave server.

### H2O_WAVE_ACCESS_KEY_SECRET

The API access key secret to use when communicating with the Wave server.

### H2O_WAVE_APP_ACCESS_KEY_ID

The API access key ID to use when communicating with the app server. Automatically generated if not specfied.

### H2O_WAVE_APP_ACCESS_KEY_SECRET

The API access key secret to use when communicating with the app server. Automatically generated if not specified.

### H2O_WAVE_CHECKPOINT_DIR

The directory to save/load application and session state. Enables checkpointing. If set, the app saves the contents of `q.app` and `q.user` before exiting. When restarted, the contents of `q.app` and `q.user` are restored. The directory is automatically created if it does not exist.

You can use checkpointing as a simple way to save/load your app's data while prototyping.

The checkpoint file is named `h2o_wave.checkpoint`, and is serialized using Python's [pickle](https://docs.python.org/3/library/pickle.html) protocol. Due to the nature of the `pickle` format, checkpointing is only guaranteed to work if the Python version and the versions of your app's dependencies are a perfect match, down to the patch version. In other words, do not expect checkpointing to work if an app is restarted using a newer/older Python version or a newer/older package. If you use checkpointing, it is recommended that you explicitly use `==` to set the `major.minor.patch` version of every package in your app's `requirements.txt` or `setup.py`.

### H2O_WAVE_NO_AUTOSTART

Disable/enable Wave server boot during `wave run`. Defaults to `false`. Available values: `1`, `t`, `true` to disable autostart; `0`, `f`, `false` to enable autostart (case insensitive). Same as calling `wave run --no-autostart`.

### H2O_WAVE_BASE_URL

The base URL (path prefix) to be used for resolving relative URLs (e.g. /foo/ or /foo/bar/, without the host (default "/"). If you run your Wave server (waved) and Wave app separately, it's necessary to set this env variable for both.

### H2O_WAVE_NO_COPY_UPLOAD

If the Wave server and Wave app run on the same machine, `q.site.upload()` will copy files instead of using HTTP requests. This is done for performance reasons (avoids serialization overheads). To disable this optimization and force HTTP requests every time, set `H2O_WAVE_NO_COPY_UPLOAD` to `1` or `t` or `true`.

### H2O_WAVE_WAVED_DIR

Provides the location of the Wave server's root directory to the Wave app, if both the server and the app are running on the same machine. Useful for performance optimizations during file uploads. Makes `q.site.upload()` copy files instead of using HTTP requests.

### H2O_WAVE_RELOAD_EXCLUDE

Excludes certain files or directories from being watched for app reload. Only relative paths are allowed and requires [watchfiles](https://pypi.org/project/watchfiles/) to be installed. See [Uvicorn docs](https://www.uvicorn.org/settings/#reloading-with-watchfiles).

Multiple values are supported. Use OS path separator (`:` for Unix and `;` for Windows) as a delimiter. E.g. `H2O_WAVE_RELOAD_EXCLUDE=tmp_dir1/*.py:tmp_dir2/*.txt`.

### H2O_WAVE_APP_CONNECT_TIMEOUT

The connect timeout specifies the maximum amount of time to wait until a socket connection to the Wave server is established. If unable to connect within this time frame, a ConnectTimeout exception is raised.

Defaults to `5` (seconds). Use `-1` to turn off.

### H2O_WAVE_APP_WRITE_TIMEOUT

The write timeout specifies the maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body). If unable to send data within this time frame, a WriteTimeout exception is raised.

Defaults to `5` (seconds). Use `-1` to turn off.

### H2O_WAVE_APP_READ_TIMEOUT

The read timeout specifies the maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body). If unable to receive data within this time frame, a ReadTimeout exception is raised.

Defaults to `5` (seconds). Use `-1` to turn off.

### H2O_WAVE_APP_POOL_TIMEOUT

The pool timeout specifies the maximum duration to wait for acquiring a connection from the connection pool. If unable to acquire a connection within this time frame, a PoolTimeout exception is raised.

Defaults to `5` (seconds). Use `-1` to turn off.

## Web Analytics

You can configure your app's web pages to send basic usage information to a third-party web analytics or tracking site. This lets you measure and analyze how users are interacting with various parts of your app.

By default, Wave apps do not load any third-party trackers or capture usage data. Third-party trackers have to be enabled explicitly by the application's author, and are loaded on-demand.

Once enabled, your app's UI will send events every time the user performs some kind of action that triggers a request from the browser to your app. Only two kinds of information are sent to the third-party trackers:

- The names of the elements that were possibly interacted with (and not values). For example, if a button named `foo` was clicked on, the value `foo=true` is tracked.
- The hash part of the URL, if any. For example if the page `/foo/bar` was navigated to, the value `#=/foo/bar` is tracked.

### Google Analytics

To enable usage tracking via Google Analytics, create and set a `ui.tracker()` on your page's meta card, with the `id` set to the measurement ID of your web property.

```py {2}
q.page['meta'] = ui.meta_card('',
    tracker=ui.tracker(type=ui.TrackerType.GA, id='G-XXXXXXXXXX')
)
```

## FAQ

### How to start a Wave app on a different port?

You first need to set `H2O_WAVE_LISTEN` env variable, which is a string prefixed with `:` to make your Wave server expose the port you want. Afterwards set `H2O_WAVE_ADDRESS='http://127.0.0.1:10102'` to tell your Wave app where it should connect to.

```sh
H2O_WAVE_LISTEN=":10102" H2O_WAVE_ADDRESS='http://127.0.0.1:10102' wave run app.py
```

---

### Contributing (`website/docs/contributing.md`)

---
title: Contributing
---

We appreciate all contributions. If you are planning to contribute back bug-fixes, please do so without any further discussion.

If you plan to contribute new features, please first [open an issue](https://github.com/h2oai/wave/issues/new/choose) and discuss the feature with us. Sending a PR without discussion might end up resulting in a rejected PR because we might be taking the software in a different direction than you might be aware of.

(Based on the [PyTorch Contribution Guide](https://pytorch.org/docs/stable/community/contribution_guide.html).)

## About open source development

If this is your first time contributing to an open source project, some aspects of the development process may seem unusual to you.

- **There is no way to "claim" issues.** People often want to "claim" an issue when they decide to work on it, to ensure that there isn’t wasted work when someone else ends up working on it. This doesn’t really work too well in open source, since someone may decide to work on something, and end up not having time to do it. Feel free to give information in an advisory fashion, but at the end of the day, we will take running code and rough consensus.

- **There is a high bar for new functionality that is added.** Unlike in a corporate environment, where the person who wrote code implicitly "owns" it and can be expected to take care of it in the beginning of its lifetime, once a pull request is merged into an open source project, it immediately becomes the collective responsibility of all maintainers on the project. When we merge code, we are saying that we, the maintainers, are able to review subsequent changes and make a bugfix to the code. This naturally leads to a higher standard of contribution.

## Proposing new features

New feature ideas are best discussed on a specific issue. Please include as much information as you can, any accompanying data, and your proposed solution. The H2O Wave team and community frequently reviews new issues and comments where they think they can help. If you feel confident in your solution, go ahead and implement it.

## Reporting issues

If you’ve identified an issue, first search through the list of existing issues on the repo. If you are unable to find a similar issue, then create a new one. Supply as much information you can to reproduce the problematic behavior. Also, include any additional insights like the behavior you expect.

## Implementing features

If you want to fix a specific issue, it’s best to comment on the individual issue with your intent. However, we do not lock or assign issues except in cases where we have worked with the developer before. It’s best to strike up a conversation on the issue and discuss your proposed solution. The H2O Wave team can provide guidance that saves you time.

Issues that are labeled **good first issue**, **low** or **medium** priority are great places to start. Only issues that have assigned a milestone or are tagged with **help needed** / **good first issue** will be merged.

Please be respectful of maintainer's time and do not ask questions like "Which file should I edit" etc. Do some due diligence first. Chances are that if you are not able to find the file that needs to be changed, the feature implementation might be overwhelming for you as well.

## Improving documentation and tutorials

We aim to produce high quality documentation and tutorials. On rare occasions that content includes typos or bugs. If you find something you can fix, send us a pull request for consideration.

## Submitting pull requests

You can view a list of all [open issues](https://github.com/h2oai/wave/issues). Commenting on an issue is a great way to get the attention of the team. From here you can share your ideas and how you plan to resolve the issue.

For more challenging issues, the team will provide feedback and direction for how to best solve the issue.

If you’re not able to fix the issue yourself, commenting and sharing whether you can reproduce the issue can be useful for helping the team identify problem areas.

## Improving code readability

Improved code readability helps everyone. It is often better to submit a small number of pull requests that touch few files versus a large pull request that touches many files. Opening an issue related to your improvement is the best way to get started.

## Adding test cases

Additional test coverage is appreciated.  Help us make the codebase more robust.

## Security vulnerabilities

If you discover a security vulnerability within H2O Wave, please send an email to Prithvi Prabhu at <prithvi@h2o.ai>. All security vulnerabilities will be promptly addressed.

## Code of Conduct

See <https://github.com/h2oai/wave/blob/main/.github/CODE_OF_CONDUCT.md>.

## Development Setup

Prerequisites:

- [Go](https://golang.org/) v1.19+
- [Node.js](http://nodejs.org) v16+
- [Python](https://www.python.org/) 3.8+
- A C/C++ compiler [XCode](https://developer.apple.com/xcode/) on OSX, `build-essential` on Debian, `base-devel` on Arch, etc.) to build Python/Node.js dependencies.

:warning: This project is best developed on OSX or Linux at the moment. If you develop on Windows, [WSL](https://docs.microsoft.com/en-us/windows/wsl/about) is recommended.

You will need [Go](https://golang.org/) 1.19+, [Node.js](http://nodejs.org) 16+, [Python](https://www.python.org/) 3.7+. You should already have Python 3.7+ on a modern OS. It is recommended that you get Go and Node.js from their websites, since your OS package managers (`apt`, `brew`, etc.) are likely to have old packages.

### Getting started

To set up all development dependencies, clone the repo and run:

``` bash
make all
```

To launch the Wave server, run:

``` bash
make run
```

Try running the button example to verify if your setup is functional:

``` bash
cd py && ./venv/bin/wave run examples.button
```

You should now see the button example at <http://localhost:10101/demo>.

For front-end development, you'll also need to start the a UI dev server:

``` bash
make run-ui
```

You should now see the button example at <http://localhost:3000/demo>.

Once you have the UI dev server running, you should be able to visualize any changes to `./ui` in real time.

Happy hacking!

### Daily development

For daily development, you'll only need to pull `main` from git and run `make run` and `make run-ui`. Running `make setup` is not necessary unless `make run` or `make run-ui` fail due to missing dependencies.

### Other make targets

``` bash
# Generates Python and R APIs.
# Run only if you add new cards/components, attributes or fix docstrings.
$ make generate

# Displays all the available make targets.
$ make help
```

## Repo Structure

- `cmd`: Go executables
- `data`: contains data created by a Wave-app itself (e.g. file upload files)
- `py`: Python package
- `r`: R package
- `tools/wavegen`: Typescript to Python/R code-generator
- `ui`: Typescript + React sources, primarily built using Fluent UI.
  - `config` contains webpack configuration
  - `eslint` contains custom eslint rules for `ts` and `tsx` files.  It is required to run `npm ci` after changing `linter.js` in order for changes to take effect.
- `website`: documentation sources

### Committing Changes

Commit messages must follow [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).

Make sure your commit message also ends with an issue number e.g. `fix: Typo #11`.

---

### Custom Css (`website/docs/custom-css.md`)

---
title: Custom CSS
---

Wave tries to handle all the styling for you to both save you time and provide the most optimal user experience. However, there might be cases, when you are not 100% happy with it:

* You feel like some components are hard to use and you have some ideas about improvement. In this case, please don't hesitate to [file a new feature request](https://github.com/h2oai/wave/issues/new?assignees=&labels=feature&template=feature_request.md&title=) where we can further discuss and improve Wave for everybody.
* You do not like alignment or some particular color - subjective case for which you need to use custom CSS.

:::info
It's recommended to keep CSS changes at minimum (ideally at 0) since the CSS selectors that you will need to use are not guaranteed to work across Wave updates. The HTML code in Wave is considered an "implementation detail" and is prone to change in order to deliver new features and bugfixes.
:::

## Loading external stylesheets

If the stylesheet you want to use is hosted somewhere, e.g. CDN, you need to use the `stylesheets` attribute. This is also handy for cases when you wish to load a well-known CSS lib like `Bootstrap` and use it within your `ui.markup_card` for example.

```py
q.page['meta'] = ui.meta_card(
    box='',
    stylesheets=[
      ui.stylesheet('https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css')
    ]
)
```

### Using local CSS stylesheet

Prior to using your own CSS files, they need to be uploaded to Wave server. There are 2 ways of doing this:

* Uploading the CSS files from your Wave app via [q.site.upload](/docs/files/#provide-file-downloads).
* Serving the CSS files directly from Wave server via [public/private dir](/docs/files/#serving-files-directly-from-the-wave-server).

```py
from h2o_wave import Q, main, app, ui
import os

current_dir = os.path.dirname(os.path.realpath(__file__))

@app('/')
async def serve(q: Q):
    # Upload the `styles.css` file to the Wave server 
    # and save its path into the `stylesheet_path` variable.
    stylesheet_path, = await q.site.upload([os.path.join(current_dir, 'styles.css')])
    # Use the uploaded file path in the `ui.stylesheet`.
    q.page['meta'] = ui.meta_card(
      box='', 
      stylesheets=[
        ui.stylesheet(stylesheet_path)
      ]
    )

    await q.page.save()
```

## Inline stylesheets

On the other hand, if all you want to do is tweak a few things here and there, the simpler solution would be to use the `stylesheet` attribute that accepts a CSS string.

```py
# Use red colors for text in all paragraphs.
style = '''
p {
  color: red;
}
'''

page['meta'] = ui.meta_card(box='', stylesheet=ui.inline_stylesheet(style))
```

## Targeting the desired element

CSS class names are autogenerated by our build tooling which makes them inappropriate for CSS selectors - they change with every build (new version) of Wave. Instead, you can notice there are `data-test` attributes all over the HTML markup. These are filled based on either card name (`q.page['<card-name>']`) or on the `name` attribute in case of components. It's recommended to use the `data-test` attributes where possible instead.

Below, you can find a simple example on how to change the alignment of header title and subtitle.

```py
from h2o_wave import main, app, Q, ui


@app('/')
async def serve(q: Q):
    # Use "my_header" as card name.
    q.page['my_header'] = ui.header_card(box='1 1 -1 1', title='Title', subtitle='Subtitle')
    # Target all divs with data-test attribute that have value of "my_header",
    # then target it's first immediate child div.
    q.page['meta'] = ui.meta_card(box='', stylesheet=ui.inline_stylesheet('''
div[data-test="my_header"]>div {
  flex-grow: 1;
  justify-content: center;
}
'''))

    await q.page.save()
```

---

### Deployment (`website/docs/deployment.md`)

---
title: Deployment
---

At the end of the day, Wave produces web-apps with their own web server. This means any VM provider can be used for Wave deployment.

## Deploying Wave scripts

Wave scripts are regular Python scripts. Deploy them as you would any Python script.

## Deploying Wave apps

If you are aiming for the most simplest deployment, use `wave run your_app --no-reload`. The `-no-reload` flag is very important from performance and security standpoints.

Wave uses 2 separate servers under the hood - `waved` (Go server) and `wave app` (Python server). See [architecture](/docs/architecture/) to learn more.

Wave apps are [ASGI](https://asgi.readthedocs.io/en/latest/)-compatible, based on [Uvicorn](https://www.uvicorn.org) / [Starlette](https://www.starlette.io/), a [high-performance](https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=fortune&l=zijzen-1) Python server.

You can run Wave apps behind any ASGI server, like [uvicorn](https://www.uvicorn.org), [gunicorn](https://gunicorn.org/), [daphne](https://github.com/django/daphne/), [hypercorn](https://pgjones.gitlab.io/hypercorn/), etc.

It's highly recommended to run Wave server (waved) [separately](/docs/tutorial-hello/#step-1-start-the-wave-server) from Wave app for production deployment in order to achieve the highest flexibility (custom ASGI server configuration). However, if you don't need that, you are good to go with a simple `wave run --no-reload app`.

To run your app using an ASGI server, append `:main` to the `app` argument. For example, if you were normally executing your app `foo.py` using `wave run foo`, and want to run your app using Uvicorn, use `uvicorn foo:main`.

These commands are equivalent:

```shell
(venv) $ wave run --no-reload --no-autostart foo
```

```shell
(venv) $ uvicorn foo:main
```

For more information, see [uvicorn.org/deployment](https://www.uvicorn.org/deployment/) and [starlette.io/#performance](https://www.starlette.io/#performance).

To configure a different port, see [configuring ports](/docs/configuration/#how-to-start-a-wave-app-on-a-different-port).

:::tip
`uvicorn` accepts `--env-file` flag to specify `.env` configuration file for the Wave app. Note that as of time of writing, `--env-file` does not support configuration of Uvicorn itself. Using this flag may require running `pip install python-dotenv` first.
:::

More info about various configuration options can be found in the [configuration section](/docs/configuration).

### Beyond defaults

If different than default ports are used for Wave server (<http://localhost:10101>) or Wave app (<http://localhost:8000>), it's necessary to properly set env variables:

- Wave server - `H2O_WAVE_LISTEN`.
- Wave app - `H2O_WAVE_APP_ADDRESS` and `H2O_WAVE_ADDRESS`.

### Deploying with HTTPS

Wave consists of 2 servers and both need to be configured separately.

- Wave server - `H2O_WAVE_TLS_CERT_FILE` and `H2O_WAVE_TLS_KEY_FILE` environment variables.
- Wave app - `uvicorn foo:main <other-params> --ssl-keyfile=<key file> --ssl-certfile=<cert file>`. See [uvicorn docs](https://www.uvicorn.org/deployment/#running-with-https).

HTTPS is all or nothing meaning either both server and app use TLS or none does.

Once set, the Wave app needs to know it should talk to the Wave server via `https` and not `http` as it does by default. This can be set using `H2O_WAVE_ADDRESS="https://127.0.0.1:10101"` env variable when starting the Wave app.

### Deployment to separate machines

Although most people deploy their Wave server and Wave app to the same machine, it's not the only way. When deploying to separate machines, the main challenge is to make sure Wave server and Wave app can communicate properly.

For wave app machine, the following needs to be set:

- `H2O_WAVE_APP_ADDRESS` - the address of the Wave app machine that Wave server connects to. Defaults to `http://localhost:8000`. In real world, this would be something like `http://myapp1.mycompany.com:8000`.
- `H2O_WAVE_ADDRESS` - the address of the Wave server machine. Defaults to `http://localhost:10101`. In real world, this would be something like `http://myapp1server.mycompany.com:10101`.
- `--host 0.0.0.0` - Uvicorn exposes `localhost` by default, which would only listen for local network requests. `0.0.0.0` tells it to listen on all addresses.

:::tip
Do not specify addresses with `https` protocol if you haven't configured Wave server and Wave app to use TLS.
:::

## AWS EC2

See a step-by-step [blog post](https://medium.com/@gfousas/deploy-a-wave-app-on-an-aws-ec2-instance-1fe508f36ef) by [Greg Fousas](https://github.com/fousasg).

## Azure

See a step-by-step [blog post](https://medium.com/@leteve.audrey/run-h2o-wave-app-with-azure-app-service-342d32a009f8) by [Audrey Leteve](https://github.com/audreyleteve).

## Hugging Face Spaces

See a step-by-step [blog post](https://medium.com/@unusualcode/deploy-a-wave-app-to-hugging-face-spaces-8b9a2bda5e46).

## Behind Nginx (reverse proxy)

Since Wave relies on websockets, the following configuration is necessary.

```conf
# Enable websockets.
# http://nginx.org/en/docs/http/websocket.html
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    location / {
        proxy_http_version 1.1;
        ; Change to your wave server address.
        proxy_pass http://localhost:10101/;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto http;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_redirect off;
    }
}
```

Read more at [official Nginx docs](http://nginx.org/en/docs/http/websocket.html) or see an [example repo](https://github.com/mturoci/wave-nginx).

---

### Development (`website/docs/development.md`)

---
title: Development
---

Wave scripts are plain Python programs. Wave apps are ASGI programs. You can develop, debug and test them from the command-line, from the Python REPL, or from your favorite text editor.

Both [PyCharm Community Edition](https://www.jetbrains.com/pycharm/download) and [Visual Studio Code](https://code.visualstudio.com/) are excellent for Python programming.

:::tip
At the time of writing, PyCharm's type-checking and error-detection is superior to Visual Studio Code's Python plugin.
:::

## Getting started

The simplest way to get started in either PyCharm or Visual Studio Code is the same:

1. Create a working directory.
2. Set up a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html).
3. Install the `h2o-wave` package.
4. Open the directory in your IDE.

```shell
mkdir $HOME/wave-apps
cd $HOME/wave-apps
python3 -m venv venv
./venv/bin/pip install h2o-wave
```

### Using PyCharm

1. Launch PyCharm
2. Click "File" -> "Open...", then choose `$HOME/wave-apps`.
3. Right-click on `wave-apps` in the "Project" tree, then click "New" -> "Python File".
4. Enter a file name, say, `foo.py`.
5. Write some code (see sample below).
6. Right-click anywhere inside the file and choose "Run foo" or "Debug foo".

:::tip
To make Wave app development easier, we suggest installing our [PyCharm plugin](https://plugins.jetbrains.com/plugin/18530-h2o-wave). If you want to learn more about all of its features check out [this blog post](https://wave.h2o.ai/blog/h2o-wave-pycharm-plugin).
:::

### Using Visual Studio Code

1. Launch Visual Studio Code
2. Click "File" -> "Open...", then choose `$HOME/wave-apps`.
3. Click "File" -> "New File"; save the file as, say, `foo.py`.
4. You should now get a prompt asking if you want to install extensions for Python. Click "Install".
5. Write some code (see sample below).
6. Hit `Ctrl+F5` to run, or `F5` to debug.

:::tip
To make Wave app development easier, we suggest installing our [VSCode extension](https://marketplace.visualstudio.com/items?itemName=h2oai.h2o-wave). If you want to learn more about all of its features check out [this blog post](https://wave.h2o.ai/blog/h2o-wave-vscode-extension).
:::

## Debugging Apps

To debug Wave apps, set your IDE or editor's configuration to execute the command `python -m h2o_wave run --no-reload foo` instead of `python foo.py`.

:::tip
The command `wave run --no-reload foo` is equivalent to `python -m h2o_wave run --no-reload foo`.
:::

### Debug in PyCharm

- Open the "Run/Debug Configurations" dialog for your script.
- Under "Configuration", change the "Script path" dropdown to "Module name".
- Set "Module name" to `h2o_wave`.
- Set "Parameters" to `run foo` (assuming your app's source code is in `foo.py`)

### Debug in Visual Studio Code

- Run "Open launch.json" command (cmd + shift + P).
- Add the configuration listed below.
- Go to Debug pane, select your newly added config and run it to start debugging.

```json
{
  "name": "Debug Wave App",
  "type": "python",
  "request": "launch",
  // Path to your wave installation, usually inside virtual env.
  "program": "${workspaceFolder}/venv/bin/wave",
  // Path to python you are using, usually inside virtual env.
  "python": "${workspaceFolder}/venv/bin/python",
  "args": [
    "run",
    "app"
  ]
}
```

## Using OpenID Connect

You can set up a local [Keycloak](https://www.keycloak.org/) instance for developing apps that use OpenID Connect, OAuth 2.0, or SAML 2.0 for authentication, single sign-on, and so on.

### Run Keycloak

First, create a local directory to persist all the realms, clients, and users you'll be adding to Keycloak:

```sh
mkdir ~/.keycloak
```

Run Keycloak using Docker:

```sh
docker run \
  -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  --volume ~/.keycloak:/opt/jboss/keycloak/standalone/data \
  quay.io/keycloak/keycloak:21.0.1 \
  start-dev
```

Keycloak should now be running at <http://localhost:8080/>.

### Add a client

Next, create a *client* in Keycloak to represent our app:

- Go to Keycloak at <http://localhost:8080/admin>.
- Log in with username `admin`, password `admin`.
- In the top left, click on `Clients`
  - Click the `Create client` button to create a new client.
  - Set `Client ID` to `wave`.
  - Click "Next".
  - Change the "Client authentication" to `On` to change access type of the to confidential.
  - Click `Next`.
  - Set `Valid Redirect URIs` to `*` (never do this in production).
  - Click `Save`.
  - Click the `Credentials` tab.
  - Copy the `Client secret` field (e.g. `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).

### Add test users

Next, add one or more users to Keycloak:

- In the top left, click on `Users`.
  - Click the `Add User` button to create a new user.
  - Set the `Username` field.
  - Click `Create`.
  - Go to the `Credentials` tab.
  - Click `Set password`.
  - Set the password fields.
  - Change `Temporary` to `Off`.
  - Click `Save`.

:::tip
You can also use admin user for logging in with credentials `admin/admin` if you just need a quick testing.
:::

### Point Wave to Keycloak

We're ready to use Keycloak now. Make sure you log out of Keycloak, otherwise you'll be logged in as `admin` when you access your Wave app!

Finally, start the Wave daemon with the following `-oidc-` command line arguments (use the client secret you copied above):

```
./waved \
    -oidc-client-id wave \
    -oidc-client-secret xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
    -oidc-redirect-url http://localhost:10101/_auth/callback \
    -oidc-provider-url http://localhost:8080/realms/master \
    -oidc-end-session-url http://localhost:8080/realms/master/protocol/openid-connect/logout

```

Now, when you launch and access your Wave app, you should see a OpenID prompt which you can use to log in one of the users you created in Keycloak.

---

### Expressions (`website/docs/expressions.md`)

---
title: Data-binding
---

Some cards in the Wave SDK support *data-binding expressions*, a mini language that allows computing a card's attribute from the card's data.

## Rationale

A card's attribute can be set directly when created:

```py {4}
card = page.add(f'donut', ui.small_stat_card(
    box='1 1 2 2',
    title='Donut',
    value='2.99',
))
```

The attribute can be updated later using:

```py {1}
card.value = '3.49'
```

The above approach works fine, but sometimes it's prudent to separate the data (what is displayed) from the presentation (how it is displayed). This is especially important if you care about [internationalization](https://en.wikipedia.org/wiki/Internationalization_and_localization) and displaying language-sensitive number, date, and time formatting.

The above example can be rewritten by pulling the donut price out into `data`, and pointing the `value` to the `data` using an expression:

```py {4-5}
card = page.add(f'donut', ui.small_stat_card(
    box='1 1 2 2',
    title='Donut',
    value='=${{intl price minimum_fraction_digits=2 maximum_fraction_digits=2}}',
    data=dict(price=2.99),
))
```

The attribute can be update later using:

```py
card.data.price = 3.49
```

## Syntax

Data-binding expressions are indicated with a leading `=` (similar to spreadsheet formulae). For example, `'={{price}}'` is an expression, but `'{{price}}'` is not.

Placeholders are enclosed between `{{` and `}}`. The placeholder `price` in `={{price}}` points to `data.price`. If `data.price` is `2.99`:

- `={{price}}` translates to `2.99`.
- `=${{price}}` translates to `$2.99`.
- `=Donuts cost ${{price}}` translates to `Donuts cost $2.99`.

An expression can have multiple placeholders. For example, `={{product}} costs {{price}}`

If an expression has a placeholder and nothing else, the `{{` and `}}` can be elided. For example, `=price` is shorthand for `={{price}}`.

Functions can be applied to placeholders using the general form `{{function_name placeholder param1=arg1 param2=arg2 ...}}`. For example `{{intl price minimum_fraction_digits=2 maximum_fraction_digits=2}}` applies the formatting function `intl` to `price` with arguments `minimum_fraction_digits=2` and `maximum_fraction_digits=2`.

## Functions

Expressions currently support only one function, `intl`, which provides language-sensitive number, date, and time formatting using the [ECMAScript Internationalization API](https://tc39.es/ecma402/).

:::tip
Options in the [Internationalization API](https://tc39.es/ecma402/) are `camelCased`, but you can use both `camelCased` and `snake_cased` options in data-binding expressions. For example, both `maximum_fraction_digits` and `maximumFractionDigits` are valid.
:::

### Formatting numbers

|         **Param**        |                                                                     **Available values**                                                                    |
|:------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------:|
|           style          |                                                        'decimal' \| 'percent' \| 'currency' \| 'unit'                                                       |
|         currency         |                   [ISO 4217 currency codes](https://docs.1010data.com/1010dataReferenceManual/DataTypesAndFormats/currencyUnitCodes.html)                   |
|           unit           | [Available units](https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier) |
|        unitDisplay       |                                                                'long' \| 'short' \| 'narrow'                                                                |
|       currencySign       |                                                                  'standard' \| 'accounting'                                                                 |
|      currencyDisplay     |                                                        'symbol' \| 'code' \| 'name' \| 'narrowSymbol'                                                       |
|         notation         |                                                   'standard' \| 'scientific' \| 'engineering' \| 'compact'                                                  |
|      compactDisplay      |                                                                      'short' \| 'long'                                                                      |
|        signDisplay       |                                                        'auto' \| 'always' \| 'never' \| 'exceptZero'                                                        |
|        useGrouping       |                                                                        true \| false                                                                        |
|   minimumIntegerDigits   |                                                                            number                                                                           |
|   minimumFractionDigits  |                                                                            number                                                                           |
|   maximumFractionDigits  |                                                                            number                                                                           |
| minimumSignificantDigits |                                                                            number                                                                           |
| maximumSignificantDigits |                                                                            number                                                                           |
|      numberingSystem     |         [Available numbering systems](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem)         |

### Formatting date

|        **Param**       |                                                             **Available values**                                                            |
|:----------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------:|
|         hour12         |                                                                true \| false                                                                |
|         weekday        |                                                        'long' \| 'short' \| 'narrow'                                                        |
|           era          |                                                        'long' \| 'short' \| 'narrow'                                                        |
|          year          |                                           'numeric' \| '2-digit' \| 'long' \| 'short' \| 'narrow'                                           |
|          month         |                                           'numeric' \| '2-digit' \| 'long' \| 'short' \| 'narrow'                                           |
|           day          |                                                            'numeric' \| '2-digit'                                                           |
|          hour          |                                                            'numeric' \| '2-digit'                                                           |
|         minute         |                                                            'numeric' \| '2-digit'                                                           |
|         second         |                                                            'numeric' \| '2-digit'                                                           |
|      timeZoneName      |                            'long' \| 'short' \| 'shortOffset' \| 'longOffset' \| 'shortGeneric'  \| 'longGeneric'                           |
|        hourCycle       |                                                       'h11' \| 'h12' \| 'h23' \| 'h24'                                                      |
|        dateStyle       |                                                   'full' \| 'long' \| 'medium' \| 'short'                                                   |
|        timeStyle       |                                                   'full' \| 'long' \| 'medium' \| 'short'                                                   |
|     numberingSystem    | [Available numbering systems](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem) |
|        calendar        |         [Available calendars](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar)        |
|      formatMatcher     |                                                     "basic" \| "best fit" \| "best fit"                                                     |
|        dayPeriod       |                                                        "narrow" \| "short" \| "long"                                                        |
| fractionalSecondDigits |                                                               0 \| 1 \| 2 \| 3                                                              |

See the [official spec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters) for more info.

---

### Files (`website/docs/files.md`)

---
title: Files
---

Wave provides four functions to manage files from your app:

- `ui.file_upload()` allows uploading files from the browser to the Wave server.
- `q.site.upload()` uploads files from your app to the Wave server.
- `q.site.download()` downloads a file from the Wave server to your app.
- `q.site.unload()` deletes a file from the Wave server.

## Accept file uploads

Use a file upload component (`ui.file_upload()`) to accept file uploads from the browser. Files get uploaded from the browser and get stored on the Wave server. Use `q.site.download()` to download files from the Wave server to your app.

```py {9,13}
from h2o_wave import Q, main, app, ui

@app('/uploads')
async def serve(q: Q):
    paths = q.args.datasets
    if not paths:
        q.page['example'] = ui.form_card(box='1 1 4 10', items=[
            ui.text_xl('Upload datasets'),
            ui.file_upload(name='datasets', label='Upload', multiple=True),
        ])
    else:
        for path in paths:
            local_path = await q.site.download(path, '.')
            # Do something with the file located at local_path
            # ...
    await q.page.save()
```

:::tip
After a file is uploaded from the browser, it is stored forever on the Wave server. If you don't need the file any longer, use `q.site.unload()` to delete it from the Wave server.
:::

## Provide file downloads

Use `q.site.upload()` to upload files from your app to the Wave server. Use the returned paths to display download links in the browser and a `download` attribute to initiate the download process right after the click.

```py {5,7}
from h2o_wave import Q, main, app, ui

@app('/downloads')
async def serve(q: Q):
    download_path, = await q.site.upload(['results.csv'])
    q.page['download'] = ui.form_card(box='1 1 2 2', items = [
        ui.link(label='Download Results', path=download_path, download=True),
    ])
    await q.page.save()
```

See [download link](/docs/widgets/form/link/#download-link) for more info.

:::tip
`q.site.upload()` accepts a list of file paths, so you can upload multiple files at a time.
:::

Use `q.site.upload_dir()` to upload whole directories and preserve their structure. Useful when you want to host Javascript files having relative paths to other scripts for example. The function returns a list of uploaded paths and since it only takes a single directory at a time, the list is always going to have size of 1.

:::tip
`q.site.upload_dir()` is necessary only in very specific edgecases. For most of the time we recommend [serving files directly](/docs/files/#serving-files-directly-from-the-wave-server).
:::

:::warning
If running Wave on **Windows**, you might encounter a **500 internal error** during `q.site.upload` call due to lack of permissions since Wave server needs to write to your file system. The simple solution
is to start the Wave server (or `wave run your_app.py` for Wave > 0.20.0) within a terminal with Admin rights (open terminal as Admin).
:::

## Serving images

Use `q.site.upload()` to upload images from your app to the Wave server. Use the returned paths in `ui.image()` or `ui.image_card().

```py
image, = await q.site.upload(['path/to/my/image.png'])
q.page['example'] = ui.form_card(box='1 1 4 4', items=[
    ui.image(title='Image title', image=image, type='png'),
])
```

## Serving image streams

Use image streams to display images that can be updated in near real time, say for use cases such as real time object detection in videos or webcam streams. This feature lets you display an initial image on a web page, then follow up with updated images (or frames).

Use `q.site.uplink()` to stream a frame. This function returns the path to an image that can be passed to `ui.image()` or `ui.image_card()`, or any UI or HTML element that accepts an image source.

Use `q.site.unlink()` to signal the end of a stream.

```py
@app('/demo')
async def serve(q: Q):
    # Create stream by sending initial image
    path = await q.site.uplink('my_stream', 'image/jpeg', my_image)

    # Display image on page
    q.page['qux'] = ui.form_card(box='1 1 5 5', items=[ui.image('Image Stream', path=path)])
    await q.page.save()

    # Update image
    for i in range(100):
        await q.site.uplink('my_stream', 'image/jpeg', my_image)

    # Stop stream
    await q.site.unlink('my_stream')
```

See `examples/file_stream.py` for a complete example.

## Rendering documents

Use `q.site.upload()` to upload document from your app to the Wave server (or see the following section for other ways for serving files). Use the returned paths in `ui.frame()` component.

```py
q.app.document_path, = await q.site.upload(['path/to/my/document.pdf'])
q.page['example'] = ui.form_card(box='1 1 7 7', items=[
    ui.frame(path=document_path)
])
```

## Serving files directly from the Wave server

As an alternative to using the above `upload()` or `download()` mechanisms, you can make the Wave server (`waved`) directly serve the contents of one or more existing directories. If the Wave server and your app both have access to the directories on the file system, your app can simply create or copy files to the directories to make them accessible from web browsers.

Serve the contents of directory `/home/zaphod/data` at <http://localhost:10101/datasets/>

```
waved -public-dir /datasets/@/home/zaphod/data
```

Serve the contents of directory `/home/zaphod/data` at <http://localhost:10101/datasets/>, and `/home/zaphod/models` at <http://localhost:10101/public/models>

```
waved -public-dir /datasets/@/home/zaphod/data -public-dir /public/models/@/home/zaphod/models
```

Serve the contents of directory `/home/zaphod/data` at <http://localhost:10101/datasets/>, but only to authenticated users.

```
waved -private-dir /datasets/@/home/zaphod/data
```

Serve the contents of directory `/home/zaphod/data` at <http://localhost:10101/datasets/>, and `/home/zaphod/models` at <http://localhost:10101/public/models>, but only to authenticated users.

```
waved -private-dir /datasets/@/home/zaphod/data -private-dir /public/models/@/home/zaphod/models
```

Note that any number of `-public-dir` and `-private-dir` arguments are allowed.

---

### Graphics (`website/docs/graphics.md`)

---
title: Graphics
---

Wave provides a versatile API for rendering vector graphics (SVG) backed by [data buffers](buffers.md).

Use the graphics card [ui.graphics_card()](http://wave.h2o.ai/docs/api/ui#graphics_card) to render graphics.

## Stages and scenes

The graphics card has two attributes that control how graphics are rendered: `stage` and `scene`:
- A stage holds static shapes that are rendered as part of the background (literally, behind the scene).
- A scene holds shapes whose attributes need to be animated (causing a re-render, or, literally, cause a scene).

Use `ui.stage()` and `ui.scene()` to create the stage and the scene:
- `ui.stage()` creates a packed, static representation of its shapes (recall that the stage is not supposed to change).
- `ui.scene()` declares a data buffer to efficiently update its shapes if changed (recall that the scene is animated).

Here's an example that draws a clock with a circular `face` placed on the `stage`, and `hour`, `min` and `sec` hands placed in the `scene`:

```py
from h2o_wave import site, ui, graphics as g

page = site['/demo']
page['example'] = ui.graphics_card(
    box='1 1 2 3', view_box='0 0 100 100', width='100%', height='100%',
    stage=g.stage(
        face=g.circle(cx='50', cy='50', r='45', fill='#111', stroke_width='2px', stroke='#f55'),
    ),
    scene=g.scene(
        hour=g.rect(x='47.5', y='12.5', width='5', height='40', rx='2.5', fill='#333', stroke='#555'),
        min=g.rect(x='48.5', y='12.5', width='3', height='40', rx='2', fill='#333', stroke='#555'),
        sec=g.line(x1='50', y1='50', x2='50', y2='16', stroke='#f55', stroke_width='1px'),
    ),
)
```

## Shapes

Use drawing functions to add shapes to the stage or scene:

| Primitive | Use |
|---|---|
| [arc()](api/graphics#arc) | Circular or annular sector, as in a pie or donut chart |
| [circle()](api/graphics#circle) | [Circle](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) |
| [ellipse()](api/graphics#ellipse) | [Ellipse](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse) |
| [image()](api/graphics#image) | [Image](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image) |
| [line()](api/graphics#line) | [Line](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) |
| [path()](api/graphics#path) | [Path](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) |
| [polygon()](api/graphics#polygon) | [Polygon](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon) |
| [polyline()](api/graphics#polyline) | [Polyline](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline) |
| [spline()](api/graphics#spline) | Curves (including radial curves) using various algorithms: basis, cardinal, smooth, linear, monotone, natural, step. |
| [rect()](api/graphics#rect) | [Rect](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) |
| [text()](api/graphics#text) | [Text](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) |


## Paths

The `path()` function is special: it can draw arbitrary shapes. This is because SVG path elements support an attribute called `d` which holds drawing commands.

Here's a path that represents a red square:

```py 
g.path(d='m 25 25 h 50 v 50 h -50 z', fill='red')
```

The above snippet produces:

```html
<path d='m 25 25 h 50 v 50 h -50 z' fill='red'/>
```

The drawing commands in the above example work like this:
1. `m 25 25`: Move 25px left, 25px down
2. `h 50`: Draw a line 50px right
3. `v 50`: Draw a line 50px down
4. `h -50`: Draw a line 50px left
5. `z`: Close the path (going back to where we started)

The little drawing syntax above is part of the SVG specification, not something unique to Wave.

Authoring `path()` drawing commands by hand is tedious, so Wave provides two utilities to make it easier: `p()` and `turtle()`.

## Drawing with paths

`p()` creates a path generator with convenience methods to draw step by step. For example, the following two lines are equivalent:

```py 
red_square = g.path(d='m 25 25 h 50 v 50 h -50 z', fill='red')
red_square = g.p().m(25, 25).h(50).v(50).h(-50).z().path(fill='red')
```

### Commands

| Method | Use |
|---|---|
| `M()` | Move, absolute |
| `H()` | Horizontal line, absolute |
| `V()` | Vertical line, absolute |
| `L()` | Line, absolute |
| `A()` | Elliptical arc, absolute |
| `C()` | Cubic Bézier curve, absolute |
| `S()` | Cubic Bézier curve, smooth, absolute |
| `Q()` | Quadratic Bézier curve, absolute |
| `T()` | Quadratic Bézier curve, smooth, absolute |
| `Z()` | Close path |
| `m()` | Move, relative |
| `h()` | Horizontal line, relative |
| `v()` | Vertical line, relative |
| `l()` | Line, relative |
| `a()` | Elliptical arc, relative |
| `c()` | Cubic Bézier curve, relative |
| `s()` | Cubic Bézier curve, smooth, relative |
| `q()` | Quadratic Bézier curve, relative |
| `t()` | Quadratic Bézier curve, smooth, relative |
| `z()` | Close path |
| `d()` | Serialize this path's commands into SVG path data. |
| `path()` | Create a SVG path element |

:::info
The upper-cased commands use absolute coordinates. The lower-cased commands use relative coordinates. For example, `L(4,2)` means "draw a line to (4,2)", but `l(4,2)` means "draw a line 4px right, 2px down from the current position".
:::

## Drawing with a turtle

`turtle()` creates a path generator (similar to `p()`), but using [Turtle Geometry](https://docs.python.org/3/library/turtle.html).

### Commands

| Method | Use |
|---|---|
| `pd()` | Pen down |
| `pu()` | Pen up |
| `p()` | Set the turtle's position |
| `a()` | Set the turtle's orientation |
| `f()` | Move forward |
| `b()` | Move backward |
| `l()` | Turn left |
| `r()` | Turn right |
| `d()` | Serialize this turtle's movements into SVG path data |
| `path()` | Create a SVG path element |


### Example

Here is an example from [Python's turtle module](https://docs.python.org/3/library/turtle.html): 



Here is the above example recreated in Wave:

```py 
from turtle import *
color('red', 'yellow')
begin_fill()
while True:
    forward(200)
    left(170)
    if abs(pos()) < 1:
        break
end_fill()
done()
```

```py {3-6}
from h2o_wave import site, ui, graphics as g

t = g.turtle().f(100).r(90).pd()
for _ in range(36):
    t.f(200).l(170)
spirograph = t.pu(1).path(stroke='red', fill='yellow')

page = site['/demo']
page['example'] = ui.graphics_card(
    box='1 1 2 3', view_box='0 0 220 220', width='100%', height='100%',
    scene=g.scene(foo=spirograph),
)

page.save()
```

:::info
Turtle graphics is not just kid stuff: See [Turtle Geometry](https://mitpress.mit.edu/books/turtle-geometry) by Harold Abelson and Andrea diSessa.
:::

---

### Guide (`website/docs/guide.md`)

---
title: Introduction
slug: /guide
---

Welcome to the H2O Wave Guide! If you prefer to learn concepts step by step, you've come to the right place. 

:::info
If you haven't already, it's worthwhile reading the [Getting Started](getting-started.md) page, which offers a 5-minute overview of what H2O Wave is all about.
:::

## Before you start

If you prefer to learn by doing, [start with our tutorials](tutorial-hello.mdx) - they're short and simple, illustrate the most important concepts in a beginner-friendly way, and are the best way to get hands-on with Wave. Like any unfamiliar technology, Wave has a slight learning curve, but you will get the hang of it with practice and patience. 

## Who is this for?

This documentation assumes that you have some familiarity with the Python programming language. You don't have to be an expert, but it might be harder to learn both Wave and Python at the same time.

If you already know Python, but have no experience authoring web apps, you'll probably find Wave a fun and easy way to get started, without the hassle of learning HTML, CSS, Javascript and client-server programming.

If you have some experience building web apps using Python (or even other languages), you'll find that Wave offers an approach quite different from traditional client-server or request-reply programming. The mental model is much simpler, functional (in the functional programming sense), and lets you reason about your code in terms of a single tier (server-only) as opposed to two tiers (client and server). Additionally, Wave offers straightforward ways to integrate realtime features into your apps using the same programming model.

---

### Icons (`website/docs/icons.mdx`)

---
title: Icons
slug: /icons
---


import { IconsPage } from '@site/src/components/icons'

<IconsPage/>

---

### Installation (`website/docs/installation.mdx`)

---
title: Installation
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';


<Tabs groupId="pypi/conda">

<TabItem value="pip" label="Pip">

```shell
pip install h2o-wave
```

</TabItem>

<TabItem value="conda" label="Conda">

```shell
conda config --append channels conda-forge
conda install -c h2oai h2o-wave
```
:::info
Conda packaging does not contain Wave server which means, you will still need to run the wave server (waved) [separately](/docs/tutorial-hello#step-1-start-the-wave-server).
:::

</TabItem>

</Tabs>

To install in a virtual environment (recommended):

<Tabs groupId="operating-systems">

<TabItem value="lin" label="Unix/MacOS">

```shell
python3 -m venv venv
source venv/bin/activate
pip install h2o-wave
```

</TabItem>

<TabItem value="win" label="Windows">

```shell
python -m venv venv
.\venv\Scripts\activate
pip install h2o-wave 
```

</TabItem>

<TabItem value="conda" label="Conda">


```shell
conda create -n venv
conda activate venv
conda config --append channels conda-forge
conda install -c h2oai h2o-wave
```

:::info
Conda packaging does not contain Wave server which means, you will still need to run the wave server (waved) [separately](/docs/tutorial-hello#step-1-start-the-wave-server).
:::

</TabItem>
</Tabs>


## Project templates

Once the installation part is complete, you can use `wave init` command to pick a [predefined app template](/docs/project-templates) that will bootstrap your first app and give you a head start when building your Wave apps.

## Interactive learning

If you don't feel like going through the docs because you are more of a hands-on person, you can try our Wave University app.

Within your activated virtual environment, run:

```sh
wave learn
```

The command requires `h2o_wave_university` to be installed. To do that, run:

```sh
pip install h2o_wave_university
```

:::tip
Wave University is a standalone package that can be pip-installed and run independently. After installation, you will be able to launch it via `wave-university` command within your python environment.
:::

## Download examples

Run `wave fetch` to download app examples, demos, and the interactive tour.

```shell
(venv) $ wave fetch

Fetching examples and related files. Please wait...
Downloading https://github.com/h2oai/wave/releases/download/v0.20.0/wave-0.20.0-linux-amd64.tar.gz
Extracting...

All additional files downloaded and extracted successfully!
Examples and tour............. /home/wave/examples
Demos and layout samples...... /home/wave/demo
Automated test harness........ /home/wave/test
Wave daemon for deployments... /home/wave
```

You now have access to 200+ examples locally.

:::info
On recent versions of OSX, it's possible to run into certificate verification errors while running `wave fetch`, specifically `urllib.error.URLError: urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate`. To fix this problem, navigate to `Application/Python 3.7` on your system and execute `Install Certificates.command`.
:::

## Run the tour

The Wave tour is a Wave app that lets you play with all the examples interactively.



To run the tour, simply launch `examples/tour.py` like this:

```shell
cd wave
pip install -r examples/requirements.txt
wave run --no-reload examples.tour
```

Then navigate to [http://localhost:10101/tour](http://localhost:10101/tour) to access the tour.

`tour.py` is an ordinary Wave app that runs other apps. The tour itself runs at the route `/tour`, and each of the examples runs at `/demo`.

:::tip
To play with the tour's active example in isolation, simply open a new browser tab and head to [http://localhost:10101/demo](http://localhost:10101/demo).
:::

## Downloading Wave

If you would like to run Wave server manually (e.g. for [deployment](/docs/deployment) or [Wave scripts](/docs/scripts)), you can download it either from here or from our [Github releases](https://github.com/h2oai/wave/releases).

If unsure which package to pick, here is a helper table:

|   **OS**   | **Wave server** |  **Python client** |
|:-----------:|:---------------:|:------------------:|
|     Mac     |   darwin-amd64  | macosx_10_9_x86_64 |
|    Mac M1   |   darwin-arm64  |  macosx_11_0_arm64 |
|   Windows   |  windows-amd64  |      win_amd64     |
|    Linux    |   linux-amd64   |  manylinux1_x86_64 |

import { Downloads } from '@site/src/components/downloads'

<Downloads />

## Early adopters

For people who simply cannot wait until next release and would like to try the new cool feature merged just a day ago, we also provide so-called "**nightly builds**". These are builds that are released daily (over night).

We highly encourage using nightly builds as they allow catching possible regressions ahead of an official release. So please, if you spot something weird that is not happening in the latest release,
[file an issue](https://github.com/h2oai/wave/issues/new?labels=bug&template=bug_report.md).

To use nightly: 

* Go to [nightly release page](https://github.com/h2oai/wave/releases/tag/nightly).
* Download the appropriate `.whl` file for your OS.
* Run `pip install <downloaded-nightly>.whl` (this is an alternative to `pip install h2o-wave` with official releases).

:::tip
It's possible to include the local `.whl` file directly within your `requirements.txt` if needed.
:::

## Smoother dev experience

For Wave-specific IDE autocomplete and additional snippets that will make your life way easier, we recommend installing [VSCode extension](https://marketplace.visualstudio.com/items?itemName=h2oai.h2o-wave) or [Jetbrains Plugin](https://plugins.jetbrains.com/plugin/18530-h2o-wave) depending on the IDE of your choice.
Feel free to read more at [this](/blog/h2o-wave-vscode-extension) and [this](/blog/h2o-wave-pycharm-plugin) blog posts.

## Wrapping up

In this section, we installed Wave and then launched `tour.py` to experience the tour. In general, this is how you'd typically launch any app, including your own. There is nothing special about `tour.py`. In fact, to run any example, all you need to do is repeat the steps above in a new terminal window. For example, to run `todo.py`, simply run:

```shell
wave run examples.todo
```

You can now access the example at [http://localhost:10101/demo](http://localhost:10101/demo). Simple!

---

### Javascript (`website/docs/javascript.md`)

---
title: Javascript
---

Wave ships with a growing library of cards and components for assembling user interfaces. For most apps, the built-in components can be adequate. They're designed to work well with each other, with consistent typography, layout and theming; and the library of components keeps expanding with each new release.

However, no matter how comprehensive the Wave library gets over time, there will be situations where an app needs to use external Javascript components to supplement Wave's capabilities, like custom visualizations, UI enhancements, and graphics.

Wave 0.16+ allows importing and using third-party Javascript libraries on a page. This provides an escape-hatch of sorts, allowing you to add UI capabilities that are not yet possible with stock Wave.

## Basic Usage

The simplest way to run custom Javascript code on a page is to use an inline script (`ui.inline_script()`):

```py
q.page['meta'] = ui.meta_card(box='', script=ui.inline_script('alert("Hello World!");'))
q.page.save()
```

To execute additional bits of Javascript code, set the `meta_card`'s `script` property again:

```py
q.page['meta'].script = ui.inline_script('console.log("One Mississippi");')
q.page.save()

q.page['meta'].script = ui.inline_script('console.log("Two Mississippi");')
q.page.save()

q.page['meta'].script = ui.inline_script('console.log("Three Mississippi");')
q.page.save()
```

Passing `scripts` to `ui.meta_card()` dynamically imports external Javascript libraries.

### Import a library

```py
q.page['meta'] = ui.meta_card(box='', scripts=[ui.script('https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.min.js')])
```

### Import multiple libraries

```py
q.page['meta'] = ui.meta_card(box='', scripts=[
    ui.script('https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.min.js'),
    ui.script('https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js'),
    ui.script('https://cdnjs.cloudflare.com/ajax/libs/bokeh/2.3.2/bokeh.min.js'),
])
```

### Import and use a library

```py
q.page['meta'] = ui.meta_card(
    box='',
    # Load anime.js
    scripts=[ui.script('https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.min.js')],
    script=ui.inline_script(
        # The Javascript code for this script.
        content='anime.timeline(...);',
        # Execute this script only if the 'anime' library is available.
        requires=['anime'],
        # Execute this script only if the 'animation' element is available.
        targets=['animation'],
    )
)
q.page['example'] = ui.markup_card(
    box='1 1 10 8',
    title='Animation',
    content='<div id="animation"/>',
)
```

In the above example, we create an empty `div` HTML element on the page, load an animation library (anime.js), add a bit of Javascript to animate the `div` element. To tie things together correctly, we pass two additional arguments to `ui.inline_script()`:

- The `requires` argument ensures that the library we intend to use (in this case, `anime.js`) is downloaded, imported, and ready to use.
- The `targets` argument ensures that the HTML element the scripts operates on (in this case, the `div` element named `animation`), is available on the page. [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) can also be used to identify target elements.

### Import ESM module

When using JavaScript module files (`.mjs`), the script `type="module"` attribute needs to be specified.

```py
q.page['meta'] = ui.meta_card(box='', scripts=[ui.script(type="module", path='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.2.67/pdf.min.mjs')])
```

For other script types see the [official documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type).

## Handling Events

In most cases, when employing custom Javascript libraries, you'll want to handle and transmit events from the library to your app. To do this, use the Javascript function `wave.emit()`:

```js
// Javascript
wave.emit('event_source', 'event_name', event_data); 
```

You can then access this information in your app using:

```py
# Python
event_data = q.events.event_source.event_name
```

For example, emitting an event using `wave.emit('graph', 'node_clicked', 42)` in Javascript will set `q.events.graph.node_clicked` to `42` in Python.

All three arguments to `wave.emit()` are arbitrary. In the above example, we use:

- `graph` to indicate the source of the event.
- `node_clicked` to indicate the type of event.
- the third argument can be a string, number, boolean or any complex structure, like `{ foo: 'bar', qux: 42 }`.

Here's a complete, minimal example that prints mouse coordinates each time you click on a page.

```py
from h2o_wave import main, app, Q, ui

@app('/demo')
async def serve(q: Q):
    if not q.client.initialized:
        q.page['meta'] = ui.meta_card(box='', script=ui.inline_script(
            # Handle and transmit event
            'window.onclick = (e) => wave.emit("window", "clicked", { x: e.clientX, y: e.clientY });'
        ))
        q.client.initialized = True
    else:
        # Capture event
        if q.events.window:
            click = q.events.window.clicked
            if click:
                print(click.x, click.y)

    await q.page.save()
```

## Examples

You can play with more examples in the [Wave Tour](/docs/installation#run-the-tour) as well.

---

### Layout (`website/docs/layout.md`)

---
title: Layout
---

This section illustrates how to lay out cards on a page.

Wave supports two layout systems out of the box: a grid layout and a flex (flexible) layout.

By default, pages use the grid-based system. The grid-based system is simpler to use and ensures that your page layout looks exactly the same on every display, without surprises.

To switch to a flex layout, simply define a page layout using the `ui.layout()` API, as shown below.

## Flex Layout

The flex layout system allows you to build user interfaces and dashboards that adapt to various device and screen sizes ("responsive layout").

:::info
To view examples of flex and responsive layouts in actions, see <https://github.com/h2oai/wave/tree/main/py/demo>
:::

To define a layout for your page, attach a layout to your page's meta card using `ui.layout()`.

The following example defines a simple layout consisting of a header, a body and a footer. The body, in turn, is split horizontally into a content area and a sidebar.

```py
q.page['meta'] = ui.meta_card(box='', layouts=[
    ui.layout(
        breakpoint='xl',
        width='1200px',
        zones=[
            ui.zone('header'),
            ui.zone('body', direction=ui.ZoneDirection.ROW, zones=[
                ui.zone('content', size='75%'),
                ui.zone('sidebar', size='25%'),
            ]),
            ui.zone('footer'),
        ]
    )
])
```

A layout, at a minimum, consists of a `breakpoint` and a set of `zones`.

### Breakpoints

The `breakpoint` is the minimum viewport (browser) width at which the layout will be applied to the page. Values must be pixel widths (e.g. `0px`, `576px`, `768px`) or a named preset. The named presets are:

- `xs`: `0px` for extra small devices (portrait phones)
- `s`: `576px` for small devices (landscape phones)
- `m`: `768px` for medium devices (tablets)
- `l`: `992px` for large devices (desktops)
- `xl`: `1200px` for extra large devices (large desktops).

A breakpoint value of `xs` matches all viewport widths, unless other breakpoints are set.

### Zones

A `zone` is a named area inside the layout where cards can be placed. A zone is defined using `ui.zone()`.

Zones have a `direction`, either `ui.ZoneDirection.COLUMN` (with children laid out top to bottom) or `ui.ZoneDirection.ROW` (with children laid out left to right).

Zone can define a `size`, which translates to the zone's width in case the zone is laid out horizontally, or the zone's height in case the zone is laid out vertically.

Zones can have other zones nested inside them.

In the above example, the `content` and `sidebar` zones are laid out horizontally inside the `body`, with the `content` zone taking up 75% of the available width, and the sidebar taking up 25%.

Cards will take up an equal amount of space inside the zone, not shrinking to fit their content. To prevent a card from expanding provide a `ui.box` with `size='0'`.

```py
q.page['meta'] = ui.meta_card(box='', layouts=[
    ui.layout(
        breakpoint='xs',
        zones=[ui.zone(name='zone1', direction=ui.ZoneDirection.COLUMN, size='100%')]
    )
])

q.page['tab_card'] = ui.tab_card(
    box=ui.box(zone='zone1'),
    items=[ui.tab(name='tab1', label='tab')]
)

q.page['form_card'] = ui.form_card(box='zone1', items=[
    ui.text(content='my form card')
])
```

Default behavior:



Using `size='0'`:

```py
q.page['tab_card'] = ui.tab_card(
    box=ui.box(zone='zone1', size='0'),
    items=[ui.tab(name='tab1', label='tab')]
)
```



### Width and Height

By default, we encourage static units like `px` or `rem` for setting widths and heights (see [MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#lengths) for more info). In flex layouts, percent values such as `100%` only work if every ancestor has an explicit `size`, otherwise, the computed height can be zero and the content may not render as expected.

### Responsive Layout

To make your page adapt to multiple viewport sizes, define multiple layouts using `ui.layout()`.

```py
q.page['meta'] = ui.meta_card(box='', layouts=[
    ui.layout(
        breakpoint='xs',
        zones=[
            # Add zones here.
        ],
    ),
    ui.layout(
        breakpoint='m',
        zones=[
            # Add zones here.
        ],
    ),
    ui.layout(
        breakpoint='xl',
        width='1200px',
        zones=[
            # Add zones here.
        ]
    )
])
```

### Placing cards

To place a card inside a zone in your layout, use the card's `box` attribute.

The simplest way to assign a card to a zone is to set the `box` to the name of zone:

```py
q.page['shopping_cart'] = ui.markdown_card(box='sidebar', ...)
```

For more control over how the card is placed, use `ui.box()`:

```py
# Same as box='sidebar'
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar'), ...)
```

Make the card the third item in the sidebar:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', order=3), ...)
```

Make the card take up two parts of available space:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', size=2), ...)
```

Make cards take up available space in a 10:5:4 ratio:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', order=1, size=10), ...)
q.page['bill'] = ui.markdown_card(box=ui.box('sidebar', order=2, size=5), ...)
q.page['discounts'] = ui.markdown_card(box=ui.box('sidebar', order=3, size=4), ...)
```

Make the card 200px wide:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', width='200px'), ...)
```

Make the card 300px high:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', height='300px'), ...)
```

Make the card 200px wide and 300px high:

```py
q.page['shopping_cart'] = ui.markdown_card(box=ui.box('sidebar', width='200px', height='300px'), ...)
```

For responsive layouts, specify multiple boxes using `ui.boxes()`. Each `ui.box()` in `ui.boxes()` refers to the corresponding `ui.layout()` specified at the page level.

```py
q.page['meta'] = ui.meta_card(box='', layouts=[
    ui.layout(breakpoint='xs', zones=[...]),
    ui.layout(breakpoint='m', zones=[...]),
    ui.layout(breakpoint='xl', zones=[...]),
])
q.page['shopping_cart'] = ui.markdown_card(box=ui.boxes(
    ui.box('bottom', height='300px'), # for breakpoint xs, place in the bottom zone.
    ui.box('middle', height='200px'), # for breakpoint m, place in the middle zone.
    ui.box('sidebar', height='2'), # for breakpoint xl, place in the sidebar zone.
), ... )
```

### Aligning cards

Wave cards take all available space by default to avoid unnecessary whitespace. If the extra whitespace is desirable though, one may need to use `align` and `justify` zone attributes.



```py
q.page['meta'] = ui.meta_card(box='', layouts=[ui.layout(breakpoint='xs', zones=[
    ui.zone(
        name='content',
        direction=ui.ZoneDirection.ROW,
        # Specify a zone size, otherwise will be adapted to the biggest card in the zone.
        size='500px',
        # Align cards on the cross-axis (vertical direction for ROW and horizontal for COLUMN).
        align='center',
        # Align cards on the main-axis (vertical direction for COLUMN and horizontal for ROW).
        justify='around'
    )
])])
q.page['card1'] = ui.tall_info_card(box=ui.box('content', width='200px', height='200px'), name='',
                                    title='Card', caption='Lorem ipsum')
q.page['card2'] = ui.tall_info_card(box=ui.box('content', width='200px', height='200px'), name='',
                                    title='Card', caption='Lorem ipsum')
q.page['card3'] = ui.tall_info_card(box=ui.box('content', width='200px', height='200px'), name='',
                                    title='Card', caption='Lorem ipsum')
```

To see the full alignment effect it's important to note that card sizes have to either be explicitly set (like in the example code) or the `size` attribute has to be set to `'0'` (since it defaults to `'1'`).

## Grid Layout

By default, every page has a grid system in place, designed to fit HD displays (1920 x 1080 pixels). The grid has 12 columns and 10 rows. A column is 134 pixels wide. A row is 76 pixels high. The gap between rows and columns is set to 15 pixels.

## Placing cards

Every card has a `box` attribute that specifies how to position the card on the page, a string of the form `'column row width height'`, for example `'1 1 2 4'` or `'8 7 3 6'`.

```py {2}
page['quote'] = ui.markdown_card(
    box='1 1 2 2',
    title='Hello World',
    content='"The Internet? Is that thing still around?" - *Homer Simpson*',
)
```

The `column` and `row` indicate which column and row to position the top-left corner of the card. The `width` and `height` indicate the width and height of the cards, respectively. The `column` and `row` are 1-based. The `width` and `height` are in spans (units) of columns or rows.

| Attribute | Value | Interpreted as |
| --------- | ----- | -------------- |
| column    | N     | Nth column     |
| row       | N     | Nth row        |
| width     | N     | N columns wide |
| height    | N     | N rows high    |

For example, a `box` of `1 2 3 4` is interpreted as:

| Attribute | Value | Interpreted as |
| --------- | ----- | -------------- |
| column    | 1     | 1st column     |
| row       | 2     | 2nd row        |
| width     | 3     | 3 columns wide |
| height    | 4     | 4 rows high    |

:::tip
The default grid is designed to fit HD displays (1920 x 1080 pixels), so if you want your page to fit smaller displays, don't use up the entire grid. Instead, use up less columns and rows, like 8 columns x 6 rows.
:::

## See also

`layout.py` and `layout-size.py` in Tour app.

---

### Logging (`website/docs/logging.md`)

---
title: Logging
---

Wave apps are plain Python programs. Use Python's built-in `logging` module to configure logging.

Here's a basic configuration that logs a ISO8601 timestamp, log level, and message:

```py {3,4}
from h2o_wave import Q, main, app

import logging
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s')

@app('/demo')
async def serve(q: Q):
    logging.warning('All your base are belong to us')    
```

The above snippet makes the app log a warning every time it is accessed. This is what the logged message look like:

```
2010-12-12 11:41:42,612 WARNING All your base are belong to us
```

:::tip
To better see your log messages, you might want to turn off Wave server logging by setting the `H2O_WAVE_NO_LOG` environment variable to `1`.
:::

:::info
See [Python's logging module](https://docs.python.org/3/howto/logging.html) for more information.
:::

---

### Pages (`website/docs/pages.md`)

---
title: Pages
---

The Wave server stores and manages content. Content is stored in a page cache, called a *site*. A Wave server contains exactly one site. A site holds a collection of *pages*. A page is composed of *cards*. Cards hold content and [data buffers](buffers.md).

## Reference

To reference a site from within a Wave script, import `site`.

```py
from h2o_wave import site
```

`site` is a dictionary-like object.

To reference the current site from within a Wave app, use `q.site`.

```py
async def serve(q: Q):
    site = q.site
```

To reference a page hosted at `/foo`, use `site['/foo']`.

```py
page = site['/foo']
```

To reference the current page in a Wave app, use `q.page`.

```py
async def serve(q: Q):
    page = q.page
```

`page` is also a dictionary-like object. To reference a card named `foo`, use `page['foo']`.

```py
card = page['foo']
```

## Add

There are two ways to add a card to a page.

The first way is to assign a new card to `page['foo']`.

```py
page['foo'] = card
```

The second way is to use `page.add()`. This is useful when you want to add a card to a page and obtain a reference to the new card.

```py
card = page.add('foo', card)
```

The following two forms are equivalent. The second form is more concise.

```py
page['foo'] = ui.form_card(...)
card = page['foo']
```

```py
card = page.add('foo', ui.form_card(...)
```

## Delete

To delete a card named `foo` from a page, use `del page['foo']`:

```py
del page['foo']
```

To delete the page hosted at `/foo`, use `page.drop()` or `del site['/foo']`. The following three forms are equivalent.

```py
page = site['/foo']
page.drop()
```

```py
site['/foo'].drop()
```

```py
del site['/foo']
```

Deleting a page automatically drops all cards associated with that page. Conversely, to delete all cards from a page, simply delete the page.

To clear all cards in the active page from within a Wave app, use `q.page.drop()`:

```py
async def serve(q: Q):
    q.page.drop()
```

## Replace

Assigning a card to `page['foo']` replaces any previously assigned card named `foo`. Therefore, the following two forms are equivalent. The second form is more concise, hence preferred.

```py
page['foo'] = card1
del page['foo']
page['foo'] = card2
```

```py
page['foo'] = card1
page['foo'] = card2
```

## Update

To avoid recreating the card from scratch, but to only update a specific attribute, use `name` attribute to get the reference to the component:

```py
q.page['example'] = ui.form_card(box='1 1 4 4', items=[
    ui.button(name='my_btn', label='Old label'),
])
q.page['example'].my_btn.label = 'New label'
```

## Save

To save a page from within a Wave script, use `page.save()`.

```py
page.save()
```

To save the active page from within a Wave app, use `q.page.save()`.

```py
async def serve(q: Q):
    await q.page.save()
```

:::caution
`q.page.save()` is an `async` function, so you must `await` while calling it.
:::

You don't need to explicitly create a new page. A page is automatically created on save if it doesn't exist.

## Control non-app pages

To update other (non-app) pages from within an app, use `AsyncSite`:

```py
from h2o_wave import Q, AsyncSite

site = AsyncSite()

async def serve(q: Q):
    page = site['/foo']
    page['bar'] = card
    await page.save()
```

## Animations (experimental)

Cards can be animated during their creation by setting `animate=True` on the `ui.meta_card`.

```py
page['meta'] = ui.meta_card(box='', animate=True)
```

:::warning
An animation is considered fluid when it is able to reach `60fps` (frames per second). To achieve such fps, one needs to make sure the browser has enough resources to paint the animation at such rate. Loading too many data or doing too much browser work may result in janky animations so use at your own risk.
:::

Default animation is a simple `fade in` animation (opacity 0 => 1) lasting for `0.5s`. if you are not happy with these defaults, it's possible to override these values via [custom CSS](/docs/custom-css/).

```css
:root {
  /* Custom CSS properties to configure duration and animation. */
  --wave-animation-duration: 0.5s;
  --wave-card-animation: wave-fade-in
}

/* Override this class to get full control over animations. */
.wave-animate-card {
  animation: var(--wave-card-animation);
  animation-duration: var(--wave-animation-duration);
}

@keyframes wave-fade-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}
```

---

### Plotting (`website/docs/plotting.md`)

---
title: Plots
---

Wave provides a versatile plotting API based on [Leland Wilkinson's](https://en.wikipedia.org/wiki/Leland_Wilkinson) [Grammar of Graphics](http://www.springer.com/gp/book/9780387245447).

A *plot* is a layered graphic, created using `ui.plot()`. Each layer displays *marks*, described by `ui.mark()`. The layers are rendered on top of each other to produce the final plot.

`ui.mark()` describes a collection of marks, not one mark. Since each `ui.mark()` describes one layer in the plot, it follows that all the marks on a layer are of the same `type` (its *geometry*). A mark's `type` can be one of `point`, `interval`, `line`, `path`, `area`, `polygon`, `schema`, `edge`.

There are two ways to add plots to a page:

- Use a plot card (`ui.plot_card()`) and set its `plot` attribute using `ui.plot()`.
- Use a form card (`ui.form_card()`), insert a visualization (`ui.visualization()`) and set its `plot` attribute using `ui.plot()`. See [form visualization](/docs/widgets/form/visualization) for more info.

Here's a short example that renders a scatterplot of values between [0, 1].

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Point',
    data=data('height weight', 10, rows=[
        (170, 59),
        (159.1, 47.6),
        (166, 69.8),
        (176.2, 66.8),
        (160.2, 75.2),
        (180.3, 76.4),
        (164.5, 63.2),
        (173, 60.9),
        (183.5, 74.8),
        (175.5, 70),
    ]),
    plot=ui.plot([ui.mark(type='point', x='=weight', y='=height')])
)
```

## Annotation layers

Wave plots are rendered in layers, one upon each other. This mechanism allows mixing multiple plots
and annotations into a single canvas.

`ui.plot()` accepts a list of marks. The following example renders annotations on top of a scatterplot.

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Numeric-Numeric',
    data=data('height weight', 10, rows=[
        (170, 59),
        (159.1, 47.6),
        (166, 69.8),
        (176.2, 66.8),
        (160.2, 75.2),
        (180.3, 76.4),
        (164.5, 63.2),
        (173, 60.9),
        (183.5, 74.8),
        (175.5, 70),
    ]),
    plot=ui.plot([
        ui.mark(type='point', x='=weight', y='=height', x_min=0, x_max=100, y_min=0, y_max=100),  # the plot
        ui.mark(x=50, y=50, label='point'),  # A single reference point
        ui.mark(x=40, label='vertical line'),
        ui.mark(y=40, label='horizontal line'),
        ui.mark(x=70, x0=60, label='vertical region'),
        ui.mark(y=70, y0=60, label='horizontal region'),
        ui.mark(x=30, x0=20, y=30, y0=20, label='rectangular region')
    ])
)
```

## Events

In order to be able to handle wave events, simply register them via `events` attribute of the
plot. Depending on the event type registered, when the event fires, it fills
`q.events.<card-name>.<event-name>` within python serve function.

Wave currently supports one plot selection event called `select_marks`. Registering and firing
this event will thus result in `q.events.example.select_marks` filled.

You can also visit [handling events section](/docs/routing#handling-events) to learn more about
advanced techniques.

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Interval, range',
    data=data('profession salary', 5, rows=[
        ('medicine', 33000),
        ('fire fighting', 18000),
        ('pedagogy', 24000),
        ('psychology', 22500),
        ('computer science', 36000),
    ]),
    events=['select_marks'],
    plot=ui.plot([ui.mark(type='interval', x='=salary', y='=profession', y_min=0)])
)
```

### Disable events

If you do not wish to raise wave events on interaction with a specific mark, simply specify the `interactive=False` attribute.

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
        box='1 1 4 5',
        title='Interval, range',
        data=data('year value', 8, rows=[
            ('1991', 3),
            ('1992', 4),
            ('1993', 3.5),
            ('1994', 5),
            ('1995', 4.9),
            ('1996', 6),
            ('1997', 7),
            ('1998', 9),
            ('1999', 13),
        ]),
        events=['select_marks'],
        plot=ui.plot([
            ui.mark(type='line', x_scale='time', x='=year', y='=value', y_min=0, interactive=False), 
            ui.mark(type='point', x='=year', y='=value', size=8, fill_color='red')
        ]),
)
```

## Custom axis title

If you are not happy with the defaults provided, simply use either `x_title` or `y_title` attribute.

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Line title',
    data=data('month price', 12, rows=[
        ('Jan', 51),
        ('Feb', 91),
        ('Mar', 34),
        ('Apr', 47),
        ('May', 63),
        ('June', 58),
        ('July', 56),
        ('Aug', 77),
        ('Sep', 99),
        ('Oct', 106),
        ('Nov', 88),
        ('Dec', 56),
    ]),
    plot=ui.plot([
        ui.mark(type='line', x='=month', y='=price', y_min=0, x_title='Month', y_title='Price')
    ])
)
```

## Custom scales

You can format your scales any way you want basically since `x` and `y` can be [expressions](/docs/expressions). The following example displays a way to show date and time from timestamp data.

```py
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Line',
    data=data('year value', 9, rows=[
        ('2020-05-04T10:22:21', 3),
        ('2020-05-04T11:22:21', 4),
        ('2020-05-04T12:22:21', 3.5),
        ('2020-05-04T13:22:21', 5),
        ('2020-05-04T14:22:21', 4.9),
        ('2020-05-04T15:22:21', 6),
        ('2020-05-04T16:22:21', 7),
        ('2020-05-04T17:22:21', 9),
        ('2020-05-04T18:22:21', 13),
    ]),
    plot=ui.plot([
        ui.mark(
            type='line',
            x_scale='time',
            x='={{intl year type="time" month="numeric" day="numeric" hour="numeric" minute="numeric" hourCycle="h24" }}',
            y='=value',
            y_min=0
        )
    ])
)
```

See [formatting options](/docs/expressions#functions) for more info.

## Horizontal

By default, all the plot cards are rendered vertically. However, there might be cases when you also want to provide a plot caption and conserve the space.

```py
from h2o_wave import data

q.page['example'] = ui.wide_plot_card(
    box='1 1 5 4',
    title='Wide Plot Card',
    caption='''
    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia aliquam maxime quos facere
    necessitatibus tempore eum odio, qui illum. Repellat modi dolor facilis odio ex possimus
    ''',
    data=data('profession salary', 5, rows=[
        ('medicine', 23000),
        ('fire fighting', 18000),
        ('pedagogy', 24000),
        ('psychology', 22500),
        ('computer science', 36000),
    ]),
    plot=ui.plot([ui.mark(type='interval', x='=profession', y='=salary', y_min=0)])
)
```

## Animations (experimental)

Plots support basic animations when `animate=True`.

:::warning
An animation is considered fluid when it is able to reach `60fps` (frames per second). To achieve such fps, one needs to make sure the browser has enough resources to paint the animation at such rate. Loading too many data or doing too much browser work may result in janky animations so use at your own risk.
:::



```py {6}
from h2o_wave import data

q.page['example'] = ui.plot_card(
    box='1 1 4 5',
    title='Line',
    animate=True,
    data=data('year value', 8, rows=[
        ('1991', 3),
        ('1992', 4),
        ('1993', 3.5),
        ('1994', 5),
        ('1995', 4.9),
        ('1996', 6),
        ('1997', 7),
        ('1998', 9),
        ('1999', 13),
    ]),
    plot=ui.plot([ui.mark(type='line', x_scale='time', x='=year', y='=value', y_min=0)])
)
```

## Point

- [Basic](/docs/examples/plot-point): Make a scatterplot.
- [Shapes](/docs/examples/plot-point-shapes): Make a scatterplot with categories encoded as mark shapes.
- [Sizes](/docs/examples/plot-point-sizes): Make a scatterplot with mark sizes mapped to a continuous variable (a "bubble plot").
- [Map](/docs/examples/plot-point-map): Make a plot to compare quantities across categories. Similar to a heatmap, but using size-encoding instead of color-encoding.
- [Groups](/docs/examples/plot-point-groups): Make a scatterplot with categories encoded as colors.
- [Annotation](/docs/examples/plot-point-annotation): Add annotations (points, lines and regions) to a plot.
- [Custom](/docs/examples/plot-point-custom): Customize a plot's fill/stroke color, size and opacity.

## Interval

### Columns

- [Basic](/docs/examples/plot-interval): Make a column plot.
- [Groups](/docs/examples/plot-interval-groups): Make a grouped column plot.
- [Range](/docs/examples/plot-interval-range): Make a column plot with each bar representing high/low (or start/end) values. Transposing this produces a gantt plot.
- [Labels](/docs/examples/plot-interval-labels): Make a column plot with labels on each bar.
- [Stacked](/docs/examples/plot-interval-stacked): Make a stacked column plot.
- [Stacked, Grouped](/docs/examples/plot-interval-stacked-grouped): Make a column plot with both stacked and grouped bars.
- [Annotation](/docs/examples/plot-interval-annotation): Add annotations to a column plot.
- [Theta](/docs/examples/plot-interval-theta): Make a "racetrack" plot (a column plot in polar coordinates).
- [Theta, Stacked](/docs/examples/plot-interval-theta-stacked): Make a stacked "racetrack" plot.

### Bars

- [Basic](/docs/examples/plot-interval-transpose): Make a bar plot.
- [Groups](/docs/examples/plot-interval-groups-transpose): Make a grouped bar plot.
- [Range](/docs/examples/plot-interval-range-transpose): Make a bar plot with each bar representing high/low (or start/end) values. Transposing this produces a gantt plot.
- [Stacked](/docs/examples/plot-interval-stacked-transpose): Make a stacked bar plot.
- [Stacked, Grouped](/docs/examples/plot-interval-stacked-grouped-transpose): Make a bar plot with both stacked and grouped bars.
- [Annotation](/docs/examples/plot-interval-annotation-transpose): Add annotations to a bar plot.
- [Polar](/docs/examples/plot-interval-polar): Make a rose plot (a bar plot in polar coordinates).
- [Polar, Stacked](/docs/examples/plot-interval-polar-stacked): Make a stacked rose plot (a stacked bar plot in polar coordinates).
- [Helix](/docs/examples/plot-interval-helix): Make a bar plot in helical coordinates.

## Line

- [Basic](/docs/examples/plot-line): Make a line plot.
- [Groups](/docs/examples/plot-line-groups): Make a multi-series line plot.
- [Smooth](/docs/examples/plot-line-smooth): Make a line plot using a smooth curve.
- [Step](/docs/examples/plot-step): Make a line plot with a step curve.
- [Step, After](/docs/examples/plot-step-after): Make a line plot with a step-after curve.
- [Step, Before](/docs/examples/plot-step-before): Make a line plot with a step-before curve.
- [Labels](/docs/examples/plot-line-labels): Add labels to a line plot.
- [Labels, Custom](/docs/examples/plot-line-labels-custom): Add customized labels to a line plot.
- [Labels, Stroked](/docs/examples/plot-line-labels-stroked): Customize label rendering: add a subtle outline to labels to improve readability.
- [Labels, Occlusion](/docs/examples/plot-line-labels-no-overlap): Make a line plot with non-overlapping labels.
- [Annotation](/docs/examples/plot-line-annotation): Add annotations to a line plot.

## Path

- [Basic](/docs/examples/plot-path): Make a path plot.
- [Point](/docs/examples/plot-path-point): Make a path plot with an additional layer of points.
- [Smooth](/docs/examples/plot-path-smooth): Make a path plot with a smooth curve.

## Area

- [Basic](/docs/examples/plot-area): Make an area plot.
- [Groups](/docs/examples/plot-area-groups): Make an area plot showing multiple categories.
- [Negative](/docs/examples/plot-area-negative): Make an area plot showing positive and negative values.
- [Range](/docs/examples/plot-area-range): Make an area plot representing a range (band) of values.
- [Smooth](/docs/examples/plot-area-smooth): Make an area plot with a smooth curve.
- [Stacked](/docs/examples/plot-area-stacked): Make a stacked area plot.

## Area + Line

- [Area + Line](/docs/examples/plot-area-line): Make an area plot with an additional line layer on top.
- [Area + Smooth](/docs/examples/plot-area-line-smooth): Make a combined area + line plot using a smooth curve.
- [Area + Groups](/docs/examples/plot-area-line-groups): Make an combined area + line plot showing multiple categories.

## Polygon

- [Basic](/docs/examples/plot-polygon): Make a heatmap.

## Schema

- [Histogram](/docs/examples/plot-histogram): Make a histogram.

## Other

- [Axis Titles](/docs/examples/plot-axis-title): Display custom axis titles on a plot.
- [Form](/docs/examples/plot-form): Display a plot inside a form.

---

### Project Templates (`website/docs/project-templates.md`)

---
title: "Project templates"
---

Wave comes with useful app templates to boost the app development startup time. Use `wave init` CLI command and pick the one you like or use `w_app_<template-name>` code snippet coming with the [IDE plugins](https://wave.h2o.ai/docs/development/#using-pycharm).



## Hello World app



## App with header



## App with header and navigation



## App with sidebar and navigation



## App with header, sidebar and navigation

---

### Realtime (`website/docs/realtime.md`)

---
title: Realtime Sync
---

Wave's realtime sync feature enables all connected users to see up to date content. 

Pages created by Wave scripts are automatically synced across all users. 

Pages created by Wave apps need to explicitly enable realtime sync. This is because apps support multiple users and multiple clients (browser tabs) by default, and, depending on the problem you're trying to solve, it's up to you to decide whether your app's UI should be synced across all users, synced across one user, or not synced at all.

To enable realtime sync in a Wave app, pass the `mode` argument to `@app()`:
- `mode='broadcast'` syncs across all users.
- `mode='multicast'` syncs across one user (in other words, all the clients for that user).
- `mode='unicast'` disables sync. This is the default.

```py {3}
from h2o_wave import Q, main, app, ui

@app('/foo', mode='broadcast')
async def serve(q: Q):
    pass
```

:::tip
You can also set the mode using the `H2O_WAVE_APP_MODE` environment variable.
:::

If you change the mode, make sure you store run-time state appropriate to the mode. Generally:

| `mode` | Store state in |
|---|---|
|`broadcast`| `q.app` |
|`multicast`| `q.user` |
|`unicast`| `q.client` |

---

### Routing (`website/docs/routing.md`)

---
title: Routing
---

## App routing

Your Wave app gets hosted at the route you passed to `@app()`.

```py {3}
from h2o_wave import Q, main, app

@app('/foo')
async def serve(q: Q):
    pass
```

To host your app at `localhost:10101/foo` or `www.example.com/foo`, pass `/foo` to `@app()`.

To host your app at `localhost:10101` or `www.example.com`, pass `/` to `@app()`. Do this if you plan to host exactly one app and nothing else.

You can host multiple apps behind a single Wave server.

:::caution
`/foo` and `/foo/bar` are two distinct paths. `/foo/bar` is not interpreted as a sub-path of `/foo`.
:::

## Hash routing

Wave apps support *hash routing*, a popular client-side mechanism where the location hash (the `baz/qux` in `/foo/bar#baz/qux`) can be used to decide which part of the UI to display.

### Setting the location hash

To set the location hash, prefix `#` to the `name` attribute of command-like components. When the command is invoked, the location hash is set to the name of the command.

For example, if a button is named `foo` is clicked, `q.args.foo` is set to `True`. Instead, if a button named `#foo` is clicked, the location hash is set to `foo` (`q.args.foo` is not set).

```py {8-9}
from h2o_wave import Q, main, app, ui

@app('/toss')
async def serve(q: Q):
    q.page['sides'] = ui.form_card(
        box='1 1 4 4',
        items=[
            ui.button(name='#heads', label='Heads'),
            ui.button(name='#tails', label='Tails'),
        ],
    )
    await q.page.save()
```

Names don't have to be alphanumeric, so you can use names with nested sub-paths like `#foo/bar`, `#foo/bar/baz`, `#foo/bar/baz/qux` to make route-handling more manageable.

The components that support setting a location hash are:

- `ui.button()`
- `ui.command()`
- `ui.nav_item()`
- `ui.tab()`
- `ui.breadcrumb()`

### Setting the location hash from python

To set the hash route explicitly without command invocation as described above, `ui.meta_card` is the way to go.

```py
# If meta_card does not yet exist, create one. If it does, skip this line.
q.page['meta'] = ui.meta_card(box='')
# Redirect to a hash.
q.page['meta'].redirect = '#widgets'
# Apply the change in browser.
await q.page.save()
```

### Getting the location hash

To get the location hash, read `q.args['#']` (a string). If the route in the browser's address bar is `/foo/bar#baz/qux`, `q.args['#']` is set to `baz/qux`.

```py {5-9}
from h2o_wave import Q, main, app, ui

@app('/toss')
async def serve(q: Q):
    hash = q.args['#']
    if hash == 'heads':
        print('Heads!')
    elif hash == 'tails':
        print('Tails!')

    q.page.save()
```

### Hash route switching

Combining the two examples above gives us a basic pattern for handling routes and updating the user interface:

```py {7,9,11}
from h2o_wave import Q, main, app, ui

@app('/toss')
async def serve(q: Q):
    hash = q.args['#']

    if hash == 'heads':
        q.page['sides'].items = [ui.message_bar(text='Heads!')]
    elif hash == 'tails':
        q.page['sides'].items = [ui.message_bar(text='Tails!')]
    else:
        q.page['sides'] = ui.form_card(
            box='1 1 4 4',
            items=[
                ui.button(name='#heads', label='Heads'),
                ui.button(name='#tails', label='Tails'),
            ],
        )

    await q.page.save()
```

### Organizing code

In most sizeable applications, the logic in the above `if/elif/else` conditionals can call into sub-functions, possibly spread across other modules:

```py {23,25,27}
from h2o_wave import Q, main, app, ui

async def on_heads(q: Q):
    q.page['sides'].items = [ui.message_bar(text='Heads!')]

async def on_tails(q: Q):
    q.page['sides'].items = [ui.message_bar(text='Tails!')]

async def setup_page(q: Q):
    q.page['sides'] = ui.form_card(
        box='1 1 4 4',
        items=[
            ui.button(name='#heads', label='Heads'),
            ui.button(name='#tails', label='Tails'),
        ],
    )

@app('/toss')
async def serve(q: Q):
    hash = q.args['#']

    if hash == 'heads':
        await on_heads(q)
    elif hash == 'tails':
        await on_tails(q)
    else:
        await setup_page(q)

    await q.page.save()
```

### Reducing boilerplate

As your application gets larger, using the above `if/elif/else` conditionals can seem tedious or repetitive. If so, you can use `on` and `run_on` to reduce the boilerplate.

:::warning
In versions `<1.0` this function used to be called `handle_on`, but is now deprecated due to its drawbacks that the new `run_on` mechanism addressed.
:::

```py {3,7,22}
from h2o_wave import Q, main, app, ui, on, run_on

@on('#heads')
async def on_heads(q: Q):
    q.page['sides'].items = [ui.message_bar(text='Heads!')]

@on('#tails')
async def on_tails(q: Q):
    q.page['sides'].items = [ui.message_bar(text='Tails!')]

async def setup_page(q: Q):
    q.page['sides'] = ui.form_card(
        box='1 1 4 4',
        items=[
            ui.button(name='#heads', label='Heads'),
            ui.button(name='#tails', label='Tails'),
        ],
    )

@app('/toss')
async def serve(q: Q):
    if not await run_on(q):
        await setup_page(q)

    await q.page.save()
```

In the above example, the `@on('#heads')` is read as "if `q.args['#']` is `'heads'`, then invoke the function the `@on()` is applied to" - in this case, `on_heads()`.

### Pattern matching

The `@on()` annotation supports pattern matching.

This function is called when `q.args['#'] == 'menu'`:

```py
@on('#menu')
async def show_menu(q: Q):
    pass
```

This function is called when `q.args['#'] == 'menu/donuts'`:

```py
@on('#menu/donuts')
async def show_donuts(q: Q):
    pass
```

This function is called when `q.args['#']` matches, say, 'menu/donuts/chocolate', with the parameter `donut_name` set to 'chocolate':

```py
@on('#menu/donuts/{donut_name}')
async def show_donut(q: Q, donut_name: str):
    pass
```

Same as above, but `donut_name` is explicitly set to a string:

```py
@on('#menu/donuts/{donut_name:str}')
async def show_donut(q: Q, donut_name: str):
    pass
```

This function is called when `q.args['#']` matches, say, 'menu/donuts/42',  with the parameter `donut_id` set to 42:

```py
@on('#menu/donuts/{donut_id:int}')
async def show_donut(q: Q, donut_id: int):
    pass
```

This function is called when `q.args['#']` matches, say, 'menu/donuts/7e21c93f-3a8f-4994-b63e-4275bc975e60', with the parameter `donut_id` set to the UUID:

```py
@on('#menu/donuts/{donut_id:uuid}')
async def show_donut(q: Q, donut_id: uuid.UUID):
    pass
```

This function is called when `q.args['#']` matches, say, 'menu/donuts/below/2.99', with the parameter `donut_price` set to 2.99:

```py
@on('#menu/donuts/below/{donut_price:float}')
async def show_donuts_below(q: Q, donut_price: float):
    pass
```

### Handling query arguments

The `@on()` annotation can test the contents of `q.args` and invoke the corresponding handler.

This function is called when `q.args.buy_now` is found and the value is truthy:

```py
@on('buy_now')
async def buy_donuts(q: Q):
    print(q.args.buy_now)
```

The handler can accept the value of the argument as well. Compare:

```py
@on('buy_now')
async def buy_donuts(q: Q, buy_now: bool):
    print(buy_now)
```

This function is called when `q.args.jam_filled` is False:

```py
@on('jam_filled', lambda x: x is False)
async def buy_plain_donuts(q: Q):
    pass
```

This function is called when `q.args.jam_filled` is True or False:

```py
@on('jam_filled', lambda x: isinstance(x, bool)
async def buy_donuts(q: Q, jam_filled: bool):
    pass
```

This function is called when `q.args.quantity` between 42 and 420:

```py
@on('quantity', lambda x: 42 <= x <= 420)
async def buy_donuts(q: Q, quantity: bool):
    pass
```

### Handling events

The `@on()` annotation can also test the contents of `q.events` and invoke the corresponding handler.

This function is called when `q.events.donut_plot.select_marks` is found and the value is truthy:

```py
@on('donut_plot.select_marks')
async def on_marks_selected(q: Q):
    pass
```

This function is called when `q.events.donut_plot.select_marks` is 0:

```py
@on('donut_plot.select_marks', lambda x: x == 0)
async def on_marks_selected(q: Q):
    pass
```

This function is called when `q.events.donut_plot.select_marks` is an integer:

```py
@on('donut_plot.select_marks', lambda x: isinstance(x, int))
async def on_marks_selected(q: Q, count: int):
    pass
```

This function is called when `q.events.donut_plot.select_marks` between 42 and 420:

```py
@on('donut_plot.select_marks', lambda x: 42 <= x <= 420)
async def on_marks_selected(q: Q, count: int):
    pass
```

### Handling user logout

To get notified when a user logs out of your apps, use the system-wide `@system.logout` event.

```py
@on('@system.logout')
async def on_user_logout(q: Q):
    print(f'User {q.auth.username} logged out.')
```

Note that when a user logs out of the Wave daemon, all the apps linked to the daemon get notified with a `@system.logout` event.

### Handling client (browser tab) disconnect

To get notified when a user closes the tab, use the system-wide `@system.client_disconnect` event. The time if takes for this function to be called depends on the value of `H2O_WAVE_RECONNECT_TIMEOUT` (which defaults to `5s`).

```py
@on('@system.client_disconnect')
async def on_client_disconnect(q: Q):
    print('Client disconnected')
```

---

### Scripts (`website/docs/scripts.md`)

---
title: Wave Scripts
---

A Wave script is the simplest way to publish content with Wave, especially live web content:

- Dashboards and visualizations for live monitoring.
- Live information displays: news, tickers, health, or performance data.

A Wave script is one kind of program you can author to interact with Wave. The other kind is a [Wave App](apps.md). The primary difference between an app and a script is that apps are interactive (able to handle user interactions) and scripts are not. If you are not interested in handling user interactions, and only want to publish content, use a Wave script.

Here is the skeleton of a Wave script ([example](tutorial-hello.mdx)):

```py
from h2o_wave import site, ui

# Grab a reference to a page
page = site['/foo']

# Modify the page
page['qux'] = ui.some_card()

# Save the page
page.save()
```

You can run it by using `python my_script.py` assuming your [Wave server](/docs/tutorial-hello#step-1-start-the-wave-server) is running.

Here is the skeleton of a Wave script that continuously updates a page ([example](tutorial-monitor.md)):

```py
import time
from h2o_wave import site, ui

# Grab a reference to a page
page = site['/foo']

# Grab a reference to a card on the page
card = page['qux']

# Update the page with stats every second
while True:
    # Read data from somewhere
    cpu_percent, mem_usage, disk_usage = read_system_stats()

    # Update card's data
    card.data[-1] = [cpu_percent, mem_usage, disk_usage]
    
    # Save the page
    page.save()
    
    # Wait a second
    time.sleep(1)
```

To update pages at a much lower frequency (say, minutes, hours, days), use a scheduler like [cron](https://en.wikipedia.org/wiki/Cron) along with a simpler Wave script:

```py {10-17}
import time
from h2o_wave import site, ui

# Grab a reference to a page
page = site['/foo']

# Grab a reference to a card on the page
card = page['qux']

# Read data from somewhere
cpu_percent, mem_usage, disk_usage = read_system_stats()

# Update card's data
card.data[-1] = [cpu_percent, mem_usage, disk_usage]

# Save the page
page.save()
```

Multiple Wave scripts running on multiple devices can update the same Wave page. You can use this capability to publish a single page that displays content originating from multiple sources. For example, a single page that displays stats for all the systems in your network, or a single page that displays tickers from different stock exchanges.

---

### Security (`website/docs/security.md`)

---
title: Security
---

## Wave Server API Access Keys

Wave apps and scripts access the Wave server using access keys via [HTTP Basic Authentication](https://tools.ietf.org/html/rfc7617).

An application access key is a pair of strings: ID and Secret.

### Development

The default ID and secret are the strings `access_key_id`/`access_key_secret`, which are set for development convenience, but are obviously not secure.

During development, you can change the default ID and secret when you start the Wave server like this:

```shell
./waved -access-key-id <id> -access-key-secret <secret>
```

If you change the ID and secret, you'll need to ensure that your app or script uses the new credentials by setting the `H2O_WAVE_ACCESS_KEY_ID` and `H2O_WAVE_ACCESS_KEY_SECRET` environment variables accordingly.

### Production

For production deployments, you should generate cryptographically secure random ID/secret pairs like this:

```shell
$ ./waved -create-access-key

SUCCESS!

Make sure to copy your new access key ID and secret now.
You won't be able to see it again!

H2O_WAVE_ACCESS_KEY_ID=ENHL90KR2HZD6X2ZIYLZ
H2O_WAVE_ACCESS_KEY_SECRET=dxQPcenUJJgLes8rxkp7rUj02t2y3hCqBteyvY2I

Your key was also added to the keychain located at
.wave-keychain

```

The above command also stores the credentials in a file named `.wave-keychain` in the current working directory. The file format is similar to a [.htpasswd](https://en.wikipedia.org/wiki/.htpasswd) file, but always uses [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) hashes. Note that the access key secret displayed on the console is not stored anywhere, and cannot be recovered. If you lose the secret, simply generate a new one and reconfigure your app to use the new secret.

You can also make the `-create-access-key` command use a keychain file located elsewhere, like this:

```shell
./waved -create-access-key -access-keychain /path/to/file.extension
```

The Wave server uses the keychain file to authenticate requests from apps and scripts. By default, it automatically loads the `.wave-keychain` file if present in the current working directory.

To make the Wave server use a specific keychain file, launch it like this:

```shell
./waved -access-keychain /path/to/file.extension
```

To view a sorted list of all the keys in a keychain file, use `-list-access-keys`, like this:

```shell
$ ./waved -list-access-keys
ENHL90KR2HZD6X2ZIYLZ
IDID44ZK0L7NG8NDD7IC
N8CK63JT6OZIOX2SWALR
PCBTV5TNQKOSAU2EBD6D
VAKQY6QJN3RRQDU5LD0E
```

To remove a key from a keychain file, use `-remove-access-key`, like this:

```shell
./waved -remove-access-key ENHL90KR2HZD6X2ZIYLZ
```

To remove a key from a keychain file located elsewhere, do this:

```shell
./waved -remove-access-key ENHL90KR2HZD6X2ZIYLZ -access-keychain /path/to/file.extension
```

## HTTPS

To enable HTTP over TLS to secure your Wave server, pass the following flags when starting the Wave server:

- `-tls-cert-file`: path to certificate file or using `H2O_WAVE_TLS_CERT_FILE` env variable.
- `-tls-key-file`: path to private key file or using `H2O_WAVE_TLS_KEY_FILE` env variable.

File paths need to be either absolute or relative to the Wave server (waved) location.

Once set, the Wave app needs to know it should talk to the Wave server via `https` and not `http` as it does by default. This can be set using `H2O_WAVE_ADDRESS="https://127.0.0.1:10101"` env variable when starting the Wave app.

### Self Signed Certificate

To enable TLS during development, use a self-signed certificate.

To create a private key and a self-signed certificate from scratch, use `openssl`:

```shell
openssl req \
   -newkey rsa:2048 -nodes -keyout domain.key \
   -x509 -days 365 -out domain.crt
```

The above command creates a 2048-bit private key (`domain.key`) and a self-signed x509 certificate (`domain.crt`) valid for 365 days.

## Single Sign On

Wave has built-in support for [OpenID Connect](https://openid.net/connect/).

To enable OpenID Connect, pass the following flags when starting the Wave server:

- `-oidc-provider-url`: The URL for authentication (the identity provider's URL).
- `-oidc-redirect-url`: The URL to redirect back to after authentication. This is typically `/_auth/callback` appended to the Wave server's address. For example, if the Wave server is running at `https://192.168.42.42:80`, set this to `https://192.168.42.42:80/_auth/callback`. If you're testing your app's authorization workflow during development and the Wave server is running at `http://localhost:10101`, you can set this argument to `http://localhost:10101/_auth/callback`. If you also specified the `-base-url` argument for Wave server, then make sure the redirect URL includes the base URL. For example, if the base URL is set to `/my/app/`, set the redirect URL to `https://192.168.42.42:80/my/app/_auth/callback`.
- `-oidc-client-id`: Client ID (refer to your identity provider's documentation).
- `-oidc-client-secret`: Client secret (refer to your identity provider's documentation).
- `-oidc-end-session-url`: (Optional) URL to log out (refer to your identity provider's documentation). This flag is optional and might not be supported by your identity provider.
- `-oidc-scopes`: (Optional) Comma-separated scopes that will override defaults (`openid,profile`).
- `-oidc-skip-login`: (Optional) Don't show the built-in login form during OIDC authorization. Instead, navigate directly to the identity provider's login form.
- `-oidc-auth-url-params`: (Optional) Additional URL parameters to pass during OIDC authorization.

Once authenticated, you can access user's authentication and authorization information from your app using `q.auth` (see the [Auth](/docs/api/server#h2o_wave_server_Auth) class for details):

```py
from h2o_wave import Q, main, app

@app('/example')
async def serve(q: Q):
    print(q.auth.username)
    print(q.auth.access_token)
```

### Azure

By default, Azure provides you with URL like <https://login.microsoftonline.com/$UUID/oauth2/v2.0/authorize>, resulting in an error:

```sh
panic: failed connecting to OIDC provider: 404 Not Found:
```

Change the URL to `https://sts.windows.net/$UUID/` instead to make OpenID work. You can also try with `https://login.microsoftonline.com/$UUID/v2.0`

Note: When configuring redirect URIs in Microsoft Entra ID, select the Web platform. For local development, the URI should be set to: `http://localhost:10101/_auth/callback`

### Explicit token refresh

Note that access token is not refreshed automatically and it's not suited for long running jobs. The lifespan of a token depends on a provider settings but usually it's short. Access token is refreshed each time user performs an action i.e. the query handler `serve()` is called. However, if your UI is blocked (no user interacitons that could automatically refresh the token) and you are performing a long-running job, and still need fresh access token, you can call `ensure_fresh_token` function that refreshes and sets the token explicitly. Additionally, it also returns the access token if needed for async token providers.

```py
from h2o_wave import Q, main, app

@app('/example')
async def serve(q: Q):
    # Refreshes the token and makes it available in q.auth.access_token.
    new_access_token = await q.auth.ensure_fresh_token()
```

Synchronous version `ensure_fresh_token_sync` is also supported if your token provider is synchronous. However, using it is heavily discouraged due to its blocking nature - will make the Wave app super slow for all users, thus only recommended for throwaway, single user PoCs. **Async version is the preferred choice** to mitigate this.

### FAQ

- **I'm not sure what my oidc provider url is:** The openid connect configuration for any provider is made accessible through the `.well-known/openid-configuration` endpoint. The value of `-oidc-provider-url` must be the base url of your provider. For example, if the configuraton address is at `http://localhost:8080/realms/master/.well-known/openid-configuration`, then the provider url that you have to pass to wave is `http://localhost:8080/realms/master`. Do not use a trailing slash at the end of the provider url!
- **Do I have to implement the authenticaton callback myself?** No, the callback is handled by the wave server. As mentioned in the description for `-oidc-redirect-url` in the list above, the host part or the base-url suffix is what usually changes between deployment environments, so that's what you need to check for correctness.
- **The callback is working in my development environment but not in production, or vice versa:** Providers usually allow to register multiple callback URI's. Ensure that the correct and necessary callback URI's for all your deployments are registered in your provider's configuration (ergo, the value you use for `-oidc-redirect-url` is in the list of registered URI's). Otherwise, the redirect will fail with an error `The redirect URI included is not valid`.
- **My identity provider uses `http` but the authentication link points to `https` which makes the login fail:** This can happen for using a private deployment of an authentication provider where the custom setup might not match the expected setup of the authentication service (check the endpoints in your `.well-known/openid-configuration`). In general, when transferring private data, it should be encrypted by using methods like ssl or tsl. To solve this issue, you will need to check if the openid configuration of your provider can be customized, or [change the protocol](/docs/security#https) (HTTP/HTTPS) of your Wave server to match the one used by your provider.

## App Server API Access Keys

Access to a Wave app is controlled via [HTTP Basic Authentication](https://tools.ietf.org/html/rfc7617). The basic authentication username/password pair is automatically generated on app launch, and is visible only to the Wave server. You can manually override this behavior by setting the `$WAVE_APP_ACCESS_KEY_ID` / `$WAVE_APP_ACCESS_KEY_SECRET` environment variables (for development/testing only - not recommended in production).

## Additional HTTP Response Headers

You can make the Wave daemon include additional HTTP response headers by using the `-http-headers-file` command line argument to `waved`, pointing to a [MIME-formatted](https://en.wikipedia.org/wiki/MIME#MIME_header_fields) file.

[A sample file](https://github.com/h2oai/wave/blob/main/headers.txt) (make sure there are 2 empty lines at the end):

```txt title="headers.txt"
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block

```

## Accessing forwarded headers

It's possible to access the HTTP headers that were set on **websocket HTTP connection** request via `q.headers`. This might be useful in case built-in OIDC auth is not an option and you already have an existing, company-wide auth based on intercepting all the traffic and verifying if the requests are authenticated.

:::important
Since Wave is websocket-based, the headers retrieved do not come from the initial `GET index.html` request, but from the websocket `/_s/` one.
:::

For a more fine-grained control over which HTTP headers are forwarded or not, check the `H2O_WAVE_FORWARDED_HTTP_HEADERS` [configuration option](/docs/configuration#configuring-the-server).

## Content Security Policy

For XSS protection, it's possible to set [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) through [additional HTTP Response headers](/docs/security/#additional-http-response-headers).

Keep in mind that while turning off inline scripts and styles is a good idea security-wise, it may negatively affect some of the components that rely on inlining. These are usually 3rd party visualization libs that render their output in an iframe, notably Altair, Vega, Bokeh etc. In fact, even Wave's native visualization lib [G2](https://github.com/antvis/G2) uses inlining for some of its features.

---

### State (`website/docs/state.md`)

---
title: State
---

How you manage your Wave app's state depends on your app's requirements. In most cases, an app's data is best stored in a database or data store of some kind. But sometimes it's convenient to store run-time information in your app's memory, especially during prototyping when you're trying to iterate quickly and refine ideas.

In a Wave app, you can store run-time information at three levels:
- **App-level:** Information shared across all users.
- **User-level:** Information private to a user, but shared across all browser tabs.
- **Client-level:** Information private to a browser tab.

In real world apps, the decision on whether to store information at the client, user, or app level depends on the problem you're trying to solve. For example, if you were building an online store, you'd probably want to store product inventory at the app level and shopping carts at the user level. Most other kinds of information - search results, past orders, or product details - are best stored at the client-level (searching for products in one tab and having search results appear in another tab would drive even your most loyal customers up the wall).

In other words, your Wave app is multi-user by default, but how your app manages data at the app-level, at the user-level, and at the client-level is up to you.

The Wave query context `q` (of type [Q](api/server#q)) carries `q.app`, `q.user`, and `q.client`, three dictionary-like objects for storing information at the app-level, user-level, and client-level, respectively. 

:::tip
`q.app`, `q.user`, and `q.client` are all [Expando](api/core#Expando) instances, which means they behave both like dictionaries and objects: `q.client['foo']` is the same as `q.client.foo`. `q.client.foo` is easier to read.
:::


In most non-trivial apps, you'll find yourself frequently copying values from `q.args` into `q.client` (or `q.user`, depending on the problem you're solving). If this gets too repetitive for your taste, use `copy_expando()` to copy everything in `q.args` to `q.client` at the beginning of your `@app()` handler:

```py {5}
from h2o_wave import Q, main, app, copy_expando

@app('/foo')
async def serve(q: Q):
    copy_expando(q.args, q.client)
    # Do something else...
```

---

### Tutorial Beer (`website/docs/tutorial-beer.md`)

---
title: "Tutorial: Beer Wall"
---

In this tutorial, we'll learn how to update a web page periodically and observe our changes live in the browser. Completing this tutorial should improve your understanding of how easy it is to use Wave to publish information in realtime.

The program we'll be writing is a verse generator for the runaway mid-20th century smash hit, [99 Bottles of Beer](https://en.wikipedia.org/wiki/99_Bottles_of_Beer), which looks something like this:

> 99 bottles of beer on the wall, 99 bottles of beer.<br/>
> Take one down, pass it around, 98 bottles of beer on the wall...
>
> 98 bottles of beer on the wall, 98 bottles of beer.<br/>
> Take one down, pass it around, 97 bottles of beer on the wall...
>
> ...

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-beer__demo.mp4').default} type='video/mp4'/></video>

Our program will be analogous to [our "Hello, World!" program](tutorial-hello.mdx), with the addition of a loop. We'll generate a verse every second, and observe the verse change in the browser in realtime. After that, we'll take a stab at making our program a bit more efficient, introducing how [expressions](expressions.md) work.

(Incidentally, Donald Knuth proved that this song has a complexity of `O(log N)` in ["The Complexity of Songs"](http://www.cs.bme.hu/~friedl/alg/knuth_song_complexity.pdf), but we won't let that little detail deter us for now.)

## Prerequisites

This tutorial assumes your Wave server is up and running, and you have a working directory for authoring programs. If not, head over to the [Hello World tutorial](tutorial-hello.mdx) and complete steps 1 and 2.

## Step 1: Write your program

Our program looks like this. It's mostly similar to the one in the  [Hello World tutorial](tutorial-hello.mdx), with one exception: we're setting the markdown card's content inside a `for` loop.

```py {13-18} title="beer_wall.py"
import time
from h2o_wave import site, ui

page = site['/beer']

beer_card = page.add('wall', ui.markdown_card(
    box='1 1 4 2',
    title='99 Bottles of Beer',
    content='',
))

for i in range(99, 0, -1):
    beer_card.content = f"""
{i} bottles of beer on the wall, {i} bottles of beer.

Take one down, pass it around, {i - 1} bottles of beer on the wall...
"""
    page.save()
    time.sleep(1)
```

## Step 2: Run your program

```shell
python beer_wall.py
```

## Step 3: Watch updates live

Point your browser to [http://localhost:10101/beer](http://localhost:10101/beer), and watch the verses fly by.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-beer__demo.mp4').default} type='video/mp4'/></video>

## Step 4: Make it more efficient

Our program is accurate, but not necessarily efficient. Every second, it sends the entire verse in its entirety to the Wave server, with only minuscule changes (`i` and `i-1`). You can observe this in the Wave server's log (switch to the terminal window running the Wave server to view the log):  

```sh
2020/10/02 12:13:43 * /beer {"d":[{"k":"wall content","v":"\n98 bottles of beer on the wall, 98 bottles of beer.\n\nTake one down, pass it around, 97 bottles of beer on the wall...\n"}]}
2020/10/02 12:13:44 * /beer {"d":[{"k":"wall content","v":"\n97 bottles of beer on the wall, 97 bottles of beer.\n\nTake one down, pass it around, 96 bottles of beer on the wall...\n"}]}
2020/10/02 12:13:46 * /beer {"d":[{"k":"wall content","v":"\n96 bottles of beer on the wall, 96 bottles of beer.\n\nTake one down, pass it around, 95 bottles of beer on the wall...\n"}]}
2020/10/02 12:13:47 * /beer {"d":[{"k":"wall content","v":"\n95 bottles of beer on the wall, 95 bottles of beer.\n\nTake one down, pass it around, 94 bottles of beer on the wall...\n"}]}
2020/10/02 12:13:48 * /beer {"d":[{"k":"wall content","v":"\n94 bottles of beer on the wall, 94 bottles of beer.\n\nTake one down, pass it around, 93 bottles of beer on the wall...\n"}]}
```

We can do better. Instead of sending the verse over and over again, we can send the verse once with placeholders for `i` and `i-1`, and then send only `i` and `i-1` during subsequent updates. That will keep network traffic down to a minimum and allow the Wave server to perform less work while sending your updates to your website's bajillion visitors.

Here's how we approach this. First, when we create our markdown card, we set the content to an *expression* (or *formula*). An expression is a string that starts with `=` (similar to formulae in spreadsheet software). Our expression looks like this:

```py
beer_verse = '''={{before}} bottles of beer on the wall, {{before}} bottles of beer.

Take one down, pass it around, {{after}} bottles of beer on the wall...
'''
```

We've used a triple-quoted multi-line string here, but you can use single- or double-quoted strings as well. The tokens inside the `{{` and `}}` are placeholders. Note how the verse string starts with a `=`, indicating that this is an expression.

Next, we create the markdown card as usual using our verse as the `content`. But more importantly, we set the card's `data` attribute to a Python dictionary containing values for the placeholders `before` and `after`:

```py {4-5}
beer_card = page.add('wall', ui.markdown_card(
    box='1 1 4 2',
    title='99 Bottles of Beer',
    content=beer_verse,
    data=dict(before='99', after='98'),
))
```

Finally, instead of updating the entire verse every second, we update only the `.data.before` and `.data.after` attributes of the markdown card:

```py {2-3}
for i in range(99, 0, -1):
    beer_card.data.before = str(i)
    beer_card.data.after = str(i - 1)
    page.save()
    time.sleep(1)
```

Our final program looks like the listing below. You'll notice that the size of our program is mostly unchanged from before.

```py
import time
from h2o_wave import site, ui

page = site['/beer']

beer_verse = '''={{before}} bottles of beer on the wall, {{before}} bottles of beer.

Take one down, pass it around, {{after}} bottles of beer on the wall...
'''

beer_card = page.add('wall', ui.markdown_card(
    box='1 1 4 2',
    title='99 Bottles of Beer',
    content=beer_verse,
    data=dict(before='99', after='98'),
))

for i in range(99, 0, -1):
    beer_card.data.before = str(i)
    beer_card.data.after = str(i - 1)
    page.save()
    time.sleep(1)
```

Run your program again. You should see the same results in your browser as before, but you'll notice that the information flowing through the Wave server is significantly less than before:

```sh
2020/10/02 13:53:11 * /beer {"d":[{"k":"wall data before","v":"98"},{"k":"wall data after","v":"97"}]}
2020/10/02 13:53:12 * /beer {"d":[{"k":"wall data before","v":"97"},{"k":"wall data after","v":"96"}]}
2020/10/02 13:53:13 * /beer {"d":[{"k":"wall data before","v":"96"},{"k":"wall data after","v":"95"}]}
2020/10/02 13:53:14 * /beer {"d":[{"k":"wall data before","v":"95"},{"k":"wall data after","v":"94"}]}
2020/10/02 13:53:15 * /beer {"d":[{"k":"wall data before","v":"94"},{"k":"wall data after","v":"93"}]}
2020/10/02 13:53:16 * /beer {"d":[{"k":"wall data before","v":"93"},{"k":"wall data after","v":"92"}]}
```

## Summary

In this tutorial, we learned how to send periodic updates to the Wave server and observe changes in realtime.

In the next tutorial, we'll put these principles to real-world use, popping up charts for a song instead of chart topping pop songs.

---

### Tutorial Counter (`website/docs/tutorial-counter.md`)

---
title: "Tutorial: Bean Counter"
---

In the previous tutorials, we've been authoring [Wave scripts](scripts.md). A Wave script is a perfectly fine and dandy way to modify pages in Wave and build dashboards. However, to build *interactive applications* in Wave - user interfaces that can dynamically respond to *events* (the user's actions) -  [Wave apps](apps.md) are the way to go.

In the fine tradition of keeping tutorials succinct and useful, we'll author a little app that counts beans of the virtual kind, or, a bean counter, not to be confused with dubiously intentioned financial officers.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-counter__demo.mp4').default} type='video/mp4'/></video>

This tutorial outlines the basics of how to handle events, update the UI, manage state and easily add realtime sync capabilities to your app. It's probably the most important tutorial to wrap your head around if you're interested in authoring interactive applications.

## Prerequisites

This tutorial assumes you have a working directory for authoring programs. If not, head over to the [Hello World tutorial](tutorial-hello.mdx) and complete step 2.

## Step 1: Start listening

The first step towards listening to events from the UI is to define an `@app` function:

```py title="counter.py"
from h2o_wave import Q, main, app

@app('/counter')
async def serve(q: Q):
    pass
```

`@app` is a decorator that takes one required argument - the route to listen to, in this case `/counter` (which translates to `localhost:10101/counter` or `www.example.com/counter`).

The `@app` decorator is applied to a function called `serve()`, which is called every single time the user interacts with the UI (access the page, reload it, click a button, access a menu, enter text, and so on).

We named the function `serve()`, but you can call it anything you please, like `shoe_strings()`.

Lastly, note that we've imported the symbol `main` into our `.py` file. You don't have to do anything with `main` except import it.

To run your app, use `wave run` within your activated virtual environment:

```shell
wave run counter
```

The `wave run` command runs your app in *development mode*, and the app is automatically reloaded when edited. Additionally, it starts Wave server under the hood for you if not already running or explicitly told not to.

At this point, your app will be up and running, but it doesn't do anything yet. Let's change that in a second.

## Step 2: Display a button

Let's add a button to our app. Our goal is to increment and display the bean count each time the button is clicked.

To do this, we declare a variable called `bean_count`, and use `form_card()` to add a [form](https://en.wikipedia.org/wiki/Form_(document)) to our page. A form card is a special type of card that displays a vertical stack of [components](/docs/widgets/form/overview) (also called *widgets*). In this case, our form contains a solitary button named `increment`, with a caption showing the current `bean_count`. The button is marked as `primary`, which serves no other purpose than to make it look tall, dark, and handsome.

```py {5-18} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter')
async def serve(q: Q):
    bean_count = 0

    q.page['beans'] = ui.form_card(
        box='1 1 1 2',
        items=[
            ui.button(
                name='increment',
                label='Click me!',
                caption=f'{bean_count} beans',
                primary=True,
            ),
        ],
    )
    await q.page.save()
```

Notice how the technique to modify and save pages in a Wave app is different from that of a Wave script.

| Task | Wave Script | Wave App |
|---|---|---|
| Access page at route `/foo` | `page = site['/foo']` | `page = q.page` |
| Access card named `foo` | `card = page['foo']` | `card = q.page['foo']` |
| Save page | `page.save()` | `await q.page.save()` |

In a Wave app, we always access the page using the *query context* `q`. The query context carries useful information about the active UI event, including who issued the event, what information was entered, which buttons were clicked, and so on; and `q.page` always refers to the page located at the route you passed to `@app()` (in this case).

Point your browser to [http://localhost:10101/counter](http://localhost:10101/counter). You should see a nice big button, waiting to be clicked on:



If you click on the button, you'll notice that it doesn't do anything. This is because we haven't handled button clicks yet. Instead, when you click the button, the events are sent to your `serve()` function, but the `serve()` function blindly adds the form (with the button) to the page all over again and saves it, causing the page to display the same form all over again. Let's fix this next.

## Step 3: Handle button clicks

Add a condition to check if the button is clicked, and if so, increment the bean count.

```py {5-7} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter')
async def serve(q: Q):
    bean_count = q.client.bean_count or 0
    if q.args.increment:
        q.client.bean_count = bean_count = bean_count + 1

    q.page['beans'] = ui.form_card(
        box='1 1 1 2',
        items=[
            ui.button(
                name='increment',
                label='Click me!',
                caption=f'{bean_count} beans',
                primary=True,
            ),
        ],
    )
    await q.page.save()
```

The above edit introduces two important concepts: `q.client` and `q.args`.

- `q.client` is a dictionary-like object for storing and retrieving arbitrary information related to the active *client* (a browser tab or browser session). Anything you put into `q.client` while handling an event can be read back while handling a subsequent event, provided the subsequent event originates from the same client. In our case, we read `q.client.bean_count` to get the bean count. If it's unavailable (which means this is a new client), we default the bean count to `0`.
- `q.args`, also a dictionary-like object, carries *event arguments*, which contains information about what the user did. In our case, if the user clicked on our button (named `increment`), `q.args.increment` will be `True`. If not, `q.args.increment` will be `None`. Therefore, if `q.args.increment` is `True`, we increment the client's `bean_count`. If not, we do nothing.

:::note
From now on, any time you see the term 'client', recall that it's a technical term for 'browser tab' or 'browser session'. In other words, any time you open a new browser tab to access your app, you're accessing your app from a new client.
:::

:::info
Each component in the Wave component library populates `q.args` with its own value. For more information, see [Components](/docs/widgets/form/overview).
:::

Your button should now count beans when clicked:

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-counter__demo.mp4').default} type='video/mp4'/></video>

## Step 4: Make it more efficient

Our program is accurate, but not necessarily efficient. On every button-click, it is recreating the form card and the button over and over again instead of updating the existing button's caption with the current bean count. You can observe how chatty the app is by gaping in abject horror at the Wave server's log.

:::info
To view the Wave server's log, switch to the terminal window running the Wave server.
:::

Let's wrinkle our noses in disgust and fix this gross inefficiency right away to make our little bean counter web-scale for great good.

```py {9-10,22-23} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter')
async def serve(q: Q):
    bean_count = q.client.bean_count or 0
    if q.args.increment:
        q.client.bean_count = bean_count = bean_count + 1

    if not q.client.initialized:
        q.client.initialized = True
        q.page['beans'] = ui.form_card(
            box='1 1 1 2',
            items=[
                ui.button(
                    name='increment',
                    label='Click me!',
                    caption=f'{bean_count} beans',
                    primary=True,
                ),
            ],
        )
    else:
        q.page['beans'].increment.caption = f'{bean_count} beans'
    await q.page.save()
```

In the above edit, we check for an arbitrary flag in `q.client` called `initialized`.

- If the flag is not set, we assume that the request is originating from a new, empty client. If so, we set `initialized` to `True` (to make a note to ourselves that the client is not empty anymore), then add our form card to the page.
- If the flag is already set, we assume that the form card (and the button inside it) are already on the page, and simply update the button's caption with the new bean count. We can refereence the button by simply using its `name` attribute - `increment`.

## Intermission: Understanding state

In the above steps, we learned about `q.args` (request arguments submitted from the browser) and `q.client` (arbitrary information associated with the client). Let's expand on these and introduce two new concepts: `q.user` and `q.app`.

Before we do that, try accessing [http://localhost:10101/counter](http://localhost:10101/counter) from two different browser tabs and playing with them. You'll notice that each browser tab (or, each client) maintains separate bean counts.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-counter__client.mp4').default} type='video/mp4'/></video>

Maintaining separate bean counts per client is one way to count beans. You can also maintain bean counts at the user level and the app level.

In real world apps, the decision on whether to store information at the client, user, or app level depends on the problem you're trying to solve. For example, if you were building an online store, you'd probably want to store product inventory at the app level and shopping carts at the user level. Most other kinds of information - search results, past orders, or product details - are best stored at the client-level (searching for products in one tab and having search results appear in another tab would drive even your most loyal customers up the wall).

Similar to how `q.client` stores arbitrary information associated with the client, `q.user` and `q.app` store arbitrary information associated with the user and the app, respectively.

In most apps, you'll end up using a mix of `q.client`, `q.user` and `q.app` to correctly handle requests originating from:

1. Different users.
2. Different browser tabs belonging to the same user (possibly from different devices).
3. The same browser tab.

In other words, your Wave app is multi-user by default, but how the app manages data at the app-level, at the user-level and at the client-level is up to you.

## Step 5: User-level realtime sync

To maintain bean counts at the user level, all we have to do is store `bean_count` in `q.user` instead of `q.client`.

```py {5,7} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter')
async def serve(q: Q):
    bean_count = q.user.bean_count or 0
    if q.args.increment:
        q.user.bean_count = bean_count = bean_count + 1

    if not q.client.initialized:
        q.client.initialized = True
        q.page['beans'] = ui.form_card(
            box='1 1 1 2',
            items=[
                ui.button(
                    name='increment',
                    label='Click me!',
                    caption=f'{bean_count} beans',
                    primary=True,
                ),
            ],
        )
    else:
        q.page['beans'].increment.caption = f'{bean_count} beans'
    await q.page.save()
```

Now when you play with your app, you'll see that the counts are being maintained across browser tabs, but the updated counts don't show up immediately across all tabs. Instead, they show up only when you interact with the other tabs, or reload them.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-counter__user-nosync.mp4').default} type='video/mp4'/></video>

This would be considered normal behavior for a typical web application, and most users familiar with the interwebs are also familiar with the reload button, but we can do better, because having to hit the reload button to get updates is about as exciting as waiting for paint to dry.

This is easier done than said - simply change the app mode to `multicast` to enable realtime sync across clients:

```py {27} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter', mode='multicast')
async def serve(q: Q):
    bean_count = q.user.bean_count or 0
    if q.args.increment:
        q.user.bean_count = bean_count = bean_count + 1

    if not q.client.initialized:
        q.client.initialized = True
        q.page['beans'] = ui.form_card(
            box='1 1 1 2',
            items=[
                ui.button(
                    name='increment',
                    label='Click me!',
                    caption=f'{bean_count} beans',
                    primary=True,
                ),
            ],
        )
    else:
        q.page['beans'].increment.caption = f'{bean_count} beans'
    await q.page.save()
```

:::info
The default app mode is `unicast`, which means "don't sync across clients". On the other hand, `multicast` means "sync across clients". There's also a third mode, `broadcast`, which means "sync across users", which we'll see in the next step.
:::

If you play with your app now, you'll see that the user-level bean count indeed syncs across tabs:

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-counter__user-sync.mp4').default} type='video/mp4'/></video>

## Step 6: App-level realtime sync

Going from user-level bean counting to app-level bean counting is easy: simply store `bean_count` on `q.app` instead of `q.user`, and switch the app mode to `broadcast`:

```py {5,7,27} title="counter.py"
from h2o_wave import Q, main, app, ui

@app('/counter', mode='broadcast')
async def serve(q: Q):
    bean_count = q.app.bean_count or 0
    if q.args.increment:
        q.app.bean_count = bean_count = bean_count + 1

    if not q.client.initialized:
        q.client.initialized = True
        q.page['beans'] = ui.form_card(
            box='1 1 1 2',
            items=[
                ui.button(
                    name='increment',
                    label='Click me!',
                    caption=f'{bean_count} beans',
                    primary=True,
                ),
            ],
        )
    else:
        q.page['beans'].increment.caption = f'{bean_count} beans'
    await q.page.save()
```

:::tip
The `broadcast` mode can be used to build collaborative apps that need to synchronize state across all users, like group chat or multiplayer games.
:::

## Summary

In this tutorial, we learned how to author interactive applications, or *apps*, and easily add realtime sync capabilities to our apps. More importantly, we learned how to deal with events and manage state using four dictionary-like objects:

| Attribute | Type | Use |
|---|---|---|
| `q.args` | Read-only | Stores command arguments |
| `q.client` | Read/Write | Stores client-level state |
| `q.user` | Read/Write | Stores user-level state |
| `q.app` | Read/Write | Stores app-level state|

Also, we built ourselves a little app that counts beans, and you can now put that knowledge to good use, like build an online voting app for the upcoming elections in the banana republic you're running on your private island (a smidgen of democracy can't hurt).

In the next section, we'll build something a bit more substantial and useful: a to-do list with realtime sync.

---

### Tutorial Hello (`website/docs/tutorial-hello.mdx`)

---
title: "Tutorial: Hello World"
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

In this section, we'll learn how to author our first Wave program from scratch, and understand the basics of how to display content in a web browser.

:::note
These tutorials assume that you have some familiarity with the Python programming language. You don't have to be an expert, but it might be harder to learn both Wave and Python at the same time.
:::

## Step 1: Start the Wave server

[Download](/docs/installation#downloading-wave) the most recent version `.tar.gz` file for your operating system, and extract (unzip) it to a directory of your choice.
Once done, open a terminal, cd into the extracted dir and start the Wave server.

<Tabs groupId="operating-systems">

<TabItem value="unix" label="Unix/MacOS">

```shell
./waved
```
</TabItem>

<TabItem value="win" label="Windows">

```shell
waved.exe
```

</TabItem>

</Tabs>


```sh
2020/10/27 16:16:34 # 
2020/10/27 16:16:34 # ┌─────────────────────────┐
2020/10/27 16:16:34 # │  ┌    ┌ ┌──┐ ┌  ┌ ┌──┐  │ H2O Wave
2020/10/27 16:16:34 # │  │ ┌──┘ │──│ │  │ └┐    │ (version) (build)
2020/10/27 16:16:34 # │  └─┘    ┘  ┘ └──┘  └─┘  │ © 2020 H2O.ai, Inc.
2020/10/27 16:16:34 # └─────────────────────────┘
2020/10/27 16:16:34 # 
2020/10/27 16:16:34 # {"address":":10101","t":"listen","webroot":"/home/elp/wave/www"}
```

The Wave server should now be running at <http://localhost:10101>.

:::caution Don't close this terminal window!
To run any Wave app or script, you need the Wave server up and running at all times. Your web browser communicates with the Wave server, and the Wave server in turn communicates with the Wave app.

Apps can start Wave server automatically via `wave run` command. So this step is only necessary for Wave scripts.
:::

## Step 2: Set up a working directory

Next, let's set up a working directory to author our program. Create a new directory and open it in a terminal.

### Set up a virtual environment

A [virtual environment](https://docs.python.org/3/tutorial/venv.html) helps us manage our program's dependencies without interfering with system-wide packages.

<Tabs groupId="operating-systems">

<TabItem value="unix" label="Unix/MacOS">

```shell
python3 -m venv venv
source venv/bin/activate
```
</TabItem>

<TabItem value="win" label="Windows">

```shell
python -m venv venv
.\venv\Scripts\activate
```

</TabItem>

<TabItem value="conda" label="Conda">

```shell
conda create -n venv
conda activate venv
```
</TabItem>

</Tabs>

### Install the Wave Python driver

<Tabs groupId="package-manager">

<TabItem value="pip-pkg" label="Pip">

```shell
pip install h2o-wave
```
</TabItem>

<TabItem value="conda" label="Conda">

```shell
conda config --append channels conda-forge
conda install -c h2oai h2o-wave
```
</TabItem>

</Tabs>

## Step 3: Write your program

Next, open your preferred text editor, create a Python script called `hello_world.py`, and copy-paste the following.

For now, don't worry too much about what this program is doing. We'll get to that shortly.

```py title="hello_world.py"
from h2o_wave import site, ui

# Grab a reference to the page at route '/hello'
page = site['/hello']

# Add a markdown card to the page.
page['quote'] = ui.markdown_card(
    box='1 1 2 2',
    title='Hello World',
    content='"The Internet? Is that thing still around?" - *Homer Simpson*',
)

# Finally, save the page.
page.save()
```


## Step 4: Run your program

```shell
python hello_world.py
```

## Step 5: Admire your creation

Point your browser to [http://localhost:10101/hello](http://localhost:10101/hello), and pause to reflect on a particularly pithy quote from the venerable Homer Simpson.



## Step 6: Understand your program

Let's walk through this program step by step.

This program (technically a [script](scripts.md)), illustrates the core of Wave's programming model, or, "How to think in Wave."

1. Your Wave server instance holds a collection of pages.
2. To change a page, simply grab a reference to a page, change it, and save it.

That's it. Your changes are now visible to everyone.

Let's understand this principle in practice using the little program we just created.

### Grab a reference to a page

A `site` represents a dictionary of all the pages on the Wave server. To get a reference to a page hosted at the route `/hello` (which translates to `http://localhost:10101/hello`), simply grab the value at key `/hello`.

```py
page = site['/hello']
```

### Change the page

Similar to how a `site` represents a collection of pages, a `page` represents a collection of *cards*. A card represents a block of content: text, graphics, widgets, or some combination of those.

```py
page['quote'] = ui.markdown_card(
    box='1 1 2 2',
    title='Hello World',
    content='"The Internet? Is that thing still around?" - *Homer Simpson*',
)
```

Pages support different kinds of cards. In this case, we add a card named `quote` that displays markdown content (`markdown_card()`). The position and size of the card on the page is specified by the `box` attribute. In this case, the card is placed at column `1`, row `1`, sized `2 x 2` units. The `content` attribute supports [Github Flavored Markdown](https://guides.github.com/features/mastering-markdown/).

### Save the page

Finally, we call `save()` on the `page`, which broadcasts our changes to all connected web browsers.

```py
page.save()
```


So far, so good.

## Step 7: Edit your page from a REPL

Finally, just for kicks, let's make some changes to our hello world page using a Python REPL and watch our page reflect those changes in realtime.

### Start a Python REPL

```shell
cd $HOME/wave-apps
./venv/bin/python
```

### Grab a reference to our page

```py title=">>>"
from h2o_wave import site
page = site['/hello']
```

### Grab a reference to our card

```py title=">>>"
quote = page['quote']
```

### Change the title
```py title=">>>"
quote.title = 'Hello Again!'
page.save()
```



### Change the content

```py title=">>>"
quote.content = "D'oh! - *Homer Simpson*"
page.save()
```



### Quit your REPL

```py title=">>>"
quit()
```

## Summary

What we just did - add content from one program and make edits to it from another - illustrates another important aspect of Wave's programming model: *The Wave server retains content*. Your `hello_world.py` program did its thing and exited. So did your REPL. But your content was retained for the viewing pleasure of future visitors to `/hello`.

Next, we'll take the principles we learned from this tutorial and apply it towards a supposedly spirited folk song involving arithmetic progressions.

---

### Tutorial Monitor (`website/docs/tutorial-monitor.md`)

---
title: "Tutorial: System Monitor"
---

In this tutorial, we'll put our learnings from the [first](tutorial-hello.mdx) and [second](tutorial-beer.md) tutorials to some real-world use: a simple system monitoring tool that displays CPU, memory and network stats on a web page.



For example, if you have a spare 256-node Raspberry Pi cluster lying somewhere, you can run this program to each node and monitor your entire cluster's system utilization from one place. How cool is that?

We'll also introduce a new concept, called [data buffers](buffers.md), which allows you to use the Wave server to store *rows* (also called *tuples* or *records*) of information - much like how you would use tables in a database, or dataframes in Python or R - to deal with structured data.

## Prerequisites

This tutorial assumes your Wave server is up and running, and you have a working directory for authoring programs. If not, head over to the [Hello World tutorial](tutorial-hello.mdx) and complete steps 1 and 2.

## Step 1: Install dependencies

We'll be using the excellent `psutil` package to read system stats. Let's go ahead and install that in our virtual environment:

```shell
pip install psutil
```

## Step 2: Monitor CPU usage

Here's what our program looks like:

```py {7,12,25} title="system_monitor.py"
import time
import psutil
from h2o_wave import site, ui, data

page = site['/monitor']

cpu_card = page.add('cpu_stats', ui.small_series_stat_card(
    box='1 1 1 1',
    title='CPU',
    value='={{usage}}%',
    data=dict(usage=0.0),
    plot_data=data('tick usage', -15),
    plot_category='tick',
    plot_value='usage',
    plot_zero_value=0,
    plot_color='$red',
))

tick = 0
while True:
    tick += 1

    cpu_usage = psutil.cpu_percent(interval=1)
    cpu_card.data.usage = cpu_usage
    cpu_card.plot_data[-1] = [tick, cpu_usage]

    page.save()
    time.sleep(1)
```

## Step 3: Run your program

```shell
python system_monitor.py
```

Point your browser to [http://localhost:10101/monitor](http://localhost:10101/monitor).



## Step 4: Understand your program

You'll notice that the above program is quite similar to the program we wrote during the [Beer Wall](tutorial-beer.md) tutorial, with three important differences (see highlighted lines above):

1. We use a `ui.small_series_stat_card()` instead of a `ui.markdown_card()`.
2. The card is capable of dealing with multiple rows of data.
3. To display information on the card, you only need to send it new values (and not all the data rows all over again).

Let's explore these topics one by one.

### Using a stats card

The Wave SDK ships with a variety of *stats cards*, which are cards that display values or graphics, or a combination of both (see [Gallery](examples) for more).

In this case, we use `small_series_stats_card()`, which displays a value and a time series visualization.

```py
cpu_card = page.add('cpu_stats', ui.small_series_stat_card(...)
```

### Declaring a data buffer

The stats card is capable of rendering its visualization using a [data buffer](buffers.md). A data buffer is similar to a database table in that it has a predefined structure (columns and rows), but is write-only (you cannot query information; only insert, update or delete them).

:::info
The [data buffer](buffers.md) topic covers different types of buffers in more detail.
:::

In this case, we declare a *cyclic buffer*, a [FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) data structure that holds a fixed number of rows, and can only be appended to. Our buffer holds at most 15 rows, and has exactly two columns: `tick` (a one-up integer) and `usage` (the CPU usage).

```py
plot_data=data('tick usage', -15),
```

Internally, the card's data buffer might look like this in memory when first created:

| # | tick | usage |
|---|------|-------|
| 0 | | |
| 1 | | |
| ...  | ... | ... |
| 13 | | |
| 14 | | |

### Inserting into the data buffer

Lastly, we measure CPU usage every second and append a new row to the end of card's data buffer, like this:

```py
cpu_card.plot_data[-1] = [tick, cpu_usage]
```

Internally, the card's data buffer might look like this in memory while in use:

| # | tick | usage |
|---|------|-------|
| 0 | 1015 | 4.2 |
| 1 | 1016 | 4.9 |
| ... | ...  | ... |
| 13 | 1028 | 5.8 |
| 14 | 1039 | 7.5 |

## Step 5: Monitor memory usage

As a final step, we can duplicate parts of our program to create another card that displays memory stats. The two cards behave identically, except that one displays CPU usage and the other, memory.

```py {18-28,38-40} title="system_monitor.py"
import time
import psutil
from h2o_wave import site, ui, data

page = site['/monitor']

cpu_card = page.add('cpu_stats', ui.small_series_stat_card(
    box='1 1 1 1',
    title='CPU',
    value='={{usage}}%',
    data=dict(usage=0.0),
    plot_data=data('tick usage', -15),
    plot_category='tick',
    plot_value='usage',
    plot_zero_value=0,
    plot_color='$red',
))
mem_card = page.add('mem_stats', ui.small_series_stat_card(
    box='1 2 1 1',
    title='Memory',
    value='={{usage}}%',
    data=dict(usage=0.0),
    plot_data=data('tick usage', -15),
    plot_category='tick',
    plot_value='usage',
    plot_zero_value=0,
    plot_color='$blue',
))

tick = 0
while True:
    tick += 1

    cpu_usage = psutil.cpu_percent(interval=1)
    cpu_card.data.usage = cpu_usage
    cpu_card.plot_data[-1] = [tick, cpu_usage]

    mem_usage = psutil.virtual_memory().percent
    mem_card.data.usage = mem_usage
    mem_card.plot_data[-1] = [tick, mem_usage]

    page.save()
    time.sleep(1)
```

## Step 6: Run your program again

Terminate your program (`CTRL + C`) and restart it:

```shell
python system_monitor.py
```

Point your browser to [http://localhost:10101/monitor](http://localhost:10101/monitor). You should now see both CPU and memory stats live:



## Exercise

Explore other kinds of cards in the [Widgets](/docs/widgets/overview) and display additional stats gleaned from `psutil` (network, disk, processes, etc.).

## Summary

In this tutorial, we learned how to use stats cards to display live information. The knowledge you've gained from these first few tutorials should be enough to design and deploy live dashboards using Wave. You will also have noticed that you don't need to keep your Python program running all the time to continue displaying your pages. You can terminate your Python program any time, and the Wave server will happily display the last known state of all your pages.

The programs you've been authoring till now are one kind of programs, called [Wave scripts](scripts.md). Wave scripts are not interactive. They can modify pages on the Wave server, but cannot respond to user actions, like handling button clicks, menu commands, dropdown changes, and so on. To handle user interactions, you need to author [Wave Apps](apps.md), which are long-running programs (*servers* or *services*) that are capable of modifying pages in response to user actions. Let's see how to do that in the next tutorial.

---

### Tutorial Todo (`website/docs/tutorial-todo.md`)

---
title: "Tutorial: Todo List"
---

In this tutorial, we'll build something a bit more substantial and useful: a to-do list with realtime sync, in under 75 lines of (well-formatted, well-commented) code.

More importantly, this tutorial will not introduce any new concepts. Everything you need to know about authoring interactive apps using Wave is already covered in the [previous tutorial](tutorial-counter.md). From this point on, it's mostly a matter of [abstraction](https://htdp.org/2020-8-1/Book/part_three.html), which is a fancy term for how you solve the problem at hand using short, simple, clear, elegant, modular functions that [do one thing and do it well](https://en.wikipedia.org/wiki/Unix_philosophy).

Above all, prefer brevity and clarity. [The best code is no code at all](https://blog.codinghorror.com/the-best-code-is-no-code-at-all/).

## Step 1: Listen

We'll start with a basic skeleton and then work our way up from there.

The first step is to define an `@app` function. Also, we want the landing page to show a list of to-dos, so we'll throw in an empty `show_todos()` function for now, and call it from `serve()`.

```py {6,10-11} title="todo.py"
from h2o_wave import Q, main, app, ui


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    pass
```

## Step 2: What's in a to-do?

A to-do item has some basic attributes: an ID, some text content, and whether it's completed or not. Let's define a `class` for that, with a global one-up `id`.

```py {3-13} title="todo.py"
from h2o_wave import Q, main, app, ui

_id = 0


# A simple class that represents a to-do item.
class TodoItem:
    def __init__(self, text):
        global _id
        _id += 1
        self.id = f'todo_{_id}'
        self.text = text
        self.done = False


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    pass
```

## Step 3: Make a to-do list

Since we want each user to have a separate to-do list, it's appropriate to keep the to-do list in `q.user`.

Here, we attempt to fetch the list from `q.user` and create one if it doesn't exist. We also throw in some sample to-do items for good measure.

```py {1,23-28} title="todo.py"
from typing import List
from h2o_wave import Q, main, app, ui

_id = 0

# A simple class that represents a to-do item.
class TodoItem:
    def __init__(self, text):
        global _id
        _id += 1
        self.id = f'todo_{_id}'
        self.text = text
        self.done = False


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    # Get items for this user.
    todos: List[TodoItem] = q.user.todos

    # Create a sample list if we don't have any.
    if todos is None:
        q.user.todos = todos = [TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]
```

## Step 4: Show the to-do list

Next, we turn each incomplete to-do item into a checkbox (using `ui.checkbox()`), and display it in a form card (using `ui.form_card()`).
Also, we want each checkbox to raise an event immediately when checked, so we set its `trigger` attribute to `True`.

:::tip
Several [components](/docs/widgets/form/overview) have a `trigger` attribute. Normally, an event is triggered only when a command-like component (a button, menu, or tab) is clicked. If you want a component to immediately trigger an event when changed, set `trigger` to `True`.
:::

```py {1,30-37} title="todo.py"
from typing import List
from h2o_wave import Q, main, app, ui

_id = 0

# A simple class that represents a to-do item.
class TodoItem:
    def __init__(self, text):
        global _id
        _id += 1
        self.id = f'todo_{_id}'
        self.text = text
        self.done = False


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    # Get items for this user.
    todos: List[TodoItem] = q.user.todos

    # Create a sample list if we don't have any.
    if todos is None:
        q.user.todos = todos = [TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]

    # Create checkboxes.
    not_done = [ui.checkbox(name=todo.id, label=todo.text, trigger=True) for todo in todos if not todo.done]

    # Display list
    q.page['form'] = ui.form_card(box='1 1 3 10', items=[
        ui.text_l('To Do'),
        *not_done,
    ])
```

## Step 5: Show the 'done' list

We also turn each completed to-do item into another list of checkboxes, checked by default (using its `value` attribute). We append this to the form card and put a separator in between (using `ui.separator()`) to distinguish the completed items from the incomplete ones.

```py {31,38-39} title="todo.py"
from typing import List
from h2o_wave import Q, main, app, ui

_id = 0

# A simple class that represents a to-do item.
class TodoItem:
    def __init__(self, text):
        global _id
        _id += 1
        self.id = f'todo_{_id}'
        self.text = text
        self.done = False


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    # Get items for this user.
    todos: List[TodoItem] = q.user.todos

    # Create a sample list if we don't have any.
    if todos is None:
        q.user.todos = todos = [TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]

    # Create done/not-done checkboxes.
    done = [ui.checkbox(name=todo.id, label=todo.text, value=True, trigger=True) for todo in todos if todo.done]
    not_done = [ui.checkbox(name=todo.id, label=todo.text, trigger=True) for todo in todos if not todo.done]

    # Display list
    q.page['form'] = ui.form_card(box='1 1 3 10', items=[
        ui.text_l('To Do'),
        *not_done,
        *([ui.separator('Done')] if len(done) else []),
        *done,
    ])
```

At this point, try running your app.

```shell
cd $HOME/wave-apps
source venv/bin/activate
wave run todo
```

Point your browser to [http://localhost:10101/todo](http://localhost:10101/todo).



You should be able to see your todo list in all its glory. Unfortunately, checking any of the items seems to have no effect. Let's fix that next.

## Step 6: Handle checkboxes

Each time a checkbox is checked or unchecked, our `serve()` function is called, which in turn calls `show_todos()`.

- If a checkbox is checked, `q.args` will contain a `True` for that checkbox.
- If a checkbox is unchecked, `q.args` will contain a `False` for that checkbox.

So, we iterate through all the to-do items and set their `done` attribute based on the value of their corresponding checkbox.

```py {30-33} title="todo.py"
from typing import List
from h2o_wave import Q, main, app, ui

_id = 0

# A simple class that represents a to-do item.
class TodoItem:
    def __init__(self, text):
        global _id
        _id += 1
        self.id = f'todo_{_id}'
        self.text = text
        self.done = False


@app('/todo')
async def serve(q: Q):
    show_todos(q)
    await q.page.save()


def show_todos(q: Q):
    # Get items for this user.
    todos: List[TodoItem] = q.user.todos

    # Create a sample list if we don't have any.
    if todos is None:
        q.user.todos = todos = [TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]

    # If the user checked/unchecked an item, update our list.
    for todo in todos:
        if todo.id in q.args:
            todo.done = q.args[todo.id]

    # Create done/not-done checkboxes.
    done = [ui.checkbox(name=todo.id, label=todo.text, value=True, trigger=True) for todo in todos if todo.done]
    not_done = [ui.checkbox(name=todo.id, label=todo.text, trigger=True) for todo in todos if not todo.done]

    # Display list
    q.page['form'] = ui.form_card(box='1 1 3 10', items=[
        ui.text_l('To Do'),
        *not_done,
        *([ui.separator('Done')] if len(done) else []),
        *done,
    ])
```

You should now be able to check/uncheck the items in your todo list.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-todo__2.mp4').default} type='video/mp4'/></video>

Next, let's see how to add new items to the list.

## Step 7: The 'new todo' form

Next, let's display a form to add new items to our list. For that, we'll add a new button to our existing form, named `new_todo`, and direct the `serve()` function to the `new_todo()` function if the button is clicked. Recall that when buttons are clicked, `q.args.button_name` will be `True`, so we check if `q.args.new_todo` is `True`.

In the `new_todo()` function, we display a new form containing a textbox (using `ui.textbox()`) and a set of buttons to add the item or return to to-do list (a `ui.buttons()` helps us display buttons side-by-side).

```
/* Detailed source-code truncated for AI context efficiency. */
```

You should now be able to bring up the new to-do form.

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-todo__3.mp4').default} type='video/mp4'/></video>

## Step 8: Add to-do and return

Finally, we handle the `add_todo` button-click, redirecting `serve()` to a new `add_todo()` function, which simply inserts a the new to-do item into our user-level todo list and calls `show_todos()` to redraw the to-do list.

:::note
In this example, for clarity, we named both the buttons and their corresponding functions `new_todo` and `add_todo`, but this is not necessary.
:::

```
/* Detailed source-code truncated for AI context efficiency. */
```

You should now be able to add new to-do items to your list. Congratulations!

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-todo__4.mp4').default} type='video/mp4'/></video>

## Step 9: Make it realtime

To make your app realtime, simply pass `mode='multicast'` to `@app()`.

```py title="todo.py"
@app('/todo', mode='multicast')
```

Now try opening [http://localhost:10101/todo](http://localhost:10101/todo) from multiple browser tabs:

<video autoPlay='autoplay' loop='loop' muted='muted'><source src={require('./assets/tutorial-todo__5.mp4').default} type='video/mp4'/></video>

Groovy!

## Exercise

A little housekeeping goes a long way: add a "Clear" button on the main page to clear all completed to-dos.

## Next steps

Congratulations! You've completed all the tutorials (hopefully). There are three paths you can take from here:

- [Widgets](/docs/widgets/overview). Widgets that cover best UX practices and a lot of widgets variations.
- [Gallery](examples). 150+ examples that cover everything that Wave has to offer.
- [Guide](guide). An in-depth look at each of Wave's features.
- [API](/docs/api/index). Reference-level documentation for the Python API.

Happy hacking!

---

### Wavedb (`website/docs/wavedb.md`)

---
title: WaveDB
---

**WaveDB** is [SQLite](https://www.sqlite.org/index.html) with a HTTP interface.

It is a ~6MB (~2MB [UPX](https://upx.github.io/)-compressed) self-contained, zero-dependency executable that bundles SQLite 3.35.5 (2021-04-19) with JSON1, RTREE, FTS5, GEOPOLY, STAT4, and SOUNDEX.

If you are already a fan of SQLite, WaveDB acts as a thin HTTP-server wrapper that lets you access your SQLite databases over a network.

WaveDB can be used as a lightweight, cross-platform, installation-free companion SQL database for Wave apps. The `h2o-wave` package includes non-blocking `async` functions to access WaveDB.

Database files managed by WaveDB are 100% interoperable with SQLite, which means you can manage them with the `sqlite3` CLI, backup/restore/transfer them as usual, or use [Litestream](https://litestream.io/) for replication.

## Install

### Linux, OSX, Windows

[Download the executable](https://github.com/h2oai/wave/releases) for your platform. No installation required.

### Other Platforms

Compiling WaveDB on other platforms is easy if you have the [Go](https://golang.org) and C compilers handy (uses [cgo](https://golang.org/cmd/cgo/)):

```sh
git clone --depth 1 https://github.com/h2oai/wave.git
cd wave
go build -o wavedb cmd/wavedb/main.go
```

## Launch

Launch WaveDB (defaults to <http://localhost:10100/>):

```sh
./wavedb
```

```sh
2021/05/07 14:00:41
2021/05/07 14:00:41 ┌────────┐┌────┐ H2O WaveDB
2021/05/07 14:00:41 │  ┐┌┐┐┌─┘│─┐  │ DEV 20210507132041
2021/05/07 14:00:41 │  └┘└┘└─┘└─┘  │ © 2021 H2O.ai, Inc.
2021/05/07 14:00:41 └──────────────┘
2021/05/07 14:00:41
2021/05/07 14:00:41 listening :10100
```

Launch WaveDB on a different port:

```sh
./wavedb -listen :8080
```

Launch WaveDB with verbose logging (useful during development):

```sh
./wavedb -verbose
```

Launch WaveDB with a custom API access key/secret pair:

```sh
./wavedb -access-key-id uzer -access-key-secret pa55word
```

Serve your existing SQLite database files using WaveDB (defaults to current directory):

```sh
./wavedb -dir /path/to/my/db/files
```

## Examples

- [Using WaveDB from a Wave app - A To-do list](https://github.com/h2oai/wave/blob/main/py/examples/db_todo.py)
- [Using WaveDB from a Wave app - Paginated table](https://github.com/h2oai/wave/blob/main/py/examples/table_pagination_wavedb.py)
- [Using WaveDB from a standalone script](https://github.com/h2oai/wave/blob/main/py/examples/db.py)

## Usage

For this example, we'll use the [Chinook Sample Database](https://www.sqlitetutorial.net/sqlite-sample-database/) from [sqlitetutorial.net](https://www.sqlitetutorial.net/).

```sh
$ wget https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip
$ unzip chinook.zip
$ ls -l
-rw-r--r-- 1 elp elp  884736 May  7 12:34 chinook.db
-rwxr-xr-x 1 elp elp 9678432 May  7 13:20 wavedb
$ ./wavedb -access-key-id uzer -access-key-secret pa55word
```

### From h2o-wave

The `h2o-wave` Python package provides non-blocking `async` functions to access WaveDB.

```py
from h2o_wave import connect

# Create a database connection
connection = connect(key_id='uzer', key_secret='pa55word')

# Access the 'chinook' database.
# Automatically creates a new database if it does not exist.
db = connection["chinook"]

# Execute a query
results, err = await db.exec('SELECT name, composer FROM tracks LIMIT 5')

# Print results
if err:
    print(err)
else:
    print(results)
```

The query API is simple:

1. `db.exec(...)`: Execute one query.
2. `db.exec_many(...)`: Execute multiple queries.
3. `db.exec_atomic(...)`: Excute multiple queries as a transaction (rollback all on failure)

All three APIs support both DDL (`CREATE`, `ALTER`, `DROP`, etc.) and DML (`SELECT`, `INSERT`, `UPDATE`, etc.) statements:

```py
result, error = db.exec('CREATE TABLE student(name TEXT, age INTEGER)')
result, error = db.exec('INSERT INTO student VALUES ("Alice", 18)')
result, error = db.exec('SELECT name, age FROM student WHERE age > 17')
```

Queries can be parameterized (use on user-supplied input to prevent SQL injection):

```py
result, error = db.exec('INSERT INTO student VALUES (?, ?)', "Bob", 19)
result, error = db.exec('SELECT name, age FROM student WHERE age > ?', 17)
```

You can use any combination of the above in an `exec_many()` to batch all your queries into one request:

```py
results, error = db.exec_many(
    'CREATE TABLE student(name TEXT, age INTEGER)',
    'INSERT INTO student VALUES ("Alice", 18)',
    ('INSERT INTO student VALUES (?, ?)', "Bob", 19),
    'SELECT name, age FROM student WHERE age > 17',
    ('SELECT name, age FROM student WHERE age > ?', 17),
)
```

In the above example, substituting `exec_many()` with `exec_atomic()` executes all the queries in the batch as part of a transaction, rolling back all queries on any failures.

### From curl

Use `curl` to send a query:

```sh
curl -s -u uzer:pa55word -d '{"e":{"d":"chinook", "s":[{"q": "select name, composer from tracks limit 5"}]}}' http://localhost:10100
```

```json
{"r":[[["For Those About To Rock (We Salute You)","Angus Young, Malcolm Young, Brian Johnson"],["Balls to the Wall",null],["Fast As a Shark","F. Baltes, S. Kaufman, U. Dirkscneider \u0026 W. Hoffman"],["Restless and Wild","F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider \u0026 W. Hoffman"],["Princess of the Dawn","Deaffy \u0026 R.A. Smith-Diesel"]]]}
```

Use [jq](https://stedolan.github.io/jq/) to pretty-print the response:

```sh
curl -s -u uzer:pa55word -d '{"e":{"d":"chinook", "s":[{"q": "SELECT name, composer FROM tracks LIMIT 5"}]}}' http://localhost:10100 | jq '.'
```

```js
{
  "r": [
    [
      [
        "For Those About To Rock (We Salute You)",
        "Angus Young, Malcolm Young, Brian Johnson"
      ],
      [
        "Balls to the Wall",
        null
      ],
      [
        "Fast As a Shark",
        "F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman"
      ],
      [
        "Restless and Wild",
        "F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman"
      ],
      [
        "Princess of the Dawn",
        "Deaffy & R.A. Smith-Diesel"
      ]
    ]
  ]
}
```

## JSON protocol

### Grammar

```ts
type request = exec_request | drop_request
type reply = exec_reply | drop_reply

type exec_request = { d: database_name, s: statement[], a: atomicity }
type database_name = string
type statement = { q: query, p: parameter[] }
type query = string
type parameter = primitive
type atomicity = 0 | 1

type exec_reply = { r: result[], e: error }
type result = row[]
type row = primitive[]
type error = string

type drop_request = { d: database_name }

type drop_reply = { e: error }

type primitive = number | string | boolean | null
```

### Examples

Sample `exec_request`:

```js
{
  "e": {
    "d": "chinook",
    "s": [
      {
        "q": "select name, composer from tracks limit 5"
      }
    ]
  }
}
```

Sample `reply`:

```js
{
  "r": [
    [
      [
        "For Those About To Rock (We Salute You)",
        "Angus Young, Malcolm Young, Brian Johnson"
      ],
      [
        "Balls to the Wall",
        null
      ],
      [
        "Fast As a Shark",
        "F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman"
      ],
      [
        "Restless and Wild",
        "F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman"
      ],
      [
        "Princess of the Dawn",
        "Deaffy & R.A. Smith-Diesel"
      ]
    ]
  ]
}
```

## Deployment

For production deployments:

- Use the `-access-keychain` option to control API keys. [See documentation](security.md#production).
- Use the `-tls-cert-file` and `-tls-key-file` options to enable HTTPS.

```
$ ./wavedb \
    -access-keychain /path/to/keychain \
    -tls-cert-file /path/to/cert-file \
    -tls-key-file /path/to/key-file
```

## Configuration

```
$ ./wavedb -help
Usage of ./wavedb:
  -access-key-id string
     default API access key ID (default "access_key_id")
  -access-key-secret string
     default API access key secret (default "access_key_secret")
  -access-keychain string
     path to file containing API access keys (default ".wave-keychain")
  -benchmark int
     run benchmarks for the given number of iterations
  -dir string
     path to directory containing database (.db) files (default ".")
  -listen string
     listen on this address (default ":10100")
  -tls-cert-file string
     path to certificate file (TLS only)
  -tls-key-file string
     path to private key file (TLS only)
  -verbose
     enable verbose logging
  -version
     print version and exit

```

## Acknowledgements

WaveDB is based on [SQLite](https://www.sqlite.org) and [bvinc/go-sqlite-lite](https://github.com/bvinc/go-sqlite-lite), built with [Go](https://golang.org/). The Python client uses [HTTPX](https://www.python-httpx.org/) under the hood. Thank you to the authors and contributors.

WaveDB can be used from any language or platform that has libraries for HTTP and JSON. The reference implementation of the client is [~75 lines of code](https://github.com/h2oai/wave/blob/main/py/h2o_wave/h2o_wave/db.py) if you're interested in porting it to other languages.

---

--- METRICS ---
- Files Extracted: 39
- Estimated Token Budget: ~61764 tokens
- Canonical Reference: https://codewiki.google/github.com/h2oai/wave
