Download dev/server.py from klein-ml/blog: direct link, hf CLI and curl.
- Browser
- Download file 4.72 kB
-
https://huggingface.co/spaces/klein-ml/blog/resolve/main/dev/server.py
- Command line
-
hf download hf://spaces/klein-ml/blog/dev/server.py
-
curl -L -o server.py https://huggingface.co/spaces/klein-ml/blog/resolve/main/dev/server.py
4.72 kB
| #!/usr/bin/env python3 | |
| """Local dev server for the Klein ML blog, with an in-browser editor for posts. | |
| python3 dev/server.py # http://127.0.0.1:8000 | |
| python3 dev/server.py --port 8001 # another port | |
| Serves the site like `python3 -m http.server`. HTML pages get dev/editor.js added on the way out, so the | |
| files on disk stay clean. Open a post, click "Edit page", change the title, byline or article text, then | |
| "Save" (or Ctrl+S). Saving rewrites the title/byline and article sections of that .html file. | |
| Localhost only. Nothing here is used by the published Hugging Face Space. | |
| """ | |
| import argparse | |
| import json | |
| import re | |
| from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer | |
| from pathlib import Path | |
| HERE = Path(__file__).resolve().parent | |
| EDITOR = HERE / "editor.js" | |
| INJECT = b'<script src="/__dev/editor.js" defer></script>\n</body>' | |
| # Regions the editor may rewrite. Everything else in the file is left as it is. | |
| BAND_RE = re.compile(r'(<div class="band">)(.*?)(</div>\s*<figure)', re.S) | |
| ARTICLE_RE = re.compile(r'(<article class="article">)(.*?)(</article>)', re.S) | |
| class Handler(SimpleHTTPRequestHandler): | |
| root = HERE.parent # replaced in main() | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, directory=str(Handler.root), **kwargs) | |
| def _send(self, code, body, ctype): | |
| self.send_response(code) | |
| self.send_header("Content-Type", ctype) | |
| self.send_header("Content-Length", str(len(body))) | |
| self.send_header("Cache-Control", "no-store") | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def _json(self, code, obj): | |
| self._send(code, json.dumps(obj).encode("utf-8"), "application/json; charset=utf-8") | |
| def do_GET(self): | |
| path = self.path.split("?", 1)[0] | |
| if path == "/__dev/editor.js": | |
| return self._send(200, EDITOR.read_bytes(), "text/javascript; charset=utf-8") | |
| target = Path(self.translate_path(path)) | |
| if target.is_dir(): | |
| target = target / "index.html" | |
| if target.suffix == ".html" and target.is_file(): | |
| html = target.read_bytes() | |
| html = html.replace(b"</body>", INJECT, 1) if b"</body>" in html else html + INJECT | |
| return self._send(200, html, "text/html; charset=utf-8") | |
| return super().do_GET() | |
| def do_POST(self): | |
| if self.path != "/__dev/save": | |
| return self._json(404, {"error": "not found"}) | |
| # A custom header and a JSON body make the browser check cross-site requests first, | |
| # so other sites can't write to your files through this server. | |
| if self.headers.get("X-Dev-Editor") != "1" or "application/json" not in (self.headers.get("Content-Type") or ""): | |
| return self._json(400, {"error": "bad request"}) | |
| try: | |
| size = int(self.headers.get("Content-Length", "0")) | |
| data = json.loads(self.rfile.read(size) or b"{}") | |
| root = Handler.root.resolve() | |
| page = (root / str(data["page"]).lstrip("/")).resolve() | |
| if not page.is_relative_to(root) or page.suffix != ".html" or not page.is_file(): | |
| raise ValueError("not a page in this site") | |
| text = page.read_text(encoding="utf-8") | |
| if "band" in data: | |
| text, n = BAND_RE.subn(lambda m: m.group(1) + "\n " + str(data["band"]).strip() + "\n" + m.group(3), text, count=1) | |
| if n == 0: | |
| raise ValueError("this page has no title block to save into") | |
| if "article" in data: | |
| text, n = ARTICLE_RE.subn(lambda m: m.group(1) + "\n" + str(data["article"]).strip() + "\n " + m.group(3), text, count=1) | |
| if n == 0: | |
| raise ValueError("this page has no article to save into") | |
| page.write_text(text, encoding="utf-8") | |
| return self._json(200, {"ok": True, "page": str(page.relative_to(root))}) | |
| except (KeyError, ValueError, OSError, json.JSONDecodeError) as e: | |
| return self._json(400, {"error": str(e)}) | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("--port", type=int, default=8000) | |
| ap.add_argument("--root", default=str(HERE.parent), help="site folder (default: the repo)") | |
| args = ap.parse_args() | |
| Handler.root = Path(args.root).resolve() | |
| srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler) | |
| print(f"Serving {Handler.root} at http://127.0.0.1:{args.port}") | |
| print("Open a post and click 'Edit page'. Ctrl+S or 'Save' writes it back to the .html file.") | |
| srv.serve_forever() | |
| if __name__ == "__main__": | |
| main() | |