ags / utils /firebase_config.py
Mark Cuizon
fix: restore Hugging Face Space build and startup reliability
c9a0fae
Raw History Blame Contribute Delete
15.7 kB
import os
import json
import streamlit as st
from typing import Optional
try:
from utils.secrets_utils import get_secret_section, has_secret
except ImportError:
from secrets_utils import get_secret_section, has_secret
# Optional Firebase Admin imports
try:
import firebase_admin
from firebase_admin import credentials, firestore, auth, storage
from google.cloud.firestore import FieldFilter
FIREBASE_AVAILABLE = True
except ImportError as e:
firebase_admin = None
credentials = None
firestore = None
auth = None
storage = None
FieldFilter = None
FIREBASE_AVAILABLE = False
print(f"Warning: Firebase Admin SDK not available: {e}")
# Do not load from .env in deployment; rely on Streamlit secrets only
# Avoid global monkey patching of Google Auth entirely
def initialize_firebase() -> bool:
"""Initialize Firebase Admin SDK
Returns:
bool: True if initialization successful, False otherwise
"""
if not FIREBASE_AVAILABLE:
error_msg = "Firebase Admin SDK is not installed. Please install it with: pip install firebase-admin"
st.error(error_msg)
print(error_msg)
return False
try:
# Prefer project_id from Streamlit secrets
project_id = None
firebase_secrets = get_secret_section('firebase')
if firebase_secrets is not None:
project_id = firebase_secrets.get('project_id')
# Check if Firebase is already initialized
if firebase_admin and firebase_admin._apps:
print("Firebase already initialized")
return True
print("Initializing Firebase...")
# Get Firebase credentials from environment or Streamlit secrets
firebase_creds = get_firebase_credentials()
if firebase_creds:
print(f"Found Firebase credentials for project: {firebase_creds.get('project_id', 'unknown')}")
if not credentials:
print("Firebase credentials module not available")
return False
# Initialize Firebase with credentials
cred = credentials.Certificate(firebase_creds)
# Get storage bucket from Streamlit secrets
storage_bucket = None
if firebase_secrets is not None:
storage_bucket = (
firebase_secrets.get('firebase_storage_bucket') or
firebase_secrets.get('storage_bucket')
)
if not storage_bucket and firebase_creds.get('project_id'):
storage_bucket = f"{firebase_creds.get('project_id')}.firebasestorage.app"
# Initialize Firebase with explicit configuration
if firebase_admin:
firebase_admin.initialize_app(cred, {
'storageBucket': storage_bucket,
'projectId': firebase_creds.get('project_id', project_id or 'agriai-cbd8b')
})
else:
print("Firebase Admin module not available")
return False
# Do not modify global google.auth defaults; rely on initialized app
print(f"Firebase initialized successfully with storage bucket: {storage_bucket}")
return True
else:
# Provide more specific guidance about missing credentials
if not has_secret('firebase') and not os.getenv("FIREBASE_SERVICE_ACCOUNT_KEY"):
error_msg = (
"Firebase credentials not found. Set FIREBASE_SERVICE_ACCOUNT_KEY "
"in Hugging Face Space secrets, or add a [firebase] section."
)
elif has_secret('firebase'):
firebase_cfg = get_secret_section('firebase') or {}
missing_fields = []
if not firebase_cfg.get('project_id'):
missing_fields.append('project_id')
if not firebase_cfg.get('firebase_private_key'):
missing_fields.append('firebase_private_key')
if not firebase_cfg.get('firebase_client_email'):
missing_fields.append('firebase_client_email')
if missing_fields:
error_msg = (
"Missing required Firebase credentials in Space secrets: "
f"{', '.join(missing_fields)}."
)
else:
error_msg = (
"Firebase credentials found but failed validation. "
"Check private_key, client_email, and project_id."
)
else:
error_msg = (
"Firebase credentials not configured. "
"Add FIREBASE_SERVICE_ACCOUNT_KEY to your Hugging Face Space secrets."
)
print(error_msg)
return False
except Exception as e:
error_type = type(e).__name__
error_details = str(e)
# Provide more specific error messages
if "Certificate" in error_type or "private_key" in error_details.lower():
error_msg = f"Firebase credential error: Invalid private key or certificate. Please check your Firebase service account credentials in Streamlit secrets."
elif "project_id" in error_details.lower() or "project" in error_details.lower():
error_msg = f"Firebase project error: {error_details}. Please verify your project_id in Streamlit secrets."
elif "already exists" in error_details.lower() or "already initialized" in error_details.lower():
# Firebase already initialized - this is actually OK
print(f"Firebase already initialized: {error_details}")
return True
else:
error_msg = f"Failed to initialize Firebase: {error_details}. Please check your configuration in Streamlit secrets."
print(f"Failed to initialize Firebase ({error_type}): {error_details}")
return False
def get_firebase_credentials() -> Optional[dict]:
"""Get Firebase credentials from environment variables or Streamlit secrets
Returns:
dict: Firebase service account credentials or None
"""
try:
# 1) Prefer JSON from environment variable (e.g. on Hugging Face Spaces)
env_creds = os.getenv("FIREBASE_SERVICE_ACCOUNT_KEY")
if env_creds:
try:
print("Loading Firebase credentials from FIREBASE_SERVICE_ACCOUNT_KEY env var")
return json.loads(env_creds)
except Exception as e:
print(f"Error parsing FIREBASE_SERVICE_ACCOUNT_KEY: {e}")
# 2) Fallback to Streamlit secrets
try:
firebase_secrets = get_secret_section('firebase')
if firebase_secrets is not None:
print("Loading Firebase credentials from Streamlit secrets")
# Construct credentials from individual fields in secrets
firebase_config = {
"type": firebase_secrets.get('firebase_type', 'service_account'),
"project_id": firebase_secrets.get('project_id'),
"private_key_id": firebase_secrets.get('firebase_private_key_id'),
"private_key": firebase_secrets.get('firebase_private_key', '').replace('\\n', '\n'),
"client_email": firebase_secrets.get('firebase_client_email'),
"client_id": firebase_secrets.get('firebase_client_id'),
"auth_uri": firebase_secrets.get('firebase_auth_uri', 'https://accounts.google.com/o/oauth2/auth'),
"token_uri": firebase_secrets.get('firebase_token_uri', 'https://oauth2.googleapis.com/token'),
"auth_provider_x509_cert_url": firebase_secrets.get('firebase_auth_provider_x509_cert_url', 'https://www.googleapis.com/oauth2/v1/certs'),
"client_x509_cert_url": firebase_secrets.get('firebase_client_x509_cert_url'),
"universe_domain": firebase_secrets.get('firebase_universe_domain', 'googleapis.com')
}
# Check if all required fields are present
required_fields = ['project_id', 'private_key', 'client_email']
missing_fields = [field for field in required_fields if not firebase_config.get(field)]
if not missing_fields:
print("Successfully loaded Firebase credentials from Streamlit secrets")
return firebase_config
else:
print(f"Missing required Firebase credentials in secrets: {missing_fields}")
elif has_secret('FIREBASE_SERVICE_ACCOUNT_KEY'):
print("Loading Firebase credentials from st.secrets['FIREBASE_SERVICE_ACCOUNT_KEY']")
return dict(st.secrets['FIREBASE_SERVICE_ACCOUNT_KEY'])
except Exception as e:
print(f"Error loading Firebase credentials from Streamlit secrets: {e}")
# If nothing found, return None to signal missing credentials
return None
except Exception as e:
print(f"Error loading Firebase credentials: {e}")
return None
def get_firestore_client():
"""Get Firestore client instance
Returns:
firestore.Client: Firestore client instance
"""
if not FIREBASE_AVAILABLE:
return None
try:
# Ensure Firebase is initialized before creating the client
if firebase_admin and not firebase_admin._apps:
initialize_firebase()
# Return Firestore client from the initialized Firebase app
return firestore.client() if firestore else None
except Exception as e:
# Firebase initialization handled elsewhere, silently return None
return None
def get_storage_bucket():
"""Get Firebase Storage bucket instance
Returns:
storage.Bucket: Storage bucket instance
"""
if not FIREBASE_AVAILABLE:
return None
try:
# Get credentials explicitly to avoid metadata service
firebase_creds = get_firebase_credentials()
if firebase_creds and credentials:
cred = credentials.Certificate(firebase_creds)
return storage.bucket(credentials=cred) if storage else None
else:
# Fallback to default bucket (should work if Firebase is initialized)
return storage.bucket() if storage else None
except Exception as e:
st.error(f"Failed to get Storage bucket: {str(e)}")
return None
def get_auth_client():
"""Get Firebase Auth client
Returns:
auth: Firebase Auth client
"""
if not FIREBASE_AVAILABLE:
return None
try:
return auth if auth else None
except Exception as e:
st.error(f"Failed to get Auth client: {str(e)}")
return None
# Collection names
COLLECTIONS = {
'users': 'users',
'analyses': 'analyses',
'analysis_results': 'analysis_results',
'analysis_prompts': 'analysis_prompts',
'feedback': 'feedback',
'admin_requests': 'admin_requests',
'admin_codes': 'admin_codes',
'reference_documents': 'reference_documents',
'ai_configuration': 'ai_configuration',
'reference_materials': 'reference_materials',
'output_formats': 'output_formats',
'tagging_config': 'tagging_config',
'prompt_templates': 'prompt_templates'
}
# Default MPOB standards - Accurate values for Malaysian Oil Palm cultivation (matching actual data format)
DEFAULT_MPOB_STANDARDS = {
'leaf_standards': {
'N (%)': {'min': 2.4, 'max': 2.8, 'unit': '%', 'optimal': 2.6},
'P (%)': {'min': 0.14, 'max': 0.20, 'unit': '%', 'optimal': 0.17},
'K (%)': {'min': 0.9, 'max': 1.3, 'unit': '%', 'optimal': 1.1},
'Mg (%)': {'min': 0.25, 'max': 0.45, 'unit': '%', 'optimal': 0.35},
'Ca (%)': {'min': 0.5, 'max': 0.9, 'unit': '%', 'optimal': 0.7},
'B (mg/kg)': {'min': 18, 'max': 28, 'unit': 'mg/kg', 'optimal': 23},
'Cu (mg/kg)': {'min': 8, 'max': 18, 'unit': 'mg/kg', 'optimal': 13},
'Zn (mg/kg)': {'min': 18, 'max': 35, 'unit': 'mg/kg', 'optimal': 26}
},
'soil_standards': {
'pH': {'min': 4.5, 'max': 6.0, 'unit': '', 'optimal': 5.25},
'N (%)': {'min': 0.15, 'max': 0.25, 'unit': '%', 'optimal': 0.20},
'Org. C (%)': {'min': 1.5, 'max': 2.5, 'unit': '%', 'optimal': 2.0},
'Total P (mg/kg)': {'min': 15, 'max': 25, 'unit': 'mg/kg', 'optimal': 20},
'Avail P (mg/kg)': {'min': 10, 'max': 20, 'unit': 'mg/kg', 'optimal': 15},
'Exch. K (meq%)': {'min': 0.20, 'max': 0.40, 'unit': 'meq%', 'optimal': 0.30},
'Exch. Ca (meq%)': {'min': 2.0, 'max': 4.0, 'unit': 'meq%', 'optimal': 3.0},
'Exch. Mg (meq%)': {'min': 0.6, 'max': 1.2, 'unit': 'meq%', 'optimal': 0.9},
'CEC (meq%)': {'min': 15, 'max': 25, 'unit': 'meq%', 'optimal': 20}
}
}
def initialize_admin_codes():
"""Initialize default admin codes in Firestore"""
import secrets
import string
from datetime import datetime, timedelta, timezone
try:
db = get_firestore_client()
if not db:
print("Failed to get Firestore client")
return None
admin_codes_ref = db.collection(COLLECTIONS['admin_codes'])
# Check if default admin code already exists
if FieldFilter:
existing_codes = admin_codes_ref.where(filter=FieldFilter('is_default', '==', True)).limit(1).get()
else:
# Fallback if FieldFilter is not available
existing_codes = admin_codes_ref.where('is_default', '==', True).limit(1).get()
if existing_codes:
# Return existing default code
for doc in existing_codes:
code_data = doc.to_dict()
expires_at = code_data.get('expires_at')
# Handle both timezone-aware and naive datetime objects
if expires_at:
if hasattr(expires_at, 'tzinfo') and expires_at.tzinfo is not None:
current_time = datetime.now(timezone.utc)
else:
current_time = datetime.now()
if expires_at > current_time:
print("Default admin code already exists")
return code_data['code']
# Generate new default admin code
alphabet = string.ascii_uppercase + string.digits
default_code = ''.join(secrets.choice(alphabet) for _ in range(8))
# Set expiration to 1 year from now (timezone-aware)
current_time = datetime.now(timezone.utc)
expires_at = current_time + timedelta(days=365)
# Store in Firestore
admin_code_data = {
'code': default_code,
'is_default': True,
'created_at': current_time,
'expires_at': expires_at,
'used': False,
'created_by': 'system',
'description': 'Default admin registration code'
}
admin_codes_ref.add(admin_code_data)
print("Default admin code created successfully")
return default_code
except Exception as e:
print(f"Error initializing admin codes: {str(e)}")
return None