From d60548dfc448d93d4b0f792448ecddba8f7d6eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 23 Aug 2023 17:17:34 +0200 Subject: [PATCH] integrate webserver into cli --- pkgs/clan-cli/clan_cli/__init__.py | 7 ++-- pkgs/clan-cli/clan_cli/webui/__init__.py | 39 ++++++++++++++++++++++ pkgs/clan-cli/clan_cli/webui/server.py | 41 ++++++++++++++++++++++++ pkgs/clan-cli/default.nix | 10 ++++-- 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/webui/__init__.py create mode 100644 pkgs/clan-cli/clan_cli/webui/server.py diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 18022fe5b..52777182a 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -3,9 +3,9 @@ import sys from types import ModuleType from typing import Optional -from . import admin, secrets, update +from . import admin, secrets, update, webui -# from . import admin, config, secrets, update +# from . import admin, config, secrets, update, webui from .errors import ClanError from .ssh import cli as ssh_cli @@ -39,6 +39,9 @@ def main() -> None: ) update.register_parser(parser_update) + parser_webui = subparsers.add_parser("webui", help="start webui") + webui.register_parser(parser_webui) + if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/webui/__init__.py b/pkgs/clan-cli/clan_cli/webui/__init__.py new file mode 100644 index 000000000..013468bc4 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/__init__.py @@ -0,0 +1,39 @@ +import argparse +from typing import Callable, NoReturn, Optional + +start_server: Optional[Callable] = None +ServerImportError: Optional[ImportError] = None +try: + from .server import start_server +except ImportError as e: + ServerImportError = e + + +def fastapi_is_not_installed(_: argparse.Namespace) -> NoReturn: + assert ServerImportError is not None + print( + f"Dependencies for the webserver is not installed. The webui command has been disabled ({ServerImportError})" + ) + exit(1) + + +def register_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--port", type=int, default=2979, help="Port to listen on") + parser.add_argument("--host", type=str, default="::1", help="Host to listen on") + parser.add_argument( + "--no-open", action="store_true", help="Don't open the browser", default=False + ) + parser.add_argument( + "--reload", action="store_true", help="Don't reload on changes", default=False + ) + parser.add_argument( + "--log-level", + type=str, + default="info", + help="Log level", + choices=["critical", "error", "warning", "info", "debug", "trace"], + ) + if start_server is None: + parser.set_defaults(func=fastapi_is_not_installed) + else: + parser.set_defaults(func=start_server) diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py new file mode 100644 index 000000000..7205496bc --- /dev/null +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -0,0 +1,41 @@ +import argparse +import time +import urllib.request +import webbrowser +from threading import Thread + +from fastapi import FastAPI + +# XXX: can we dynamically load this using nix develop? +from uvicorn import run + +app = FastAPI() + + +@app.get("/health") +async def read_root() -> str: + return "OK" + + +def defer_open_browser(base_url: str) -> None: + for i in range(5): + try: + urllib.request.urlopen(base_url + "/health") + break + except OSError: + time.sleep(i) + webbrowser.open(base_url) + + +def start_server(args: argparse.Namespace) -> None: + if not args.no_open: + Thread( + target=defer_open_browser, args=(f"http://[{args.host}]:{args.port}",) + ).start() + run( + "clan_cli.webui.server:app", + host=args.host, + port=args.port, + log_level=args.log_level, + reload=args.reload, + ) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 746775a0c..de9dfb297 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -1,5 +1,7 @@ { age , argcomplete +, fastapi +, uvicorn , bubblewrap , installShellFiles , nix @@ -22,7 +24,11 @@ let # evaluating the flake .# CLAN_OPTIONS_FILE = ./clan_cli/config/jsonschema/options.json; - dependencies = [ argcomplete ]; + dependencies = [ + argcomplete # optional dependency: if not enabled, shell completion will not work + fastapi + uvicorn # optional dependencies: if not enabled, webui subcommand will not work + ]; testDependencies = [ pytest @@ -73,7 +79,7 @@ python3.pkgs.buildPythonPackage { wheel ] ++ testDependencies; - passthru.testDependencies = testDependencies; + passthru.testDependencies = dependencies ++ testDependencies; makeWrapperArgs = [ "--set CLAN_FLAKE ${self}"