Spaces:
Running
Running
| """ | |
| PhilVerify β Firebase / Firestore Client | |
| Initializes firebase-admin SDK and provides typed helpers for persistence. | |
| Setup: | |
| 1. Go to Firebase Console β Project Settings β Service Accounts | |
| 2. Click "Generate new private key" β save as `serviceAccountKey.json` | |
| in the PhilVerify project root (already in .gitignore) | |
| 3. Set FIREBASE_PROJECT_ID in .env | |
| Collections: | |
| verifications/ β one doc per verification run | |
| trends/summary β aggregated entity/topic counters | |
| """ | |
| import logging | |
| import os | |
| from functools import lru_cache | |
| from pathlib import Path | |
| logger = logging.getLogger(__name__) | |
| _SERVICEACCOUNT_PATH = Path(__file__).parent / "serviceAccountKey.json" | |
| _db = None # Firestore client singleton | |
| def get_firestore(): | |
| """Return the Firestore client, or None if Firebase is not configured.""" | |
| global _db | |
| if _db is not None: | |
| return _db | |
| try: | |
| import firebase_admin | |
| from firebase_admin import credentials, firestore | |
| if firebase_admin._DEFAULT_APP_NAME in firebase_admin._apps: | |
| _db = firestore.client() | |
| return _db | |
| if _SERVICEACCOUNT_PATH.exists(): | |
| # Service account key file available (local dev + CI) | |
| cred = credentials.Certificate(str(_SERVICEACCOUNT_PATH)) | |
| firebase_admin.initialize_app(cred) | |
| logger.info("Firebase initialized via service account key") | |
| elif os.getenv("GOOGLE_APPLICATION_CREDENTIALS") or os.getenv("K_SERVICE"): | |
| # Cloud Run (K_SERVICE is always set) or explicit ADC path | |
| cred = credentials.ApplicationDefault() | |
| firebase_admin.initialize_app(cred) | |
| logger.info("Firebase initialized via Application Default Credentials") | |
| else: | |
| logger.warning( | |
| "Firebase not configured β no serviceAccountKey.json and no " | |
| "GOOGLE_APPLICATION_CREDENTIALS env var. History will use in-memory store." | |
| ) | |
| return None | |
| _db = firestore.client() | |
| return _db | |
| except ImportError: | |
| logger.warning("firebase-admin not installed β Firestore disabled") | |
| return None | |
| except Exception as e: | |
| logger.error("Firebase init error: %s β falling back to in-memory store", e) | |
| return None | |
| async def save_verification(data: dict) -> bool: | |
| """ | |
| Persist a verification result to Firestore. | |
| Returns True on success, False if Firebase is unavailable. | |
| """ | |
| db = get_firestore() | |
| if db is None: | |
| return False | |
| try: | |
| db.collection("verifications").document(data["id"]).set(data) | |
| logger.debug("Verification %s saved to Firestore", data["id"]) | |
| return True | |
| except Exception as e: | |
| logger.error("Firestore write error: %s", e) | |
| return False | |
| async def get_verifications( | |
| limit: int = 20, | |
| offset: int = 0, | |
| verdict_filter: str | None = None, | |
| ) -> list[dict]: | |
| """Fetch verification history from Firestore ordered by timestamp desc.""" | |
| db = get_firestore() | |
| if db is None: | |
| return [] | |
| try: | |
| from google.cloud.firestore_v1.base_query import FieldFilter | |
| query = ( | |
| db.collection("verifications") | |
| .order_by("timestamp", direction="DESCENDING") | |
| ) | |
| if verdict_filter: | |
| query = query.where(filter=FieldFilter("verdict", "==", verdict_filter)) | |
| docs = query.limit(limit + offset).stream() | |
| results = [doc.to_dict() for doc in docs] | |
| return results[offset : offset + limit] | |
| except Exception as e: | |
| logger.error("Firestore read error: %s", e) | |
| return [] | |
| def get_all_verifications_sync() -> list[dict]: | |
| """Synchronously fetch ALL verification records from Firestore (used by trends aggregation).""" | |
| db = get_firestore() | |
| if db is None: | |
| return [] | |
| try: | |
| docs = ( | |
| db.collection("verifications") | |
| .order_by("timestamp", direction="DESCENDING") | |
| .limit(10_000) # hard cap β more than enough for trends analysis | |
| .stream() | |
| ) | |
| return [doc.to_dict() for doc in docs] | |
| except Exception as e: | |
| logger.error("Firestore get_all_verifications_sync error: %s", e) | |
| return [] | |
| async def get_verification_count(verdict_filter: str | None = None) -> int: | |
| """Return total count of verifications (with optional verdict filter).""" | |
| db = get_firestore() | |
| if db is None: | |
| return 0 | |
| try: | |
| from google.cloud.firestore_v1.base_query import FieldFilter | |
| query = db.collection("verifications") | |
| if verdict_filter: | |
| query = query.where(filter=FieldFilter("verdict", "==", verdict_filter)) | |
| # Use aggregation query (Firestore native count) | |
| result = query.count().get() | |
| return result[0][0].value | |
| except Exception as e: | |
| logger.error("Firestore count error: %s", e) | |
| return 0 | |