-
-
Notifications
You must be signed in to change notification settings - Fork 473
/
misc.py
341 lines (272 loc) · 11.7 KB
/
misc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
Miscellaneous widgets which do not fit into the other main categories.
"""
from __future__ import annotations
from base64 import b64encode
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar, Mapping
import param
from param.parameterized import eval_function_with_deps, iscoroutinefunction
from pyviz_comms import JupyterComm
from ..io.notebook import push
from ..io.resources import CDN_DIST
from ..io.state import state
from ..models import (
FileDownload as _BkFileDownload, VideoStream as _BkVideoStream,
)
from ..util import lazy_load
from .base import Widget
from .button import BUTTON_STYLES, BUTTON_TYPES, IconMixin
from .indicators import Progress # noqa
if TYPE_CHECKING:
from bokeh.model import Model
class VideoStream(Widget):
"""
The `VideoStream` displays a video from a local stream (for example from a webcam) and allows
accessing the streamed video data from Python.
Reference: https://panel.holoviz.org/reference/widgets/VideoStream.html
:Example:
>>> VideoStream(name='Video Stream', timeout=100)
"""
format = param.ObjectSelector(default='png', objects=['png', 'jpeg'],
doc="""
The file format as which the video is returned.""")
paused = param.Boolean(default=False, doc="""
Whether the video is currently paused""")
timeout = param.Number(default=None, doc="""
Interval between snapshots in millisecons""")
value = param.String(default='', doc="""
A base64 representation of the video stream snapshot.""")
_widget_type: ClassVar[type[Model]] = _BkVideoStream
_rename: ClassVar[Mapping[str, str | None]] = {'name': None}
def snapshot(self):
"""
Triggers a snapshot of the current VideoStream state to sync
the widget value.
"""
for ref, (m, _) in self._models.items():
m.snapshot = not m.snapshot
(self, root, doc, comm) = state._views[ref]
if comm and 'embedded' not in root.tags:
push(doc, comm)
class FileDownload(IconMixin):
"""
The `FileDownload` widget allows a user to download a file.
It works either by sending the file data to the browser on initialization
(`embed`=True), or when the button is clicked.
Reference: https://panel.holoviz.org/reference/widgets/FileDownload.html
:Example:
>>> FileDownload(file='IntroductionToPanel.ipynb', filename='intro.ipynb')
"""
auto = param.Boolean(default=True, doc="""
Whether to download on the initial click or allow for
right-click save as.""")
button_type = param.ObjectSelector(default='default', objects=BUTTON_TYPES, doc="""
A button theme; should be one of 'default' (white), 'primary'
(blue), 'success' (green), 'info' (yellow), 'light' (light),
or 'danger' (red).""")
button_style = param.ObjectSelector(default='solid', objects=BUTTON_STYLES, doc="""
A button style to switch between 'solid', 'outline'.""")
callback = param.Callable(default=None, allow_refs=False, doc="""
A callable that returns the file path or file-like object.""")
data = param.String(default=None, doc="""
The data being transferred.""")
embed = param.Boolean(default=False, doc="""
Whether to embed the file on initialization.""")
file = param.Parameter(default=None, doc="""
The file, Path, file-like object or file contents to transfer. If
the file is not pointing to a file on disk a filename must
also be provided.""")
filename = param.String(default=None, doc="""
A filename which will also be the default name when downloading
the file.""")
label = param.String(default="Download file", doc="""
The label of the download button""")
description = param.String(default=None, doc="""
An HTML string describing the function of this component.""")
_clicks = param.Integer(default=0)
_transfers = param.Integer(default=0)
_mime_types = {
'application': {
'pdf': 'pdf', 'zip': 'zip'
},
'audio': {
'mp3': 'mp3', 'ogg': 'ogg', 'wav': 'wav', 'webm': 'webm'
},
'image': {
'apng': 'apng', 'bmp': 'bmp', 'gif': 'gif', 'ico': 'x-icon',
'cur': 'x-icon', 'jpg': 'jpeg', 'jpeg': 'jpeg', 'png': 'png',
'svg': 'svg+xml', 'tif': 'tiff', 'tiff': 'tiff', 'webp': 'webp'
},
'text': {
'css': 'css', 'csv': 'plain;charset=UTF-8', 'js': 'javascript',
'html': 'html', 'txt': 'plain;charset=UTF-8'
},
'video': {
'mp4': 'mp4', 'ogg': 'ogg', 'webm': 'webm'
}
}
_rename: ClassVar[Mapping[str, str | None]] = {
'callback': None, 'button_style': None, 'file': None, '_clicks': 'clicks'
}
_stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/button.css']
_widget_type: ClassVar[type[Model]] = _BkFileDownload
def __init__(self, file=None, **params):
self._default_label = 'label' not in params
self._synced = False
super().__init__(file=file, **params)
if self.embed:
self._transfer()
self._update_label()
if "filename" not in params:
self._update_filename()
def _process_param_change(self, params):
if 'button_style' in params or 'css_classes' in params:
params['css_classes'] = [
params.pop('button_style', self.button_style)
] + params.get('css_classes', self.css_classes)
return super()._process_param_change(params)
@param.depends('label', watch=True)
def _update_default(self):
self._default_label = False
@property
def _is_file_path(self)->bool:
return isinstance(self.file, (str, Path))
@property
def _file_path(self)->Path:
return Path(self.file)
@param.depends('file', watch=True)
def _update_filename(self):
if self._is_file_path:
self.filename = self._file_path.name
@param.depends('auto', 'file', 'filename', watch=True)
def _update_label(self):
label = 'Download' if self._synced or self.auto else 'Transfer'
if self._default_label:
if self.file is None and self.callback is None:
label = 'No file set'
else:
try:
filename = self.filename or self._file_path.name
except TypeError:
raise ValueError('Must provide filename if file-like '
'object is provided.') from None
label = f'{label} {filename}'
self.label = label
self._default_label = True
@param.depends('embed', 'file', 'callback', watch=True)
def _update_embed(self):
if self.embed:
self._transfer()
def _sync_data(self, fileobj):
filename = self.filename
if isinstance(fileobj, (str, Path)):
fileobj = Path(fileobj)
if not fileobj.exists():
raise FileNotFoundError(f'File "{fileobj}" not found.')
with open(fileobj, 'rb') as f:
b64 = b64encode(f.read()).decode("utf-8")
if filename is None:
filename = fileobj.name
elif hasattr(fileobj, 'read'):
bdata = fileobj.read()
if not isinstance(bdata, bytes):
bdata = bdata.encode("utf-8")
b64 = b64encode(bdata).decode("utf-8")
if filename is None:
raise ValueError('Must provide filename if file-like '
'object is provided.')
else:
raise ValueError(f'Cannot transfer unknown object of type {type(fileobj).__name__}')
ext = filename.split('.')[-1]
stype, mtype = None, None
for mime_type, subtypes in self._mime_types.items():
if ext in subtypes:
mtype = mime_type
stype = subtypes[ext]
break
if stype is None:
mime = 'application/octet-stream'
else:
mime = f'{mtype}/{stype}'
data = f"data:{mime};base64,{b64}"
self._synced = True
self.param.update(data=data, filename=filename)
self._update_label()
self._transfers += 1
async def _async_sync_data(self):
fileobj = await eval_function_with_deps(self.callback)
self._sync_data(fileobj)
@param.depends('_clicks', watch=True)
def _transfer(self):
if self.file is None and self.callback is None:
if self.embed:
raise ValueError('Must provide a file or a callback '
'if it is to be embedded.')
return
if self.callback is None:
fileobj = self.file
else:
if iscoroutinefunction(self.callback):
state.execute(self._async_sync_data)
return
else:
fileobj = eval_function_with_deps(self.callback)
self._sync_data(fileobj)
class JSONEditor(Widget):
"""
The `JSONEditor` provides a visual editor for JSON-serializable
datastructures, e.g. Python dictionaries and lists, with functionality for
different editing modes, inserting objects and validation using JSON
Schema.
Reference: https://panel.holoviz.org/reference/widgets/JSONEditor.html
:Example:
>>> JSONEditor(value={
... 'dict' : {'key': 'value'},
... 'float' : 3.14,
... 'int' : 1,
... 'list' : [1, 2, 3],
... 'string': 'A string',
... }, mode='code')
"""
menu = param.Boolean(default=True, doc="""
Adds main menu bar - Contains format, sort, transform, search
etc. functionality. true by default. Applicable in all types
of mode.""")
mode = param.Selector(default='tree', objects=[
"tree", "view", "form", "text", "preview"], doc="""
Sets the editor mode. In 'view' mode, the data and
datastructure is read-only. In 'form' mode, only the value can
be changed, the data structure is read-only. Mode 'code'
requires the Ace editor to be loaded on the page. Mode 'text'
shows the data as plain text. The 'preview' mode can handle
large JSON documents up to 500 MiB. It shows a preview of the
data, and allows to transform, sort, filter, format, or
compact the data.""")
search = param.Boolean(default=True, doc="""
Enables a search box in the upper right corner of the
JSONEditor. true by default. Only applicable when mode is
'tree', 'view', or 'form'.""")
selection = param.List(default=[], doc="""
Current selection.""")
schema = param.Dict(default=None, doc="""
Validate the JSON object against a JSON schema. A JSON schema
describes the structure that a JSON object must have, like
required properties or the type that a value must have.
See http://json-schema.org/ for more information.""")
templates = param.List(doc="""
Array of templates that will appear in the context menu, Each
template is a json object precreated that can be added as a
object value to any node in your document.""")
value = param.Parameter(default={}, doc="""
JSON data to be edited.""")
_rename: ClassVar[Mapping[str, str | None]] = {
'name': None, 'value': 'data'
}
def _get_model(self, doc, root=None, parent=None, comm=None):
if self._widget_type is None:
self._widget_type = lazy_load(
"panel.models.jsoneditor", "JSONEditor", isinstance(comm, JupyterComm)
)
model = super()._get_model(doc, root, parent, comm)
return model