Files
computer-playbook/roles/web-app-docs/files/python/infinito_docs/server.py
T
kevinveenbirkenbachandClaude Opus 5 f5eff500c1 feat(web-app-docs): build a translated site on request instead of all of them inline
build() ran sphinx once per translated language before returning, so a deploy blocked for roughly 30 minutes per language. With 29 translated catalogs German sat eighth and its site appeared after about three and a half hours, which is why the language spec polled /deployed/de/ into a 404 for its whole run. The server already answered an unbuilt version with 202 and a build request; an unbuilt language got a bare 404.

Give languages the same treatment. request() takes a code and writes a version:code marker, build_language() builds exactly one from its own checkout via the extracted _checkout(), and the server answers a translated but unbuilt language with 202 while an untranslated one keeps its 404. No language is dropped: build() now queues every translated one into queue/background/, which next_queued() drains only after the lane a visitor is waiting on, so the requested language overtakes the backlog.

languages.json grows a translated list beside known, and _language_index() reads a pre-split file as known-only so an existing volume keeps naming its languages instead of rendering an empty switcher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 12:30:06 +02:00

178 lines
6.3 KiB
Python

"""HTTP front of the documentation library.
Environment:
DOCS_SOURCE_REPO: git URL of the documented repository.
DOCS_DATA_DIR: volume holding the mirror and the site of every version.
DOCS_BUILD_JOBS: parallel Sphinx processes per build.
DOCS_FETCH_INTERVAL: seconds between two fetches of new commits and tags.
DOCS_PORT: port to listen on.
DOCS_SNAPSHOT_DIR: the deployed working tree, served as version ``deployed``.
"""
from __future__ import annotations
import html
import json
import os
import threading
from functools import partial
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlsplit
from infinito_docs.library import LATEST, Library
PACKAGE_DIR = Path(__file__).resolve().parent
SCRIPT = PACKAGE_DIR / "assets" / "js" / "versions.js"
PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<script src="/_docs/versions.js" defer></script>
</head>
<body>
<main>
<h1>{title}</h1>
{body}
</main>
</body>
</html>
"""
OVERVIEW = """<table>
<thead><tr><th>Version</th><th>Status</th><th>Progress</th></tr></thead>
<tbody data-versions></tbody>
</table>
"""
BUILDING = """<section data-build="{version}">
<p>This version is not built yet. The build started and this page reloads once it is done.</p>
<p data-phase>queued</p>
<progress max="100" value="0" aria-label="Build progress of {version}"></progress>
<pre></pre>
</section>
<p><a href="/versions/">All versions</a></p>
"""
class DocsHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, library, **kwargs):
self.library = library
super().__init__(*args, **kwargs)
def do_GET(self):
self._route(head=False)
def do_HEAD(self):
self._route(head=True)
def _send(self, status, content_type, body, head):
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
if not head:
self.wfile.write(body)
def _redirect(self, location):
self.send_response(HTTPStatus.FOUND)
self.send_header("Location", location)
self.send_header("Content-Length", "0")
self.end_headers()
def _page(self, status, title, body, head):
page = PAGE.format(title=html.escape(title), body=body)
self._send(status, "text/html; charset=utf-8", page.encode("utf-8"), head)
def _file(self, path, head):
self._send(HTTPStatus.OK, self.guess_type(str(path)), path.read_bytes(), head)
def _route(self, head):
path = unquote(urlsplit(self.path).path)
if path == "/":
self._redirect(f"/{LATEST}/")
elif path in {"/versions", "/versions/"}:
self._page(HTTPStatus.OK, "Documentation versions", OVERVIEW, head)
elif path == "/api/versions":
body = json.dumps(self.library.status()).encode("utf-8")
self._send(HTTPStatus.OK, "application/json", body, head)
elif path == "/_docs/versions.js":
self._file(SCRIPT, head)
elif path.startswith("/api/languages/"):
self._languages(path.removeprefix("/api/languages/"), head)
else:
self._version(path, head)
def _languages(self, version, head):
if version not in self.library.versions():
self.send_error(HTTPStatus.NOT_FOUND)
return
known, built = self.library.languages(version)
languages = [{"code": "en", "native": known.get("en", "English")}]
languages += [{"code": code, "native": known[code]} for code in built]
body = json.dumps({"version": version, "languages": languages}).encode("utf-8")
self._send(HTTPStatus.OK, "application/json", body, head)
def _version(self, path, head):
version, slash, rest = path.lstrip("/").partition("/")
if version not in self.library.versions():
self.send_error(HTTPStatus.NOT_FOUND)
elif not slash:
self._redirect(f"/{version}/")
elif not self.library.servable(version):
self.library.request(version)
body = BUILDING.format(version=html.escape(version))
self._page(HTTPStatus.ACCEPTED, f"Building {version}", body, head)
elif (code := rest.partition("/")[0]) in self.library.languages(version)[0]:
self._translation(version, code, path, head)
elif (target := self.library.resolve(version, rest)) is None:
self._redirect(f"/{version}/")
elif target.name == "index.html" and rest and not rest.endswith(("/", ".html")):
self._redirect(f"/{version}/{rest}/")
else:
self._file(target, head)
def _translation(self, version, code, path, head):
rest = path.lstrip("/").partition("/")[2].partition("/")[2]
if not self.library.translation_servable(version, code):
if not self.library.translates(version, code):
self.send_error(HTTPStatus.NOT_FOUND)
return
self.library.request(version, code)
body = BUILDING.format(version=html.escape(f"{version}/{code}"))
self._page(HTTPStatus.ACCEPTED, f"Building {version}/{code}", body, head)
elif (not rest and not path.endswith("/")) or (
target := self.library.resolve_translation(version, code, rest)
) is None:
self._redirect(f"/{version}/{code}/")
elif target.name == "index.html" and rest and not rest.endswith(("/", ".html")):
self._redirect(f"/{version}/{code}/{rest}/")
else:
self._file(target, head)
def main():
library = Library(
os.environ["DOCS_SOURCE_REPO"],
os.environ["DOCS_DATA_DIR"],
int(os.environ["DOCS_BUILD_JOBS"]),
PACKAGE_DIR,
os.environ["DOCS_SNAPSHOT_DIR"],
)
threading.Thread(
target=library.run_builder,
args=(int(os.environ["DOCS_FETCH_INTERVAL"]),),
daemon=True,
).start()
handler = partial(DocsHandler, library=library)
ThreadingHTTPServer(("", int(os.environ["DOCS_PORT"])), handler).serve_forever()
if __name__ == "__main__":
main()