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/py.typed ADDED
File without changes
@@ -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
@@ -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]