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 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 listen, Q, app, main
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
- waved = 'waved.exe' if IS_WINDOWS else './waved'
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(os.path.join(sys.exec_prefix, waved))
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([waved], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
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
- finally:
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
- @main.command()
169
- def ide():
170
- uvicorn.run('h2o_wave.ide:ide', host=_localhost, port=10100)
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([inquirer.List('project', message="Choose a starter template",
249
- choices=[
250
- 'Hello World app (for beginners)',
251
- 'App with header',
252
- 'App with header + navigation',
253
- 'App with sidebar + navigation',
254
- 'App with header & sidebar + navigation'
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
- if 'Hello World' in project:
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='?new', help='Subdomain to use. If not available, a random one is generated.')
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
- def share(port: int, subdomain: str, remote_host: str):
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
- res = httpx.get(f'https://{remote_host}/{subdomain}', headers={'Content-Type': 'application/json'})
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
- print(f'BETA: Proxying localhost:{port} ==> {res["url"]}')
343
- print('\x1b[7;30;43mDO NOT SHARE YOUR APP IF IT CONTAINS SENSITIVE INFO\x1b[0m.')
344
- print('Press Ctrl+C to stop sharing.')
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, res['port'])))
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, res['port'])))
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.internal_address = _get_env('INTERNAL_ADDRESS', _default_internal_address)
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
- for k, v in props.items():
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
- del props[k]
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
@@ -1,4 +1,5 @@
1
1
 
2
- # Generated in setup.py.
3
- __platform__ = "any"
4
- __arch__ = "amd64"
2
+ # Generated in hatch_build.py.
3
+ __platform__ = "macosx_12_0_arm64"
4
+ __arch__ = "arm64"
5
+
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
- if not callable(predicate):
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
- else:
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 handle_on(q: Q) -> bool:
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
- try:
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
- if contextvars is not None: # Python 3.7+ only.
195
- return await loop.run_in_executor(
196
- executor,
197
- contextvars.copy_context().run,
198
- functools.partial(func, *args, **kwargs)
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
- pass
30
+ retries += 1
24
31
  finally:
25
- local_writer.close()
26
- remote_writer.close()
32
+ if local_writer:
33
+ local_writer.close()
34
+ if remote_writer:
35
+ remote_writer.close()