sonix-ml-api / src /database.py
github-actions[bot]
deploy: production build for sonix-ml-api
597500e
Raw
History Blame
3.84 kB
"""
Sonix-ML Database Integration Layer
-----------------------------------
Handles Supabase operations, data ingestion for ML training, and
real-time interaction persistence. Refactored for O(1) complexity
using guard clauses and vectorized mappings.
"""
import os
import logging
import pandas as pd
from supabase import create_client, Client
from typing import Optional, List
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
def _initialize_supabase() -> Optional[Client]:
"""Safely initializes the Supabase client."""
if not SUPABASE_URL or not SUPABASE_KEY:
logger.error("Critical: Supabase credentials missing.")
return None
try:
return create_client(SUPABASE_URL, SUPABASE_KEY)
except Exception as e:
logger.error(f"Failed to initialize Supabase client: {e}")
return None
supabase = _initialize_supabase()
def fetch_and_merge_training_data() -> pd.DataFrame:
"""
Retrieves interaction data (Favorites & Reviews) and merges them.
Utilizes vectorized pandas operations to eliminate loop complexity.
Returns:
pd.DataFrame: Unified dataframe containing user_id, item_id, rating.
"""
empty_df = pd.DataFrame(columns=['user_id', 'item_id', 'rating'])
if not supabase: return empty_df
try:
frames = []
# 1. Process Favorites
res_fav = supabase.table("favorites").select("user_id, shoe_id").execute()
if res_fav.data:
df_fav = pd.DataFrame(res_fav.data).rename(columns={'shoe_id': 'item_id'})
df_fav['score'] = 1.0
frames.append(df_fav[['user_id', 'item_id', 'score']])
# 2. Process Reviews via Vectorized Mapping
res_rate = supabase.table("reviews").select("user_id, shoe_id, rating").execute()
if res_rate.data:
df_rate = pd.DataFrame(res_rate.data).rename(columns={'shoe_id': 'item_id'})
rating_map = {5: 2.0, 4: 1.0, 3: 0.1, 2: -1.0, 1: -2.0}
df_rate['score'] = df_rate['rating'].map(rating_map).fillna(0.1)
frames.append(df_rate[['user_id', 'item_id', 'score']])
if not frames: return empty_df
# 3. Merge & Aggregate
df_combined = pd.concat(frames)
df_final = df_combined.groupby(['user_id', 'item_id'], as_index=False)['score'].sum()
return df_final.rename(columns={'score': 'rating'})
except Exception as e:
logger.error(f"Error fetching training data: {e}")
return empty_df
def save_interaction_routed(user_id: int, shoe_id: str, action_type: str, rating: Optional[int] = None) -> None:
"""
Persists real-time user interactions using UPSERT to prevent duplicates.
"""
if not supabase: return
try:
action = action_type.lower()
if action == 'like':
data = {"user_id": user_id, "shoe_id": shoe_id}
supabase.table("favorites").upsert(data, on_conflict="user_id, shoe_id").execute()
elif action == 'rate' and rating is not None:
data = {"user_id": user_id, "shoe_id": shoe_id, "rating": rating}
supabase.table("reviews").upsert(data, on_conflict="user_id, shoe_id").execute()
except Exception as e:
logger.error(f"Failed to persist interaction: {e}")
def fetch_shoes_by_type(shoe_type: str) -> pd.DataFrame:
"""Retrieves raw shoe metadata directly from the database."""
if not supabase: return pd.DataFrame()
try:
response = supabase.table("shoes").select("*").execute()
return pd.DataFrame(response.data) if response.data else pd.DataFrame()
except Exception as e:
logger.error(f"Failed to fetch shoes: {e}")
return pd.DataFrame()