Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lightwave #1818

Merged
merged 35 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fc6bb15
feat: Initial impl. #1796
mturoci Jan 17, 2023
8aeabdd
feat: Initial packaging. #1796
mturoci Jan 18, 2023
bc6a848
fix: Make the wavelite work for args, events and UI rendering. #1796
mturoci Jan 18, 2023
c5ae852
chore: Strip down unused stuff. #1796
mturoci Jan 18, 2023
b698d37
chore: Strip down unused stuff. #1796
mturoci Jan 18, 2023
9ddfac8
chore: Separate into proper python package. #1796
mturoci Jan 18, 2023
a44a72e
chore: Update python setup target #1796
mturoci Jan 18, 2023
6be7072
chore: Restructure python packages. #1796
mturoci Jan 19, 2023
3199752
chore: Split wavelite and its web assets into separate python package…
mturoci Jan 19, 2023
5831602
feat: Update types for wavelite as well when generating python UI cla…
mturoci Jan 19, 2023
1eb69e0
docs: Add proper readme. #1796
mturoci Jan 20, 2023
ce91efb
chore: Get rid of user state. #1796
mturoci Jan 20, 2023
2ffe51d
ci: Add job for wavelite. #1796
mturoci Jan 20, 2023
045a0a5
feat: Keep client state only. #1796
mturoci Jan 26, 2023
929b11a
chore: Refactor. #1796
mturoci Jan 26, 2023
86b28d5
chore: Revert wizard changes. #1796
mturoci Jan 30, 2023
f550afa
feat: Allow injecting wavelite into existing pages. Allow ws config. …
mturoci Jan 30, 2023
db75742
feat: Scope id of the root HTML container. #1796
mturoci Jan 31, 2023
5837c4c
docs: Add FastAPI example. #1796
mturoci Jan 31, 2023
ec13ec8
chore: Mark wavelite as beta. #1796
mturoci Jan 31, 2023
67c212a
fix: Remove leading slash from prefix if specified. #1796
mturoci Feb 1, 2023
3e970ba
docs: Add running instructions. #1796
mturoci Feb 1, 2023
aa531ba
docs: Add integration example. #1796
mturoci Feb 1, 2023
461c549
Update py/h2o_wavelite/README.md
mturoci Feb 16, 2023
3c9df27
Update py/h2o_wavelite/README.md
mturoci Feb 16, 2023
44725fa
Update py/h2o_wavelite/README.md
mturoci Feb 16, 2023
ee6bc6f
Update py/h2o_wavelite/README.md
mturoci Feb 16, 2023
df1f799
docs: Use HTML response in custom index.html examples to make them si…
mturoci Feb 17, 2023
ba63e3a
chore: Rename to lightwave #1796
mturoci Feb 24, 2023
aaaa990
ci: Try building lightwave_web package during setup. #1796
mturoci Feb 24, 2023
2242f77
ci: Use the same NodeJS version for publishing lightwave as is used f…
mturoci Feb 24, 2023
7e0fd86
ci: Rename PyPi tokens. #1796
mturoci Feb 24, 2023
aee1ede
chore: Rename wavelite leftovers in readme. #1796
mturoci Feb 24, 2023
0fbe810
chore: Update __init__.py. #1796
mturoci Feb 24, 2023
f610095
chore: Typo. #1796
mturoci Feb 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/publish-lightwave.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish Lightwave

on:
workflow_dispatch:
inputs:
version:
description: 'Release Version'
required: true

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: "${{ github.event.inputs.version }}"

jobs:
publish:
name: Publish Lightwave
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.GIT_TOKEN }}

- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'

- name: Build lightwave
run: make publish-lightwave

- name: Publish to PyPI - lightwave
uses: pypa/gh-action-pypi-publish@master
with:
packages_dir: py/h2o_lightwave/dist
password: ${{ secrets.PYPI_LIGTHWAVE_TOKEN }}

