Spaces:
Running on Zero
Running on Zero
| import os | |
| import re | |
| import logging | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Any | |
| from datetime import timezone | |
| from urllib.parse import urlparse | |
| BASE_DIR = Path(__file__).resolve().parent.parent.parent | |
| MODELS_DIR = BASE_DIR / "Models" | |
| MODEL_PATHS = { | |
| "plate": MODELS_DIR / "car_plate_best.pt", | |
| "ocr": MODELS_DIR / "model_ocr.pt", | |
| } | |
| LOGGER = logging.getLogger("ain_el_aql") | |
| if not LOGGER.handlers: | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") | |
| TZ_UTC = timezone.utc | |
| def _bool_env(var_name: str, default: bool = False) -> bool: | |
| raw = os.getenv(var_name) | |
| if raw is None: | |
| return default | |
| return raw.strip().lower() in {"1", "true", "yes", "on"} | |
| def _float_env(var_name: str, default: float) -> float: | |
| raw = os.getenv(var_name) | |
| if raw is None: | |
| return default | |
| try: | |
| return float(raw.strip()) | |
| except ValueError: | |
| LOGGER.warning("Invalid float env for %s=%r. Using default %s", var_name, raw, default) | |
| return default | |
| def _int_env(var_name: str, default: int) -> int: | |
| raw = os.getenv(var_name) | |
| if raw is None: | |
| return default | |
| try: | |
| return int(raw.strip()) | |
| except ValueError: | |
| LOGGER.warning("Invalid int env for %s=%r. Using default %s", var_name, raw, default) | |
| return default | |
| SUPABASE_URL = os.getenv("SUPABASE_URL", "").strip().rstrip("/") | |
| SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "").strip() | |
| SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "").strip() | |
| SUPABASE_ENFORCE_AUTH = _bool_env("SUPABASE_ENFORCE_AUTH", default=False) | |
| SUPABASE_RAW_BUCKET = os.getenv("SUPABASE_RAW_BUCKET", "car-raw-images").strip() | |
| SUPABASE_PROCESSED_BUCKET = os.getenv("SUPABASE_PROCESSED_BUCKET", "car-processed-images").strip() | |
| SUPABASE_TIMEOUT_SECONDS = float(os.getenv("SUPABASE_TIMEOUT_SECONDS", "20")) | |
| BARRIER_API_TOKEN = os.getenv("BARRIER_API_TOKEN", "").strip() | |
| _raw_barrier_tokens = [BARRIER_API_TOKEN] | |
| _raw_barrier_tokens.extend(part.strip() for part in os.getenv("BARRIER_API_TOKENS", "").split(",")) | |
| BARRIER_API_TOKENS = {token for token in _raw_barrier_tokens if token} | |
| del _raw_barrier_tokens | |
| BARRIER_SYNTHETIC_USER_ID = os.getenv("BARRIER_SYNTHETIC_USER_ID", "barrier-device").strip() or "barrier-device" | |
| PASSWORD_RESET_REDIRECT_URL = os.getenv("PASSWORD_RESET_REDIRECT_URL", "").strip() | |
| INTERNAL_LOGIN_EMAIL_SUFFIX = "@ainel-aql.local" | |
| PARKING_HOURLY_RATE_EGP = _float_env("PARKING_HOURLY_RATE_EGP", 30.0) | |
| def _normalize_location_key_for_config_internal(value: Optional[str]) -> Optional[str]: | |
| if value is None: | |
| return None | |
| raw = str(value).strip().upper() | |
| if not raw: | |
| return None | |
| return "".join(raw.split()) or None | |
| def _parse_location_hourly_rates(raw: str) -> Dict[str, float]: | |
| result: Dict[str, float] = {} | |
| if not raw or not raw.strip(): | |
| return result | |
| for item in raw.split(","): | |
| part = item.strip() | |
| if not part: | |
| continue | |
| location_part, separator, rate_part = part.partition(":") | |
| if not separator: | |
| LOGGER.warning("Ignoring invalid PARKING_LOCATION_HOURLY_RATES item '%s'. Expected format LOCATION:RATE", part) | |
| continue | |
| location_key = _normalize_location_key_for_config_internal(location_part) | |
| if not location_key: | |
| LOGGER.warning("Ignoring empty parking location key in PARKING_LOCATION_HOURLY_RATES item '%s'", part) | |
| continue | |
| try: | |
| rate_value = float(rate_part.strip()) | |
| except ValueError: | |
| LOGGER.warning("Ignoring invalid rate in PARKING_LOCATION_HOURLY_RATES item '%s'", part) | |
| continue | |
| if rate_value < 0: | |
| LOGGER.warning("Ignoring negative rate in PARKING_LOCATION_HOURLY_RATES item '%s'", part) | |
| continue | |
| result[location_key] = rate_value | |
| return result | |
| def _parse_location_int_values(raw: str, *, env_name: str) -> Dict[str, int]: | |
| result: Dict[str, int] = {} | |
| if not raw or not raw.strip(): | |
| return result | |
| for item in raw.split(","): | |
| part = item.strip() | |
| if not part: | |
| continue | |
| location_part, separator, value_part = part.partition(":") | |
| if not separator: | |
| LOGGER.warning("Ignoring invalid %s item '%s'. Expected format LOCATION:VALUE", env_name, part) | |
| continue | |
| location_key = _normalize_location_key_for_config_internal(location_part) | |
| if not location_key: | |
| LOGGER.warning("Ignoring empty location key in %s item '%s'", env_name, part) | |
| continue | |
| try: | |
| parsed_value = int(value_part.strip()) | |
| except ValueError: | |
| LOGGER.warning("Ignoring invalid int value in %s item '%s'", env_name, part) | |
| continue | |
| if parsed_value < 0: | |
| LOGGER.warning("Ignoring negative value in %s item '%s'", env_name, part) | |
| continue | |
| result[location_key] = parsed_value | |
| return result | |
| def _parse_billing_mode(raw: Optional[str], *, default: str = "hourly", env_name: str = "") -> str: | |
| mode = str(raw or "").strip().lower() | |
| if mode in {"hourly", "daily"}: | |
| return mode | |
| if mode: | |
| label = env_name or "billing_mode" | |
| LOGGER.warning("Invalid %s=%r. Falling back to %s", label, raw, default) | |
| return default | |
| def _parse_location_pricing_configs(raw: str) -> Dict[str, Dict[str, Any]]: | |
| result: Dict[str, Dict[str, Any]] = {} | |
| if not raw or not raw.strip(): | |
| return result | |
| for item in raw.split(","): | |
| part = item.strip() | |
| if not part: | |
| continue | |
| pieces = [piece.strip() for piece in part.split(":")] | |
| if len(pieces) != 5: | |
| LOGGER.warning("Ignoring invalid PARKING_LOCATION_PRICING item '%s'. Expected LOCATION:MODE:BASE:EXTRA_AFTER_FIRST_HOUR:CAPACITY", part) | |
| continue | |
| location_key = _normalize_location_key_for_config_internal(pieces[0]) | |
| if not location_key: | |
| LOGGER.warning("Ignoring empty location key in PARKING_LOCATION_PRICING item '%s'", part) | |
| continue | |
| billing_mode = _parse_billing_mode(pieces[1], default="hourly", env_name="PARKING_LOCATION_PRICING.mode") | |
| try: | |
| base_rate_egp = float(pieces[2]) | |
| extra_after_first_hour_egp = float(pieces[3]) | |
| capacity = int(pieces[4]) | |
| except ValueError: | |
| LOGGER.warning("Ignoring invalid numeric values in PARKING_LOCATION_PRICING item '%s'", part) | |
| continue | |
| if base_rate_egp < 0 or extra_after_first_hour_egp < 0 or capacity < 0: | |
| LOGGER.warning("Ignoring negative values in PARKING_LOCATION_PRICING item '%s'", part) | |
| continue | |
| result[location_key] = { | |
| "billing_mode": billing_mode, | |
| "base_rate_egp": base_rate_egp, | |
| "extra_after_first_hour_egp": extra_after_first_hour_egp, | |
| "capacity": capacity, | |
| } | |
| return result | |
| PARKING_LOCATION_HOURLY_RATES = _parse_location_hourly_rates(os.getenv("PARKING_LOCATION_HOURLY_RATES", "")) | |
| PARKING_BILLING_MODE = _parse_billing_mode(os.getenv("PARKING_BILLING_MODE", "hourly"), default="hourly", env_name="PARKING_BILLING_MODE") | |
| PARKING_DAILY_RATE_EGP = _float_env("PARKING_DAILY_RATE_EGP", 200.0) | |
| PARKING_EXTRA_AFTER_FIRST_HOUR_EGP = _float_env("PARKING_EXTRA_AFTER_FIRST_HOUR_EGP", PARKING_HOURLY_RATE_EGP) | |
| GARAGE_TOTAL_CAPACITY = max(0, _int_env("GARAGE_TOTAL_CAPACITY", 1000)) | |
| PARKING_LOCATION_CAPACITIES = _parse_location_int_values(os.getenv("PARKING_LOCATION_CAPACITIES", ""), env_name="PARKING_LOCATION_CAPACITIES") | |
| PARKING_LOCATION_PRICING = _parse_location_pricing_configs(os.getenv("PARKING_LOCATION_PRICING", "")) | |
| APP_SERVICE_FEE_EGP = _float_env("APP_SERVICE_FEE_EGP", 0.0) | |
| CORS_ALLOW_ORIGINS = [origin.strip() for origin in os.getenv("CORS_ALLOW_ORIGINS", "*").split(",") if origin.strip()] | |
| def _parse_csv_env_values(raw: str) -> List[str]: | |
| if not raw or not raw.strip(): | |
| return [] | |
| values: List[str] = [] | |
| for part in raw.split(","): | |
| normalized = part.strip().lower() | |
| if normalized: | |
| values.append(normalized) | |
| return values | |
| REDIRECT_ALLOWED_HTTP_HOSTS = set(_parse_csv_env_values(os.getenv("ALLOWED_REDIRECT_HOSTS", ""))) | |
| _configured_password_reset_redirect_host = (urlparse(PASSWORD_RESET_REDIRECT_URL).hostname or "").strip().lower() | |
| if _configured_password_reset_redirect_host: | |
| REDIRECT_ALLOWED_HTTP_HOSTS.add(_configured_password_reset_redirect_host) | |
| del _configured_password_reset_redirect_host | |
| REDIRECT_ALLOWED_DEEPLINK_SCHEMES = set( | |
| _parse_csv_env_values(os.getenv("ALLOWED_REDIRECT_DEEPLINK_SCHEMES", "ainelaql")) | |
| ) | |
| if not REDIRECT_ALLOWED_DEEPLINK_SCHEMES: | |
| REDIRECT_ALLOWED_DEEPLINK_SCHEMES = {"ainelaql"} | |
| PAYMOB_BASE_URL = os.getenv("PAYMOB_BASE_URL", "https://accept.paymob.com/api").strip().rstrip("/") | |
| PAYMOB_API_KEY = os.getenv("PAYMOB_API_KEY", "").strip() | |
| PAYMOB_INTEGRATION_ID = os.getenv("PAYMOB_INTEGRATION_ID", "").strip() | |
| PAYMOB_IFRAME_ID = os.getenv("PAYMOB_IFRAME_ID", "").strip() | |
| PAYMOB_CURRENCY = os.getenv("PAYMOB_CURRENCY", "EGP").strip().upper() or "EGP" | |
| MODEL_INFERENCE_PROVIDER = os.getenv("MODEL_INFERENCE_PROVIDER", "remote").strip().lower() | |
| MODEL_SERVICE_URL = os.getenv("MODEL_SERVICE_URL", "https://pant0x-ealpr-ocr-v2.hf.space").strip().rstrip("/") | |
| MODEL_SERVICE_INFER_PATH = os.getenv("MODEL_SERVICE_INFER_PATH", "/predict").strip() or "/predict" | |
| MODEL_SERVICE_ENABLE_LOCAL_ENDPOINT = _bool_env("MODEL_SERVICE_ENABLE_LOCAL_ENDPOINT", default=False) | |
| MODEL_SERVICE_TIMEOUT_SECONDS = _float_env("MODEL_SERVICE_TIMEOUT_SECONDS", 45.0) | |
| MODEL_SERVICE_SHARED_SECRET = os.getenv("MODEL_SERVICE_SHARED_SECRET", "").strip() | |
| MODEL_SERVICE_HEADER_NAME = os.getenv("MODEL_SERVICE_HEADER_NAME", "X-Model-Service-Secret").strip() or "X-Model-Service-Secret" | |
| HF_SPACE_ID = os.getenv("HF_SPACE_ID", "Pant0x/EALPR_OCR_V2").strip() | |
| STAFF_ROLES = {"admin", "security"} | |
| BARRIER_ROLES = {"barrier"} | |
| DEVELOPER_ROLES = {"developer"} | |
| CORE_TABLES = {"profiles", "vehicles", "parking_sessions", "car_events"} | |
| SUPPORTED_EVENT_TYPES = {"entry", "exit", "ocr_scan"} | |
| SUPPORTED_OTP_CHANNELS = {"email", "phone"} | |
| USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9_.-]{3,32}$") | |
| PHONE_PATTERN = re.compile(r"^\+?[0-9]{7,15}$") | |
| NATIONAL_ID_PATTERN = re.compile(r"^[A-Za-z0-9-]{6,32}$") | |
| PARKING_LOCATION_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 _-]{0,31}(?:\s*,\s*[A-Za-z0-9][A-Za-z0-9 _-]{0,31})*$") | |
| CAMERA_SOURCE_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$") | |
| MOBILE_REDIRECT_SCHEME_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*$") | |
| PROFILE_SELECT_FIELDS = "id,first_name,last_name,full_name,username,phone_number,email,national_id,role,staff_id" | |
| VEHICLE_SELECT_FIELDS = "id,owner_id,plate_letters_ar,plate_numbers_ar,plate_letters_en,plate_numbers_en,car_name,car_model,car_color,is_active,is_verified,verified_at,verified_by,created_at,updated_at" | |
| def supabase_configured() -> bool: | |
| return bool(SUPABASE_URL and SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY) | |
| def paymob_configured() -> bool: | |
| return bool(PAYMOB_BASE_URL and PAYMOB_API_KEY and PAYMOB_INTEGRATION_ID and PAYMOB_IFRAME_ID) | |
| def model_service_configured() -> bool: | |
| return bool(MODEL_SERVICE_URL) | |
| def resolve_model_service_infer_url() -> Optional[str]: | |
| if not model_service_configured(): | |
| return None | |
| path = MODEL_SERVICE_INFER_PATH | |
| if not path.startswith("/"): | |
| path = f"/{path}" | |
| return f"{MODEL_SERVICE_URL}{path}" | |
| def should_load_local_models() -> bool: | |
| return MODEL_INFERENCE_PROVIDER == "local" | |
| def is_staff_role(role: Optional[str]) -> bool: | |
| return (role or "").strip().lower() in STAFF_ROLES | |
| def is_barrier_role(role: Optional[str]) -> bool: | |
| return (role or "").strip().lower() in BARRIER_ROLES | |
| def can_view_global_records(role: Optional[str]) -> bool: | |
| return is_staff_role(role) or is_barrier_role(role) | |
| def is_developer_role(role: Optional[str]) -> bool: | |
| return (role or "").strip().lower() in DEVELOPER_ROLES | |