"""Production entrypoint: build the map, serve it, refresh it twice a day. Runs as the single process inside a container (e.g. a Hugging Face Docker Space). It builds the map immediately on startup, serves the output/ directory over HTTP on $PORT (default 7860), and rebuilds at 07:00 and 19:00 in a background thread so the served prices stay fresh. """ from __future__ import annotations import gzip import http.server import os import socketserver import sys import threading import time import traceback from datetime import datetime import schedule from build_map import OUTPUT_DIR, generate PORT = int(os.environ.get("PORT", "7860")) REFRESH_TIMES = ["07:00", "19:00"] # twice a day, container local time # Content types worth gzipping (text-ish). Images/PNG are already compressed. COMPRESSIBLE = { "text/html", "text/css", "text/javascript", "text/plain", "application/javascript", "application/json", "application/manifest+json", "application/xml", "image/svg+xml", } def log(msg: str) -> None: """Timestamped line, flushed immediately so it shows up in container logs.""" print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {msg}", flush=True) def safe_generate() -> None: """Rebuild the map, logging failures instead of crashing the loop.""" log("Refreshing fuel data and rebuilding the map…") try: generate() log("Rebuild complete.") except Exception: log("Map generation FAILED:") traceback.print_exc() sys.stdout.flush() class Handler(http.server.SimpleHTTPRequestHandler): """Serve output/ with correct MIME types and no-cache on the live files.""" extensions_map = { **http.server.SimpleHTTPRequestHandler.extensions_map, ".webmanifest": "application/manifest+json", ".js": "text/javascript", ".json": "application/json", ".xml": "application/xml", ".txt": "text/plain", } def __init__(self, *args, **kwargs): super().__init__(*args, directory=OUTPUT_DIR, **kwargs) def end_headers(self): path = self.path.split("?")[0] if path in ("/", "/index.html", "/sw.js"): self.send_header("Cache-Control", "no-cache") # always revalidate elif path.endswith((".png", ".ico", ".webmanifest", ".svg")): self.send_header("Cache-Control", "public, max-age=86400") # rarely change super().end_headers() def do_GET(self): """Serve files with gzip for text assets (big win for the embedded data).""" path = self.translate_path(self.path) if os.path.isdir(path): path = os.path.join(path, "index.html") if not os.path.isfile(path): return super().do_GET() # let the base class produce a 404 try: with open(path, "rb") as fh: body = fh.read() except OSError: return super().do_GET() ctype = self.guess_type(path) base_ct = ctype.split(";")[0].strip() if base_ct in COMPRESSIBLE and "charset" not in ctype: ctype = base_ct + "; charset=utf-8" encoding = None if ("gzip" in self.headers.get("Accept-Encoding", "") and base_ct in COMPRESSIBLE and len(body) > 1024): body = gzip.compress(body, 6) encoding = "gzip" self.send_response(200) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) if encoding: self.send_header("Content-Encoding", encoding) self.send_header("Vary", "Accept-Encoding") self.end_headers() # adds the Cache-Control policy above self.wfile.write(body) def log_message(self, fmt, *args): # verbose, timestamped request logging log(f"{self.address_string()} {fmt % args}") def handle(self): try: super().handle() except (BrokenPipeError, ConnectionResetError): pass # client disconnected mid-response; nothing to do class Server(socketserver.ThreadingTCPServer): allow_reuse_address = True daemon_threads = True def scheduler_loop() -> None: for when in REFRESH_TIMES: schedule.every().day.at(when).do(safe_generate) while True: schedule.run_pending() time.sleep(30) def main() -> None: log(f"Starting petrol-map on port {PORT} (TZ={os.environ.get('TZ', 'system')})") safe_generate() # ensure the map exists before we start serving threading.Thread(target=scheduler_loop, daemon=True).start() with Server(("0.0.0.0", PORT), Handler) as httpd: log(f"Serving on http://0.0.0.0:{PORT} (auto-refresh at {', '.join(REFRESH_TIMES)})") httpd.serve_forever() if __name__ == "__main__": main()