From 7a4544731d8957cf0788591c6c6626142ce863b4 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 31 Aug 2026 20:50:38 +0000 Subject: [PATCH] Fastapi-vue-setup updated to 1.4.0: prettier logs. --- frontend/vite-plugin-fastapi.js | 2 + pagerite/__main__.py | 3 +- pyproject.toml | 3 +- scripts/devserver.py | 5 ++- scripts/fastapi-vue/devutil.py | 69 +++++++++++++++++++++++---------- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/frontend/vite-plugin-fastapi.js b/frontend/vite-plugin-fastapi.js index cae1f47..dbf41d8 100644 --- a/frontend/vite-plugin-fastapi.js +++ b/frontend/vite-plugin-fastapi.js @@ -5,6 +5,7 @@ * Configures Vite for FastAPI backend integration: * - Proxies /api/* requests to the FastAPI backend * - Builds to the Python module's frontend-build directory + * - Disables Vite's screen clearing on startup * * Options: * paths - Array of paths to proxy (default: ["/api"]) @@ -26,6 +27,7 @@ export default function fastapiVue({ paths = ["/api"] } = {}) { return { name: "vite-plugin-fastapi-pagerite", config: () => ({ + clearScreen: false, server: { proxy }, build: { outDir: "../pagerite/frontend-build", diff --git a/pagerite/__main__.py b/pagerite/__main__.py index b5b352e..eab9e67 100644 --- a/pagerite/__main__.py +++ b/pagerite/__main__.py @@ -98,13 +98,12 @@ def main() -> None: os.environ["PAGERITE_HOSTNAME"] = args.hostname if args.dbip: _download_dbip() - dev = {"reload": True, "reload_dirs": ["pagerite"]} if DEVMODE else {} server.run( "pagerite.app:app", listen=args.listen, default_port=DEFAULT_PORT, server_header=False, - **dev, + reload=Path(__file__).parent if DEVMODE else False, ) diff --git a/pyproject.toml b/pyproject.toml index 0b9183c..37f3d4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "blake3>=1.0.9", - "fastapi-vue>=1.3.1", + "fastapi-vue~=1.4.0", "fastapi[standard]>=0.141.1", "html5tagger>=2.0.0", "httpx>=0.28.1", @@ -54,6 +54,7 @@ packages = ["pagerite"] # the frontend build). artifacts = ["pagerite/frontend-build", "pagerite/themes", "pagerite/seed-assets"] only-packages = true +packages = ["pagerite"] [tool.hatch.build.targets.sdist.hooks.custom] path = "scripts/fastapi-vue/buildhook.py" diff --git a/scripts/devserver.py b/scripts/devserver.py index 8e4a583..cedea45 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -8,6 +8,8 @@ import sys from contextlib import suppress from pathlib import Path +import tracerite + # Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path) sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue"))) from devutil import ( @@ -39,7 +41,7 @@ async def run_devserver( viteurl, npm_install, vite = setup_vite(listen, DEFAULT_VITE_PORT) backurl, pagerite = setup_cli("pagerite", backend, DEFAULT_DEV_PORT) - # Tell the everyone by environment (vite proxy and backend devmode use these) + # Tell everyone via environment (vite proxy and backend devmode use these) os.environ["PAGERITE_VITE_URL"] = viteurl os.environ["PAGERITE_BACKEND_URL"] = backurl os.environ["PAGERITE_DEV"] = "1" @@ -54,6 +56,7 @@ async def run_devserver( def main() -> None: """Parse CLI arguments and run the devserver.""" + tracerite.load() parser = argparse.ArgumentParser( description="Run Vite and FastAPI development servers", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/scripts/fastapi-vue/devutil.py b/scripts/fastapi-vue/devutil.py index c4f08ec..80a28dc 100644 --- a/scripts/fastapi-vue/devutil.py +++ b/scripts/fastapi-vue/devutil.py @@ -7,8 +7,8 @@ import sys from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING, Any, Self +from urllib.parse import urlsplit -import httpx from buildutil import find_dev_tool, find_install_tool, logger from fastapi_vue.hostutil import parse_endpoint @@ -105,18 +105,47 @@ class ProcessGroup: await p.wait() +async def http_get_server(url: str, timeout: float) -> str | None: # noqa: ASYNC109 + """GET url with plain asyncio streams, return the response Server header. + + Returns an empty string when the server responds without a Server header, + and None when the server is unreachable or doesn't answer in time. + """ + parts = urlsplit(url) + host = parts.hostname or "localhost" + port = parts.port or (443 if parts.scheme == "https" else 80) + path = parts.path or "/" + if parts.query: + path += f"?{parts.query}" + try: + async with asyncio.timeout(timeout): + reader, writer = await asyncio.open_connection(host, port) + try: + writer.write(f"GET {path} HTTP/1.0\r\nHost: {host}\r\n\r\n".encode()) + await writer.drain() + data = await reader.readuntil(b"\r\n\r\n") + finally: + writer.close() + except OSError, EOFError, ValueError, TimeoutError: + return None + for line in data.decode("latin-1").split("\r\n"): + if line.lower().startswith("server:"): + return line.split(":", 1)[1].strip() + return "" + + async def check_ports_free(*urls: str) -> None: """Verify URLs are not responding (ports are free). Raise SystemExit if any respond.""" - async def check(client: httpx.AsyncClient, url: str) -> None: - with suppress(httpx.RequestError): - res = await client.get(url, timeout=0.1) - server = res.headers.get("server", "server") - logger.warning("Conflicting %s already running at %s", server, url) + async def check(url: str) -> None: + server = await http_get_server(url, timeout=0.1) + if server is not None: + logger.warning( + "Conflicting %s already running at %s", server or "server", url + ) raise SystemExit(1) - async with httpx.AsyncClient() as client: - await asyncio.gather(*[check(client, url) for url in urls]) + await asyncio.gather(*[check(url) for url in urls]) async def ready(url: str, path: str = "", max_attempts: int = 50) -> None: @@ -128,18 +157,14 @@ async def ready(url: str, path: str = "", max_attempts: int = 50) -> None: if not path: return - async with httpx.AsyncClient() as client: - for attempt in range(max_attempts): - try: - await client.get(f"{url}{path}", timeout=1.0) - except httpx.RequestError: - if attempt == max_attempts - 1: - logger.warning("Backend didn't start in time") - raise SystemExit(1) from None - await asyncio.sleep(0.1) - else: - logger.info("✓ Backend ready!") - return + for attempt in range(max_attempts): + if await http_get_server(f"{url}{path}", timeout=1.0) is not None: + logger.info("✓ Backend ready!") + return + if attempt == max_attempts - 1: + logger.warning("Backend didn't start in time") + raise SystemExit(1) + await asyncio.sleep(0.1) def setup_vite( @@ -222,5 +247,7 @@ def setup_cli( host = endpoints[0]["host"] port = endpoints[0]["port"] - cmd = [cli, f"--listen={host}:{port}"] + # Run the package as a module with the current interpreter, instead of + # relying on a PATH-installed CLI entry point. + cmd = [sys.executable, "-m", cli, f"--listen={host}:{port}"] return f"http://{host}:{port}", cmd