- name: Publish to PyPI - lightwave_web
uses: pypa/gh-action-pypi-publish@master
with:
packages_dir: py/h2o_lightwave_web/dist
password: ${{ secrets.PYPI_LIGTHWAVE_WEB_TOKEN }}
14 changes: 13 additions & 1 deletion .github/workflows/release-wave.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,20 @@ jobs:
- name: Build university
run: make publish-university

- name: Publish Wave University to PyPI
- name: Publish to PyPI - wave_university
uses: pypa/gh-action-pypi-publish@master
with:
packages_dir: university/dist
password: ${{ secrets.PYPI_UNIVERSITY_TOKEN }}

- name: Publish to PyPI - lightwave
uses: pypa/gh-action-pypi-publish@master
with:
packages_dir: py/h2o_lightwave/dist
password: ${{ secrets.PYPI_LIGTHWAVE_TOKEN }}

- name: Publish to PyPI - lightwave_web
uses: pypa/gh-action-pypi-publish@master
with:
packages_dir: py/h2o_lightwave_web/dist
password: ${{ secrets.PYPI_LIGTHWAVE_WEB_TOKEN }}
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ publish-vsc-extension: ## Publish VS Code extension
publish-university:
cd university && $(MAKE) publish

publish-lightwave:
cd ui && npm ci && npm run build
cd py && $(MAKE) setup
cd py && $(MAKE) build-lightwave
cd py && $(MAKE) build-lightwave-web

.PHONY: tag
tag: ## Bump version and tag
cd py && $(MAKE) tag
Expand Down
4 changes: 2 additions & 2 deletions py/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ dmypy.json
# Custom
*.checkpoint
*.gz
h2o_wave/metadata.py
h2o_wave/h2o_wave/metadata.py

# Wave ML
/h2o_wave_ml
examples/_tour_apps_tmp
examples/_tour_apps_tmp
35 changes: 25 additions & 10 deletions py/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ all: build ## Build h2o_wave wheel

.PHONY: build
build: purge
H2O_WAVE_BUILD_OS=windows ./venv/bin/python3 setup.py bdist_wheel --plat-name=win_amd64
H2O_WAVE_BUILD_OS=linux ./venv/bin/python3 setup.py bdist_wheel --plat-name=manylinux1_x86_64
H2O_WAVE_BUILD_OS=darwin ./venv/bin/python3 setup.py bdist_wheel --plat-name=macosx_10_9_x86_64
H2O_WAVE_BUILD_OS=darwin H2O_WAVE_BUILD_ARCH=arm64 ./venv/bin/python3 setup.py bdist_wheel --plat-name=macosx_11_0_arm64
H2O_WAVE_BUILD_OS=darwin H2O_WAVE_BUILD_ARCH=arm64 ./venv/bin/python3 setup.py bdist_wheel --plat-name=macosx_12_0_arm64
H2O_WAVE_BUILD_OS=any ./venv/bin/python3 setup.py bdist_wheel
H2O_WAVE_BUILD_OS=windows ./venv/bin/python3 h2o_wave/setup.py bdist_wheel --plat-name=win_amd64
H2O_WAVE_BUILD_OS=linux ./venv/bin/python3 h2o_wave/setup.py bdist_wheel --plat-name=manylinux1_x86_64
H2O_WAVE_BUILD_OS=darwin ./venv/bin/python3 h2o_wave/setup.py bdist_wheel --plat-name=macosx_10_9_x86_64
H2O_WAVE_BUILD_OS=darwin H2O_WAVE_BUILD_ARCH=arm64 ./venv/bin/python3 h2o_wave/setup.py bdist_wheel --plat-name=macosx_11_0_arm64
H2O_WAVE_BUILD_OS=darwin H2O_WAVE_BUILD_ARCH=arm64 ./venv/bin/python3 h2o_wave/setup.py bdist_wheel --plat-name=macosx_12_0_arm64
H2O_WAVE_BUILD_OS=any ./venv/bin/python3 h2o_wave/setup.py bdist_wheel
$(MAKE) build-lightwave
$(MAKE) build-lightwave-web

