diff --git a/jupyverse/cli.py b/jupyverse/cli.py new file mode 100644 index 00000000..f486e116 --- /dev/null +++ b/jupyverse/cli.py @@ -0,0 +1,5 @@ +from .dashboard import Dashboard + + +def app(): + Dashboard.run(title="Jupyverse Dashboard", log="jupyverse.log") diff --git a/jupyverse/dashboard.py b/jupyverse/dashboard.py new file mode 100644 index 00000000..87c65601 --- /dev/null +++ b/jupyverse/dashboard.py @@ -0,0 +1,129 @@ +import asyncio +import atexit +import json +import sys +import subprocess +from typing import List, Optional + +from rich.text import Text # type: ignore +from rich.table import Table # type: ignore +from textual import events # type: ignore +from textual.app import App # type: ignore +from textual.reactive import Reactive # type: ignore +from textual.widgets import Footer, ScrollView # type: ignore +from textual.widget import Widget # type: ignore + +FPS: Optional[subprocess.Popen] = None + + +def stop_fps(): + if FPS is not None: + FPS.terminate() + + +atexit.register(stop_fps) + + +class Dashboard(App): + """A dashboard for Jupyverse""" + + async def on_load(self, event: events.Load) -> None: + await self.bind("e", "show_endpoints", "Show endpoints") + await self.bind("l", "show_log", "Show log") + await self.bind("q", "quit", "Quit") + + show = Reactive("endpoints") + body_change = asyncio.Event() + text = Text() + table = Table(title="API Summary") + + def action_show_log(self) -> None: + self.show = "log" + + def action_show_endpoints(self) -> None: + self.show = "endpoints" + + def watch_show(self, show: str) -> None: + self.body_change.set() + + async def on_mount(self, event: events.Mount) -> None: + + footer = Footer() + body = ScrollView(auto_width=True) + + await self.view.dock(footer, edge="bottom") + 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 + ) + queues = [asyncio.Queue() for i in range(2)] + asyncio.create_task(get_log(queues)) + asyncio.create_task(self._show_log(queues[0])) + asyncio.create_task(self._show_endpoints(queues[1])) + asyncio.create_task(self._change_body(body)) + + await self.call_later(add_content) + + async def _change_body(self, body: Widget): + while True: + await self.body_change.wait() + self.body_change.clear() + if self.show == "endpoints": + await body.update(self.table) + elif self.show == "log": + await body.update(self.text) + + async def _show_endpoints(self, queue: asyncio.Queue): + endpoint_marker = "ENDPOINT:" + get_endpoint = False + endpoints = [] + 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) + + self.table.add_column("Path", justify="left", style="cyan", no_wrap=True) + self.table.add_column("Methods", justify="right", style="green") + self.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}[/]" + self.table.add_row(path, methods, plugin) + + self.body_change.set() + + async def _show_log(self, queue: asyncio.Queue): + while True: + line = await queue.get() + self.text.append(line) + self.body_change.set() + + +async def get_log(queues: List[asyncio.Queue]): + assert FPS is not None + assert FPS.stderr is not None + while True: + line = await FPS.stderr.readline() + if line: + line = line.decode() + for queue in queues: + await queue.put(line) + else: + break diff --git a/plugins/auth/README.md b/plugins/auth/README.md new file mode 100644 index 00000000..c54ac96e --- /dev/null +++ b/plugins/auth/README.md @@ -0,0 +1,3 @@ +# fps-auth + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing authentication. diff --git a/plugins/auth/setup.cfg b/plugins/auth/setup.cfg index ef2411d2..79521841 100644 --- a/plugins/auth/setup.cfg +++ b/plugins/auth/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_auth version = attr: fps_auth.__version__ +description = An FPS plugin implementing authentication +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/contents/README.md b/plugins/contents/README.md new file mode 100644 index 00000000..22ab5ec1 --- /dev/null +++ b/plugins/contents/README.md @@ -0,0 +1,3 @@ +# fps-contents + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the contents API. diff --git a/plugins/contents/setup.cfg b/plugins/contents/setup.cfg index 25ec3d08..7cab3aba 100644 --- a/plugins/contents/setup.cfg +++ b/plugins/contents/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_contents version = attr: fps_contents.__version__ +description = An FPS plugin implementing the contents API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/jupyterlab/README.md b/plugins/jupyterlab/README.md new file mode 100644 index 00000000..abd94c72 --- /dev/null +++ b/plugins/jupyterlab/README.md @@ -0,0 +1,3 @@ +# fps-jupyterlab + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the JupyterLab API. diff --git a/plugins/jupyterlab/setup.cfg b/plugins/jupyterlab/setup.cfg index 268acaea..e75b84f9 100644 --- a/plugins/jupyterlab/setup.cfg +++ b/plugins/jupyterlab/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_jupyterlab version = attr: fps_jupyterlab.__version__ +description = An FPS plugin implementing the JupyterLab API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/kernels/README.md b/plugins/kernels/README.md new file mode 100644 index 00000000..3524b89d --- /dev/null +++ b/plugins/kernels/README.md @@ -0,0 +1,3 @@ +# fps-kernels + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the kernels API. diff --git a/plugins/kernels/setup.cfg b/plugins/kernels/setup.cfg index eff6aedb..9609f485 100644 --- a/plugins/kernels/setup.cfg +++ b/plugins/kernels/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_kernels version = attr: fps_kernels.__version__ +description = An FPS plugin implementing the kernels API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/lab/README.md b/plugins/lab/README.md new file mode 100644 index 00000000..4487360b --- /dev/null +++ b/plugins/lab/README.md @@ -0,0 +1,3 @@ +# fps-lab + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the JupyterLab/RetroLab API. diff --git a/plugins/lab/setup.cfg b/plugins/lab/setup.cfg index a637ed4b..4d0faf86 100644 --- a/plugins/lab/setup.cfg +++ b/plugins/lab/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_lab version = attr: fps_lab.__version__ +description = An FPS plugin implementing the JupyterLab/RetroLab API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/login/README.md b/plugins/login/README.md new file mode 100644 index 00000000..63a42c68 --- /dev/null +++ b/plugins/login/README.md @@ -0,0 +1,3 @@ +# fps-login + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the login API. diff --git a/plugins/login/setup.cfg b/plugins/login/setup.cfg index 63c735d1..7d06316a 100644 --- a/plugins/login/setup.cfg +++ b/plugins/login/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_login version = attr: fps_login.__version__ +description = An FPS plugin implementing the login API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/nbconvert/README.md b/plugins/nbconvert/README.md new file mode 100644 index 00000000..f74d99b9 --- /dev/null +++ b/plugins/nbconvert/README.md @@ -0,0 +1,3 @@ +# fps-nbconvert + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the nbconvert API. diff --git a/plugins/nbconvert/setup.cfg b/plugins/nbconvert/setup.cfg index 1cfdb644..4b1041b6 100644 --- a/plugins/nbconvert/setup.cfg +++ b/plugins/nbconvert/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_nbconvert version = attr: fps_nbconvert.__version__ +description = An FPS plugin implementing the nbconvert API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/retrolab/README.md b/plugins/retrolab/README.md new file mode 100644 index 00000000..6930f6db --- /dev/null +++ b/plugins/retrolab/README.md @@ -0,0 +1,3 @@ +# fps-login + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the RetroLab API. diff --git a/plugins/retrolab/setup.cfg b/plugins/retrolab/setup.cfg index 7431b3fd..92006f1f 100644 --- a/plugins/retrolab/setup.cfg +++ b/plugins/retrolab/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_retrolab version = attr: fps_retrolab.__version__ +description = An FPS plugin implementing the RetroLab API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/terminals/README.md b/plugins/terminals/README.md new file mode 100644 index 00000000..ba852ba0 --- /dev/null +++ b/plugins/terminals/README.md @@ -0,0 +1,3 @@ +# fps-terminals + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the terminals API. diff --git a/plugins/terminals/setup.cfg b/plugins/terminals/setup.cfg index 56bf3adb..3d4ce5dd 100644 --- a/plugins/terminals/setup.cfg +++ b/plugins/terminals/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_terminals version = attr: fps_terminals.__version__ +description = An FPS plugin implementing the terminals API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/plugins/yjs/README.md b/plugins/yjs/README.md new file mode 100644 index 00000000..082cd38c --- /dev/null +++ b/plugins/yjs/README.md @@ -0,0 +1,3 @@ +# fps-yjs + +An [FPS](https://github.com/jupyter-server/fps) plugin implementing the Yjs API. diff --git a/plugins/yjs/setup.cfg b/plugins/yjs/setup.cfg index 2fac47f6..f9733b5e 100644 --- a/plugins/yjs/setup.cfg +++ b/plugins/yjs/setup.cfg @@ -1,6 +1,9 @@ [metadata] name = fps_yjs version = attr: fps_yjs.__version__ +description = An FPS plugin implementing the Yjs API +long_description = file: README.md +long_description_content_type = text/markdown [options] include_package_data = True diff --git a/setup.cfg b/setup.cfg index 1267281d..75bf3312 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,8 @@ install_requires = fps-terminals fps-nbconvert fps-yjs + rich + textual [options.extras_require] jupyterlab = @@ -47,7 +49,7 @@ test = [options.entry_points] console_scripts = - jupyverse = fps_uvicorn.cli:app + jupyverse = jupyverse.cli:app [flake8] max-line-length = 100