philverify-api / firebase_client.py
Ryan Christian D. Deniega
fix: cold start 502, favicon, verify state persistence
b1c84b5
"""
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