.PHONY: build-lightwave
build-lightwave:
cd h2o_lightwave && $(MAKE) build

.PHONY: build-lightwave-web
build-lightwave-web:
cd h2o_lightwave_web && $(MAKE) build

setup: ## Install dependencies
git clone --depth 1 --branch $(WAVE_ML_VERSION) https://github.com/h2oai/wave-ml.git h2o_wave_ml
Expand All @@ -21,9 +31,12 @@ setup: ## Install dependencies
./venv/bin/python -m pip install h2o_wave_ml/
# TODO examples pip install is wasteful for CI
./venv/bin/python -m pip install -r examples/requirements.txt
./venv/bin/python -m pip install --editable .
rm -f h2o_wave/metadata.py
echo "# Generated in setup.py\n__platform__ = 'linux'\n__arch__ = 'amd64'" > h2o_wave/metadata.py
./venv/bin/python -m pip install --editable h2o_wave
[ -d ../ui/build ] && $(MAKE) build-lightwave-web || echo "WARN: ../ui/build doesn't exist, skipping build-lightwave-web"
./venv/bin/python -m pip install --editable h2o_lightwave
./venv/bin/python -m pip install --editable h2o_lightwave_web
rm -f h2o_wave/h2o_wave/metadata.py
echo "# Generated in setup.py\n__platform__ = 'linux'\n__arch__ = 'amd64'" > h2o_wave/h2o_wave/metadata.py

.PHONY: docs
docs: ## Build API docs
Expand All @@ -48,7 +61,9 @@ clean: purge ## Clean

.PHONY: tag
tag: # Bump version
$(SED) -i -r -e "s/__version__.+/__version__ = '$(VERSION)'/" h2o_wave/version.py
$(SED) -i -r -e "s/__version__.+/__version__ = '$(VERSION)'/" h2o_wave/h2o_wave/version.py
$(SED) -i -r -e "s/__version__.+/__version__ = '$(VERSION)'/" h2o_lightwave/h2o_lightwave/version.py
$(SED) -i -r -e "s/__version__.+/__version__ = '$(VERSION)'/" h2o_lightwave_web/h2o_lightwave_web/version.py

help: ## List all make tasks
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions py/h2o_lightwave/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.PHONY: build
build:
../venv/bin/python setup.py bdist_wheel
141 changes: 141 additions & 0 deletions py/h2o_lightwave/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# H2O Lightwave

