From da0d7b77582bfba69a3b1978d62e54d54079c70f Mon Sep 17 00:00:00 2001 From: wangweimin Date: Sun, 3 May 2020 18:05:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20bokeh=20support=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pywebio/__init__.py | 5 ++ pywebio/html/index.html | 23 ++++++- pywebio/platform/bokeh.py | 126 ++++++++++++++++++++++++++++++++++++ pywebio/platform/tornado.py | 13 ++++ requirements.txt | 1 + setup.py | 1 + 6 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 pywebio/platform/bokeh.py diff --git a/pywebio/__init__.py b/pywebio/__init__.py index 3543ebb2..19026c24 100644 --- a/pywebio/__init__.py +++ b/pywebio/__init__.py @@ -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()) diff --git a/pywebio/html/index.html b/pywebio/html/index.html index 237b9d30..1eb507f7 100644 --- a/pywebio/html/index.html +++ b/pywebio/html/index.html @@ -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'], + }, } }); diff --git a/pywebio/platform/bokeh.py b/pywebio/platform/bokeh.py new file mode 100644 index 00000000..d09e0ad2 --- /dev/null +++ b/pywebio/platform/bokeh.py @@ -0,0 +1,126 @@ +import asyncio +import re +from collections.abc import Sequence + +from pywebio.output import * + +requirejs_tpl = """ +%s + +""" + + +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 += """ +
+ + Loading BokehJS ... +
+ """.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'([\s\S]*?)', r""" + + """, 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 diff --git a/pywebio/platform/tornado.py b/pywebio/platform/tornado.py index 7db02d27..bc0193f1 100644 --- a/pywebio/platform/tornado.py +++ b/pywebio/platform/tornado.py @@ -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): @@ -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: @@ -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")) diff --git a/requirements.txt b/requirements.txt index fa087f24..6fb258d0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ tornado>=4.3.0 flask django aiohttp +bokeh # test requirements selenium==3.* diff --git a/setup.py b/setup.py index 0b94e11d..9c3e5a8a 100644 --- a/setup.py +++ b/setup.py @@ -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())