Skip to content

Commit

Permalink
Create dashboard plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Feb 15, 2022
1 parent 41a3ed5 commit c9646e0
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 35 deletions.
51 changes: 17 additions & 34 deletions fps/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
from types import ModuleType
from typing import Callable, Dict, List
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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():
Expand All @@ -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
1 change: 1 addition & 0 deletions fps/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions plugins/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0.0.1 (February 15, 2022)
=========================

Initial release
11 changes: 11 additions & 0 deletions plugins/dashboard/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
Empty file added plugins/dashboard/MANIFEST.in
Empty file.
3 changes: 3 additions & 0 deletions plugins/dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# FPS Dashboard

An [FPS](https://github.com/jupyter-server/fps) plugin to show a dashboard using Rich/Textual.
1 change: 1 addition & 0 deletions plugins/dashboard/fps_dashboard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from fps_dashboard._version import __version__ # noqa
2 changes: 2 additions & 0 deletions plugins/dashboard/fps_dashboard/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version_info = (0, 0, 1)
__version__ = ".".join(map(str, version_info))
5 changes: 5 additions & 0 deletions plugins/dashboard/fps_dashboard/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .dashboard import Dashboard


def app():
Dashboard.run(title="API Summary")
10 changes: 10 additions & 0 deletions plugins/dashboard/fps_dashboard/config.py
Original file line number Diff line number Diff line change
@@ -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")
83 changes: 83 additions & 0 deletions plugins/dashboard/fps_dashboard/dashboard.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions plugins/dashboard/setup.cfg
Original file line number Diff line number Diff line change
@@ -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 = [email protected]
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
3 changes: 3 additions & 0 deletions plugins/dashboard/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import setuptools

setuptools.setup()
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ install_requires =
fastapi
pluggy>=1.0,<2.0
click
rich

[options.extras_require]
uvicorn =
Expand Down

0 comments on commit c9646e0

Please sign in to comment.