Spaces:
Runtime error
Runtime error
Download server.py from KassimYassin/palate: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/KassimYassin/palate/resolve/main/server.py
- Command line
-
hf download hf://spaces/KassimYassin/palate/server.py
-
curl -L -o server.py https://huggingface.co/spaces/KassimYassin/palate/resolve/main/server.py
15.8 kB
| """ | |
| Lightweight, Zero-Dependency HTTP API & Static Server for Palate / TasteOS Global Multi-City Engine. | |
| Supports Amsterdam, Dubai, Istanbul with zero paid Google APIs. | |
| Runs on localhost:3050. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import urllib.parse | |
| from http.server import HTTPServer, SimpleHTTPRequestHandler, ThreadingHTTPServer | |
| from engine import MultiCityEngineManager | |
| from spatial_copilot_service import copilot_service | |
| # Ensure UTF-8 output | |
| if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| PORT = int(os.environ.get("PORT", 3050)) | |
| PUBLIC_DIR = os.path.join(os.path.dirname(__file__), "public") | |
| manager = MultiCityEngineManager() | |
| class PalateRequestHandler(SimpleHTTPRequestHandler): | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, directory=PUBLIC_DIR, **kwargs) | |
| def do_GET(self): | |
| parsed = urllib.parse.urlparse(self.path) | |
| path = parsed.path | |
| query = urllib.parse.parse_qs(parsed.query) | |
| # 0. API: List Available Global Cities | |
| if path == "/api/cities": | |
| cities = manager.list_cities() | |
| self.send_json(cities) | |
| return | |
| # Extract city context (defaults to Amsterdam for backward compatibility) | |
| city_id = query.get("city", ["amsterdam"])[0].lower().strip() | |
| engine = manager.get_engine(city_id) | |
| default_center = engine.city_config.get("center", {"lat": 52.3680, "lon": 4.8900}) | |
| # 1. API: Search with Rich Dietary & Amenity Filters | |
| if path == "/api/search": | |
| q = query.get("q", [""])[0] | |
| has_user_coords = "lat" in query and "lon" in query | |
| lat = float(query.get("lat", [default_center["lat"]])[0]) | |
| lon = float(query.get("lon", [default_center["lon"]])[0]) | |
| offset = int(query.get("offset", [0])[0]) | |
| limit = int(query.get("limit", [40])[0]) | |
| cuisine = query.get("cuisine", [""])[0] | |
| diet = query.get("diet", [""])[0] | |
| outdoor = query.get("outdoor", ["false"])[0].lower() == "true" | |
| canal = query.get("canal", ["false"])[0].lower() == "true" | |
| wheelchair = query.get("wheelchair", ["false"])[0].lower() == "true" | |
| accredited = query.get("accredited", ["false"])[0].lower() == "true" | |
| monument = query.get("monument", ["false"])[0].lower() == "true" | |
| independent = query.get("independent", ["false"])[0].lower() == "true" | |
| celiac = query.get("celiac", ["false"])[0].lower() == "true" | |
| if diet and diet.lower().strip() in ("celiac", "celiac_safe"): | |
| celiac = True | |
| diet = "" | |
| scratch = query.get("scratch", ["false"])[0].lower() == "true" | |
| water = query.get("water", ["false"])[0].lower() == "true" | |
| clean = query.get("clean", ["false"])[0].lower() == "true" | |
| late_night = query.get("late_night", ["false"])[0].lower() == "true" | |
| roastery = query.get("roastery", ["false"])[0].lower() == "true" | |
| historic = query.get("historic", ["false"])[0].lower() == "true" | |
| price_raw = query.get("price", [""])[0] | |
| price_tiers = None | |
| if price_raw: | |
| try: | |
| price_tiers = [int(p.strip()) for p in price_raw.split(",") if p.strip().isdigit()] | |
| if not price_tiers: | |
| price_tiers = None | |
| except Exception: | |
| price_tiers = None | |
| min_score = int(query.get("min_score", [0])[0]) | |
| sort_by = query.get("sort", ["quality"])[0] | |
| # Bounding box filter (min_lat, min_lon, max_lat, max_lon) | |
| bbox_raw = query.get("bbox", [""])[0] | |
| bbox = None | |
| if bbox_raw: | |
| try: | |
| parts = [float(x.strip()) for x in bbox_raw.split(",") if x.strip()] | |
| if len(parts) == 4: | |
| bbox = parts | |
| except Exception: | |
| bbox = None | |
| # Walking / Transit distance radius filter in meters | |
| max_dist_raw = query.get("max_dist", [""])[0] | |
| max_dist = None | |
| if max_dist_raw: | |
| try: | |
| max_dist = int(max_dist_raw.strip()) | |
| except Exception: | |
| max_dist = None | |
| results, total_count = engine.search( | |
| query=q, | |
| user_lat=lat, | |
| user_lon=lon, | |
| offset=offset, | |
| max_results=limit, | |
| cuisine=cuisine if cuisine else None, | |
| diet=diet if diet else None, | |
| outdoor=outdoor, | |
| canal=canal, | |
| wheelchair=wheelchair, | |
| accredited_only=accredited, | |
| monuments_only=monument, | |
| independent_only=independent, | |
| celiac_safe_only=celiac, | |
| scratch_only=scratch, | |
| tap_water_only=water, | |
| clean_only=clean, | |
| late_night_only=late_night, | |
| roastery_only=roastery, | |
| historic_only=historic, | |
| price_tiers=price_tiers, | |
| min_score=min_score, | |
| sort_by=sort_by, | |
| has_user_coords=has_user_coords, | |
| return_total=True, | |
| bbox=bbox, | |
| max_dist_m=max_dist | |
| ) | |
| include_facets = query.get("facets", ["false"])[0].lower() in ["true", "1"] | |
| headers = { | |
| "X-Total-Count": str(total_count), | |
| "X-Offset": str(offset), | |
| "X-Limit": str(limit), | |
| "Access-Control-Expose-Headers": "X-Total-Count, X-Offset, X-Limit" | |
| } | |
| if include_facets: | |
| active_filters = { | |
| "price": price_tiers, | |
| "cuisine": cuisine if cuisine else None, | |
| "diet": diet if diet else None, | |
| "outdoor": outdoor, | |
| "canal": canal, | |
| "wheelchair": wheelchair, | |
| "accredited": accredited, | |
| "monument": monument, | |
| "independent": independent, | |
| "celiac": celiac, | |
| "scratch": scratch, | |
| "water": water, | |
| "clean": clean, | |
| "late_night": late_night, | |
| "roastery": roastery, | |
| "historic": historic, | |
| "bbox": bbox, | |
| "max_dist": max_dist, | |
| "user_lat": lat if has_user_coords else None, | |
| "user_lon": lon if has_user_coords else None | |
| } | |
| taxonomy = engine.get_city_taxonomy(city_id, active_filters=active_filters) | |
| self.send_json({ | |
| "places": results, | |
| "total": total_count, | |
| "taxonomy": taxonomy | |
| }, headers=headers) | |
| return | |
| else: | |
| self.send_json(results, headers=headers) | |
| return | |
| # 2. API: Head-to-Head Compare | |
| elif path == "/api/compare": | |
| id1 = query.get("id1", [""])[0] | |
| id2 = query.get("id2", [""])[0] | |
| comparison = engine.compare_head_to_head(id1, id2) | |
| if comparison: | |
| self.send_json(comparison) | |
| else: | |
| self.send_json({"error": "Places not found for comparison"}, status=404) | |
| return | |
| # 3. API: Dynamic City Taxonomy Engine | |
| elif path == "/api/taxonomy": | |
| price_raw = query.get("price", [""])[0] | |
| price_tier = None | |
| if price_raw and price_raw.strip().isdigit(): | |
| price_tier = int(price_raw.strip()) | |
| cuisine = query.get("cuisine", [""])[0] | |
| diet = query.get("diet", [""])[0] | |
| outdoor = query.get("outdoor", ["false"])[0].lower() == "true" | |
| canal = query.get("canal", ["false"])[0].lower() == "true" | |
| wheelchair = query.get("wheelchair", ["false"])[0].lower() == "true" | |
| accredited = query.get("accredited", ["false"])[0].lower() == "true" | |
| monument = query.get("monument", ["false"])[0].lower() == "true" | |
| independent = query.get("independent", ["false"])[0].lower() == "true" | |
| celiac = query.get("celiac", ["false"])[0].lower() == "true" | |
| if diet and diet.lower().strip() in ("celiac", "celiac_safe"): | |
| celiac = True | |
| diet = "" | |
| scratch = query.get("scratch", ["false"])[0].lower() == "true" | |
| water = query.get("water", ["false"])[0].lower() == "true" | |
| clean = query.get("clean", ["false"])[0].lower() == "true" | |
| late_night = query.get("late_night", ["false"])[0].lower() == "true" | |
| roastery = query.get("roastery", ["false"])[0].lower() == "true" | |
| historic = query.get("historic", ["false"])[0].lower() == "true" | |
| has_filters = any([ | |
| price_tier is not None, cuisine, diet, outdoor, canal, wheelchair, | |
| accredited, monument, independent, celiac, scratch, water, clean, | |
| late_night, roastery, historic | |
| ]) | |
| active_filters = None | |
| if has_filters: | |
| active_filters = { | |
| "price": price_tier, | |
| "cuisine": cuisine if cuisine else None, | |
| "diet": diet if diet else None, | |
| "outdoor": outdoor, | |
| "canal": canal, | |
| "wheelchair": wheelchair, | |
| "accredited": accredited, | |
| "monument": monument, | |
| "independent": independent, | |
| "celiac": celiac, | |
| "scratch": scratch, | |
| "water": water, | |
| "clean": clean, | |
| "late_night": late_night, | |
| "roastery": roastery, | |
| "historic": historic | |
| } | |
| taxonomy = engine.get_city_taxonomy(city_id, active_filters=active_filters) | |
| self.send_json(taxonomy) | |
| return | |
| # 4. API: Facets & Aggregations | |
| elif path == "/api/facets": | |
| facets = engine.get_facets(authentic_only=True) | |
| self.send_json(facets) | |
| return | |
| # 5. API: Stats & System Metrics | |
| elif path == "/api/stats": | |
| facets = engine.get_facets() | |
| self.send_json({ | |
| "city": engine.city_config.get("name", city_id.capitalize()), | |
| "city_id": city_id, | |
| "country": engine.city_config.get("country", ""), | |
| "flag": engine.city_config.get("flag", ""), | |
| "total_eateries_indexed": facets["total_eateries"], | |
| "craft_artisans_detected": facets["craft_artisans_detected"], | |
| "tourist_traps_flagged": facets["tourist_traps_flagged"], | |
| "mono_specialists": facets["mono_specialists"], | |
| "halal_eateries": facets["halal_count"], | |
| "vegan_eateries": facets["vegan_count"], | |
| "canal_terraces": facets["canal_terrace_count"], | |
| "protected_monuments": facets["monuments_count"], | |
| "independent_artisans": facets["independent_count"], | |
| "celiac_safe_kitchens": facets["celiac_safe_count"], | |
| "scratch_kitchens": facets["scratch_count"], | |
| "tap_water_welcoming": facets["tap_water_count"], | |
| "clean_kitchens_verified": facets.get("clean_kitchen_count", 0), | |
| "data_source": "Overture Maps, OpenStreetMap & Government Registries", | |
| "google_api_cost": "$0.00" | |
| }) | |
| return | |
| # 6. API: Spatial God Mode AI Copilot | |
| elif path == "/api/copilot/ask": | |
| q = query.get("q", [""])[0] | |
| city_id = query.get("city", ["dubai"])[0].lower().strip() | |
| if not q: | |
| self.send_json({"error": "Query parameter 'q' is required"}, status=400) | |
| return | |
| result = copilot_service.ask(q, city=city_id) | |
| self.send_json(result) | |
| return | |
| # Serve static assets (index.html, etc.) | |
| return super().do_GET() | |
| def do_POST(self): | |
| parsed = urllib.parse.urlparse(self.path) | |
| if parsed.path == "/api/copilot/ask": | |
| content_length = int(self.headers.get("Content-Length", 0)) | |
| body = self.rfile.read(content_length).decode("utf-8") | |
| try: | |
| data = json.loads(body) | |
| q = data.get("q", "") | |
| city_id = data.get("city", "dubai").lower().strip() | |
| if not q: | |
| self.send_json({"error": "Field 'q' is required"}, status=400) | |
| return | |
| result = copilot_service.ask(q, city=city_id) | |
| self.send_json(result) | |
| except Exception as e: | |
| self.send_json({"error": str(e)}, status=400) | |
| return | |
| elif parsed.path == "/api/endorse": | |
| content_length = int(self.headers.get("Content-Length", 0)) | |
| body = self.rfile.read(content_length).decode("utf-8") | |
| try: | |
| data = json.loads(body) | |
| place_id = data.get("place_id") | |
| positive = data.get("positive", True) | |
| city_id = data.get("city", "amsterdam").lower().strip() | |
| engine = manager.get_engine(city_id) | |
| # Find place and record endorsement | |
| p = next((x for x in engine.places if x["id"] == place_id), None) | |
| if p: | |
| p["endorsement_count"] = p.get("endorsement_count", 0) + 1 | |
| pos_count = (p.get("endorsement_positive_pct", 85) / 100.0) * (p["endorsement_count"] - 1) | |
| if positive: | |
| pos_count += 1 | |
| p["endorsement_positive_pct"] = int((pos_count / p["endorsement_count"]) * 100) | |
| self.send_json({"success": True, "place": p}) | |
| else: | |
| self.send_json({"error": "Place not found"}, status=404) | |
| except Exception as e: | |
| self.send_json({"error": str(e)}, status=400) | |
| return | |
| self.send_json({"error": "Not found"}, status=404) | |
| def send_json(self, data, status=200, headers=None): | |
| body = json.dumps(data, ensure_ascii=False, default=str).encode("utf-8") | |
| self.send_response(status) | |
| self.send_header("Content-Type", "application/json; charset=utf-8") | |
| self.send_header("Access-Control-Allow-Origin", "*") | |
| if headers: | |
| for k, v in headers.items(): | |
| self.send_header(k, v) | |
| self.send_header("Content-Length", str(len(body))) | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def run(port=PORT): | |
| server_address = ("", port) | |
| httpd = ThreadingHTTPServer(server_address, PalateRequestHandler) | |
| print(f"==================================================") | |
| print(f" Palate / TasteOS Global Engine is RUNNING on {port}!") | |
| for c in manager.cities: | |
| print(f" - {c.get('flag', '🌐')} {c['name']}") | |
| print(f" Google API Cost: $0.00 ") | |
| print(f"==================================================") | |
| try: | |
| httpd.serve_forever() | |
| except KeyboardInterrupt: | |
| print("\nServer stopped.") | |
| if __name__ == "__main__": | |
| run() | |