
Bokeh Server API 实战指南ASGI 嵌入、反向代理、认证与 bokeh.client 会话交互【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh本篇指南围绕 Bokeh 的 Server API 展开覆盖两条主线一是框架无关的 ASGI 前端BokehASGI——如何直接以 ASGI 3 应用运行、挂载进 FastAPI/Starlette、配置反向代理、从宿主进程推送数据给所有活跃会话以及基于AuthPolicy的认证二是经典 Tornado 前端Server的嵌入方式以及通过bokeh.client直接修改服务器上已有会话文档的客户端编程接口。读完本文你将掌握如何把 Bokeh 应用嵌入任意 Web 框架、如何在 nginx/Apache 之后正确部署 WebSocket以及如何按用户维度定制线上会话。Bokeh Server API 的两种前端Bokeh 的服务器能力以两种前端形态对外暴露ASGI 前端由BokehASGI提供是一个不依赖任何 ASGI 框架或服务器的 ASGI 3 应用可以直接交给 Uvicorn、Hypercorn 等服务也可以挂载到其他 ASGI 应用内部Tornado 前端由Server提供适合嵌入已有的 Tornado 应用或复用 Jupyter 的 IOLoop也支持直接创建并控制 IOLoop 的独立脚本用法。从源码结构看二者共享底层的BokehServerCore它承载了与传输层无关的应用、会话与连接状态管理因此无论走哪条前端会话生命周期、令牌校验、会话清理等机制是一致的。使用 BokehASGI 构建 ASGI 应用最小可用示例BokehASGI的构造函数接受一个从应用路径到应用规格的映射最简洁的形态是直接传入脚本路径from pathlib import Path from bokeh.server.asgi import BokehASGI application BokehASGI({/plot: Path(bkapp.py)})将该代码保存为main.py然后使用任意 ASGI 3 服务器运行例如python -m uvicorn main:application应用代码会在每个新会话创建时执行一次并通过修改curdoc()来构建文档路径类应用的相对路径从服务器进程的工作目录解析。三种应用规格路径、可调用对象与 Application 实例BokehASGI的参数类型定义在 asgi.py 的构造签名中Mapping[str, Application | ModifyDoc | PathLike] | Application | ModifyDoc | PathLike。也就是说除了脚本路径还支持以下两种显式形式from bokeh.application import Application from bokeh.application.handlers.function import FunctionHandler explicit Application(FunctionHandler(modify_document)) BokehASGI({/explicit: explicit, /callable: modify_document})其中modify_document是接收一个Document并修改它的回调函数。在BokehServerCore.__init__内部Application实例直接使用字符串路径按「目录或脚本」自动选择DirectoryHandler或ScriptHandler可调用对象则包装为FunctionHandler。映射中的路径必须以/开头且会统一去掉尾部的/根路径/除外。目录式应用支持的结构路径类应用与bokeh serve使用相同的格式既可以是一个 Python 脚本也可以是一个目录式应用。目录式应用支持以下内容主文件main.py或main.ipynb生命周期与钩子app_hooks.py、server_lifecycle.py静态资源目录static自定义页面模板templates/index.html主题文件theme.yaml。对于不含文档生命周期处理器的应用BokehServerCore会自动为其追加DocumentLifecycleHandler见 core.py 的规范化逻辑保证会话生命周期钩子总是被触发。挂载到 FastAPI 或 Starlette将BokehASGI挂载进 FastAPI/Starlette 时有一个关键陷阱被挂载的应用不会收到 lifespan 事件。因此必须由父应用的 lifespan 显式地启动和停止 Bokehfrom contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from bokeh.server.asgi import BokehASGI bokeh_app BokehASGI({/: Path(bkapp.py)}) asynccontextmanager async def lifespan(site): # Mounted FastAPI/Starlette applications dont receive lifespan events. await bokeh_app.core.start() try: yield finally: await bokeh_app.core.stop() site FastAPI(lifespanlifespan) site.mount(/bokeh, bokeh_app)挂载点携带的 ASGIroot_path会被自动并入 Bokeh 生成资源与 WebSocket URL 的前缀因此在带前缀部署如挂载在/bokeh时页面中的脚本与 WebSocket 地址都能正确生成。仓库中提供了完整的等效示例可以直接对照阅读examples/server/api/asgi/fastapi_embed.pyexamples/server/api/asgi/fastapi_shared_data.pyexamples/server/api/asgi/starlette_embed.pyexamples/server/api/asgi/django_embed.pyexamples/server/api/asgi/streamlit_simple.pyexamples/server/api/asgi/streamlit_particles/app.pyexamples/server/api/asgi/framework_free.pyASGI 前端处理的路由与生命周期从 asgi.py 的__call__与_http分派逻辑可以确认ASGI 前端按scope[type]分三类处理lifespan响应lifespan.startup/lifespan.shutdown事件启动失败会发送lifespan.startup.failedhttp处理 Bokeh 文档页/、/metadata、/autoload.js、/static/静态资源等路由并区分全局静态资源与应用级static目录websocket校验子协议与令牌、校验 Origin、认证随后建立会话连接并循环消费协议消息。另外BokehASGI还提供core属性BokehServerCore实例与update_sessions便捷方法分别用于生命周期管理与批量更新会话。反向代理部署代理必须保留的头部在 nginx、Apache 等反向代理之后部署时代理必须保留公共的Host与浏览器提供的Origin请求头原样转发Upgrade、Connection与Sec-WebSocket-Protocol头。Bokeh 的 WebSocket 握手要求子协议为bokeh其后跟随会话令牌如果页面来源不是 Bokeh 的公共主机需要把该来源加入extra_websocket_originsBokehServerCore构造函数参数如果代理剥掉了公共路径前缀应在 ASGI 服务器或父挂载处把该前缀配置到 ASGIroot_path让 Bokeh 生成与之匹配的资源与 WebSocket URL。关于信任边界文档明确提醒只应从已知的代理信任转发过来的客户端、主机和 scheme 头。WebSocket 保活方面由于 ASGI 协议本身没有可移植的 ping 帧与消息大小控制ping 间隔与超时、代理空闲超时、WebSocket 最大消息大小都需要在 ASGI 服务器层面配置——Bokeh 无法干预这些设置。作为参考BokehServerCore中默认的session_token_expiration为 300 秒WebSocket 最大消息大小常量DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES为 20 MB见 core.py。前缀保留模式prefix 参数如果代理保留公共路径前缀例如/services/bokeh则在构造BokehASGI时传入同样的prefix让 Bokeh 生成带前缀的资源与 WebSocket 地址application BokehASGI({/myapp: modify_document}, prefix/services/bokeh)如果代理剥掉了前缀则不要设置prefix改为按上一节所述配置root_path。prefix在BokehServerCore中会被规范化去掉首尾/最终以/services/bokeh这样的形式参与路由匹配与资源 URL 生成。Nginx 配置以下 nginx 配置把公共路径/services/bokeh代理到监听 5100 端口的 ASGI 服务器该配置由 Bokeh 的 nightly 部署测试实际演练验证server { listen 8080; server_name _; location /services/bokeh/ { proxy_pass http://127.0.0.1:5100; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Origin $http_origin; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol; proxy_buffering off; } }关键点Host与Origin使用请求原始值$http_host/$http_originUpgrade/Connection按升级请求原样透传Sec-WebSocket-Protocol必须转发以便 Bokeh 完成bokeh子协议握手同时关闭缓冲以支持长连接。Apache 配置Apache 侧等效配置同样保留/services/bokeh前缀Listen 8080 VirtualHost *:8080 ServerName localhost ProxyRequests Off ProxyPreserveHost On ProxyPass /services/bokeh/myapp/ws ws://127.0.0.1:5100/services/bokeh/myapp/ws ProxyPassReverse /services/bokeh/myapp/ws ws://127.0.0.1:5100/services/bokeh/myapp/ws ProxyPass /services/bokeh/ http://127.0.0.1:5100/services/bokeh/ ProxyPassReverse /services/bokeh/ http://127.0.0.1:5100/services/bokeh/ /VirtualHost注意 WebSocket 端点必须单独用ws://代理规则先行匹配HTTP 流量再按前缀转发。两份完整配置分别位于 nginx.conf 与 apache.conf。多进程部署注意事项Bokeh 的会话文档与回调是进程本地的因此多 worker 部署需要满足会话亲和性session affinity同一会话的请求必须落在同一 worker启用签名会话时所有 worker 必须使用同一个强secret_key外部数据生产者必须把更新投递到每一个worker单个 worker 内推送不会跨进程生效。当 Bokeh 被挂载在其他框架中时只有父框架不向挂载应用传播 lifespan 事件如 FastAPI/Starlette才需要使用前面展示的「父 lifespan 模式」手动启动/停止。从宿主应用更新活动会话update_sessions 用法如果你在宿主 ASGI 应用的 lifespan 中拥有一个后台数据生产者可以通过bokeh_app.update_sessions(app_path, update_document)把产出发布给每一个当前活跃的 Bokeh 会话。update_document是一个接收单个Document的可调用对象def update_document(doc): source doc.get_model_by_name(shared-source) source.data latest_snapshot await bokeh_app.update_sessions(/, update_document)从 core.py 的update_sessions实现可以看到其行为语义Bokeh 会对调用开始时刻已存在的每个本地会话调用一次update_document且调用时持有该会话文档的锁通过session.with_document_locked实现回调可以是同步的也可以是异步的不同会话的更新通过asyncio.gather并发执行调用开始之后才创建的会话不会被包含所以应用的文档构造逻辑也应从最新快照初始化新文档同步应用代码读取的共享数据必须是不可变的或者对 worker 线程的访问做好保护。完整示例与多 worker 数据分发这一模式在 examples/server/api/asgi/fastapi_shared_data.py 中有完整的 lifespan 托管实现它用asyncio.create_task启动后台produce()协程每 0.1 秒生成一次快照并调用update_sessions推送给所有会话新会话则通过线程安全的Latest.read()从最新快照构建文档。该示例对「新会话初始化」与「存量会话更新」两条路径都做了处理是学习共享数据架构的最佳范本。同样地update_sessions是进程本地操作多 worker 部署需要一个外部 broker 或数据服务把每个快照投递到每个 worker。认证AuthPolicy框架无关的认证策略认证可以由宿主框架执行、由AuthPolicy执行、或两者结合。AuthPolicy的独特价值在于它保护 Bokeh 的动态 HTTP 路由与 WebSocket 握手却不依赖任何 ASGI 框架。其定义在 auth.py构造函数接收一个 authenticator接收ServerRequest、返回用户或None以及可选的login_url与logout_url。authenticator 可以是同步或异步的同步函数会被放入执行器运行不阻塞事件循环。import os from bokeh.server.asgi import BokehASGI from bokeh.server.auth import AuthPolicy async def authenticate(request): authorization request.headers.get(authorization) if authorization fBearer {os.environ[SITE_TOKEN]}: return alice return None policy AuthPolicy( authenticate, login_url/login, logout_url/logout, ) application BokehASGI( {/: bkapp.py}, auth_policypolicy, sign_sessionsTrue, secret_keyos.environ[BOKEH_SECRET_KEY].encode(), )行为语义对应 asgi.py 的_authenticate/_authenticate_httpauthenticator 返回当前用户返回None则拒绝请求未认证的 HTTP 请求配置了login_url时 302 重定向到该地址否则返回 HTTP 401未认证的 WebSocket在 Bokeh 接受连接之前直接关闭login_url既可以是字符串也可以是按请求返回 URL 的可调用对象登录与登出端点本身由宿主应用负责实现logout_url会暴露为curdoc().session_context.logout_url。与宿主框架认证集成常见的认证中间件如 Starlette 的会把认证结果写入 ASGIscope[user]。Bokeh 会将其复制到request.user因此策略可以直接强制采纳宿主框架的认证结果def authenticate(request): user request.user if user is not None and getattr(user, is_authenticated, False): return user return None bokeh_app BokehASGI( {/: bkapp.py}, auth_policyAuthPolicy(authenticate, login_url/login), ) site.mount(/bokeh, bokeh_app)如上挂载时同样需要按前面的模式在父应用的 lifespan 中启动/停止bokeh_app。认证之后的用户可在会话代码中通过curdoc().session_context.request.user获取ASGI 的scope[state]也会以request.state的形式提供给 authenticator。会话令牌与敏感信息需要特别强调的安全要点会话令牌是 bearer 凭证不能替代对 HTTP 与 WebSocket 请求的认证启用认证的部署应开启签名会话sign_sessionsTrue并配置强共享secret_key令牌载荷是签名但未加密的因此应使用include_headers、exclude_headers、include_cookies、exclude_cookies参数避免把密钥等敏感信息复制进令牌。这些参数在BokehServerCore的令牌生成逻辑core.py 的create_session与_filtered_headers/_filtered_cookies中生效且include_*与exclude_*不能同时声明否则抛ValueError。另外从 asgi.py 的_valid_websocket_token可以确认握手时除校验签名外还会校验session_expiry时间戳过期令牌会被拒绝WebSocket 关闭码 1008。旧版 AuthProvider对于更早的AuthProvider定义于 auth_provider.py与--auth-module命令行接口它们基于 Tornado 请求处理器仅在 Tornado 前端中保留使用AuthPolicy并不需要它们。会话构造的并发模型会话文档的构建运行在 worker 线程中昂贵的同步应用代码不会阻塞事件循环接受无关的 HTTP/WebSocket 工作独立会话可以并发初始化。脚本类应用则会串行化它们所需的临时进程级全局状态包括sys.path、sys.argv与工作目录。嵌入 Tornado 应用Server 与已有 IOLoop如果希望把 Bokeh Server 嵌入一个更大的 Tornado 应用或 Jupyter notebook并复用其已有的 Tornado IOLoop可以使用Serverfrom bokeh.server.server import Server server Server( bokeh_applications, # list of Bokeh applications io_looploop, # Tornado IOLoop **server_kwargs # port, num_procs, etc. ) # start timers and services and immediately return server.start()server.start()会启动定时器与服务后立即返回适合在已有事件循环中嵌入。Server.from_settings继承 bokeh serve 的配置如果希望Server尊重bokeh serve使用的环境变量与配置文件例如BOKEH_AUTH_MODULE、BOKEH_SSL_CERTFILE、BOKEH_SIGN_SESSIONS应改用Server.from_settings工厂方法源码见 server.pyfrom bokeh.server.server import Server server Server.from_settings( bokeh_applications, # list of Bokeh applications io_looploop, # Tornado IOLoop **server_kwargs ) server.start()从from_settings的签名可以看到它会自动应用全局 settings 中对应的值secret_key、sign_sessions、ssl_certfile、ssl_keyfile、cookie_secret、xsrf_cookies、ico_path等除非这些参数被显式传入覆盖。手动控制 IOLoop 的独立脚本你也可以直接创建并控制一个IOLoop。这在编写独立运行的「普通」Python 脚本、或在不启动独立 Bokeh 服务器进程的前提下把 Bokeh 应用嵌入 Flask、Django 等框架时非常有用。仓库中的相关示例examples/server/api/flask_embed.pyexamples/server/api/notebook_embed.ipynbexamples/server/api/standalone_embed.pyexamples/server/api/tornado_embed.py此外bokeh serve的每一个命令行参数都有对应的Server关键字参数。例如命令行--allow-websocket-origin等价于传入allow_websocket_origin...--port对应port...以此类推迁移成本很低。使用 bokeh.client 连接服务器除浏览器外还可以通过bokeh.client模块从 Python 直接与 Bokeh 服务器交互修改已有会话中的 Bokeh 文档。典型场景是Bokeh 应用被 Flask 或 Django 等框架嵌入而宿主框架希望在把输出交给用户之前做用户级定制。下面的 Flask 示例中一个端点嵌入服务器上已在运行的 sliders 应用但先把绘图标题改成面向特定用户的文案from flask import Flask, render_template from bokeh.client import pull_session from bokeh.embed import server_session app Flask(__name__) app.route(/, methods[GET]) def bkapp_page(): with pull_session(urlhttp://localhost:5006/sliders) as session: # update or customize that session session.document.roots[0].children[1].title.text Special sliders for a specific user! # generate a script to load the customized session script server_session(session_idsession.id, urlhttp://localhost:5006/sliders) # use the script in the rendered page return render_template(embed.html, scriptscript, templateFlask) if __name__ __main__: app.run(port8080)工作流分三步用pull_session(url...)拉取服务器上已有会话上下文管理器保证会话正确释放在拉取到的session.document上做定制修改再用server_session(session_idsession.id, url...)生成加载该定制会话的嵌入脚本最终渲染进页面。这样同一个服务器端会话可以为不同用户呈现不同的内容而无需为每个用户单独部署应用。小结Bokeh 的 Server API 提供了从「单脚本 ASGI 应用」到「嵌入大型 Web 框架」的完整能力谱系BokehASGI让 Bokeh 成为任何 ASGI 生态中的一等公民prefix/root_path与头部透传规则让反向代理部署有据可依update_sessions打通了宿主进程到会话文档的数据通道AuthPolicy提供了不依赖框架的认证边界而Server与bokeh.client则分别覆盖了 Tornado 嵌入与 Python 侧会话定制的场景。在动手实现时建议优先对照 examples/server/api/asgi 下的完整示例并牢记两条底线多 worker 下所有状态都是进程本地的令牌载荷是签名而非加密的。【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考