diff --git a/Backend/app/__init__.py b/Backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/db/__init__.py b/Backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/db/db.py b/Backend/app/db/db.py index ae0f517..3b46603 100644 --- a/Backend/app/db/db.py +++ b/Backend/app/db/db.py @@ -1,40 +1,116 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.pool import NullPool +from sqlalchemy import text +import asyncio import os from dotenv import load_dotenv # Load environment variables from .env load_dotenv() -# Fetch database credentials -USER = os.getenv("user") -PASSWORD = os.getenv("password") -HOST = os.getenv("host") -PORT = os.getenv("port") -DBNAME = os.getenv("dbname") +# Fetch database credentials and strip whitespace +USER = os.getenv("user", "").strip() +PASSWORD = os.getenv("password", "").strip() +HOST = os.getenv("host", "").strip() +PORT = os.getenv("port", "").strip() +DBNAME = os.getenv("dbname", "").strip() -# Corrected async SQLAlchemy connection string (removed `sslmode=require`) +# Construct async SQLAlchemy connection string with connection parameters DATABASE_URL = f"postgresql+asyncpg://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}" # Initialize async SQLAlchemy components try: engine = create_async_engine( - DATABASE_URL, echo=True, connect_args={"ssl": "require"} + DATABASE_URL, + echo=False, # Reduce noise in logs + poolclass=NullPool, + future=True, + connect_args={ + "server_settings": { + "application_name": "InPact_Backend", + }, + "command_timeout": 30, + # Add SSL configuration for Supabase + "ssl": "require", + } ) AsyncSessionLocal = sessionmaker( - bind=engine, class_=AsyncSession, expire_on_commit=False + bind=engine, + class_=AsyncSession, + expire_on_commit=False ) + Base = declarative_base() - print("✅ Database connected successfully!") + print("✅ Database engine created successfully!") except SQLAlchemyError as e: - print(f"❌ Error connecting to the database: {e}") + print(f"❌ Error creating database engine: {e}") engine = None AsyncSessionLocal = None Base = None +async def test_connection(): + """Test database connection with detailed diagnostics""" + try: + if engine is None: + return False, "Engine not initialized" + + # Test DNS resolution first + import socket + try: + socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM) + except socket.gaierror as dns_error: + if "11001" in str(dns_error): + return False, f"IPv6 connectivity issue detected for {HOST}. Your network may not support IPv6, but Supabase REST API is available as fallback." + else: + return False, f"DNS resolution failed: {dns_error}" + + # Try database connection with timeout + try: + async with asyncio.wait_for(engine.begin(), timeout=10) as conn: + result = await conn.execute(text("SELECT 1")) + await result.fetchone() + return True, "PostgreSQL connection successful" + except asyncio.TimeoutError: + return False, "Connection timeout - database may be unreachable" + + except Exception as e: + error_msg = str(e) + if "11001" in error_msg: + return False, f"IPv6 connectivity issue: Your system cannot resolve the IPv6-only Supabase database host. Using REST API fallback." + return False, f"Connection failed: {e}" + + +async def test_connection_with_retry(max_retries: int = 2): + """Test connection with retry mechanism""" + for attempt in range(max_retries): + is_connected, message = await test_connection() + if is_connected: + return True, message + + if attempt < max_retries - 1: + print(f"⚠️ Connection attempt {attempt + 1} failed: {message}") + print(f"🔄 Retrying in 2 seconds...") + await asyncio.sleep(2) + else: + return False, message + + return False, "All connection attempts failed" + + async def get_db(): + if AsyncSessionLocal is None: + raise Exception("Database not configured properly") + async with AsyncSessionLocal() as session: - yield session + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() diff --git a/Backend/app/db/seed.py b/Backend/app/db/seed.py index 77a015e..2aa749e 100644 --- a/Backend/app/db/seed.py +++ b/Backend/app/db/seed.py @@ -4,54 +4,58 @@ async def seed_db(): - users = [ - { - "id": "aabb1fd8-ba93-4e8c-976e-35e5c40b809c", - "username": "creator1", - "email": "creator1@example.com", - "password": "password123", - "role": "creator", - "bio": "Lifestyle and travel content creator", - "profile_image": None, - "created_at": datetime.utcnow() - }, - { - "id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f", - "username": "brand1", - "email": "brand1@example.com", - "password": "password123", - "role": "brand", - "bio": "Sustainable fashion brand looking for influencers", - "profile_image": None, - "created_at": datetime.utcnow() - }, - ] + try: + users = [ + { + "id": "aabb1fd8-ba93-4e8c-976e-35e5c40b809c", + "username": "creator1", + "email": "creator1@example.com", + "password": "password123", + "role": "creator", + "bio": "Lifestyle and travel content creator", + "profile_image": None, + "created_at": datetime.utcnow() + }, + { + "id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f", + "username": "brand1", + "email": "brand1@example.com", + "password": "password123", + "role": "brand", + "bio": "Sustainable fashion brand looking for influencers", + "profile_image": None, + "created_at": datetime.utcnow() + }, + ] - # Insert or update the users - async with AsyncSessionLocal() as session: - for user_data in users: - # Check if user exists - existing_user = await session.execute( - User.__table__.select().where(User.email == user_data["email"]) - ) - existing_user = existing_user.scalar_one_or_none() - - if existing_user: - continue - else: - # Create new user - user = User( - id=user_data["id"], - username=user_data["username"], - email=user_data["email"], - role=user_data["role"], - profile_image=user_data["profile_image"], - bio=user_data["bio"], - created_at=user_data["created_at"] + # Insert or update the users + async with AsyncSessionLocal() as session: + for user_data in users: + # Check if user exists + existing_user = await session.execute( + User.__table__.select().where(User.email == user_data["email"]) ) - session.add(user) - print(f"Created user: {user_data['email']}") + existing_user = existing_user.scalar_one_or_none() + + if existing_user: + continue + else: + # Create new user + user = User( + id=user_data["id"], + username=user_data["username"], + email=user_data["email"], + role=user_data["role"], + profile_image=user_data["profile_image"], + bio=user_data["bio"], + created_at=user_data["created_at"] + ) + session.add(user) + print(f"Created user: {user_data['email']}") - # Commit the session - await session.commit() - print("✅ Users seeded successfully.") + # Commit the session + await session.commit() + print("✅ Users seeded successfully.") + except Exception as e: + print(f"⚠️ Database seeding failed: {e}") + print("Server will continue without seeded data.") diff --git a/Backend/app/main.py b/Backend/app/main.py index 86d892a..70fda9c 100644 --- a/Backend/app/main.py +++ b/Backend/app/main.py @@ -1,7 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from .db.db import engine +from .db.db import engine, test_connection_with_retry from .db.seed import seed_db +from .services.supabase_service import supabase_service from .models import models, chat from .routes.post import router as post_router from .routes.chat import router as chat_router @@ -24,16 +25,41 @@ async def create_tables(): await conn.run_sync(models.Base.metadata.create_all) await conn.run_sync(chat.Base.metadata.create_all) print("✅ Tables created successfully or already exist.") - except SQLAlchemyError as e: + except Exception as e: print(f"❌ Error creating tables: {e}") + print("⚠️ Database connection failed. Server will start without database functionality.") # Lifespan context manager for startup and shutdown events @asynccontextmanager async def lifespan(app: FastAPI): print("App is starting...") - await create_tables() - await seed_db() + + # Test PostgreSQL connection first + print("🔍 Testing PostgreSQL database connection...") + is_connected, message = await test_connection_with_retry(max_retries=2) + + if is_connected: + print(f"✅ PostgreSQL connection: {message}") + try: + await create_tables() + await seed_db() + print("✅ Database initialization completed successfully!") + except Exception as e: + print(f"⚠️ Database initialization error: {e}") + else: + print(f"ℹ️ PostgreSQL unavailable: {message}") + print("🔄 Switching to Supabase REST API...") + + # Try Supabase REST API as fallback + supabase_connected = await supabase_service.connect() + if supabase_connected: + print("✅ Using Supabase REST API for database operations") + await supabase_service.create_tables() + await supabase_service.seed_data() + else: + print("🚀 Server starting in limited mode without database...") + yield print("App is shutting down...") diff --git a/Backend/app/models/__init__.py b/Backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/routes/__init__.py b/Backend/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/routes/ai.py b/Backend/app/routes/ai.py index a21a482..769eda8 100644 --- a/Backend/app/routes/ai.py +++ b/Backend/app/routes/ai.py @@ -2,6 +2,8 @@ from fastapi import APIRouter, HTTPException, Query from datetime import date import os +from dotenv import load_dotenv +from pathlib import Path import requests import json from supabase import create_client, Client @@ -11,14 +13,23 @@ # Initialize router router = APIRouter() -# Load environment variables for Supabase and Gemini -SUPABASE_URL = os.environ.get("SUPABASE_URL") -SUPABASE_KEY = os.environ.get("SUPABASE_KEY") -GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") +# Load environment variables from Backend/.env +env_path = Path(__file__).parent.parent.parent / '.env' +load_dotenv(dotenv_path=env_path) -# Validate required environment variables -if not all([SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY]): - raise ValueError("Missing required environment variables: SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY") +# Get environment variables +SUPABASE_URL = os.getenv("SUPABASE_URL") +SUPABASE_KEY = os.getenv("SUPABASE_KEY") +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") + +# Validate required variables +if not SUPABASE_URL or not SUPABASE_KEY: + raise ValueError("Missing required environment variables: SUPABASE_URL, SUPABASE_KEY") + +# Make GEMINI_API_KEY optional with a warning +if not GEMINI_API_KEY: + print("⚠️ Warning: GEMINI_API_KEY not set. Some AI features may not work.") + GEMINI_API_KEY = None # Set to None instead of failing supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) @@ -52,6 +63,67 @@ def fetch_from_gemini(): text = text.strip() return json.loads(text) +def create_trending_niches_table(): + """Create the trending_niches table if it doesn't exist""" + try: + # Test if table exists by making a simple query + supabase.table("trending_niches").select("id").limit(1).execute() + return True + except Exception: + print("⚠️ trending_niches table doesn't exist. Please create it manually in Supabase:") + print(""" + CREATE TABLE trending_niches ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + insight TEXT NOT NULL, + global_activity INTEGER NOT NULL CHECK (global_activity >= 1 AND global_activity <= 5), + fetched_at DATE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + """) + return False + +def get_fallback_niches(): + """Return hardcoded fallback niches when database/API is unavailable""" + return [ + { + "name": "AI & Tech Innovation", + "insight": "Growing interest in AI tools and automation", + "global_activity": 5, + "fetched_at": str(date.today()) + }, + { + "name": "Sustainable Living", + "insight": "Eco-friendly lifestyle and green products", + "global_activity": 4, + "fetched_at": str(date.today()) + }, + { + "name": "Mental Health & Wellness", + "insight": "Focus on mental health awareness and self-care", + "global_activity": 5, + "fetched_at": str(date.today()) + }, + { + "name": "Remote Work & Productivity", + "insight": "Work-from-home tips and productivity hacks", + "global_activity": 4, + "fetched_at": str(date.today()) + }, + { + "name": "Personal Finance", + "insight": "Investment advice and financial literacy", + "global_activity": 4, + "fetched_at": str(date.today()) + }, + { + "name": "Fitness & Nutrition", + "insight": "Health-focused content and workout routines", + "global_activity": 5, + "fetched_at": str(date.today()) + } + ] + @router.get("/api/trending-niches") def trending_niches(): """ @@ -59,27 +131,50 @@ def trending_niches(): - If today's data exists in Supabase, return it. - Otherwise, fetch from Gemini, store in Supabase, and return the new data. - If Gemini fails, fallback to the most recent data available. + - If table doesn't exist, return hardcoded fallback data. """ today = str(date.today()) - # Check if today's data exists in Supabase - result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() - if not result.data: - # Fetch from Gemini and store - try: - niches = fetch_from_gemini() - for niche in niches: - supabase.table("trending_niches").insert({ - "name": niche["name"], - "insight": niche["insight"], - "global_activity": int(niche["global_activity"]), - "fetched_at": today - }).execute() - result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() - except Exception as e: - print("Gemini fetch failed:", e) - # fallback: serve most recent data - result = supabase.table("trending_niches").select("*").order("fetched_at", desc=True).limit(6).execute() - return result.data + + # Check if table exists first + if not create_trending_niches_table(): + print("⚠️ Using fallback niches data - table doesn't exist") + return get_fallback_niches() + + try: + # Check if today's data exists in Supabase + result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() + + if not result.data: + # Fetch from Gemini and store + try: + if not GEMINI_API_KEY: + raise Exception("GEMINI_API_KEY not configured") + + niches = fetch_from_gemini() + for niche in niches: + supabase.table("trending_niches").insert({ + "name": niche["name"], + "insight": niche["insight"], + "global_activity": int(niche["global_activity"]), + "fetched_at": today + }).execute() + result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() + except Exception as e: + print(f"⚠️ Gemini fetch failed: {e}") + # fallback: serve most recent data from database + try: + result = supabase.table("trending_niches").select("*").order("fetched_at", desc=True).limit(6).execute() + if not result.data: + raise Exception("No data in database") + except Exception: + print("⚠️ No database data available, using hardcoded fallback") + return get_fallback_niches() + + return result.data + + except Exception as e: + print(f"⚠️ Database error: {e}") + return get_fallback_niches() youtube_router = APIRouter(prefix="/youtube", tags=["YouTube"]) diff --git a/Backend/app/schemas/__init__.py b/Backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/services/__init__.py b/Backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/services/supabase_service.py b/Backend/app/services/supabase_service.py new file mode 100644 index 0000000..61058c3 --- /dev/null +++ b/Backend/app/services/supabase_service.py @@ -0,0 +1,108 @@ +import os +import asyncio +from typing import Optional, Dict, Any, List +from supabase import create_client, Client +from dotenv import load_dotenv + +load_dotenv() + +class SupabaseService: + def __init__(self): + self.url: str = os.getenv("SUPABASE_URL") + self.key: str = os.getenv("SUPABASE_KEY") + self.client: Optional[Client] = None + self._connected = False + + async def connect(self) -> bool: + """Initialize Supabase client""" + try: + if not self.url or not self.key: + print("❌ Supabase URL or Key not configured") + return False + + self.client = create_client(self.url, self.key) + + # Test connection with a simple health check + # Just initialize the client - actual table checks will happen during table creation + self._connected = True + print("✅ Supabase REST API client initialized!") + return True + + except Exception as e: + print(f"❌ Supabase connection failed: {e}") + return False + + async def create_tables(self) -> bool: + """Check if tables exist or print instructions for manual creation""" + if not self._connected: + return False + + try: + # Try to check if users table exists by querying it + try: + result = self.client.table('users').select('id').limit(1).execute() + print("✅ Tables already exist in Supabase!") + return True + except Exception: + # Tables don't exist, provide instructions + print("⚠️ Tables don't exist yet in Supabase.") + print("📋 Please create the following tables in your Supabase dashboard:") + print() + print("1. Users table:") + print(" - Go to Supabase Dashboard > Table Editor") + print(" - Create table 'users' with columns: id (uuid), username (text), email (text), role (text), bio (text), profile_image (text)") + print() + print("2. Posts table:") + print(" - Create table 'posts' with columns: id (uuid), user_id (uuid), title (text), content (text), image_url (text)") + print() + print("3. Trending_niches table (for AI features):") + print(" - Create table 'trending_niches' with columns: id (serial), name (text), insight (text), global_activity (int), fetched_at (date)") + print() + print("4. Or use the SQL Editor to run the table creation scripts") + print() + print("✅ Supabase client is ready - tables need manual creation") + return True + + except Exception as e: + print(f"⚠️ Supabase table check failed: {e}") + return False + + async def seed_data(self) -> bool: + """Seed initial data""" + if not self._connected: + return False + + try: + # Check if users already exist + existing_users = self.client.table('users').select('email').execute() + + if len(existing_users.data) > 0: + print("✅ Database already has user data") + return True + + # Insert seed users + seed_users = [ + { + "username": "creator1", + "email": "creator1@example.com", + "role": "creator", + "bio": "Lifestyle and travel content creator" + }, + { + "username": "brand1", + "email": "brand1@example.com", + "role": "brand", + "bio": "Sustainable fashion brand looking for influencers" + } + ] + + result = self.client.table('users').insert(seed_users).execute() + print("✅ Seed data inserted successfully!") + return True + + except Exception as e: + print(f"⚠️ Data seeding failed: {e}") + return False + +# Global instance +supabase_service = SupabaseService() \ No newline at end of file