EALPR_FLASK / services /api_service.py
Pant0x's picture
Update API URLs to point to Hugging Face Backend and support HF_TOKEN
23e2bb0
Raw
History Blame Contribute Delete
15.3 kB
"""
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)}