h2o-lightwave 1.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- h2o_lightwave/__init__.py +30 -0
- h2o_lightwave/core.py +508 -0
- h2o_lightwave/graphics.py +841 -0
- h2o_lightwave/py.typed +0 -0
- h2o_lightwave/routing.py +249 -0
- h2o_lightwave/server.py +109 -0
- h2o_lightwave/types.py +14098 -0
- h2o_lightwave/ui.py +4978 -0
- h2o_lightwave/ui_ext.py +52 -0
- h2o_lightwave/version.py +1 -0
- h2o_lightwave-1.7.6.dist-info/METADATA +172 -0
- h2o_lightwave-1.7.6.dist-info/RECORD +14 -0
- h2o_lightwave-1.7.6.dist-info/WHEEL +4 -0
- h2o_lightwave-1.7.6.dist-info/licenses/LICENSE +1 -0
h2o_lightwave/py.typed
ADDED
|
File without changes
|
h2o_lightwave/routing.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Copyright 2020 H2O.ai, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Optional, Callable
|
|
16
|
+
from inspect import signature
|
|
17
|
+
import logging
|
|
18
|
+
from starlette.routing import compile_path
|
|
19
|
+
from .core import expando_to_dict
|
|
20
|
+
from .server import Q
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_event_handlers = {} # dictionary of event_source => [(event_type, predicate, handler)]
|
|
25
|
+
_arg_handlers = {} # dictionary of arg_name => [(predicate, handler)]
|
|
26
|
+
_path_handlers = []
|
|
27
|
+
_arg_with_params_handlers = []
|
|
28
|
+
_handle_on_deprecated_warning_printed = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_arity(func: Callable) -> int:
|
|
32
|
+
return len(signature(func).parameters)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _add_handler(arg: str, func: Callable, predicate: Optional[Callable]):
|
|
36
|
+
if arg not in _arg_handlers:
|
|
37
|
+
_arg_handlers[arg] = handlers = []
|
|
38
|
+
else:
|
|
39
|
+
handlers = _arg_handlers[arg]
|
|
40
|
+
handlers.append((predicate, func, _get_arity(func)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _add_event_handler(source: str, event: str, func: Callable, predicate: Optional[Callable]):
|
|
44
|
+
if source not in _event_handlers:
|
|
45
|
+
_event_handlers[source] = handlers = []
|
|
46
|
+
else:
|
|
47
|
+
handlers = _event_handlers[source]
|
|
48
|
+
handlers.append((event, predicate, func, _get_arity(func)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def on(arg: str = None, predicate: Optional[Callable] = None):
|
|
52
|
+
"""
|
|
53
|
+
Indicate that a function is a query handler that should be invoked when `q.args` or `q.events` contains an argument that matches a specific name or pattern or value.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
A function annotated with `@on('foo')` is invoked whenever `q.args.foo` is found and the value is truthy.
|
|
57
|
+
A function annotated with `@on('foo', lambda x: x is False)` is invoked whenever `q.args.foo` is False.
|
|
58
|
+
A function annotated with `@on('foo', lambda x: isinstance(x, bool)` is invoked whenever `q.args.foo` is True or False.
|
|
59
|
+
A function annotated with `@on('foo', lambda x: 42 <= x <= 420)` is invoked whenever `q.args.foo` between 42 and 420.
|
|
60
|
+
A function annotated with `@on('foo.bar')` is invoked whenever `q.events.foo.bar` is found and the value is truthy.
|
|
61
|
+
A function annotated with `@on('foo.bar', lambda x: x is False)` is invoked whenever `q.events.foo.bar` is False.
|
|
62
|
+
A function annotated with `@on('foo.bar', lambda x: isinstance(x, bool)` is invoked whenever `q.events.foo.bar` is True or False.
|
|
63
|
+
A function annotated with `@on('foo.bar', lambda x: 42 <= x <= 420)` is invoked whenever `q.events.foo.bar` between 42 and 420.
|
|
64
|
+
A function annotated with `@on('#foo')` is invoked whenever `q.args['#']` equals 'foo'.
|
|
65
|
+
A function annotated with `@on('#foo/bar')` is invoked whenever `q.args['#']` equals 'foo/bar'.
|
|
66
|
+
A function annotated with `@on('#foo/{"{fruit}"}')` is invoked whenever `q.args['#']` matches 'foo/apple', 'foo/orange', etc. The parameter 'fruit' is passed to the function (in this case, 'apple', 'orange', etc.)
|
|
67
|
+
|
|
68
|
+
Parameters in patterns (indicated within curly braces) can be converted to `str`, `int`, `float` or `uuid.UUID` instances by suffixing the parameters with `str`, `int`, `float` or `uuid`, respectively.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
- `user_id:int`: `user_id` is converted to an integer.
|
|
72
|
+
- `amount:float`: `amount` is converted to a float.
|
|
73
|
+
- `id:uuid`: `id` is converted to a `uuid.UUID`.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
arg: The name of the `q.arg` argument (in case of plain arguments) or the event_source.event_type (in case of events) or a pattern (in case of hash arguments, or `q.args['#']`). If not provided, the `q.arg` argument is assumed to be the same as the name of the function.
|
|
77
|
+
predicate: A function (or lambda) to test the value of the argument. If provided, the query handler is invoked if the function returns a truthy value.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def wrap(func):
|
|
81
|
+
func_name = func.__name__
|
|
82
|
+
|
|
83
|
+
# This check fails in Cythonized apps.
|
|
84
|
+
# Related:
|
|
85
|
+
# - https://bugs.python.org/issue38225
|
|
86
|
+
# - https://github.com/cython/cython/issues/2273
|
|
87
|
+
# if not asyncio.iscoroutinefunction(func):
|
|
88
|
+
# raise ValueError(f"@on function '{func_name}' must be async")
|
|
89
|
+
|
|
90
|
+
if predicate and not callable(predicate):
|
|
91
|
+
raise ValueError(f"@on predicate must be callable for '{func_name}'")
|
|
92
|
+
if isinstance(arg, str) and len(arg):
|
|
93
|
+
if arg.startswith('#'): # location hash
|
|
94
|
+
rx, _, conv = compile_path(arg[1:])
|
|
95
|
+
_path_handlers.append((rx, conv, func, _get_arity(func)))
|
|
96
|
+
elif '.' in arg: # event
|
|
97
|
+
source, event = arg.split('.', 1)
|
|
98
|
+
if not len(source):
|
|
99
|
+
raise ValueError(f"@on event source cannot be empty in '{arg}' for '{func_name}'")
|
|
100
|
+
if not len(event):
|
|
101
|
+
raise ValueError(f"@on event type cannot be empty in '{arg}' for '{func_name}'")
|
|
102
|
+
_add_event_handler(source, event, func, predicate)
|
|
103
|
+
elif "{" in arg and "}" in arg:
|
|
104
|
+
rx, _, conv = compile_path(arg)
|
|
105
|
+
_arg_with_params_handlers.append((predicate, func, _get_arity(func), rx, conv))
|
|
106
|
+
else:
|
|
107
|
+
_add_handler(arg, func, predicate)
|
|
108
|
+
else:
|
|
109
|
+
_add_handler(func_name, func, predicate)
|
|
110
|
+
logger.debug(f'Registered event handler for {func_name}')
|
|
111
|
+
return func
|
|
112
|
+
|
|
113
|
+
return wrap
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any, **params: any):
|
|
117
|
+
if arity == 0:
|
|
118
|
+
await func()
|
|
119
|
+
elif arity == 1:
|
|
120
|
+
await func(q)
|
|
121
|
+
elif len(params) == 0:
|
|
122
|
+
await func(q, arg)
|
|
123
|
+
elif arity == len(params) + 1:
|
|
124
|
+
await func(q, **params)
|
|
125
|
+
else:
|
|
126
|
+
await func(q, arg, **params)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any, **params: any) -> bool:
|
|
130
|
+
if predicate:
|
|
131
|
+
if predicate(arg):
|
|
132
|
+
await _invoke_handler(func, arity, q, arg, **params)
|
|
133
|
+
return True
|
|
134
|
+
else:
|
|
135
|
+
if arg is not None:
|
|
136
|
+
await _invoke_handler(func, arity, q, arg, **params)
|
|
137
|
+
return True
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def run_on(q: Q) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Handle the query using a query handler (a function annotated with `@on()`).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
q: The query context.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if a matching query handler was found and invoked, else False.
|
|
150
|
+
"""
|
|
151
|
+
submitted = str(q.args['__wave_submission_name__'])
|
|
152
|
+
|
|
153
|
+
# Event handlers.
|
|
154
|
+
for event_source in expando_to_dict(q.events):
|
|
155
|
+
for entry in _event_handlers.get(event_source, []):
|
|
156
|
+
event_type, predicate, func, arity = entry
|
|
157
|
+
event = q.events[event_source]
|
|
158
|
+
if event_type in event:
|
|
159
|
+
arg_value = event[event_type]
|
|
160
|
+
if await _match_predicate(predicate, func, arity, q, arg_value):
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
# Hash handlers.
|
|
164
|
+
if submitted == '#':
|
|
165
|
+
for rx, conv, func, arity in _path_handlers:
|
|
166
|
+
match = rx.match(q.args[submitted])
|
|
167
|
+
if match:
|
|
168
|
+
params = match.groupdict()
|
|
169
|
+
for key, value in params.items():
|
|
170
|
+
params[key] = conv[key].convert(value)
|
|
171
|
+
if len(params):
|
|
172
|
+
if arity <= 1:
|
|
173
|
+
await _invoke_handler(func, arity, q, None)
|
|
174
|
+
else:
|
|
175
|
+
await func(q, **params)
|
|
176
|
+
else:
|
|
177
|
+
await _invoke_handler(func, arity, q, None)
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
# Arg handlers.
|
|
181
|
+
for entry in _arg_handlers.get(submitted, []):
|
|
182
|
+
predicate, func, arity = entry
|
|
183
|
+
if await _match_predicate(predicate, func, arity, q, q.args[submitted]):
|
|
184
|
+
return True
|
|
185
|
+
for predicate, func, arity, rx, conv in _arg_with_params_handlers:
|
|
186
|
+
match = rx.match(submitted)
|
|
187
|
+
if match:
|
|
188
|
+
params = match.groupdict()
|
|
189
|
+
for key, value in params.items():
|
|
190
|
+
params[key] = conv[key].convert(value)
|
|
191
|
+
if await _match_predicate(predicate, func, arity, q, q.args[submitted], **params):
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def handle_on(q: Q) -> bool:
|
|
198
|
+
"""
|
|
199
|
+
DEPRECATED: Handle the query using a query handler (a function annotated with `@on()`).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
q: The query context.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if a matching query handler was found and invoked, else False.
|
|
206
|
+
"""
|
|
207
|
+
global _handle_on_deprecated_warning_printed
|
|
208
|
+
if not _handle_on_deprecated_warning_printed:
|
|
209
|
+
print('\033[93m' + 'WARNING: handle_on() is deprecated, use run_on() instead.' + '\033[0m')
|
|
210
|
+
_handle_on_deprecated_warning_printed = True
|
|
211
|
+
|
|
212
|
+
event_sources = expando_to_dict(q.events)
|
|
213
|
+
for event_source in event_sources:
|
|
214
|
+
event = q.events[event_source]
|
|
215
|
+
entries = _event_handlers.get(event_source)
|
|
216
|
+
if entries:
|
|
217
|
+
for entry in entries:
|
|
218
|
+
event_type, predicate, func, arity = entry
|
|
219
|
+
if event_type in event:
|
|
220
|
+
arg_value = event[event_type]
|
|
221
|
+
if await _match_predicate(predicate, func, arity, q, arg_value):
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
args = expando_to_dict(q.args)
|
|
225
|
+
for arg in args:
|
|
226
|
+
arg_value = q.args[arg]
|
|
227
|
+
if arg == '#':
|
|
228
|
+
for rx, conv, func, arity in _path_handlers:
|
|
229
|
+
match = rx.match(arg_value)
|
|
230
|
+
if match:
|
|
231
|
+
params = match.groupdict()
|
|
232
|
+
for key, value in params.items():
|
|
233
|
+
params[key] = conv[key].convert(value)
|
|
234
|
+
if len(params):
|
|
235
|
+
if arity <= 1:
|
|
236
|
+
await _invoke_handler(func, arity, q, None)
|
|
237
|
+
else:
|
|
238
|
+
await func(q, **params)
|
|
239
|
+
else:
|
|
240
|
+
await _invoke_handler(func, arity, q, None)
|
|
241
|
+
return True
|
|
242
|
+
else:
|
|
243
|
+
entries = _arg_handlers.get(arg)
|
|
244
|
+
if entries:
|
|
245
|
+
for entry in entries:
|
|
246
|
+
predicate, func, arity = entry
|
|
247
|
+
if await _match_predicate(predicate, func, arity, q, arg_value):
|
|
248
|
+
return True
|
|
249
|
+
return False
|
h2o_lightwave/server.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright 2020 H2O.ai, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import traceback
|
|
18
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
19
|
+
|
|
20
|
+
from .core import AsyncPage, Expando
|
|
21
|
+
from .ui import markdown_card
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Query:
|
|
27
|
+
"""
|
|
28
|
+
Represents the query context.
|
|
29
|
+
The query context is passed to the handler function whenever a query
|
|
30
|
+
arrives from the browser (page load, user interaction events, etc.).
|
|
31
|
+
The query context contains useful information about the query, including arguments
|
|
32
|
+
`args` (equivalent to URL query strings) and client-level state.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, page: AsyncPage, client_state: Expando, args: Expando, events: Expando):
|
|
36
|
+
self.page = page
|
|
37
|
+
"""A reference to the current page."""
|
|
38
|
+
self.client = client_state
|
|
39
|
+
"""An `h2o_wave.core.Expando` instance to hold client-specific state."""
|
|
40
|
+
self.args = args
|
|
41
|
+
"""A `h2o_wave.core.Expando` instance containing arguments from the active request."""
|
|
42
|
+
self.events = events
|
|
43
|
+
"""A `h2o_wave.core.Expando` instance containing events from the active request."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Q = Query
|
|
47
|
+
"""Alias for Query context."""
|
|
48
|
+
|
|
49
|
+
HandleAsync = Callable[[Q], Awaitable[Any]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def wave_serve(handle: HandleAsync, send: Optional[Callable] = None, recv: Optional[Callable] = None):
|
|
53
|
+
await _App(handle, send, recv)._run()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _App:
|
|
57
|
+
def __init__(self, handle: HandleAsync, send: Optional[Callable] = None, recv: Optional[Callable] = None):
|
|
58
|
+
self._recv = recv
|
|
59
|
+
self._handle = handle
|
|
60
|
+
self._state: Expando = Expando()
|
|
61
|
+
self._page: AsyncPage = AsyncPage(send)
|
|
62
|
+
|
|
63
|
+
async def _run(self):
|
|
64
|
+
# Handshake.
|
|
65
|
+
received = await self._recv()
|
|
66
|
+
if not received.startswith('+'):
|
|
67
|
+
raise ValueError('Wave Error: Invalid handshake')
|
|
68
|
+
await self._process({})
|
|
69
|
+
# Event loop.
|
|
70
|
+
try:
|
|
71
|
+
while True:
|
|
72
|
+
data = await self._recv()
|
|
73
|
+
data = _parse_msg(data)
|
|
74
|
+
data = json.loads(data)
|
|
75
|
+
await self._process(data)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
raise ValueError('Wave Error: Invalid message.')
|
|
78
|
+
|
|
79
|
+
async def _process(self, args: dict):
|
|
80
|
+
events_state: Optional[dict] = args.get('', None)
|
|
81
|
+
if isinstance(events_state, dict):
|
|
82
|
+
events_state = {k: Expando(v) for k, v in events_state.items()}
|
|
83
|
+
del args['']
|
|
84
|
+
q = Q(self._page, self._state, Expando(args), Expando(events_state))
|
|
85
|
+
try:
|
|
86
|
+
await self._handle(q)
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.exception('Unhandled exception')
|
|
89
|
+
try:
|
|
90
|
+
q.page.drop()
|
|
91
|
+
# TODO replace this with a custom-designed error display
|
|
92
|
+
q.page['__unhandled_error__'] = markdown_card(
|
|
93
|
+
box='1 1 -1 -1',
|
|
94
|
+
title='Error',
|
|
95
|
+
content=f'```\n{traceback.format_exc()}\n```',
|
|
96
|
+
)
|
|
97
|
+
await q.page.save()
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.exception('Failed transmitting unhandled exception')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_msg(msg: str) -> Optional[dict]:
|
|
103
|
+
# protocol: t addr data
|
|
104
|
+
parts = msg.split(' ', 2)
|
|
105
|
+
|
|
106
|
+
if len(parts) != 3:
|
|
107
|
+
raise ValueError('Invalid message')
|
|
108
|
+
|
|
109
|
+
return parts[2]
|