Skip to content

Commit

Permalink
feat: Improved handle_on alternative #1484. (#2113)
Browse files Browse the repository at this point in the history
  • Loading branch information
mturoci authored and marek-mihok committed Jan 15, 2024
1 parent 70cbdbe commit 109ad50
Show file tree
Hide file tree
Showing 25 changed files with 754 additions and 440 deletions.
4 changes: 3 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ var (
// BootMsg represents the initial message sent to an app when a client first connects to it.
type BootMsg struct {
Data struct {
Hash string `json:"#,omitempty"` // location hash
Hash string `json:"#,omitempty"` // location hash
SubmissionName string `json:"__wave_submission_name__,omitempty"` // mark the cause of the serve invocation
} `json:"data"`
Headers http.Header `json:"headers"` // forwarded headers
}
Expand Down Expand Up @@ -171,6 +172,7 @@ func (c *Client) listen() {

if len(m.data) > 0 { // location hash
boot.Data.Hash = string(m.data)
boot.Data.SubmissionName = "#"
}

body, err := json.Marshal(boot)
Expand Down
4 changes: 2 additions & 2 deletions py/examples/hash_routing_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Use the browser's [location hash](https://developer.mozilla.org/en-US/docs/Web/API/Location/hash)
# for #routing using URLs, with parameters.
# ---
from h2o_wave import main, app, Q, ui, on, handle_on
from h2o_wave import main, app, Q, ui, on, run_on

air_passengers_fields = ['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
air_passengers_rows = [
Expand Down Expand Up @@ -62,6 +62,6 @@ async def serve(q: Q):
content='Click on a cell in the table above!',
)

await handle_on(q)
await run_on(q)

await q.page.save()
4 changes: 2 additions & 2 deletions py/examples/plot_events_routing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Plot / Events / Routing
# Handle #events on a #plot card using routing.
# ---
from h2o_wave import main, app, on, handle_on, Q, ui, data
from h2o_wave import main, app, on, run_on, Q, ui, data


@on('pricing.select_marks')
Expand Down Expand Up @@ -32,4 +32,4 @@ async def serve(q: Q):
)
await q.page.save()
else:
await handle_on(q)
await run_on(q)
6 changes: 3 additions & 3 deletions py/examples/routing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Routing
# Use `on` and `handle_on` to simplify query handling by #routing queries to designated functions.
# Use `on` and `run_on` to simplify query handling by #routing queries to designated functions.
# ---
from h2o_wave import main, app, Q, ui, on, handle_on
from h2o_wave import main, app, Q, ui, on, run_on


# This function is called when q.args['empty_cart'] is True.
Expand Down Expand Up @@ -60,4 +60,4 @@ async def serve(q: Q):
],
)
await q.page.save()
await handle_on(q)
await run_on(q)
6 changes: 3 additions & 3 deletions py/examples/routing_predicates.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Routing / Predicates
# Use `on` and `handle_on` with predicates to handle routing with custom conditions.
# Use `on` and `run_on` with predicates to handle routing with custom conditions.
# ---
from h2o_wave import main, app, Q, ui, on, handle_on
from h2o_wave import main, app, Q, ui, on, run_on


# This function is called when q.args['temperature'] < 15.
Expand Down Expand Up @@ -29,7 +29,7 @@ async def serve(q: Q):
q.args.temperature = 20
await show_slider(q, "")
else:
await handle_on(q)
await run_on(q)


async def show_slider(q: Q, message: str):
Expand Down
4 changes: 2 additions & 2 deletions py/examples/tour.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import Dict, List, Optional, Tuple
from urllib.parse import urlparse

from h2o_wave import Q, app, handle_on, main, on, ui
from h2o_wave import Q, app, run_on, main, on, ui

example_dir = os.path.dirname(os.path.realpath(__file__))
tour_tmp_dir = os.path.join(example_dir, '_tour_apps_tmp')
Expand Down Expand Up @@ -416,7 +416,7 @@ async def serve(q: Q):
q.client.path = uuid.uuid4()
await setup_page(q)

await handle_on(q)
await run_on(q)

search = q.args[q.args['#'] or default_example_name]
if search and not q.events.editor:
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_lightwave/h2o_lightwave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from .core import Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
from .server import Q, wave_serve
from .routing import on, handle_on
from .routing import on, run_on, handle_on
from .types import *
from .version import __version__

Expand Down
90 changes: 79 additions & 11 deletions py/h2o_lightwave/h2o_lightwave/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from typing import Optional, Callable
from inspect import signature
import asyncio
import logging
from starlette.routing import compile_path
from .core import expando_to_dict
Expand All @@ -25,6 +24,8 @@
_event_handlers = {} # dictionary of event_source => [(event_type, predicate, handler)]
_arg_handlers = {} # dictionary of arg_name => [(predicate, handler)]
_path_handlers = []
_arg_with_params_handlers = []
_handle_on_deprecated_warning_printed = False


def _get_arity(func: Callable) -> int:
Expand Down Expand Up @@ -86,9 +87,8 @@ def wrap(func):
# if not asyncio.iscoroutinefunction(func):
# raise ValueError(f"@on function '{func_name}' must be async")

if predicate:
if not callable(predicate):
raise ValueError(f"@on predicate must be callable for '{func_name}'")
if predicate and not callable(predicate):
raise ValueError(f"@on predicate must be callable for '{func_name}'")
if isinstance(arg, str) and len(arg):
if arg.startswith('#'): # location hash
rx, _, conv = compile_path(arg[1:])
Expand All @@ -100,6 +100,9 @@ def wrap(func):
if not len(event):
raise ValueError(f"@on event type cannot be empty in '{arg}' for '{func_name}'")
_add_event_handler(source, event, func, predicate)
elif "{" in arg and "}" in arg:
rx, _, conv = compile_path(arg)
_arg_with_params_handlers.append((predicate, func, _get_arity(func), rx, conv))
else:
_add_handler(arg, func, predicate)
else:
Expand All @@ -110,28 +113,32 @@ def wrap(func):
return wrap


