""" api_service.py Thin wrapper around the Railway backend API. All HTTP calls to the remote backend go through here. """ import requests import time from flask import current_app # ── Cooldown tracking ──────────────────────────────────────── _plate_last_seen: dict[str, float] = {} import os def _base_url() -> str: try: from flask import current_app return current_app.config["BACKEND_API_URL"].rstrip("/") except: return os.getenv("BACKEND_API_URL", "https://pant0x-ain-el-aql-backend.hf.space").rstrip("/") def _headers(token: str | None = None) -> dict: h = {"Accept": "application/json"} hf_token = os.getenv("HF_TOKEN") # Hugging Face needs this to let the request through the Private Space proxy if hf_token: h["Authorization"] = f"Bearer {hf_token}" # Backend will read the user session from this new header if token: h["X-Supabase-Auth"] = f"Bearer {token}" return h def normalize_arabic_plate(text: str) -> str: """Converts Hindi digits to Western digits and handles formatting.""" if not text: return "" hindi_to_western = { '٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4', '٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9' } # Convert digits res = "" for char in text: res += hindi_to_western.get(char, char) if '|' in res: parts = res.split('|') letters = parts[0].strip() numbers = parts[1].strip().replace(" ", "") # Reverse numbers for correct LTR display in the browser return f"{letters} | {numbers[::-1]}" return res def extract_match_key(text: str) -> str: """ Extracts alphanumeric characters and sorts them (Letters then Numbers) to ensure 'SQR 777' and '777 SQR' generate the same key 'SQR777'. """ if not text: return "" text = text.replace(" ", "").replace("|", "").replace("-", "") import re letters = "".join(re.findall(r'[a-zA-Z\u0600-\u06FF]+', text)) numbers = "".join(re.findall(r'[0-9]+', text)) # Egyptian system usually prefers Letters then Numbers return f"{letters}{numbers}" # ── Health ─────────────────────────────────────────────────── def check_health() -> dict: try: r = requests.get(f"{_base_url()}/health", timeout=5) return r.json() except Exception as e: return {"status": "error", "detail": str(e)} def check_supabase_health() -> dict: try: r = requests.get(f"{_base_url()}/supabase/health", timeout=5) return r.json() except Exception as e: return {"status": "error", "detail": str(e)} # ── Auth ───────────────────────────────────────────────────── def login(identifier: str, password: str) -> dict: """POST /auth/login → returns user + session.""" try: r = requests.post( f"{_base_url()}/auth/login", json={"identifier": identifier, "password": password}, headers=_headers(), timeout=30, ) if r.status_code != 200: try: error_msg = r.json().get("detail", r.json().get("error", "Unknown error")) except: error_msg = f"Error {r.status_code}: {r.text[:100]}" return {"error": error_msg} return r.json() except Exception as e: return {"error": str(e)} def register(payload: dict) -> dict: """POST /auth/register""" try: r = requests.post( f"{_base_url()}/auth/register", json=payload, headers=_headers(), timeout=10, ) return r.json() except Exception as e: return {"error": str(e)} # ── Predict / OCR ──────────────────────────────────────────── def predict(image_bytes: bytes, filename: str, event_type: str = "entry", location: str = "OPERA", token: str | None = None) -> dict: """ POST /process-image (Railway) → send image for OCR. Then optionally call /gate/decision to get registration/gate status. """ try: # 1. Get OCR from the Main Engine # Backend expects 'image' field for the file, and 'camera_source' files = {"image": (filename, image_bytes, "image/jpeg")} data = { "camera_source": location, "parking_location": location, "event_type": event_type } ocr_resp = requests.post( f"{_base_url()}/predict", files=files, data=data, headers=_headers(token) if token else _headers(), timeout=30, ) if ocr_resp.status_code != 200: return {"error": f"Backend OCR failed: {ocr_resp.text[:100]}"} resp = ocr_resp.json() # 2. Map OCR results (Robust mapping) det = resp.get("detection") or resp urls = resp.get("urls") or resp.get("images") or {} # Try different possible plate field names including new backend format plate_info_dict = resp.get("plate_info", {}) plate_text = det.get("plate_text") or det.get("plate_ar") or det.get("plate") or plate_info_dict.get("arabic") or "" en_text = det.get("en_text") or det.get("plate_en") or plate_info_dict.get("english") or "" if plate_text or en_text: normalized_ar = normalize_arabic_plate(plate_text) match_key = extract_match_key(en_text) or extract_match_key(normalized_ar) # Use the registration info directly if the backend already provided it # Different backends might use 'app_registration' or top-level 'matched' reg_info = resp.get("app_registration") or resp.get("registration") or {} is_registered = False if isinstance(reg_info, dict): is_registered = reg_info.get("is_registered", resp.get("matched", False)) elif isinstance(reg_info, bool): is_registered = reg_info resp["matched"] = is_registered resp["plate_text_ar"] = normalized_ar resp["plate_info"] = { "plate_ar": normalized_ar, "plate_en": en_text, "plate_key": match_key, "confidence": det.get("confidence", det.get("score", 0)) } # Map Images admin_page_data = resp.get("admin_page", {}) if "admin_page" not in resp: resp["admin_page"] = {} # If backend returns base64, create a data URL if admin_page_data.get("annotated_image_base64"): b64 = admin_page_data["annotated_image_base64"] # Sometimes it might already have the prefix if b64.startswith("data:"): resp["admin_page"]["processed_image_url"] = b64 else: resp["admin_page"]["processed_image_url"] = f"data:image/jpeg;base64,{b64}" else: resp["admin_page"]["processed_image_url"] = urls.get("processed") or urls.get("annotated") or "" resp["admin_page"]["raw_image_url"] = urls.get("raw") or urls.get("original") or "" # 3. Call Gate Decision (Business Logic) if "gate_decision" not in resp: try: decision_payload = { "plate_key": resp["plate_info"]["plate_key"], "event_type": event_type, "parking_location": location } dr = requests.post( f"{_base_url()}/gate/decision", json=decision_payload, headers=_headers(token) if token else _headers(), timeout=10 ) if dr.status_code == 200: decision_data = dr.json() resp.update(decision_data) except Exception as e: resp["backend_warning"] = f"Gate decision failed: {str(e)}" return resp except Exception as e: return {"error": str(e)} def is_on_cooldown(plate_key: str) -> bool: """Returns True if plate was scanned within PLATE_COOLDOWN_SECONDS.""" cooldown = current_app.config["PLATE_COOLDOWN_SECONDS"] now = time.time() if plate_key in _plate_last_seen: if now - _plate_last_seen[plate_key] < cooldown: return True _plate_last_seen[plate_key] = now return False # ── Parking / Events (staff endpoints) ───────────────────── def get_entered_cars(token: str) -> dict: try: r = requests.get(f"{_base_url()}/events/entered-cars", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_inside_cars(token: str) -> dict: try: r = requests.get(f"{_base_url()}/events/inside-cars", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_left_cars(token: str) -> dict: try: r = requests.get(f"{_base_url()}/events/left-cars", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_leaving_soon(token: str) -> dict: try: r = requests.get(f"{_base_url()}/events/leaving-cars-within-5-minutes", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_new_cars(token: str) -> dict: """GET /events/new-cars — staff-only real-time polling feed.""" try: r = requests.get(f"{_base_url()}/events/new-cars", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_parking_history(token: str) -> dict: try: r = requests.get(f"{_base_url()}/parking/history", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_all_users(token: str) -> dict: try: r = requests.get(f"{_base_url()}/users", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_parking_locations(token: str) -> dict: try: r = requests.get(f"{_base_url()}/parking/locations", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_parking_occupancy(token: str) -> dict: try: r = requests.get(f"{_base_url()}/parking/occupancy", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} # ── Vehicles ───────────────────────────────────────────────── def get_my_vehicles(token: str) -> dict: try: r = requests.get(f"{_base_url()}/vehicles/my", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def add_vehicle(token: str, payload: dict) -> dict: try: r = requests.post(f"{_base_url()}/vehicles/add", json=payload, headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def edit_vehicle(token: str, vehicle_id: str, payload: dict) -> dict: try: r = requests.put(f"{_base_url()}/vehicles/{vehicle_id}", json=payload, headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def delete_vehicle(token: str, vehicle_id: str) -> dict: try: r = requests.delete(f"{_base_url()}/vehicles/{vehicle_id}", headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} # ── Profile ────────────────────────────────────────────────── def update_profile(token: str, payload: dict) -> dict: try: r = requests.put(f"{_base_url()}/profile/update", json=payload, headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} # ── Payments ───────────────────────────────────────────────── def confirm_cash_payment(token: str, payload: dict) -> dict: """POST /payments/manual/cash-confirm — staff-only.""" try: r = requests.post(f"{_base_url()}/payments/manual/cash-confirm", json=payload, headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} def get_gate_decision(token: str, payload: dict) -> dict: try: r = requests.post(f"{_base_url()}/gate/decision", json=payload, headers=_headers(token), timeout=30) return r.json() except Exception as e: return {"error": str(e)} # ── Gate hardware (ESP) ────────────────────────────────────── def trigger_gate_open(plate: str = "", location: str = "OPERA") -> dict: """Queue a gate-open command on the cloud backend for ESP32 devices to poll.""" try: r = requests.post( f"{_base_url()}/gate/trigger", json={ "parking_location": location, "duration_seconds": 10, "plate": plate, }, timeout=5, ) return {"status": "sent", "backend_response": r.json()} except Exception as e: return {"status": "error", "detail": str(e)} # ── Admin maintenance ──────────────────────────────────────── def weekly_refresh(token: str) -> dict: try: r = requests.post(f"{_base_url()}/admin/maintenance/weekly-refresh", headers=_headers(token), timeout=15) return r.json() except Exception as e: return {"error": str(e)}