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.
@@ -0,0 +1,30 @@
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
+ """
16
+ Python package `h2o_lightwave` provides a Python driver for H2O Wave.
17
+
18
+ H2O Lightwave is a lightweight, pure-Python version of [H2O Wave](https://wave.h2o.ai/)
19
+ that can be embedded in popular async web frameworks like FastAPI, Starlette, etc.
20
+
21
+ In other words, H2O Lightwave works without the Wave server.
22
+ """
23
+
24
+ from .core import Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
25
+ from .server import Q, wave_serve
26
+ from .routing import on, run_on, handle_on
27
+ from .types import *
28
+ from .version import __version__
29
+
30
+ __author__ = 'Martin Turoci'
h2o_lightwave/core.py ADDED
@@ -0,0 +1,508 @@
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 sys
18
+ from typing import Callable, List, Dict, Union, Tuple, Any, Optional, IO
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ Primitive = Union[bool, str, int, float, None]
24
+ PrimitiveCollection = Union[Tuple[Primitive], List[Primitive]]
25
+ FileContent = Union[IO[str], IO[bytes], str, bytes]
26
+
27
+
28
+ _key_sep = ' '
29
+
30
+
31
+ def _is_int(x: Any) -> bool: return isinstance(x, int)
32
+
33
+
34
+ def _is_str(x: Any) -> bool: return isinstance(x, str)
35
+
36
+
37
+ def _is_list(x: Any) -> bool: return isinstance(x, (list, tuple))
38
+
39
+
40
+ def _guard_str_key(key: str):
41
+ if not _is_str(key):
42
+ raise KeyError('key must be str')
43
+ if ' ' in key:
44
+ raise KeyError('keys cannot contain spaces')
45
+
46
+
47
+ def _guard_key(key: str):
48
+ if _is_str(key):
49
+ _guard_str_key(key)
50
+ else:
51
+ if not _is_int(key):
52
+ raise KeyError('invalid key type: want str or int')
53
+
54
+
55
+ def _fill_data_buffers(props: Dict, data: list, bufs: list, keys=[], is_form_card=False):
56
+ for k, v in props.items():
57
+ if isinstance(v, Data):
58
+ keys.append(k)
59
+ data.append(('.'.join(keys), len(bufs)))
60
+ bufs.append(v.dump())
61
+ keys.pop()
62
+ elif not is_form_card:
63
+ continue
64
+ elif isinstance(v, list):
65
+ keys.append(k)
66
+ for idx, e in enumerate(v):
67
+ if isinstance(e, dict):
68
+ keys.append(str(idx))
69
+ _fill_data_buffers(e, data, bufs, keys, is_form_card)
70
+ keys.pop()
71
+ keys.pop()
72
+ elif isinstance(v, dict):
73
+ keys.append(k)
74
+ _fill_data_buffers(v, data, bufs, keys, is_form_card)
75
+ keys.pop()
76
+
77
+
78
+ def _del_dict_key(d: dict, keys: List[str]):
79
+ if len(keys) == 1:
80
+ del d[keys[0]]
81
+ else:
82
+ next_key = keys[0]
83
+ key = int(next_key) if next_key.isdigit() else next_key
84
+ _del_dict_key(d[key], keys[1:])
85
+
86
+
87
+
88
+ DICT = '__kv'
89
+
90
+
91
+ class Expando:
92
+ """
93
+ Represents an object whose members (attributes) can be dynamically added and removed at run time.
94
+
95
+ Args:
96
+ args: An optional ``dict`` of attribute-value pairs to initialize the expando instance with.
97
+ """
98
+
99
+ def __init__(self, args: Optional[Dict] = None):
100
+ self.__dict__[DICT] = args if isinstance(args, dict) else dict()
101
+
102
+ def __getattr__(self, k): return self.__dict__[DICT].get(k)
103
+
104
+ def __getitem__(self, k): return self.__dict__[DICT].get(k)
105
+
106
+ def __setattr__(self, k, v): self.__dict__[DICT][k] = v
107
+
108
+ def __setitem__(self, k, v): self.__dict__[DICT][k] = v
109
+
110
+ def __contains__(self, k): return k in self.__dict__[DICT]
111
+
112
+ def __delattr__(self, k): del self.__dict__[DICT][k]
113
+
114
+ def __delitem__(self, k): del self.__dict__[DICT][k]
115
+
116
+ def __repr__(self): return repr(self.__dict__[DICT])
117
+
118
+ def __str__(self): return ', '.join([f'{k}:{repr(v)}' for k, v in self.__dict__[DICT].items()])
119
+
120
+
121
+ def expando_to_dict(e: Expando) -> dict:
122
+ """
123
+ Extract an expando's underlying dictionary.
124
+ Any modifications to the dictionary also affect the original expando.
125
+
126
+ Args:
127
+ e: The expando instance.
128
+
129
+ Returns:
130
+ The expando's dictionary.
131
+
132
+ """
133
+ return e.__dict__[DICT]
134
+
135
+
136
+ def clone_expando(source: Expando, exclude_keys: Optional[Union[list, tuple]] = None,
137
+ include_keys: Optional[Union[list, tuple]] = None) -> Expando:
138
+ """
139
+ Clone an expando instance. Creates a shallow clone.
140
+
141
+ Args:
142
+ source: The expando to clone.
143
+ exclude_keys: Keys to exclude while cloning.
144
+ include_keys: Keys to include while cloning.
145
+ Returns:
146
+ The expando clone.
147
+ """
148
+ return copy_expando(source, Expando(), exclude_keys, include_keys)
149
+
150
+
151
+ def copy_expando(source: Expando, target: Expando, exclude_keys: Optional[Union[list, tuple]] = None,
152
+ include_keys: Optional[Union[list, tuple]] = None) -> Expando:
153
+ """
154
+ Copy all entries from the source expando instance to the target expando instance.
155
+
156
+ Args:
157
+ source: The expando to copy from.
158
+ target: The expando to copy to.
159
+ exclude_keys: Keys to exclude while copying.
160
+ include_keys: Keys to include while copying.
161
+ Returns:
162
+ The target expando.
163
+ """
164
+ if include_keys:
165
+ if exclude_keys:
166
+ for k in include_keys:
167
+ if k not in exclude_keys:
168
+ target[k] = source[k]
169
+ else:
170
+ for k in include_keys:
171
+ target[k] = source[k]
172
+ else:
173
+ d = expando_to_dict(source)
174
+ if exclude_keys:
175
+ for k, v in d.items():
176
+ if k not in exclude_keys:
177
+ target[k] = v
178
+ else:
179
+ for k, v in d.items():
180
+ target[k] = v
181
+
182
+ return target
183
+
184
+
185
+ PAGE = '__page__'
186
+ KEY = '__key__'
187
+
188
+
189
+ def _set_op(o, k, v):
190
+ _guard_key(k)
191
+ k = getattr(o, KEY) + _key_sep + str(k)
192
+ if isinstance(v, Data):
193
+ op = v.dump()
194
+ op['k'] = k
195
+ else:
196
+ op = dict(k=k, v=v)
197
+ return op
198
+
199
+
200
+ def _can_dump(x: Any):
201
+ return hasattr(x, 'dump') and callable(x.dump)
202
+
203
+
204
+ def _is_numpy_obj(x: Any) -> bool:
205
+ if 'numpy' in sys.modules:
206
+ np = sys.modules['numpy']
207
+ if isinstance(x, (np.ndarray, np.dtype, np.integer, np.floating)):
208
+ return True
209
+ return False
210
+
211
+
212
+ def _dump(xs: Any):
213
+ if _is_numpy_obj(xs):
214
+ raise ValueError('NumPy objects are not serializable by Wave')
215
+
216
+ if isinstance(xs, (list, tuple)):
217
+ return [_dump(x) for x in xs]
218
+ elif isinstance(xs, dict):
219
+ return {k: _dump(v) for k, v in xs.items()}
220
+ elif _can_dump(xs):
221
+ return xs.dump()
222
+ else:
223
+ return xs
224
+
225
+
226
+ class Ref:
227
+ """
228
+ Represents a local reference to an element on a `h2o_wave.core.Page`.
229
+ Any changes made to this local reference are tracked and sent to the remote Wave server when the page is saved.
230
+ """
231
+
232
+ def __init__(self, page: 'PageBase', key: str):
233
+ self.__dict__[PAGE] = page
234
+ self.__dict__[KEY] = key
235
+
236
+ def __getattr__(self, key):
237
+ _guard_key(key)
238
+ return Ref(getattr(self, PAGE), getattr(self, KEY) + _key_sep + key)
239
+
240
+ def __getitem__(self, key):
241
+ _guard_key(key)
242
+ return Ref(getattr(self, PAGE), getattr(self, KEY) + _key_sep + str(key))
243
+
244
+ def __setattr__(self, key, value):
245
+ if isinstance(value, Data):
246
+ raise ValueError('Data instances cannot be used in assignments.')
247
+ getattr(self, PAGE)._track(_set_op(self, key, _dump(value)))
248
+
249
+ def __setitem__(self, key, value):
250
+ if isinstance(value, Data):
251
+ raise ValueError('Data instances cannot be used in assignments.')
252
+ getattr(self, PAGE)._track(_set_op(self, key, _dump(value)))
253
+
254
+ def __iadd__(self, value):
255
+ if not getattr(self, KEY).endswith('data'):
256
+ raise ValueError('+= can only be used on list data buffers.')
257
+ page = getattr(self, PAGE)
258
+ page._track(_set_op(self, '__append__', _dump(value)))
259
+ page._skip_next_track = True
260
+
261
+
262
+ class Data:
263
+ """
264
+ Represents a data placeholder. A data placeholder is used to allocate memory on the Wave server to store data.
265
+
266
+ Args:
267
+ fields: The names of the fields (columns names) in the data, either a list or tuple or string containing space-separated names.
268
+ size: The number of rows to allocate memory for. Positive for fixed buffers, negative for cyclic buffers and zero for variable length buffers.
269
+ data: Initial data. Must be either a key-row ``dict`` for variable-length buffers OR a row ``list`` for fixed-size and cyclic buffers.
270
+ t: Buffer type. One of 'list', 'map', 'cyclic' or 'fixed'. Overrides the buffer type inferred from the size.
271
+ """
272
+
273
+ def __init__(self, fields: Union[str, tuple, list], size: int = 0, data: Optional[Union[dict, list]] = None, t: Optional[str] = None):
274
+ self.fields = fields
275
+ self.data = data
276
+ self.size = size
277
+ self.type = t
278
+
279
+ def dump(self):
280
+ f = self.fields
281
+ d = self.data
282
+ n = self.size
283
+ t = self.type
284
+ if d:
285
+ if t == 'list':
286
+ return dict(l=dict(f=f, d=d))
287
+ if t == 'map' or isinstance(d, dict):
288
+ return dict(m=dict(f=f, d=d))
289
+ if t == 'cyclic' or n < 0:
290
+ return dict(c=dict(f=f, d=d))
291
+ return dict(f=dict(f=f, d=d))
292
+ else:
293
+ if t == 'list':
294
+ return dict(l=dict(f=f, n=n))
295
+ if t == 'map' or n == 0:
296
+ return dict(m=dict(f=f))
297
+ if t == 'cyclic' or n < 0:
298
+ return dict(c=dict(f=f, n=-n))
299
+ return dict(f=dict(f=f, n=n))
300
+
301
+
302
+ def data(
303
+ fields: Union[str, tuple, list],
304
+ size: int = 0,
305
+ rows: Optional[Union[dict, list]] = None,
306
+ columns: Optional[Union[dict, list]] = None,
307
+ pack=False,
308
+ t: Optional[str] = None,
309
+ ) -> Union[Data, str]:
310
+ """
311
+ Create a `h2o_wave.core.Data` instance for associating data with cards.
312
+
313
+ ``data(fields, size)`` creates a placeholder for data and allocates memory on the Wave server.
314
+
315
+ ``data(fields, size, rows)`` creates a placeholder and initializes it with the provided rows.
316
+
317
+ If ``pack`` is ``True``, the ``size`` parameter is ignored, and the function returns a packed string representing the data.
318
+
319
+ Args:
320
+ fields: The names of the fields (columns names) in the data, either a list or tuple or string containing space-separated names.
321
+ size: The number of rows to allocate memory for. Positive for fixed buffers, negative for cyclic buffers and zero for variable length buffers.
322
+ rows: The rows in this data.
323
+ columns: The columns in this data.
324
+ pack: True to return a packed string representing the data instead of a `h2o_wave.core.Data` placeholder.
325
+ t: Buffer type. One of 'list', 'map', 'cyclic' or 'fixed'. Overrides the buffer type inferred from the size.
326
+
327
+ Returns:
328
+ Either a `h2o_wave.core.Data` placeholder or a packed string representing the data.
329
+ """
330
+ if _is_str(fields):
331
+ fields = fields.strip()
332
+ if fields == '':
333
+ raise ValueError('fields is empty')
334
+ fields = fields.split()
335
+ if not _is_list(fields):
336
+ raise ValueError('fields must be tuple or list')
337
+ if len(fields) == 0:
338
+ raise ValueError('fields is empty')
339
+ for field in fields:
340
+ if not _is_str(field):
341
+ raise ValueError('field must be str')
342
+ if field == '':
343
+ raise ValueError('field cannot be empty str')
344
+
345
+ if pack:
346
+ if rows:
347
+ if not isinstance(rows, list):
348
+ # TODO validate if 2d
349
+ raise ValueError('rows must be a list')
350
+ return 'rows:' + marshal((fields, rows))
351
+ if columns:
352
+ if not isinstance(columns, list):
353
+ # TODO validate if 2d
354
+ raise ValueError('columns must be a list')
355
+ return 'cols:' + marshal((fields, columns))
356
+ raise ValueError('either rows or columns must be provided if pack=True')
357
+
358
+ if rows:
359
+ if not isinstance(rows, (list, dict)):
360
+ raise ValueError('rows must be list or dict')
361
+ elif columns: # transpose to rows
362
+ # TODO issue warning: better for caller to use pack=True
363
+ n = len(columns[0])
364
+ rows = []
365
+ for i in range(n):
366
+ rows.append([c[i] for c in columns])
367
+
368
+ if not _is_int(size):
369
+ raise ValueError('size must be int')
370
+
371
+ return Data(fields, size, rows, t)
372
+
373
+
374
+ class PageBase:
375
+ """
376
+ Represents a remote page.
377
+
378
+ Args:
379
+ url: The URL of the remote page.
380
+ """
381
+
382
+ def __init__(self):
383
+ self._changes = []
384
+ # HACK: Overloading += operator makes unnecessary __setattr__ call. Skip it to prevent redundant ops.
385
+ self._skip_next_track = False
386
+
387
+ def add(self, key: str, card: Any) -> Ref:
388
+ """
389
+ Add a card to this page.
390
+
391
+ Args:
392
+ key: The card's key. Must uniquely identify the card on the page. Overwrites any other card with the same key.
393
+ card: A card. Use one of the ``ui.*_card()`` to create cards.
394
+
395
+ Returns:
396
+ A reference to the added card.
397
+ """
398
+ _guard_str_key(key)
399
+
400
+ props: Optional[dict] = None
401
+
402
+ if isinstance(card, dict):
403
+ props = card
404
+ elif _can_dump(card):
405
+ props = _dump(card)
406
+ if not isinstance(props, dict):
407
+ raise ValueError('card must be dict or implement .dump() -> dict')
408
+
409
+ data = []
410
+ bufs = []
411
+
412
+ _fill_data_buffers(props, data, bufs, [], props.get('view') == 'form')
413
+
414
+ for k, v in data:
415
+ _del_dict_key(props, k.split('.'))
416
+ props[f'~{k}'] = v
417
+
418
+ if len(bufs) > 0:
419
+ self._track(dict(k=key, d=props, b=bufs))
420
+ else:
421
+ self._track(dict(k=key, d=props))
422
+
423
+ return Ref(self, key)
424
+
425
+ def _track(self, op: dict):
426
+ if self._skip_next_track:
427
+ self._skip_next_track = False
428
+ return
429
+ self._changes.append(op)
430
+
431
+ def _diff(self):
432
+ if len(self._changes) == 0:
433
+ return None
434
+ d = marshal(dict(d=self._changes))
435
+ self._changes.clear()
436
+ return d
437
+
438
+ def drop(self):
439
+ """
440
+ Delete this page from the remote site. Same as ``del site[url]``.
441
+ """
442
+ self._track({})
443
+
444
+ def __setitem__(self, key, card):
445
+ self.add(key, card)
446
+
447
+ def __getitem__(self, key: str) -> Ref:
448
+ _guard_str_key(key)
449
+ return Ref(self, key)
450
+
451
+ def __delitem__(self, key: str):
452
+ _guard_str_key(key)
453
+ self._track(dict(k=key))
454
+
455
+
456
+ class AsyncPage(PageBase):
457
+ """
458
+ Represents a reference to a Wave page.
459
+ """
460
+ def __init__(self, send: Optional[Callable] = None):
461
+ self.send = send
462
+ super().__init__()
463
+
464
+ async def save(self):
465
+ """
466
+ Save the page. Sends all local changes made to this page to the browser.
467
+ """
468
+ p = self._diff()
469
+ if p:
470
+ logger.debug(p)
471
+ await self.send(p)
472
+
473
+ def marshal(d: Any) -> str:
474
+ """
475
+ Marshal to JSON.
476
+
477
+ Args:
478
+ d: Any object or value.
479
+
480
+ Returns:
481
+ A string containing the JSON-serialized form.
482
+ """
483
+ return json.dumps(d, allow_nan=False, separators=(',', ':'))
484
+
485
+
486
+ def unmarshal(s: str) -> Any:
487
+ """
488
+ Unmarshal a JSON string.
489
+
490
+ Args:
491
+ s: A string containing JSON-serialized data.
492
+
493
+ Returns:
494
+ The deserialized object or value.
495
+ """
496
+ return json.loads(s)
497
+
498
+
499
+ def pack(data: Any) -> str:
500
+ """
501
+ Pack (compress) the provided value.
502
+
503
+ Args:
504
+ data: Any object or value.
505
+
506
+ The object or value compressed into a string.
507
+ """
508
+ return 'data:' + marshal(_dump(data))