async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any):
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any, **params: any):
if arity == 0:
await func()
elif arity == 1:
await func(q)
else:
elif len(params) == 0:
await func(q, arg)
elif arity == len(params) + 1:
await func(q, **params)
else:
await func(q, arg, **params)


async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any) -> bool:
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any, **params: any) -> bool:
if predicate:
if predicate(arg):
await _invoke_handler(func, arity, q, arg)
await _invoke_handler(func, arity, q, arg, **params)
return True
else:
if arg:
await _invoke_handler(func, arity, q, arg)
if arg is not None:
await _invoke_handler(func, arity, q, arg, **params)
return True
return False


async def handle_on(q: Q) -> bool:
async def run_on(q: Q) -> bool:
"""
Handle the query using a query handler (a function annotated with `@on()`).
Expand All @@ -141,6 +148,67 @@ async def handle_on(q: Q) -> bool:
Returns:
True if a matching query handler was found and invoked, else False.
"""
submitted = str(q.args['__wave_submission_name__'])

# Event handlers.
for event_source in expando_to_dict(q.events):
for entry in _event_handlers.get(event_source, []):
event_type, predicate, func, arity = entry
event = q.events[event_source]
if event_type in event:
arg_value = event[event_type]
if await _match_predicate(predicate, func, arity, q, arg_value):
return True

# Hash handlers.
if submitted == '#':
for rx, conv, func, arity in _path_handlers:
match = rx.match(q.args[submitted])
if match:
params = match.groupdict()
for key, value in params.items():
params[key] = conv[key].convert(value)
if len(params):
if arity <= 1:
await _invoke_handler(func, arity, q, None)
else:
await func(q, **params)
else:
await _invoke_handler(func, arity, q, None)
return True

# Arg handlers.
for entry in _arg_handlers.get(submitted, []):
predicate, func, arity = entry
if await _match_predicate(predicate, func, arity, q, q.args[submitted]):
return True
for predicate, func, arity, rx, conv in _arg_with_params_handlers:
match = rx.match(submitted)
if match:
params = match.groupdict()
for key, value in params.items():
params[key] = conv[key].convert(value)
if await _match_predicate(predicate, func, arity, q, q.args[submitted], **params):
return True

return False


async def handle_on(q: Q) -> bool:
"""
DEPRECATED: Handle the query using a query handler (a function annotated with `@on()`).
Args:
q: The query context.
Returns:
True if a matching query handler was found and invoked, else False.
"""
global _handle_on_deprecated_warning_printed
if not _handle_on_deprecated_warning_printed:
print('\033[93m' + 'WARNING: handle_on() is deprecated, use run_on() instead.' + '\033[0m')
_handle_on_deprecated_warning_printed = True

event_sources = expando_to_dict(q.events)
for event_source in event_sources:
event = q.events[event_source]
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_wave/h2o_wave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"""
from .core import Site, AsyncSite, site, Page, Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
from .server import Q, app, main
from .routing import on, handle_on
from .routing import on, handle_on, run_on
from .db import connect, WaveDBConnection, WaveDB, WaveDBError
from .types import *
from .test import cypress, Cypress
Expand Down
31 changes: 16 additions & 15 deletions py/h2o_wave/h2o_wave/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,17 @@ def run(app: str, no_reload: bool, no_autostart: bool):
else:
autostart = os.environ.get('H2O_WAVE_NO_AUTOSTART', 'false').lower() in ['false', '0', 'f']

waved = 'waved.exe' if IS_WINDOWS else './waved'
waved_path = os.path.join(sys.exec_prefix, 'waved.exe' if IS_WINDOWS else 'waved')
# OS agnostic wheels do not include waved - needed for HAC.
is_waved_present = os.path.isfile(os.path.join(sys.exec_prefix, waved))
is_waved_present = os.path.isfile(waved_path)

try:
if autostart and is_waved_present and server_not_running:
kwargs = {}
if IS_WINDOWS:
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP

waved_process = subprocess.Popen([waved], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
waved_process = subprocess.Popen([waved_path], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
time.sleep(1)
server_not_running = _scan_free_port(server_port) == server_port
retries = 3
Expand All @@ -166,21 +166,22 @@ def run(app: str, no_reload: bool, no_autostart: bool):
time.sleep(2)
server_not_running = _scan_free_port(server_port) == server_port
retries = retries - 1
finally:

if autostart and server_not_running:
print('Could not connect to Wave server. Please start the Wave server (waved or waved.exe) prior to running any app.')
return
try:
if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
reload_exclude = os.environ.get('H2O_WAVE_RELOAD_EXCLUDE', None)
if reload_exclude:
reload_exclude = reload_exclude.split(os.pathsep)
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload, reload_excludes=reload_exclude)
except Exception as e:
if waved_process:
waved_process.kill()
raise e

if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
reload_exclude = os.environ.get('H2O_WAVE_RELOAD_EXCLUDE', None)
if reload_exclude:
reload_exclude = reload_exclude.split(os.pathsep)
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload, reload_excludes=reload_exclude)
except Exception as e:
raise e
finally:
if waved_process:
waved_process.kill()


@main.command()
Expand Down
Loading

0 comments on commit 109ad50

Please sign in to comment.