Spaces:
Sleeping
Sleeping
File size: 4,791 Bytes
284e818 590c919 284e818 71c08ae 284e818 71c08ae 284e818 590c919 284e818 71c08ae 284e818 71c08ae 284e818 71c08ae 284e818 71c08ae 284e818 71c08ae 284e818 57ffa0b 284e818 590c919 284e818 590c919 71c08ae 284e818 71c08ae 284e818 71c08ae 284e818 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """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()
|