Spaces:
Sleeping
Sleeping
| """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() | |