H2O Lightwave is a lightweight, pure-Python version of [H2O Wave](https://wave.h2o.ai/) that can be embedded in popular async web frameworks like FastAPI, Starlette, etc.

In other words, H2O Lightwave works without the Wave server.

The integration consists of 2 steps:

* Add Wave's web assets directory to your framework's static file handler.
* Add a WebSocket handler, and use `wave_serve()` to connect Wave to your web UI.

That's it. You can now render UI elements using pure Python. Lightwave aims to be as minimal as possible and only provides:

* A simple way to render your UI.
* A simple way of getting the UI inputs (like button clicks, dropdown values etc.)
* Minimal state management.

Nothing more, nothing less.

Example FastAPI integration:

```py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from h2o_lightwave import Q, ui, wave_serve
from h2o_lightwave_web import web_directory


# Lightwave callback function.
async def serve(q: Q):
# Paint our UI on the first page visit.
if not q.client.initialized:
# Create a local state.
q.client.count = 0
# Add a "card" with a text and a button
q.page['hello'] = ui.form_card(box='1 1 2 2', items=[
ui.text_xl('Hello world'),
ui.button(name='counter', label=f'Current count: {q.client.count}'),
])
q.client.initialized = True

# Handle counter button click.
if q.args.counter:
# Increment the counter.
q.client.count += 1
# Update the counter button.
q.page['hello'].items[1].button.label = f'Current count: {q.client.count}'

# Send the UI changes to the browser.
await q.page.save()


# Run: uvicorn hello_fastapi:app.
# FastAPI boilerplate.
app = FastAPI()


# FastAPI: WebSocket must be registered before index.html handler.
@app.websocket("/_s/")
async def ws(ws: WebSocket):
try:
await ws.accept()
await wave_serve(serve, ws.send_text, ws.receive_text)
await ws.close()
except WebSocketDisconnect:
print('Client disconnected')

app.mount("/", StaticFiles(directory=web_directory, html=True), name="/")
```

<!-- TODO: Add a link to all the integration examples. -->

## Installation

```bash
pip install "h2o-lightwave[web]"
```

Lightwave requires websockets to function properly. Not all libraries come with them out of the box so you might need to install them additionally. For example, Starlette & FastAPI requires

```bash
pip install websockets
```

to be able to expose websocket handlers. This might differ from framework to framework.

## Widgets

All available widgets can be found [here](https://wave.h2o.ai/docs/widgets/overview). We are working on separate docs for Lightwave.

## Using Lightwave within an existing HTML page

Lightwave can also be used only for certain parts of your HTML pages, e.g. for charts. In addition to the integration steps above:

* Use the `get_web_files` function which HTML links to scripts and styles for you to inject into your existing HTML.
* Render a `div` with an id `wave-root` (`<div id='wave-root'></div>`) into which you want Lightwave to render.
* Render a parent container for `wave-root` that has `position: relative` and has some dimensions attached.

```html
{# index_template.html #}
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- Scripts and stylesheets required for Wave to work properly. -->
{{ wave_files }}
</head>
<style>
/* Must have position: relative and some size specified (e.g. height, flexbox, absolute positioning etc.). */
.wave-container {
position: relative;
height: 800px;
}
</style>

<!-- Websocket URL can be changed if needed. Defaults to "/_s/". -->
<body data-wave-socket-url="/custom_socket/">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="wave-container">
<!-- Wave renders here. -->
<div id="wave-root"></div>
</div>
</body>

</html>
```

## Configuration

By default, Lightwave tries to connect to websocket route at `/_s/`. This can be configured by adding a `data-wave-socket-url` attribute on the HTML body element (`<body data-wave-socket-url='/my_socket_url/'>`).

## Links

* Website: [https://wave.h2o.ai/](https://wave.h2o.ai/)
* Releases: [https://pypi.org/project/h2o-wave/](https://pypi.org/project/h2o-wave/)
* Code: [https://github.com/h2oai/wave](https://github.com/h2oai/wave)
* Issue tracker: [https://github.com/h2oai/wave/issues](https://github.com/h2oai/wave/issues)
46 changes: 46 additions & 0 deletions py/h2o_lightwave/examples/hello_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from h2o_lightwave import Q, ui, wave_serve
from h2o_lightwave_web import web_directory


# Lightwave callback function.
async def serve(q: Q):
# Paint our UI on the first page visit.
if not q.client.initialized:
# Create a local state.
q.client.count = 0
# Add a "card" with a text and a button
q.page['hello'] = ui.form_card(box='1 1 2 2', items=[
ui.text_xl('Hello world'),
ui.button(name='counter', label=f'Current count: {q.client.count}'),
])
q.client.initialized = True

# Handle counter button click.
if q.args.counter:
# Increment the counter.
q.client.count += 1
# Update the counter button.
q.page['hello'].items[1].button.label = f'Current count: {q.client.count}'

# Send the UI changes to the browser.
await q.page.save()


# Run: uvicorn hello_fastapi:app.
# FastAPI boilerplate.
app = FastAPI()


# FastAPI: WebSocket must be registered before index.html handler.
@app.websocket("/_s/")
async def ws(ws: WebSocket):
try:
await ws.accept()
await wave_serve(serve, ws.send_text, ws.receive_text)
await ws.close()
except WebSocketDisconnect:
print('Client disconnected')

app.mount("/", StaticFiles(directory=web_directory, html=True), name="/")
Loading