prix-carburant / app.py
SamDNX's picture
Quality pass: CI, accessibility, and gzip/perf
590c919
Raw
History Blame Contribute Delete
4.79 kB
"""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()