Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions Backend/app/db/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,51 @@
from sqlalchemy.exc import SQLAlchemyError
import os
from dotenv import load_dotenv
from urllib.parse import quote_plus
import socket

# Load environment variables from .env
load_dotenv()

# Fetch database credentials
USER = os.getenv("user")
PASSWORD = os.getenv("password")
PASSWORD = quote_plus(os.getenv("password") or "")

HOST = os.getenv("host")
PORT = os.getenv("port")
DBNAME = os.getenv("dbname")

# Corrected async SQLAlchemy connection string (removed `sslmode=require`)
DATABASE_URL = f"postgresql+asyncpg://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}"
# Resolve IPv4 address for HOST to avoid intermittent DNS issues on Windows
if HOST and PORT:
try:
addrinfo = socket.getaddrinfo(
HOST,
int(PORT),
socket.AF_INET,
socket.SOCK_STREAM,
)
HOST_IP = addrinfo[0][4][0] if addrinfo else HOST
except Exception:
HOST_IP = HOST
else:
HOST_IP = HOST

# Build async SQLAlchemy connection string
DATABASE_URL = f"postgresql+asyncpg://{USER}:{PASSWORD}@{HOST_IP}:{PORT}/{DBNAME}"
print(f"DB URL: postgresql+asyncpg://{USER}:***@{HOST_IP}:{PORT}/{DBNAME}")

# Initialize async SQLAlchemy components
try:
engine = create_async_engine(
DATABASE_URL, echo=True, connect_args={"ssl": "require"}
DATABASE_URL,
echo=True,
connect_args={"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!")
Expand Down
22 changes: 19 additions & 3 deletions Backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from .routes.post import router as post_router
from .routes.chat import router as chat_router
from .routes.match import router as match_router
from .routes.engagement import router as engagement_router
from sqlalchemy.exc import SQLAlchemyError
import logging
import os
from dotenv import load_dotenv
from contextlib import asynccontextmanager
from app.routes import ai
import asyncio

# Load environment variables
load_dotenv()
Expand All @@ -27,13 +29,26 @@ async def create_tables():
except SQLAlchemyError as e:
print(f"❌ Error creating tables: {e}")

async def startup_sequence():
await create_tables()
await seed_db()

# Lifespan context manager for startup and shutdown events
# Lifespan context manager for startup and shutdown events with retries
@asynccontextmanager
async def lifespan(app: FastAPI):
print("App is starting...")
await create_tables()
await seed_db()
last_error = None
for attempt in range(1, 6): # up to 5 retries
try:
await startup_sequence()
last_error = None
break
except Exception as e:
last_error = e
print(f"Startup attempt {attempt} failed: {e}")
await asyncio.sleep(2)
if last_error:
print(f"⚠️ Startup continuing without DB initialization due to repeated errors: {last_error}")
yield
print("App is shutting down...")

Expand All @@ -54,6 +69,7 @@ async def lifespan(app: FastAPI):
app.include_router(post_router)
app.include_router(chat_router)
app.include_router(match_router)
app.include_router(engagement_router)
app.include_router(ai.router)
app.include_router(ai.youtube_router)

Expand Down
20 changes: 20 additions & 0 deletions Backend/app/routes/engagement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional

from ..db.db import get_db
from ..models.models import UserPost
from ..schemas.engagement import EngagementRequest, EngagementMetrics
from ..services.engagement_service import compute_engagement_metrics

router = APIRouter(prefix="/engagement", tags=["Engagement"])


@router.post("/compute", response_model=EngagementMetrics)
async def compute_engagement(data: EngagementRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(UserPost).where(UserPost.user_id == data.creator_id))
posts = result.scalars().all()

metrics = compute_engagement_metrics(posts, data.follower_count)
return metrics
21 changes: 21 additions & 0 deletions Backend/app/schemas/engagement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pydantic import BaseModel
from typing import Optional


class EngagementRequest(BaseModel):
creator_id: str
follower_count: Optional[int] = None


class EngagementMetrics(BaseModel):
likes: int
comments: int
shares: int

avg_likes: float
avg_comments: float
avg_shares: float

engagement_rate: Optional[float] = None
total_posts: int
follower_count: Optional[int] = None
64 changes: 64 additions & 0 deletions Backend/app/services/engagement_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import List, Optional, Dict, Any

try:
from app.models.models import UserPost # for typing clarity
except Exception:
# Fallback for type hints if import is not available at runtime
class UserPost: # type: ignore
engagement_metrics: Dict[str, Any]


def compute_engagement_metrics(posts: List[UserPost], follower_count: Optional[int]) -> Dict[str, Any]:
"""
Compute standardized engagement metrics using existing UserPost.engagement_metrics JSON.

- likes, comments, shares are totals across posts
- avg_* are per-post averages (0.0 when no posts)
- engagement_rate = (likes + comments + shares) / follower_count
(None when follower_count is missing or <= 0)
- total_posts is count of posts
- follower_count echoed back in response
"""
total_likes = 0
total_comments = 0
total_shares = 0

for post in posts:
metrics = getattr(post, "engagement_metrics", {}) or {}
# Defensive defaults
likes = int(metrics.get("likes", 0) or 0)
comments = int(metrics.get("comments", 0) or 0)
shares = int(metrics.get("shares", 0) or 0)

total_likes += likes
total_comments += comments
total_shares += shares

total_posts = len(posts)

if total_posts > 0:
avg_likes = total_likes / total_posts
avg_comments = total_comments / total_posts
avg_shares = total_shares / total_posts
else:
avg_likes = 0.0
avg_comments = 0.0
avg_shares = 0.0

engagement_rate: Optional[float]
if follower_count and follower_count > 0:
engagement_rate = (total_likes + total_comments + total_shares) / follower_count
else:
engagement_rate = None

return {
"likes": total_likes,
"comments": total_comments,
"shares": total_shares,
"avg_likes": avg_likes,
"avg_comments": avg_comments,
"avg_shares": avg_shares,
"engagement_rate": engagement_rate,
"total_posts": total_posts,
"follower_count": follower_count,
}