Spaces:
Sleeping
Sleeping
| """ | |
| Supabase Client Wrapper - كل التفاعلات مع Supabase تمر من هنا. | |
| يستخدم Service Role Key لتجاوز RLS أثناء معالجة الـ backend. | |
| كل الدوال متزامنة (sync) لأن مكتبة supabase-py الحالية متزامنة، | |
| ولأنها تُستدعى من BackgroundTasks الذي يدعم الدوال المتزامنة. | |
| """ | |
| from __future__ import annotations | |
| from datetime import datetime, timezone | |
| from typing import Any, Dict, List, Optional | |
| from loguru import logger | |
| from app.config import settings | |
| _client = None | |
| def get_client(): | |
| """يُنشئ أو يعيد Supabase client (singleton).""" | |
| global _client | |
| if _client is not None: | |
| return _client | |
| if not settings.supabase_url or not settings.supabase_service_key: | |
| logger.warning( | |
| "[supabase] missing SUPABASE_URL or SUPABASE_SERVICE_KEY - " | |
| "client will not be functional" | |
| ) | |
| return None | |
| from supabase import create_client | |
| _client = create_client(settings.supabase_url, settings.supabase_service_key) | |
| logger.info("[supabase] client initialized") | |
| return _client | |
| def is_configured() -> bool: | |
| return bool(settings.supabase_url and settings.supabase_service_key) | |
| def _now_iso() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| # ===================================================================== | |
| # Analysis Requests | |
| # ===================================================================== | |
| def get_pending_requests(limit: int = 20) -> List[Dict[str, Any]]: | |
| """يجلب الطلبات بحالة pending مرتبة حسب الأقدمية.""" | |
| client = get_client() | |
| if client is None: | |
| return [] | |
| try: | |
| resp = ( | |
| client.table("analysis_requests") | |
| .select("*") | |
| .eq("status", "pending") | |
| .order("created_at", desc=False) | |
| .limit(limit) | |
| .execute() | |
| ) | |
| return resp.data or [] | |
| except Exception as e: | |
| logger.error(f"[supabase] get_pending_requests failed: {e}") | |
| return [] | |
| def get_request(request_id: str) -> Optional[Dict[str, Any]]: | |
| client = get_client() | |
| if client is None: | |
| return None | |
| try: | |
| resp = ( | |
| client.table("analysis_requests") | |
| .select("*") | |
| .eq("id", request_id) | |
| .single() | |
| .execute() | |
| ) | |
| return resp.data | |
| except Exception as e: | |
| logger.error(f"[supabase] get_request({request_id}) failed: {e}") | |
| return None | |
| def update_request_status( | |
| request_id: str, | |
| status: str, | |
| progress: int, | |
| error_message: Optional[str] = None, | |
| ) -> bool: | |
| """ | |
| يُحدّث حالة الطلب وتقدّمه. عند status=completed يحفظ completed_at. | |
| """ | |
| client = get_client() | |
| if client is None: | |
| logger.info(f"[supabase][noop] {request_id} -> {status} ({progress}%)") | |
| return False | |
| payload: Dict[str, Any] = { | |
| "status": status, | |
| "progress": progress, | |
| "updated_at": _now_iso(), | |
| } | |
| if error_message is not None: | |
| payload["error_message"] = error_message | |
| if status == "completed": | |
| payload["completed_at"] = _now_iso() | |
| payload["progress"] = 100 | |
| try: | |
| client.table("analysis_requests").update(payload).eq("id", request_id).execute() | |
| logger.info(f"[supabase] request {request_id} -> {status} ({progress}%)") | |
| return True | |
| except Exception as e: | |
| logger.error(f"[supabase] update_request_status failed: {e}") | |
| return False | |
| # ===================================================================== | |
| # Analysis Results | |
| # ===================================================================== | |
| def save_results(request_id: str, results: Dict[str, Any]) -> bool: | |
| """ | |
| يحفظ نتائج التحليل. إذا كانت موجودة مسبقاً (في حالة retry) يُحدّثها. | |
| """ | |
| client = get_client() | |
| if client is None: | |
| logger.info(f"[supabase][noop] would save results for {request_id}") | |
| return False | |
| payload = {**results, "request_id": request_id} | |
| try: | |
| # نحاول delete + insert للسلامة (Supabase upsert يحتاج primary key) | |
| client.table("analysis_results").delete().eq("request_id", request_id).execute() | |
| client.table("analysis_results").insert(payload).execute() | |
| logger.info(f"[supabase] results saved for {request_id}") | |
| return True | |
| except Exception as e: | |
| logger.error(f"[supabase] save_results failed: {e}") | |
| return False | |
| def get_results(request_id: str) -> Optional[Dict[str, Any]]: | |
| client = get_client() | |
| if client is None: | |
| return None | |
| try: | |
| resp = ( | |
| client.table("analysis_results") | |
| .select("*") | |
| .eq("request_id", request_id) | |
| .limit(1) | |
| .execute() | |
| ) | |
| if resp.data: | |
| return resp.data[0] | |
| return None | |
| except Exception as e: | |
| logger.error(f"[supabase] get_results failed: {e}") | |
| return None | |
| # ===================================================================== | |
| # Notifications | |
| # ===================================================================== | |
| def create_notification( | |
| user_id: str, | |
| title: str, | |
| body: str, | |
| notif_type: str = "analysis_complete", | |
| link: Optional[str] = None, | |
| ) -> bool: | |
| client = get_client() | |
| if client is None: | |
| return False | |
| try: | |
| client.table("notifications").insert({ | |
| "user_id": user_id, | |
| "type": notif_type, | |
| "title": title, | |
| "body": body, | |
| "link": link, | |
| "is_read": False, | |
| }).execute() | |
| return True | |
| except Exception as e: | |
| logger.error(f"[supabase] create_notification failed: {e}") | |
| return False | |
| # ===================================================================== | |
| # Audit Log | |
| # ===================================================================== | |
| def log_audit( | |
| action: str, | |
| entity_type: str, | |
| entity_id: Optional[str] = None, | |
| metadata: Optional[Dict[str, Any]] = None, | |
| actor_id: Optional[str] = None, | |
| ) -> bool: | |
| client = get_client() | |
| if client is None: | |
| return False | |
| try: | |
| client.table("audit_logs").insert({ | |
| "actor_id": actor_id, | |
| "action": action, | |
| "entity_type": entity_type, | |
| "entity_id": entity_id, | |
| "metadata": metadata or {}, | |
| }).execute() | |
| return True | |
| except Exception as e: | |
| # audit logging يجب ألا يُفشل العملية الأصلية | |
| logger.warning(f"[supabase] log_audit failed: {e}") | |
| return False | |