Browse Source

feat: bokeh support 🎉

wangweimin 5 năm trước cách đây
mục cha
commit
da0d7b7758
6 tập tin đã thay đổi với 168 bổ sung1 xóa
  1. 5 0
      pywebio/__init__.py
  2. 22 1
      pywebio/html/index.html
  3. 126 0
      pywebio/platform/bokeh.py
  4. 13 0
      pywebio/platform/tornado.py
  5. 1 0
      requirements.txt
  6. 1 0
      setup.py

+ 5 - 0
pywebio/__init__.py

@@ -8,6 +8,11 @@ from .utils import STATIC_PATH
 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())

+ 22 - 1
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'],
+            },
         }
     });
 

+ 126 - 0
pywebio/platform/bokeh.py

@@ -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 - 0
pywebio/platform/tornado.py

@@ -20,6 +20,14 @@ from ..utils import get_free_port, wait_host_port, STATIC_PATH
 
 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 start_server_in_current_thread_session():
         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"))

+ 1 - 0
requirements.txt

@@ -4,6 +4,7 @@ tornado>=4.3.0
 flask
 django
 aiohttp
+bokeh
 
 # test requirements
 selenium==3.*

+ 1 - 0
setup.py

@@ -16,6 +16,7 @@ extras_require = {
     'flask': ['flask'],
     'django': ['django'],
     'aiohttp': ['aiohttp'],
+    'bokeh': ['bokeh'],
 }
 # 可以使用 pip install pywebio[all] 安装所有额外依赖
 extras_require['all'] = reduce(lambda x, y: x + y, extras_require.values())