Skip to content

Commit e7b07c9

Browse files
committed
Add jupyverse dashboard
1 parent f37d199 commit e7b07c9

File tree

3 files changed

+137
-1
lines changed

3 files changed

+137
-1
lines changed

jupyverse/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .dashboard import Dashboard
2+
3+
4+
def app():
5+
Dashboard.run(title="Jupyverse Dashboard", log="jupyverse.log")

jupyverse/dashboard.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import asyncio
2+
import atexit
3+
import json
4+
import sys
5+
import subprocess
6+
from typing import List, Optional
7+
8+
from rich.text import Text # type: ignore
9+
from rich.table import Table # type: ignore
10+
from textual import events # type: ignore
11+
from textual.app import App # type: ignore
12+
from textual.reactive import Reactive # type: ignore
13+
from textual.widgets import Footer, ScrollView # type: ignore
14+
from textual.widget import Widget # type: ignore
15+
16+
FPS: Optional[subprocess.Popen] = None
17+
18+
19+
def stop_fps():
20+
if FPS is not None:
21+
FPS.terminate()
22+
23+
24+
atexit.register(stop_fps)
25+
26+
27+
class Dashboard(App):
28+
"""A dashboard for Jupyverse"""
29+
30+
async def on_load(self, event: events.Load) -> None:
31+
await self.bind("e", "show_endpoints", "Show endpoints")
32+
await self.bind("l", "show_log", "Show log")
33+
await self.bind("q", "quit", "Quit")
34+
35+
show = Reactive("endpoints")
36+
body_change = asyncio.Event()
37+
text = Text()
38+
table = Table(title="API Summary")
39+
40+
def action_show_log(self) -> None:
41+
self.show = "log"
42+
43+
def action_show_endpoints(self) -> None:
44+
self.show = "endpoints"
45+
46+
def watch_show(self, show: str) -> None:
47+
self.body_change.set()
48+
49+
async def on_mount(self, event: events.Mount) -> None:
50+
51+
footer = Footer()
52+
body = ScrollView(auto_width=True)
53+
54+
await self.view.dock(footer, edge="bottom")
55+
await self.view.dock(body)
56+
57+
async def add_content():
58+
global FPS
59+
cmd = ["fps-uvicorn", "--fps.show_endpoints"] + sys.argv[1:]
60+
FPS = await asyncio.create_subprocess_exec(
61+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
62+
)
63+
queues = [asyncio.Queue() for i in range(2)]
64+
asyncio.create_task(get_log(queues))
65+
asyncio.create_task(self._show_log(queues[0]))
66+
asyncio.create_task(self._show_endpoints(queues[1]))
67+
asyncio.create_task(self._change_body(body))
68+
69+
await self.call_later(add_content)
70+
71+
async def _change_body(self, body: Widget):
72+
while True:
73+
await self.body_change.wait()
74+
self.body_change.clear()
75+
if self.show == "endpoints":
76+
await body.update(self.table)
77+
elif self.show == "log":
78+
await body.update(self.text)
79+
80+
async def _show_endpoints(self, queue: asyncio.Queue):
81+
endpoint_marker = "ENDPOINT:"
82+
get_endpoint = False
83+
endpoints = []
84+
while True:
85+
line = await queue.get()
86+
if endpoint_marker in line:
87+
get_endpoint = True
88+
elif get_endpoint:
89+
break
90+
if get_endpoint:
91+
i = line.find(endpoint_marker) + len(endpoint_marker)
92+
line = line[i:].strip()
93+
if not line:
94+
break
95+
endpoint = json.loads(line)
96+
endpoints.append(endpoint)
97+
98+
self.table.add_column("Path", justify="left", style="cyan", no_wrap=True)
99+
self.table.add_column("Methods", justify="right", style="green")
100+
self.table.add_column("Plugin", style="magenta")
101+
102+
for endpoint in endpoints:
103+
path = endpoint["path"]
104+
methods = ", ".join(endpoint["methods"])
105+
plugin = ", ".join(endpoint["plugin"])
106+
if "WEBSOCKET" in methods:
107+
path = f"[cyan on red]{path}[/]"
108+
self.table.add_row(path, methods, plugin)
109+
110+
self.body_change.set()
111+
112+
async def _show_log(self, queue: asyncio.Queue):
113+
while True:
114+
line = await queue.get()
115+
self.text.append(line)
116+
self.body_change.set()
117+
118+
119+
async def get_log(queues: List[asyncio.Queue]):
120+
assert FPS is not None
121+
assert FPS.stderr is not None
122+
while True:
123+
line = await FPS.stderr.readline()
124+
if line:
125+
line = line.decode()
126+
for queue in queues:
127+
await queue.put(line)
128+
else:
129+
break

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ install_requires =
2828
fps-terminals
2929
fps-nbconvert
3030
fps-yjs
31+
rich
32+
textual
3133

3234
[options.extras_require]
3335
jupyterlab =
@@ -47,7 +49,7 @@ test =
4749

4850
[options.entry_points]
4951
console_scripts =
50-
jupyverse = fps_uvicorn.cli:app
52+
jupyverse = jupyverse.cli:app
5153

5254
[flake8]
5355
max-line-length = 100

0 commit comments

Comments
 (0)