h2o-wave 0.26.3__py3-none-any.whl → 1.0.1__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.
Potentially problematic release.
This version of h2o-wave might be problematic. Click here for more details.
- h2o_wave/__init__.py +2 -2
- h2o_wave/cli.py +69 -49
- h2o_wave/core.py +35 -34
- h2o_wave/metadata.py +4 -3
- h2o_wave/routing.py +79 -11
- h2o_wave/server.py +7 -38
- h2o_wave/share.py +13 -4
- h2o_wave/types.py +222 -23
- h2o_wave/ui.py +86 -13
- h2o_wave/version.py +1 -1
- {h2o_wave-0.26.3.dist-info → h2o_wave-1.0.1.dist-info}/METADATA +33 -32
- h2o_wave-1.0.1.dist-info/RECORD +22 -0
- {h2o_wave-0.26.3.dist-info → h2o_wave-1.0.1.dist-info}/WHEEL +1 -2
- {h2o_wave-0.26.3.dist-info → h2o_wave-1.0.1.dist-info}/entry_points.txt +0 -1
- h2o_wave-0.26.3.data/data/project_templates/README.md +0 -41
- h2o_wave-0.26.3.data/data/project_templates/header.py +0 -85
- h2o_wave-0.26.3.data/data/project_templates/header_nav.py +0 -218
- h2o_wave-0.26.3.data/data/project_templates/header_sidebar_nav.py +0 -232
- h2o_wave-0.26.3.data/data/project_templates/hello_world.py +0 -69
- h2o_wave-0.26.3.data/data/project_templates/sidebar_nav.py +0 -227
- h2o_wave-0.26.3.dist-info/RECORD +0 -29
- h2o_wave-0.26.3.dist-info/top_level.txt +0 -1
- {h2o_wave-0.26.3.dist-info → h2o_wave-1.0.1.dist-info/licenses}/LICENSE +0 -0
h2o_wave/__init__.py
CHANGED
|
@@ -30,8 +30,8 @@ realtime state synchronization between Python and web browsers.
|
|
|
30
30
|
.. include:: ../../docs/index.md
|
|
31
31
|
"""
|
|
32
32
|
from .core import Site, AsyncSite, site, Page, Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
|
|
33
|
-
from .server import
|
|
34
|
-
from .routing import on, handle_on
|
|
33
|
+
from .server import Q, app, main
|
|
34
|
+
from .routing import on, handle_on, run_on
|
|
35
35
|
from .db import connect, WaveDBConnection, WaveDB, WaveDBError
|
|
36
36
|
from .types import *
|
|
37
37
|
from .test import cypress, Cypress
|
h2o_wave/cli.py
CHANGED
|
@@ -25,6 +25,7 @@ from contextlib import closing
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from urllib import request
|
|
27
27
|
from urllib.parse import urlparse
|
|
28
|
+
import webbrowser
|
|
28
29
|
|
|
29
30
|
import click
|
|
30
31
|
import httpx
|
|
@@ -75,6 +76,22 @@ def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
|
|
|
75
76
|
tar.extractall(path, members, numeric_owner=numeric_owner)
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
def _print_launch_bar(local: str, remote: str):
|
|
80
|
+
logo = ''' _ _____ _ ________ _____ __ _____ ____ ______
|
|
81
|
+
| | / / | | / / ____/ / ___// / / / | / __ \/ ____/
|
|
82
|
+
| | /| / / /| | | / / __/ \__ \/ /_/ / /| | / /_/ / __/
|
|
83
|
+
| |/ |/ / ___ | |/ / /___ ___/ / __ / ___ |/ _, _/ /___
|
|
84
|
+
|__/|__/_/ |_|___/_____/ /____/_/ /_/_/ |_/_/ |_/_____/
|
|
85
|
+
'''
|
|
86
|
+
message = f'Sharing {local} ==> {remote}'
|
|
87
|
+
bar = "─" * (len(message) + 4)
|
|
88
|
+
print(logo)
|
|
89
|
+
print('┌' + bar + '┐')
|
|
90
|
+
print('│ ' + message + ' │')
|
|
91
|
+
print('└' + bar + '┘\n')
|
|
92
|
+
print('\x1b[7;30;43mDO NOT SHARE IF YOUR APP CONTAINS SENSITIVE INFO\x1b[0m')
|
|
93
|
+
|
|
94
|
+
|
|
78
95
|
@click.group()
|
|
79
96
|
def main():
|
|
80
97
|
pass
|
|
@@ -108,8 +125,6 @@ def run(app: str, no_reload: bool, no_autostart: bool):
|
|
|
108
125
|
port = app_address.port
|
|
109
126
|
|
|
110
127
|
addr = f'http://{host}:{port}'
|
|
111
|
-
os.environ['H2O_WAVE_INTERNAL_ADDRESS'] = addr # TODO deprecated
|
|
112
|
-
os.environ['H2O_WAVE_EXTERNAL_ADDRESS'] = addr # TODO deprecated
|
|
113
128
|
os.environ['H2O_WAVE_APP_ADDRESS'] = addr
|
|
114
129
|
|
|
115
130
|
# Make "python -m h2o_wave run" behave identical to "wave run":
|
|
@@ -132,9 +147,9 @@ def run(app: str, no_reload: bool, no_autostart: bool):
|
|
|
132
147
|
else:
|
|
133
148
|
autostart = os.environ.get('H2O_WAVE_NO_AUTOSTART', 'false').lower() in ['false', '0', 'f']
|
|
134
149
|
|
|
135
|
-
|
|
150
|
+
waved_path = os.path.join(sys.exec_prefix, 'waved.exe' if IS_WINDOWS else 'waved')
|
|
136
151
|
# OS agnostic wheels do not include waved - needed for HAC.
|
|
137
|
-
is_waved_present = os.path.isfile(
|
|
152
|
+
is_waved_present = os.path.isfile(waved_path)
|
|
138
153
|
|
|
139
154
|
try:
|
|
140
155
|
if autostart and is_waved_present and server_not_running:
|
|
@@ -142,7 +157,7 @@ def run(app: str, no_reload: bool, no_autostart: bool):
|
|
|
142
157
|
if IS_WINDOWS:
|
|
143
158
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
144
159
|
|
|
145
|
-
waved_process = subprocess.Popen([
|
|
160
|
+
waved_process = subprocess.Popen([waved_path], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
|
|
146
161
|
time.sleep(1)
|
|
147
162
|
server_not_running = _scan_free_port(server_port) == server_port
|
|
148
163
|
retries = 3
|
|
@@ -151,23 +166,22 @@ def run(app: str, no_reload: bool, no_autostart: bool):
|
|
|
151
166
|
time.sleep(2)
|
|
152
167
|
server_not_running = _scan_free_port(server_port) == server_port
|
|
153
168
|
retries = retries - 1
|
|
154
|
-
|
|
169
|
+
|
|
155
170
|
if autostart and server_not_running:
|
|
156
171
|
print('Could not connect to Wave server. Please start the Wave server (waved or waved.exe) prior to running any app.')
|
|
157
172
|
return
|
|
158
|
-
try:
|
|
159
|
-
if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
|
|
160
|
-
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
|
|
161
|
-
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload)
|
|
162
|
-
except Exception as e:
|
|
163
|
-
if waved_process:
|
|
164
|
-
waved_process.kill()
|
|
165
|
-
raise e
|
|
166
|
-
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
|
|
175
|
+
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
|
|
176
|
+
reload_exclude = os.environ.get('H2O_WAVE_RELOAD_EXCLUDE', None)
|
|
177
|
+
if reload_exclude:
|
|
178
|
+
reload_exclude = reload_exclude.split(os.pathsep)
|
|
179
|
+
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload, reload_excludes=reload_exclude)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise e
|
|
182
|
+
finally:
|
|
183
|
+
if waved_process:
|
|
184
|
+
waved_process.kill()
|
|
171
185
|
|
|
172
186
|
|
|
173
187
|
@main.command()
|
|
@@ -245,33 +259,26 @@ def init():
|
|
|
245
259
|
"""
|
|
246
260
|
try:
|
|
247
261
|
theme = inquirer.themes.load_theme_from_dict({"List": {"selection_color": "yellow"}})
|
|
248
|
-
project = inquirer.prompt([
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
262
|
+
project = inquirer.prompt([
|
|
263
|
+
inquirer.List(
|
|
264
|
+
name='project',
|
|
265
|
+
message="Choose a starter template",
|
|
266
|
+
choices=[
|
|
267
|
+
('Hello World app (for beginners)', 'hello_world.py'),
|
|
268
|
+
('Chat app', 'chat.py'),
|
|
269
|
+
('App with header', 'header.py'),
|
|
270
|
+
('App with header + navigation', 'header_nav.py'),
|
|
271
|
+
('App with sidebar + navigation', 'sidebar_nav.py'),
|
|
272
|
+
('App with header & sidebar + navigation', 'header_sidebar_nav.py'),
|
|
273
|
+
]
|
|
274
|
+
),
|
|
256
275
|
], theme=theme)['project']
|
|
257
276
|
# Ctrl-C causes TypeError within inquirer, resulting in ugly stacktrace. Catch the error and return early on CTRL-C.
|
|
258
277
|
except (KeyboardInterrupt, TypeError):
|
|
259
278
|
return
|
|
260
279
|
|
|
261
|
-
app_content = ''
|
|
262
280
|
base_path = os.path.join(sys.exec_prefix, 'project_templates')
|
|
263
|
-
|
|
264
|
-
app_content = read_file(os.path.join(base_path, 'hello_world.py'))
|
|
265
|
-
elif 'header & sidebar' in project:
|
|
266
|
-
app_content = read_file(os.path.join(base_path, 'header_sidebar_nav.py'))
|
|
267
|
-
elif 'header +' in project:
|
|
268
|
-
app_content = read_file(os.path.join(base_path, 'header_nav.py'))
|
|
269
|
-
elif 'header' in project:
|
|
270
|
-
app_content = read_file(os.path.join(base_path, 'header.py'))
|
|
271
|
-
elif 'sidebar +' in project:
|
|
272
|
-
app_content = read_file(os.path.join(base_path, 'sidebar_nav.py'))
|
|
273
|
-
|
|
274
|
-
write_file('app.py', app_content)
|
|
281
|
+
write_file('app.py', read_file(os.path.join(base_path, project)))
|
|
275
282
|
write_file('requirements.txt', f'h2o-wave=={__version__}')
|
|
276
283
|
write_file('README.md', read_file(os.path.join(base_path, 'README.md')))
|
|
277
284
|
|
|
@@ -294,9 +301,11 @@ def learn():
|
|
|
294
301
|
|
|
295
302
|
@main.command()
|
|
296
303
|
@click.option('--port', default=10101, help='Port your app is running on (defaults to 10101).')
|
|
297
|
-
@click.option('--subdomain', default='
|
|
304
|
+
@click.option('--subdomain', default='my-app', help='Subdomain to use. If not available, a random one is generated.')
|
|
298
305
|
@click.option('--remote-host', default='h2oai.app', help='Remote host to use (defaults to h2oai.app).')
|
|
299
|
-
|
|
306
|
+
@click.option('--remote-port', default=443, help='Remote port to use (defaults to 443).')
|
|
307
|
+
@click.option('--open', is_flag=True, default=False, help='Open the shared app in your browser automatically.')
|
|
308
|
+
def share(port: int, subdomain: str, remote_host: str, remote_port: int, open: bool):
|
|
300
309
|
"""Share your locally running app with the world.
|
|
301
310
|
|
|
302
311
|
\b
|
|
@@ -319,29 +328,35 @@ def share(port: int, subdomain: str, remote_host: str):
|
|
|
319
328
|
loop.create_task(wakeup())
|
|
320
329
|
|
|
321
330
|
try:
|
|
322
|
-
loop.run_until_complete(_share(port, subdomain, remote_host))
|
|
331
|
+
loop.run_until_complete(_share(port, subdomain, remote_host, remote_port, open))
|
|
323
332
|
except KeyboardInterrupt:
|
|
324
333
|
tasks = asyncio.all_tasks(loop)
|
|
325
334
|
for task in tasks:
|
|
326
335
|
task.cancel()
|
|
327
336
|
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
|
328
337
|
loop.close()
|
|
338
|
+
print('Sharing stopped.')
|
|
329
339
|
|
|
330
340
|
|
|
331
|
-
async def _share(port: int, subdomain: str, remote_host: str):
|
|
341
|
+
async def _share(port: int, subdomain: str, remote_host: str, remote_port: int, should_open: bool):
|
|
332
342
|
if _scan_free_port(port) == port:
|
|
333
343
|
print(f'Could not connect to localhost:{port}. Please make sure your app is running.')
|
|
334
344
|
exit(1)
|
|
335
345
|
|
|
336
|
-
|
|
346
|
+
protocol = 'https' if remote_port == 443 else 'http'
|
|
347
|
+
res = httpx.get(f'{protocol}://{remote_host}:{remote_port}/register/{subdomain}')
|
|
337
348
|
if res.status_code != 200:
|
|
338
349
|
print('Could not connect to the remote sharing server.')
|
|
339
350
|
exit(1)
|
|
340
351
|
|
|
341
352
|
res = res.json()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
353
|
+
share_id = res['id']
|
|
354
|
+
|
|
355
|
+
remote = f'{protocol}://{share_id}.{remote_host}'
|
|
356
|
+
if remote_port != 80 and remote_port != 443:
|
|
357
|
+
remote += f':{remote_port}'
|
|
358
|
+
|
|
359
|
+
_print_launch_bar(f'http://localhost:{port}', remote)
|
|
345
360
|
|
|
346
361
|
max_conn_count = res['max_conn_count']
|
|
347
362
|
# The server can be configured to either support 10 concurrent connections (default) or more.
|
|
@@ -351,10 +366,15 @@ async def _share(port: int, subdomain: str, remote_host: str):
|
|
|
351
366
|
tasks = []
|
|
352
367
|
for _ in range(max_conn_count // step):
|
|
353
368
|
for _ in range(step):
|
|
354
|
-
tasks.append(asyncio.create_task(listen_on_socket('127.0.0.1', port, remote_host,
|
|
369
|
+
tasks.append(asyncio.create_task(listen_on_socket('127.0.0.1', port, remote_host, remote_port, share_id)))
|
|
355
370
|
await asyncio.sleep(1)
|
|
356
371
|
# Handle the rest if any.
|
|
357
372
|
for _ in range(max_conn_count % step):
|
|
358
|
-
tasks.append(asyncio.create_task(listen_on_socket('127.0.0.1', port, remote_host,
|
|
373
|
+
tasks.append(asyncio.create_task(listen_on_socket('127.0.0.1', port, remote_host, remote_port, share_id)))
|
|
374
|
+
|
|
375
|
+
if should_open:
|
|
376
|
+
await asyncio.sleep(1)
|
|
377
|
+
webbrowser.open(remote)
|
|
359
378
|
|
|
360
379
|
await asyncio.gather(*tasks)
|
|
380
|
+
print('Could not establish connection with the server.')
|
h2o_wave/core.py
CHANGED
|
@@ -18,7 +18,6 @@ import ipaddress
|
|
|
18
18
|
import json
|
|
19
19
|
import platform
|
|
20
20
|
import secrets
|
|
21
|
-
import shutil
|
|
22
21
|
import subprocess
|
|
23
22
|
from urllib.parse import urlparse
|
|
24
23
|
from uuid import uuid4
|
|
@@ -46,14 +45,12 @@ def _get_env(key: str, value: Any):
|
|
|
46
45
|
return os.environ.get(f'H2O_WAVE_{key}', value)
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
_default_internal_address = 'http://127.0.0.1:8000'
|
|
50
48
|
_base_url = _get_env('BASE_URL', '/')
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
class _Config:
|
|
54
52
|
def __init__(self):
|
|
55
|
-
self.
|
|
56
|
-
self.app_address = _get_env('APP_ADDRESS', _get_env('EXTERNAL_ADDRESS', self.internal_address))
|
|
53
|
+
self.app_address = _get_env('APP_ADDRESS', 'http://127.0.0.1:8000')
|
|
57
54
|
self.app_mode = _get_env('APP_MODE', UNICAST)
|
|
58
55
|
self.hub_base_url = _get_env('BASE_URL', '/')
|
|
59
56
|
self.hub_host_address = _get_env('ADDRESS', 'http://127.0.0.1:10101')
|
|
@@ -99,17 +96,6 @@ def _are_primitives(xs: Any) -> bool:
|
|
|
99
96
|
return True
|
|
100
97
|
|
|
101
98
|
|
|
102
|
-
def _guard_primitive_list(xs: Any):
|
|
103
|
-
if not _are_primitives(xs):
|
|
104
|
-
raise ValueError('value must be a primitive list or tuple')
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _guard_primitive_dict_values(d: Dict[str, Any]):
|
|
108
|
-
if d:
|
|
109
|
-
for x in d.values():
|
|
110
|
-
_guard_primitive(x)
|
|
111
|
-
|
|
112
|
-
|
|
113
99
|
def _guard_str_key(key: str):
|
|
114
100
|
if not _is_str(key):
|
|
115
101
|
raise KeyError('key must be str')
|
|
@@ -271,6 +257,38 @@ def _dump(xs: Any):
|
|
|
271
257
|
return xs
|
|
272
258
|
|
|
273
259
|
|
|
260
|
+
def _fill_data_buffers(props: Dict, data: list, bufs: list, keys=[], is_form_card=False):
|
|
261
|
+
for k, v in props.items():
|
|
262
|
+
if isinstance(v, Data):
|
|
263
|
+
keys.append(k)
|
|
264
|
+
data.append(('.'.join(keys), len(bufs)))
|
|
265
|
+
bufs.append(v.dump())
|
|
266
|
+
keys.pop()
|
|
267
|
+
elif not is_form_card:
|
|
268
|
+
continue
|
|
269
|
+
elif isinstance(v, list):
|
|
270
|
+
keys.append(k)
|
|
271
|
+
for idx, e in enumerate(v):
|
|
272
|
+
if isinstance(e, dict):
|
|
273
|
+
keys.append(str(idx))
|
|
274
|
+
_fill_data_buffers(e, data, bufs, keys, is_form_card)
|
|
275
|
+
keys.pop()
|
|
276
|
+
keys.pop()
|
|
277
|
+
elif isinstance(v, dict):
|
|
278
|
+
keys.append(k)
|
|
279
|
+
_fill_data_buffers(v, data, bufs, keys, is_form_card)
|
|
280
|
+
keys.pop()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _del_dict_key(d: dict, keys: List[str]):
|
|
284
|
+
if len(keys) == 1:
|
|
285
|
+
del d[keys[0]]
|
|
286
|
+
else:
|
|
287
|
+
next_key = keys[0]
|
|
288
|
+
key = int(next_key) if next_key.isdigit() else next_key
|
|
289
|
+
_del_dict_key(d[key], keys[1:])
|
|
290
|
+
|
|
291
|
+
|
|
274
292
|
class Ref:
|
|
275
293
|
"""
|
|
276
294
|
Represents a local reference to an element on a `h2o_wave.core.Page`.
|
|
@@ -507,13 +525,10 @@ class PageBase:
|
|
|
507
525
|
|
|
508
526
|
data = []
|
|
509
527
|
bufs = []
|
|
510
|
-
|
|
511
|
-
if isinstance(v, Data):
|
|
512
|
-
data.append((k, len(bufs)))
|
|
513
|
-
bufs.append(v.dump())
|
|
528
|
+
_fill_data_buffers(props, data, bufs, [], props.get('view') == 'form')
|
|
514
529
|
|
|
515
530
|
for k, v in data:
|
|
516
|
-
|
|
531
|
+
_del_dict_key(props, k.split('.'))
|
|
517
532
|
props[f'~{k}'] = v
|
|
518
533
|
|
|
519
534
|
if len(bufs) > 0:
|
|
@@ -576,13 +591,6 @@ class Page(PageBase):
|
|
|
576
591
|
"""
|
|
577
592
|
return self.site.load(self.url)
|
|
578
593
|
|
|
579
|
-
def sync(self):
|
|
580
|
-
"""
|
|
581
|
-
DEPRECATED: Use `h2o_wave.core.Page.save` instead.
|
|
582
|
-
"""
|
|
583
|
-
warnings.warn('page.sync() is deprecated. Please use page.save() instead.', DeprecationWarning)
|
|
584
|
-
self.save()
|
|
585
|
-
|
|
586
594
|
def save(self):
|
|
587
595
|
"""
|
|
588
596
|
Save the page. Sends all local changes made to this page to the remote site.
|
|
@@ -616,13 +624,6 @@ class AsyncPage(PageBase):
|
|
|
616
624
|
"""
|
|
617
625
|
return await self.site.load(self.url)
|
|
618
626
|
|
|
619
|
-
async def push(self):
|
|
620
|
-
"""
|
|
621
|
-
DEPRECATED: Use `h2o_wave.core.AsyncPage.save` instead.
|
|
622
|
-
"""
|
|
623
|
-
warnings.warn('page.push() is deprecated. Please use page.save() instead.', DeprecationWarning)
|
|
624
|
-
await self.save()
|
|
625
|
-
|
|
626
627
|
async def save(self):
|
|
627
628
|
"""
|
|
628
629
|
Save the page. Sends all local changes made to this page to the remote site.
|
h2o_wave/metadata.py
CHANGED
h2o_wave/routing.py
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
from typing import Optional, Callable
|
|
16
16
|
from inspect import signature
|
|
17
|
-
import asyncio
|
|
18
17
|
import logging
|
|
19
18
|
from starlette.routing import compile_path
|
|
20
19
|
from .core import expando_to_dict
|
|
@@ -25,6 +24,8 @@ logger = logging.getLogger(__name__)
|
|
|
25
24
|
_event_handlers = {} # dictionary of event_source => [(event_type, predicate, handler)]
|
|
26
25
|
_arg_handlers = {} # dictionary of arg_name => [(predicate, handler)]
|
|
27
26
|
_path_handlers = []
|
|
27
|
+
_arg_with_params_handlers = []
|
|
28
|
+
_handle_on_deprecated_warning_printed = False
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _get_arity(func: Callable) -> int:
|
|
@@ -86,9 +87,8 @@ def on(arg: str = None, predicate: Optional[Callable] = None):
|
|
|
86
87
|
# if not asyncio.iscoroutinefunction(func):
|
|
87
88
|
# raise ValueError(f"@on function '{func_name}' must be async")
|
|
88
89
|
|
|
89
|
-
if predicate:
|
|
90
|
-
|
|
91
|
-
raise ValueError(f"@on predicate must be callable for '{func_name}'")
|
|
90
|
+
if predicate and not callable(predicate):
|
|
91
|
+
raise ValueError(f"@on predicate must be callable for '{func_name}'")
|
|
92
92
|
if isinstance(arg, str) and len(arg):
|
|
93
93
|
if arg.startswith('#'): # location hash
|
|
94
94
|
rx, _, conv = compile_path(arg[1:])
|
|
@@ -100,6 +100,9 @@ def on(arg: str = None, predicate: Optional[Callable] = None):
|
|
|
100
100
|
if not len(event):
|
|
101
101
|
raise ValueError(f"@on event type cannot be empty in '{arg}' for '{func_name}'")
|
|
102
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))
|
|
103
106
|
else:
|
|
104
107
|
_add_handler(arg, func, predicate)
|
|
105
108
|
else:
|
|
@@ -110,28 +113,32 @@ def on(arg: str = None, predicate: Optional[Callable] = None):
|
|
|
110
113
|
return wrap
|
|
111
114
|
|
|
112
115
|
|
|
113
|
-
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any):
|
|
116
|
+
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any, **params: any):
|
|
114
117
|
if arity == 0:
|
|
115
118
|
await func()
|
|
116
119
|
elif arity == 1:
|
|
117
120
|
await func(q)
|
|
118
|
-
|
|
121
|
+
elif len(params) == 0:
|
|
119
122
|
await func(q, arg)
|
|
123
|
+
elif arity == len(params) + 1:
|
|
124
|
+
await func(q, **params)
|
|
125
|
+
else:
|
|
126
|
+
await func(q, arg, **params)
|
|
120
127
|
|
|
121
128
|
|
|
122
|
-
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any) -> bool:
|
|
129
|
+
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any, **params: any) -> bool:
|
|
123
130
|
if predicate:
|
|
124
131
|
if predicate(arg):
|
|
125
|
-
await _invoke_handler(func, arity, q, arg)
|
|
132
|
+
await _invoke_handler(func, arity, q, arg, **params)
|
|
126
133
|
return True
|
|
127
134
|
else:
|
|
128
|
-
if arg:
|
|
129
|
-
await _invoke_handler(func, arity, q, arg)
|
|
135
|
+
if arg is not None:
|
|
136
|
+
await _invoke_handler(func, arity, q, arg, **params)
|
|
130
137
|
return True
|
|
131
138
|
return False
|
|
132
139
|
|
|
133
140
|
|
|
134
|
-
async def
|
|
141
|
+
async def run_on(q: Q) -> bool:
|
|
135
142
|
"""
|
|
136
143
|
Handle the query using a query handler (a function annotated with `@on()`).
|
|
137
144
|
|
|
@@ -141,6 +148,67 @@ async def handle_on(q: Q) -> bool:
|
|
|
141
148
|
Returns:
|
|
142
149
|
True if a matching query handler was found and invoked, else False.
|
|
143
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
|
+
|
|
144
212
|
event_sources = expando_to_dict(q.events)
|
|
145
213
|
for event_source in event_sources:
|
|
146
214
|
event = q.events[event_source]
|
h2o_wave/server.py
CHANGED
|
@@ -17,22 +17,16 @@ import datetime
|
|
|
17
17
|
import asyncio
|
|
18
18
|
from concurrent.futures import Executor
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
import contextvars # Python 3.7+ only.
|
|
22
|
-
except ImportError:
|
|
23
|
-
contextvars = None
|
|
20
|
+
import contextvars
|
|
24
21
|
|
|
25
22
|
import logging
|
|
26
23
|
import functools
|
|
27
|
-
import warnings
|
|
28
24
|
import pickle
|
|
29
25
|
import traceback
|
|
30
26
|
import base64
|
|
31
27
|
import binascii
|
|
32
28
|
from typing import Dict, List, Tuple, Callable, Any, Awaitable, Optional
|
|
33
|
-
from urllib.parse import urlparse
|
|
34
29
|
|
|
35
|
-
import uvicorn
|
|
36
30
|
import httpx
|
|
37
31
|
from starlette.types import Scope, Receive, Send
|
|
38
32
|
from starlette.applications import Router
|
|
@@ -149,8 +143,6 @@ class Query:
|
|
|
149
143
|
"""A `h2o_wave.core.Expando` instance containing arguments from the active request."""
|
|
150
144
|
self.events = events
|
|
151
145
|
"""A `h2o_wave.core.Expando` instance containing events from the active request."""
|
|
152
|
-
self.username = auth.username
|
|
153
|
-
"""The username of the user who initiated the active request. (DEPRECATED: Use q.auth.username instead)"""
|
|
154
146
|
self.route = route
|
|
155
147
|
"""The route served by the server."""
|
|
156
148
|
self.auth = auth
|
|
@@ -191,17 +183,11 @@ class Query:
|
|
|
191
183
|
|
|
192
184
|
loop = asyncio.get_event_loop()
|
|
193
185
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
if kwargs:
|
|
202
|
-
return await loop.run_in_executor(executor, functools.partial(func, *args, **kwargs))
|
|
203
|
-
|
|
204
|
-
return await loop.run_in_executor(executor, func, *args)
|
|
186
|
+
return await loop.run_in_executor(
|
|
187
|
+
executor,
|
|
188
|
+
contextvars.copy_context().run,
|
|
189
|
+
functools.partial(func, *args, **kwargs)
|
|
190
|
+
)
|
|
205
191
|
|
|
206
192
|
async def run(self, func: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
207
193
|
"""
|
|
@@ -300,7 +286,7 @@ class _App:
|
|
|
300
286
|
elapsed_time = current_time - start_time
|
|
301
287
|
if elapsed_time.seconds > connection_timeout:
|
|
302
288
|
logger.debug(f'Register: giving up after retrying for {connection_timeout} seconds')
|
|
303
|
-
raise exception
|
|
289
|
+
raise ConnectionError('Could not connect to Wave server. Make sure it is running.') from exception
|
|
304
290
|
await asyncio.sleep(1)
|
|
305
291
|
logger.debug('Register: retrying...')
|
|
306
292
|
|
|
@@ -504,20 +490,3 @@ def app(route: str, mode=None, on_startup: Optional[Callable] = None,
|
|
|
504
490
|
return handle
|
|
505
491
|
|
|
506
492
|
return wrap
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def listen(route: str, handle: HandleAsync, mode=None):
|
|
510
|
-
"""
|
|
511
|
-
Launch an application server.
|
|
512
|
-
|
|
513
|
-
Args:
|
|
514
|
-
route: The route to listen to. e.g. `'/foo'` or `'/foo/bar/baz'`.
|
|
515
|
-
handle: The handler function.
|
|
516
|
-
mode: The server mode. One of `'unicast'` (default),`'multicast'` or `'broadcast'`.
|
|
517
|
-
"""
|
|
518
|
-
warnings.warn("'listen()' is deprecated. Instead, import 'main' and annotate your 'serve()' function with '@app'.",
|
|
519
|
-
DeprecationWarning)
|
|
520
|
-
|
|
521
|
-
internal_address = urlparse(_config.internal_address)
|
|
522
|
-
logger.info(f'Listening on host "{internal_address.hostname}", port "{internal_address.port}"...')
|
|
523
|
-
uvicorn.run(_Main(_App(route, handle, mode)), host=internal_address.hostname, port=internal_address.port)
|
h2o_wave/share.py
CHANGED
|
@@ -10,17 +10,26 @@ async def pipe(r: asyncio.StreamReader, w: asyncio.StreamWriter) -> None:
|
|
|
10
10
|
await w.drain()
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
async def listen_on_socket(local_host: str, local_port: int, remote_host: str, remote_port: int) -> None:
|
|
13
|
+
async def listen_on_socket(local_host: str, local_port: int, remote_host: str, remote_port: int, id: str) -> None:
|
|
14
|
+
local_reader, local_writer = None, None
|
|
15
|
+
remote_reader, remote_writer = None, None
|
|
16
|
+
retries = 0
|
|
14
17
|
while True:
|
|
18
|
+
if retries > 5:
|
|
19
|
+
break
|
|
15
20
|
try:
|
|
16
21
|
local_reader, local_writer = await asyncio.open_connection(local_host, local_port)
|
|
17
22
|
remote_reader, remote_writer = await asyncio.open_connection(remote_host, remote_port, ssl=True)
|
|
23
|
+
remote_writer.write(f'__h2o_leap__ {id}\n'.encode())
|
|
18
24
|
|
|
25
|
+
retries = 0
|
|
19
26
|
await asyncio.gather(pipe(local_reader, remote_writer), pipe(remote_reader, local_writer))
|
|
20
27
|
|
|
21
28
|
# Swallow exceptions and reconnect.
|
|
22
29
|
except Exception:
|
|
23
|
-
|
|
30
|
+
retries += 1
|
|
24
31
|
finally:
|
|
25
|
-
local_writer
|
|
26
|
-
|
|
32
|
+
if local_writer:
|
|
33
|
+
local_writer.close()
|
|
34
|
+
if remote_writer:
|
|
35
|
+
remote_writer.close()
|