From c9646e0e5d727deb30592ff3b2ca422fa2472751 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 15 Feb 2022 20:36:18 +0100 Subject: [PATCH] Create dashboard plugin --- fps/app.py | 51 ++++-------- fps/config.py | 1 + plugins/dashboard/CHANGELOG.md | 4 + plugins/dashboard/LICENSE | 11 +++ plugins/dashboard/MANIFEST.in | 0 plugins/dashboard/README.md | 3 + plugins/dashboard/fps_dashboard/__init__.py | 1 + plugins/dashboard/fps_dashboard/_version.py | 2 + plugins/dashboard/fps_dashboard/cli.py | 5 ++ plugins/dashboard/fps_dashboard/config.py | 10 +++ plugins/dashboard/fps_dashboard/dashboard.py | 83 ++++++++++++++++++++ plugins/dashboard/setup.cfg | 32 ++++++++ plugins/dashboard/setup.py | 3 + setup.cfg | 1 - 14 files changed, 172 insertions(+), 35 deletions(-) create mode 100644 plugins/dashboard/CHANGELOG.md create mode 100644 plugins/dashboard/LICENSE create mode 100644 plugins/dashboard/MANIFEST.in create mode 100644 plugins/dashboard/README.md create mode 100644 plugins/dashboard/fps_dashboard/__init__.py create mode 100644 plugins/dashboard/fps_dashboard/_version.py create mode 100644 plugins/dashboard/fps_dashboard/cli.py create mode 100644 plugins/dashboard/fps_dashboard/config.py create mode 100644 plugins/dashboard/fps_dashboard/dashboard.py create mode 100644 plugins/dashboard/setup.cfg create mode 100644 plugins/dashboard/setup.py diff --git a/fps/app.py b/fps/app.py index bd6a011..ea58824 100644 --- a/fps/app.py +++ b/fps/app.py @@ -1,3 +1,4 @@ +import json import logging from types import ModuleType from typing import Callable, Dict, List @@ -6,8 +7,6 @@ from fastapi import FastAPI from fastapi.routing import APIWebSocketRoute from pluggy import PluginManager -from rich.console import Console -from rich.table import Table from starlette.routing import Mount from fps import hooks @@ -156,7 +155,7 @@ def _load_configurations() -> None: logger.info("No plugin configuration to load") -def _load_routers(app: FastAPI) -> Dict[str, APIWebSocketRoute]: +def _load_routers(app: FastAPI) -> None: pm = _get_pluggin_manager(HookType.ROUTER) @@ -281,34 +280,20 @@ def _load_routers(app: FastAPI) -> Dict[str, APIWebSocketRoute]: else: logger.info("No plugin API router to load") - return ws_routes - - -def show_endpoints(app: FastAPI, ws_routes: Dict[str, APIWebSocketRoute]): - table = Table(title="API Summary") - table.add_column("Path", justify="left", style="cyan", no_wrap=True) - table.add_column("Methods", justify="right", style="green") - table.add_column("Plugin", style="magenta") - - # HTTP endpoints - openapi = app.openapi() - for k, v in openapi["paths"].items(): - path = k - methods = ", ".join([method.upper() for method in v.keys()]) - plugin = ", ".join({i["tags"][0] for i in v.values()}) - table.add_row(path, methods, plugin) - - # websockets endpoints - for plugin, ws_route in ws_routes.items(): - table.add_row(f"[cyan on red]{ws_route.path}[/]", "WEBSOCKET", plugin) - - console = Console() - with console.capture() as capture: - console.print() - console.print(table) - - str_output = capture.get() - logger.info(str_output) + fps_config = Config(FPSConfig) + if fps_config.show_endpoints: + openapi = app.openapi() + logger.info("") + for k, v in openapi["paths"].items(): + path = k + methods = [method.upper() for method in v.keys()] + plugin = list({i["tags"][0] for i in v.values()}) + o = {"path": path, "methods": methods, "plugin": plugin} + logger.info(f"ENDPOINT: {json.dumps(o)}") + for plugin, ws_route in ws_routes.items(): + o = {"path": ws_route.path, "methods": ["WEBSOCKET"], "plugin": [plugin]} + logger.info(f"ENDPOINT: {json.dumps(o)}") + logger.info("") def create_app(): @@ -321,11 +306,9 @@ def create_app(): fps_config = Config(FPSConfig) app = FastAPI(**fps_config.__dict__) - ws_routes = _load_routers(app) + _load_routers(app) _load_exceptions_handlers(app) Config.check_not_used_sections() - show_endpoints(app, ws_routes) - return app diff --git a/fps/config.py b/fps/config.py index 6b54485..7faaa31 100644 --- a/fps/config.py +++ b/fps/config.py @@ -34,6 +34,7 @@ class FPSConfig(BaseModel): # plugins enabled_plugins: List[str] = [] disabled_plugins: List[str] = [] + show_endpoints: bool = False @validator("enabled_plugins", "disabled_plugins") def plugins_format(cls, plugins): diff --git a/plugins/dashboard/CHANGELOG.md b/plugins/dashboard/CHANGELOG.md new file mode 100644 index 0000000..216de48 --- /dev/null +++ b/plugins/dashboard/CHANGELOG.md @@ -0,0 +1,4 @@ +0.0.1 (February 15, 2022) +========================= + +Initial release diff --git a/plugins/dashboard/LICENSE b/plugins/dashboard/LICENSE new file mode 100644 index 0000000..ae08356 --- /dev/null +++ b/plugins/dashboard/LICENSE @@ -0,0 +1,11 @@ +Copyright 2021 David Brochart and the FPS contributors. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/dashboard/MANIFEST.in b/plugins/dashboard/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/plugins/dashboard/README.md b/plugins/dashboard/README.md new file mode 100644 index 0000000..546b446 --- /dev/null +++ b/plugins/dashboard/README.md @@ -0,0 +1,3 @@ +# FPS Dashboard + +An [FPS](https://github.com/jupyter-server/fps) plugin to show a dashboard using Rich/Textual. diff --git a/plugins/dashboard/fps_dashboard/__init__.py b/plugins/dashboard/fps_dashboard/__init__.py new file mode 100644 index 0000000..d6d6e2f --- /dev/null +++ b/plugins/dashboard/fps_dashboard/__init__.py @@ -0,0 +1 @@ +from fps_dashboard._version import __version__ # noqa diff --git a/plugins/dashboard/fps_dashboard/_version.py b/plugins/dashboard/fps_dashboard/_version.py new file mode 100644 index 0000000..cd4e3bd --- /dev/null +++ b/plugins/dashboard/fps_dashboard/_version.py @@ -0,0 +1,2 @@ +version_info = (0, 0, 1) +__version__ = ".".join(map(str, version_info)) diff --git a/plugins/dashboard/fps_dashboard/cli.py b/plugins/dashboard/fps_dashboard/cli.py new file mode 100644 index 0000000..1e5b1aa --- /dev/null +++ b/plugins/dashboard/fps_dashboard/cli.py @@ -0,0 +1,5 @@ +from .dashboard import Dashboard + + +def app(): + Dashboard.run(title="API Summary") diff --git a/plugins/dashboard/fps_dashboard/config.py b/plugins/dashboard/fps_dashboard/config.py new file mode 100644 index 0000000..2d5f097 --- /dev/null +++ b/plugins/dashboard/fps_dashboard/config.py @@ -0,0 +1,10 @@ +from fps.config import PluginModel +from fps.hooks import register_config, register_plugin_name + + +class DashboardConfig(PluginModel): + foo: bool = False + + +c = register_config(DashboardConfig) +n = register_plugin_name("dashboard") diff --git a/plugins/dashboard/fps_dashboard/dashboard.py b/plugins/dashboard/fps_dashboard/dashboard.py new file mode 100644 index 0000000..d8d6dc9 --- /dev/null +++ b/plugins/dashboard/fps_dashboard/dashboard.py @@ -0,0 +1,83 @@ +import asyncio +import atexit +import json +import sys + +from rich.table import Table +from textual import events +from textual.app import App +from textual.widgets import ScrollView + +FPS = None + + +def stop_fps(): + if FPS is not None: + FPS.terminate() + + +atexit.register(stop_fps) + + +class Dashboard(App): + """An example of a very simple Textual App""" + + async def on_load(self, event: events.Load) -> None: + await self.bind("q", "quit", "Quit") + + async def on_mount(self, event: events.Mount) -> None: + + self.body = body = ScrollView(auto_width=True) + + await self.view.dock(body) + + async def add_content(): + global FPS + cmd = ["fps-uvicorn", "--fps.show_endpoints"] + sys.argv[1:] + FPS = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + queue = asyncio.Queue() + asyncio.create_task(get_log(queue)) + endpoint_marker = "ENDPOINT:" + endpoints = [] + get_endpoint = False + while True: + line = await queue.get() + if endpoint_marker in line: + get_endpoint = True + elif get_endpoint: + break + if get_endpoint: + i = line.find(endpoint_marker) + len(endpoint_marker) + line = line[i:].strip() + if not line: + break + endpoint = json.loads(line) + endpoints.append(endpoint) + + table = Table(title="API Summary") + table.add_column("Path", justify="left", style="cyan", no_wrap=True) + table.add_column("Methods", justify="right", style="green") + table.add_column("Plugin", style="magenta") + + for endpoint in endpoints: + path = endpoint["path"] + methods = ", ".join(endpoint["methods"]) + plugin = ", ".join(endpoint["plugin"]) + if "WEBSOCKET" in methods: + path = f"[cyan on red]{path}[/]" + table.add_row(path, methods, plugin) + + await body.update(table) + + await self.call_later(add_content) + + +async def get_log(queue): + while True: + line = await FPS.stderr.readline() + if line: + await queue.put(line.decode().strip()) + else: + break diff --git a/plugins/dashboard/setup.cfg b/plugins/dashboard/setup.cfg new file mode 100644 index 0000000..2127077 --- /dev/null +++ b/plugins/dashboard/setup.cfg @@ -0,0 +1,32 @@ +[metadata] +name = fps_dashboard +version = attr: fps_dashboard._version.__version__ +description = A dashboard plugin for FPS +long_description = file: README.md +long_description_content_type = text/markdown +license_file = LICENSE +author = David Brochart +author_email = david.brochart@gmail.com +url = https://github.com/jupyter-server/fps +platforms = Windows, Linux, Mac OS X +keywords = server, fastapi, pluggy, plugins, fps, rich + +[bdist_wheel] +universal = 1 + +[options] +include_package_data = True +packages = find: +python_requires = >=3.7 + +install_requires = + fps-uvicorn + rich + textual + +[options.entry_points] +fps_config = + fps_dashboard_config = fps_dashboard.config + +console_scripts = + fps-dashboard = fps_dashboard.cli:app diff --git a/plugins/dashboard/setup.py b/plugins/dashboard/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/plugins/dashboard/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup() diff --git a/setup.cfg b/setup.cfg index 621c91c..a1c4c35 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,6 @@ install_requires = fastapi pluggy>=1.0,<2.0 click - rich [options.extras_require] uvicorn =