Skip to content

Commit

Permalink
feat: bokeh support 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
wang0618 committed May 3, 2020
1 parent 2362523 commit da0d7b7
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 1 deletion.
5 changes: 5 additions & 0 deletions pywebio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
from .__version__ import __description__, __url__, __version__
from .__version__ import __author__, __author_email__, __license__, __copyright__

from .platform.bokeh import try_install_bokeh_hook

try_install_bokeh_hook()
del try_install_bokeh_hook

# Set default logging handler to avoid "No handler found" warnings.
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
Expand Down
23 changes: 22 additions & 1 deletion pywebio/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,28 @@

require.config({
paths: {
'plotly': "https://cdn.jsdelivr.net/npm/plotly.js@1.53.0/dist/plotly.min" // 'https://cdn.plot.ly/plotly-latest.min'
'plotly': "https://cdn.jsdelivr.net/npm/plotly.js@1.53.0/dist/plotly.min", // 'https://cdn.plot.ly/plotly-latest.min'
"bokeh": "https://cdn.jsdelivr.net/npm/@bokeh/bokehjs@2.0.2/build/js/bokeh.min",
"bokeh-widgets": "https://cdn.jsdelivr.net/npm/@bokeh/bokehjs@2.0.2/build/js/bokeh-widgets.min",
"bokeh-tables": "https://cdn.jsdelivr.net/npm/@bokeh/bokehjs@2.0.2/build/js/bokeh-tables.min",
"bokeh-gl": "https://cdn.jsdelivr.net/npm/@bokeh/bokehjs@2.0.2/build/js/bokeh-gl.min",
},
shim: {
'bokeh': {
exports: 'Bokeh'
},
'bokeh-widgets': {
exports: '_',
deps:['bokeh'],
},
'bokeh-tables': {
exports: '_',
deps:['bokeh'],
},
'bokeh-gl': {
exports: '_',
deps:['bokeh'],
},
}
});

Expand Down
126 changes: 126 additions & 0 deletions pywebio/platform/bokeh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import asyncio
import re
from collections.abc import Sequence

from pywebio.output import *

requirejs_tpl = """
%s
<script type="text/javascript">
requirejs(['bokeh', 'bokeh-widgets', 'bokeh-tables', 'bokeh-gl'], function(Bokeh) {
%s
});
</script>
"""


def load_notebook(resources=None, verbose=False, hide_banner=False, load_timeout=5000):
"""加载 Bokeh 资源
:param resources: 目前不支持自定义静态资源的链接
:param verbose: 开启 Bokeh 日志 并显示 Bokeh 加载标签
:param hide_banner: 不支持
:param load_timeout: 不支持
:return: None
"""
from bokeh.util.serialization import make_id

js_gists = ["console.log('Load BokehJS complete.')"]

html = ''
if verbose:
element_id = make_id()
html += """
<div class="bk-root">
<a href="https://bokeh.org" target="_blank" class="bk-logo bk-logo-small bk-logo-notebook"></a>
<span id="{element_id}" style="font-family: Helvetica, Arial, sans-serif;font-size: 13px;">Loading BokehJS ...</span>
</div>
""".format(element_id=element_id)

js_gists.append(
"document.getElementById({element_id}).innerHTML = 'Load BokehJS complete.'".format(element_id=element_id))

js_gists.append('Bokeh.set_log_level("info");')
js_gists.append("console.log('Set bokeh log level to INFO because you set `output_notebook(verbose=True)`')")

put_html(requirejs_tpl % (html, '\n'.join(js_gists)))


def show_doc(obj, state, notebook_handle):
"""显示 Bokeh 单个 documents
:param obj:
:param state:
:param notebook_handle: 不支持
:return:
"""
from bokeh.embed import components

script, div = components(obj, wrap_script=False)
if isinstance(obj, Sequence):
div = '\n'.join(div)
elif isinstance(obj, dict):
div = '\n'.join(div[k] for k in obj.keys())

put_html(requirejs_tpl % (div, script))


def show_app(app, state, notebook_url, port=0, **kw):
"""显示 Bokeh applications
:param app: A Bokeh Application to embed in PyWebIO.
:param state: ** Unused **
:param notebook_url: PyWebIO server 的地址,用于设置 Bokeh Server origin白名单
:param port: Bokeh Server 端口
:param kw: 传给 Bokeh Server 的额外参数
"""

from bokeh.server.server import Server
from bokeh.io.notebook import _origin_url, uuid4, curstate, _server_url

from pywebio.platform.tornado import ioloop
loop = ioloop()
loop.make_current()
asyncio.set_event_loop(loop.asyncio_loop)
# loop = IOLoop.current()

if callable(notebook_url):
origin = notebook_url(None)
else:
origin = _origin_url(notebook_url)

server = Server({"/": app}, io_loop=loop, port=port, allow_websocket_origin=[origin], **kw)

server_id = uuid4().hex
curstate().uuid_to_server[server_id] = server

server.start()

if callable(notebook_url):
url = notebook_url(server.port)
else:
url = _server_url(notebook_url, server.port)

from bokeh.embed import server_document
script = server_document(url, resources=None)

script = re.sub(r'<script(.*?)>([\s\S]*?)</script>', r"""
<script \g<1>>
requirejs(['bokeh', 'bokeh-widgets', 'bokeh-tables', 'bokeh-gl'], function(Bokeh) {
\g<2>
});
</script>
""", script)

put_html(script)


def try_install_bokeh_hook():
"""尝试安装bokeh支持"""
try:
from bokeh.io import install_notebook_hook
except ImportError:
return False

install_notebook_hook('pywebio', load_notebook, show_doc, show_app)
return True
13 changes: 13 additions & 0 deletions pywebio/platform/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@

logger = logging.getLogger(__name__)

_ioloop = None


def ioloop() -> tornado.ioloop.IOLoop:
"""获得运行Tornado server的IOLoop"""
global _ioloop
return _ioloop


def _check_origin(origin, allowed_origins, handler: WebSocketHandler):
if _is_same_site(origin, handler):
Expand Down Expand Up @@ -188,6 +196,8 @@ def start_server(target, port=0, host='', debug=False,
ref: https://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings
"""
kwargs = locals()
global _ioloop
_ioloop = tornado.ioloop.IOLoop.current()

app_options = ['debug', 'websocket_max_message_size', 'websocket_ping_interval', 'websocket_ping_timeout']
for opt in app_options:
Expand Down Expand Up @@ -257,6 +267,9 @@ def server_thread():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

global _ioloop
_ioloop = tornado.ioloop.IOLoop.current()

port = 0
if os.environ.get("PYWEBIO_SCRIPT_MODE_PORT"):
port = int(os.environ.get("PYWEBIO_SCRIPT_MODE_PORT"))
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ tornado>=4.3.0
flask
django
aiohttp
bokeh

# test requirements
selenium==3.*
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'flask': ['flask'],
'django': ['django'],
'aiohttp': ['aiohttp'],
'bokeh': ['bokeh'],
}
# 可以使用 pip install pywebio[all] 安装所有额外依赖
extras_require['all'] = reduce(lambda x, y: x + y, extras_require.values())
Expand Down

0 comments on commit da0d7b7

Please sign in to comment.