從零開發無服務函數管理器:jupyter lab 插件
- 2019 年 12 月 15 日
- 筆記
本文介紹如何製作一個 jupyter lab 的插件。作為例子,我們將製作一個運行在 jupyter 中的serveless 函數的管理插件。和各種其他無服務函數不同的是:這是一個極其輕量級的 無服務函數 管理插件,不依賴任何其他組件,所有組件都會允許在 jupyter lab 內部。
1. 創建開發環境
1.1 安裝 conda/miniconda
1.2 創建開發環境,裝各種庫
conda create -n jupyterlab-ext --override-channels --strict-channel-priority -c conda-forge -c anaconda jupyterlab cookiecutter nodejs git conda activate jupyterlab-ext
2. 創建 repo
mkdir jupyter-lab-serverless
3. 創建插件項目
3.1 使用 cookiecutter 創建項目模板
➜ cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts --checkout v1.0 author_name []: u2takey extension_name [myextension]: jupyter-lab-serverless project_short_description [A JupyterLab extension.]: Create And Run Serverless Function in JupyterLab repository [https://github.com/my_name/myextension]: https://github.com/u2takey/jupyter-lab-serverless
可以發現一些模板文件已經創建出來了
➜ cd jupyter-lab-serverless ➜ ll .rw-r--r-- 475 leiwang 6 Dec 19:16 README.md .rw-r--r-- 1.2k leiwang 6 Dec 19:16 package.json drwxr-xr-x - leiwang 6 Dec 19:16 src drwxr-xr-x - leiwang 6 Dec 19:16 style .rw-r--r-- 555 leiwang 6 Dec 19:16 tsconfig.json
3.2 直接 Build安裝 試一下
jlpm install jupyter labextension install . --no-build
3.3 打開觀察 第一次 安裝的效果
jupyter lab --watch # 打開 瀏覽器 console,可以看到 > JupyterLab extension jupyter-lab-serverless is activated!
4. 開始製作 severless 插件
這個插件將分為兩個部分,一部分是 server 部分,一部分是前端部分. 我們將先創建後端部分。
4.1 server 插件部分
server 插件本質是一個 tornado handler,首先在 init 種實現load
def load_jupyter_server_extension(nb_server_app): 初始化
定義 API,我們的函數 Server API主要的作用是完成 無服務函數的 增刪查改,以及觸發.
為了讓實現更簡單,我們用 put/delete 帶函數名實現增刪改,post/get 帶函數名用於實現觸發,而get不帶函數名作為 查的實現,返回所有函數。同時為了 重啟後函數能得到保存,我們使用 sqite作為本地保存(jupyter lab serverside的 state保存可能有更好的辦法)。
class FunctionHandler(APIHandler): """ A handler that manage serverless functions. """ def initialize(self, app): self.logger = app.log self.db = app.db @web.authenticated @gen.coroutine def get(self, function=''): if function: # 觸發 else: # List def trigger_function(self, function, method, query, body): # 觸發實現 @gen.coroutine def post(self, function=''): # 觸法 @web.authenticated @gen.coroutine def put(self, function=''): # 增 @web.authenticated @gen.coroutine def delete(self, function=''): # 刪
函數的執行:
class Function(Base): __tablename__ = 'functions' # 各種字段略 def __call__(self, *args, **kwargs): import imp module = imp.new_module(self.name) exec(self.script, module.__dict__) module.handle.logger = self.logger return module.handle(*args, **kwargs)
4.2 前端插件部分
在 index.js 中實現一個 xx Plugin 的繼承
增加前端依賴的辦法:
jlpm add @jupyterlab/apputils jlpm add @jupyterlab/application jlpm run build
/** * Initialization data for the jupyter-lab-serverless extension. */ const extension: JupyterFrontEndPlugin<void> = { id: 'jupyter-lab-serverless', requires: [IStateDB], autoStart: true, activate: activate };
設計上,我們並沒有使用 jupyterlab 插件中常用的 platte,而是增加 toolbar 上的兩個 button。其中一個按鈕設計為增加函數,另一個函數用於管理包括刪除函數。
/** * Save Current Script as A Serverless Function */ export class ButtonExtensionAdd implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> { createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable { let callback = () => { // 保存函數 }; let button = new ToolbarButton({ className: 'serverless-add', iconClassName: 'fa fa-desktop', onClick: callback, tooltip: 'Save as Serverless Functions' }); panel.toolbar.addItem('severless-add', button); return new DisposableDelegate(() => { button.dispose(); }); } } /** * Manager Serverless Function */ export class ButtonExtensionManager implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> { delete(name: string){ // 刪除函數 } createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable { let callback = () => { // 發送請求 獲取函數 }; let button = new ToolbarButton({ className: 'serverless-manager', iconClassName: 'fa fa-tasks', onClick: callback, tooltip: 'Show Serverless Functions' }); panel.toolbar.addItem('severless-manager', button); return new DisposableDelegate(() => { button.dispose(); }); } }
在回調函數中,我們實現 request 發送到剛剛實現的 server 插件,獲取數據。
4.3 打包,發佈插件
python3 setup.py sdist // 發佈後端插件 twine upload --skip-existing -u xx -p yy dist/* // 發佈前端插件 npm publish --access=public
5. 使用演示
5.1. 啟動
從鏡像啟動,鏡像中到 jupyter 已經安裝了 serveless 插件
docker run --rm -p 8888:8888 ccr.ccs.tencentyun.com/leiwang/jupterlab:serverless /bin/bash -c 'jupyter lab --ip=* --port=8888 --no-browser --allow-root'
5.2. 創建一個函數
函數需要有一個命名為 'handle' 的函數.
函數有個一個 logger,可以用於debug,logger輸出內容將輸出到 jupyter 後端。
def handle(event): logger = handle.logger logger.info(event) return event
點擊保存按鈕, 保存函數

5.3. 本地測試
本地測試有兩種方式
一: 直接調用 handle 函數
二: 打開另一個 notebook,模擬 request 觸發函數,檢查效果
handle({})
注意 調用時需要帶上notebook的 Authorization,這個在 jupyter notebook 啟動時可以查看到。
import requests headers = {'Authorization': 'token 4b917c156ea968fdafb81308324b06c5a9154596ebfcfd67'} data = {"test": "testdata"} r = requests.post('http://127.0.0.1:8888/function/test1.ipynb', json=data, headers=headers) r.text '{"code": "success", "data": {"method": "POST", "query": {}, "body": {"test": "testdata"}}}'
5.4. 管理函數
可以看到已經調用了一次

點擊刪除按鈕,可以把 函數刪除。
5.5. 定期執行函數
函數支持定期執行,schedule採用類似 https://schedule.readthedocs.io/en/stable/ 的語法表達方式
schedule 支持 'every'(默認1), 'unit'(默認為day), 'at' (默認為None) 三個參數
def handle(event): logger = handle.__dict__.get('logger') if logger: logger.info(event) return event handle.schedule={'unit':'seconds'}
保存函數,觀察後端日誌,可以發現每秒被執行一次

5.6. 其他應用
可以用於接收 weekhook 做ci觸發,聊天機械人,定期執行腳步等等。
本文完整的代碼在 https://github.com/u2takey/jupyter-lab-serverless
參考
- https://blog.jupyter.org/99-ways-to-extend-the-jupyter-ecosystem-11e5dab7c54
- https://jupyterlab.readthedocs.io/en/stable/developer/notebook.html#extend-notebook-plugin
- https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html
- https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.html
- https://github.com/jupyterlab/jupyterlab-latex/blob/master/docs/advanced.md
- https://github.com/matplotlib/jupyter-matplotlib
- https://github.com/mauhai/awesome-jupyterlab