diff --git a/PythonTask.pdf b/PythonTask.pdf new file mode 100644 index 0000000..b41baad Binary files /dev/null and b/PythonTask.pdf differ diff --git a/README.md b/README.md index 048eea9..eae7ee0 100644 --- a/README.md +++ b/README.md @@ -1 +1,102 @@ -# UrbanMatch-PythonTask \ No newline at end of file +# UrbanMatch-PythonTask +# Heartbeat - AI Dating Match + +A modern dating web application powered by AI matching algorithms. Heartbeat helps users find compatible matches based on shared interests, age compatibility, and location. + + + +## Features + +- 🔐 Secure user authentication +- 👤 Detailed user profiles +- 💝 AI-powered match recommendations +- 🎯 Interest-based matching +- 📍 Location-based matching +- 💫 Modern, responsive UI + +## Tech Stack + +- **Backend**: FastAPI (Python) +- **Database**: SQLite with SQLAlchemy +- **Frontend**: HTML5, CSS3, JavaScript +- **Authentication**: JWT tokens +- **Security**: Bcrypt encryption + + +## File Descriptions + +### Backend Files +- `main.py` - Core FastAPI application with all routes and endpoints +- `database.py` - SQLAlchemy models and database configuration +- `ai_matcher.py` - AI-powered matching algorithm implementation +- `reset_db.py` - Database initialization and reset script +- `requirements.txt` - List of Python package dependencies + +### Frontend Templates +- `login.html` - User authentication interface +- `profile.html` - User profile creation and editing +- `index.html` - Main application interface showing matches + + + +## Features + +- **User Authentication**: Login and Signup functionality. +- **Profile Setup**: Users can create and update their profiles. +- **AI-Powered Matching**: Advanced AI algorithms to suggest the best matches for users. +- **Database Management**: Efficient storage and retrieval of user data. +- **Dynamic Web Pages**: Responsive HTML templates for user interactions. + +--- + +## Directory Structure + +```plaintext +heartbeat-dating/ +├── main.py # FastAPI application +├── database.py # Database models and connections +├── ai_matcher.py # AI-powered matching algorithm +├── reset_db.py # Script to initialize/reset the database +├── requirements.txt # Python dependencies +└── templates/ # HTML templates for the application + ├── login.html # Login/Signup page + ├── profile.html # Profile setup page + └── index.html # Main matches page +``` + + + +## Installation + +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/heartbeat-dating.git + cd heartbeat-dating + ``` + +2. Set up a virtual environment and install dependencies: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + pip install -r requirements.txt + ``` + +3. Initialize the database: + ```bash + python reset_db.py + ``` + +4. Run the application: + ```bash + uvicorn main:app --reload + ``` + +5. Open the application in your browser at [http://127.0.0.1:8000](http://127.0.0.1:8000). + +--- +# LayOut:- + + + + + diff --git a/__pycache__/ai_matcher.cpython-312.pyc b/__pycache__/ai_matcher.cpython-312.pyc new file mode 100644 index 0000000..7216be1 Binary files /dev/null and b/__pycache__/ai_matcher.cpython-312.pyc differ diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..bfd4de8 Binary files /dev/null and b/__pycache__/database.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..65ad131 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/schemas.cpython-312.pyc b/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000..95c48f4 Binary files /dev/null and b/__pycache__/schemas.cpython-312.pyc differ diff --git a/ai_matcher.py b/ai_matcher.py new file mode 100644 index 0000000..d1fb148 --- /dev/null +++ b/ai_matcher.py @@ -0,0 +1,117 @@ +from typing import List, Dict +import numpy as np +from sklearn.metrics.pairwise import cosine_similarity + +class AIMatcher: + def __init__(self): + # Predefined weights for different matching criteria + self.weights = { + 'interests': 0.4, + 'age': 0.3, + 'location': 0.3 + } + + # Interest categories for better matching + self.interest_categories = { + 'outdoor': ['hiking', 'nature', 'travel', 'sports', 'fitness'], + 'creative': ['art', 'music', 'writing', 'photography', 'theater'], + 'intellectual': ['reading', 'science', 'history', 'technology', 'languages'], + 'lifestyle': ['cooking', 'fashion', 'food', 'meditation', 'yoga'], + 'entertainment': ['movies', 'gaming', 'dancing', 'music', 'theater'] + } + + def calculate_interest_similarity(self, user1_interests: List[str], user2_interests: List[str]) -> float: + """Calculate similarity between two users' interests using cosine similarity""" + # Convert interests to lowercase for comparison + user1_interests = [i.lower() for i in user1_interests] + user2_interests = [i.lower() for i in user2_interests] + + # Create interest vectors + all_categories = list(self.interest_categories.keys()) + user1_vector = self._create_interest_vector(user1_interests, all_categories) + user2_vector = self._create_interest_vector(user2_interests, all_categories) + + # Calculate cosine similarity + similarity = cosine_similarity([user1_vector], [user2_vector])[0][0] + return float(similarity) + + def _create_interest_vector(self, interests: List[str], categories: List[str]) -> List[float]: + """Create a vector representation of interests based on categories""" + vector = [] + for category in categories: + category_interests = self.interest_categories[category] + score = sum(1 for interest in interests if interest in category_interests) + vector.append(score / len(category_interests)) + return vector + + def calculate_age_compatibility(self, age1: int, age2: int) -> float: + """Calculate age compatibility score""" + age_diff = abs(age1 - age2) + if age_diff <= 5: + return 1.0 + elif age_diff <= 10: + return 0.7 + elif age_diff <= 15: + return 0.4 + else: + return 0.2 + + def calculate_location_match(self, location1: str, location2: str) -> float: + """Calculate location match score""" + # Simple exact match for now, could be enhanced with geographic distance + return 1.0 if location1.lower() == location2.lower() else 0.0 + + def find_matches(self, user: Dict, potential_matches: List[Dict], limit: int = 10) -> List[Dict]: + """Find and rank matches for a user""" + matches = [] + + for potential_match in potential_matches: + try: + # Calculate individual scores + interest_score = self.calculate_interest_similarity( + user['interests'], + potential_match['interests'] + ) + + age_score = self.calculate_age_compatibility( + user['age'], + potential_match['age'] + ) + + location_score = self.calculate_location_match( + user['location'], + potential_match['location'] + ) + + # Calculate weighted total score + total_score = ( + interest_score * self.weights['interests'] + + age_score * self.weights['age'] + + location_score * self.weights['location'] + ) + + # Calculate match reasons + reasons = [] + if interest_score > 0.6: + common_interests = set(user['interests']) & set(potential_match['interests']) + reasons.append(f"You share {len(common_interests)} interests") + if age_score > 0.7: + reasons.append("You're close in age") + if location_score == 1.0: + reasons.append("You're in the same location") + + matches.append({ + 'user': potential_match, + 'compatibility_score': int(total_score * 100), + 'common_interests': list(set(user['interests']) & set(potential_match['interests'])), + 'match_reasons': reasons + }) + + except Exception as e: + print(f"Error processing match: {str(e)}") + continue + + # Sort matches by compatibility score + matches.sort(key=lambda x: x['compatibility_score'], reverse=True) + + return matches[:limit] \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..ef75ebc --- /dev/null +++ b/database.py @@ -0,0 +1,32 @@ +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(bind=engine) +Base = declarative_base() + +class UserModel(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + age = Column(Integer, nullable=True) + location = Column(String, nullable=True) + bio = Column(String, nullable=True) + interests = Column(String, nullable=True) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/images/MaIn.png b/images/MaIn.png new file mode 100644 index 0000000..927f24c Binary files /dev/null and b/images/MaIn.png differ diff --git a/images/login.png b/images/login.png new file mode 100644 index 0000000..8eebae4 Binary files /dev/null and b/images/login.png differ diff --git a/images/profile.png b/images/profile.png new file mode 100644 index 0000000..865b04a Binary files /dev/null and b/images/profile.png differ diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..08969fc --- /dev/null +++ b/init_db.py @@ -0,0 +1,9 @@ +from database import Base, engine + +# Drop all existing tables +Base.metadata.drop_all(bind=engine) + +# Create all tables fresh +Base.metadata.create_all(bind=engine) + +print("Database initialized successfully!") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..618d3db --- /dev/null +++ b/main.py @@ -0,0 +1,206 @@ +from fastapi import FastAPI, Depends, Request +from fastapi.responses import JSONResponse, FileResponse, RedirectResponse +from fastapi.middleware.cors import CORSMiddleware +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +import bcrypt +from database import get_db, UserModel, Base, engine +from sqlalchemy.orm import Session +from pydantic import BaseModel + +app = FastAPI() + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Pydantic models for request validation +class UserLogin(BaseModel): + email: str + password: str + +class UserSignup(BaseModel): + name: str + email: str + password: str + +# Constants +SECRET_KEY = "your-secret-key-here" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +# Create tables +Base.metadata.create_all(bind=engine) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + +@app.get("/") +async def root(): + return FileResponse('templates/login.html') + +@app.get("/profile") +async def profile_page(): + return FileResponse('templates/profile.html') + +@app.get("/app") +async def app_page(): + return FileResponse('templates/index.html') + +@app.post("/signup") +async def signup(user: UserSignup, db: Session = Depends(get_db)): + try: + print(f"Signup attempt for email: {user.email}") + + # Check if user exists + existing_user = db.query(UserModel).filter(UserModel.email == user.email).first() + if existing_user: + return JSONResponse( + status_code=400, + content={"message": "Email already registered"} + ) + + # Hash password + hashed_password = bcrypt.hashpw( + user.password.encode('utf-8'), + bcrypt.gensalt() + ).decode('utf-8') + + # Create new user + new_user = UserModel( + name=user.name, + email=user.email, + hashed_password=hashed_password + ) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + # Create access token + access_token = create_access_token( + data={"sub": new_user.email}, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + ) + + print(f"User created successfully: {new_user.email}") + return JSONResponse( + status_code=200, + content={ + "access_token": access_token, + "token_type": "bearer", + "user_id": new_user.id + } + ) + + except Exception as e: + print(f"Signup error: {str(e)}") + return JSONResponse( + status_code=400, + content={"message": str(e)} + ) + +@app.post("/login") +async def login(user: UserLogin, db: Session = Depends(get_db)): + try: + print(f"Login attempt for email: {user.email}") + + # Find user + db_user = db.query(UserModel).filter(UserModel.email == user.email).first() + if not db_user: + return JSONResponse( + status_code=401, + content={"message": "Invalid email or password"} + ) + + # Verify password + if not bcrypt.checkpw( + user.password.encode('utf-8'), + db_user.hashed_password.encode('utf-8') + ): + return JSONResponse( + status_code=401, + content={"message": "Invalid email or password"} + ) + + # Create access token + access_token = create_access_token( + data={"sub": db_user.email}, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + ) + + print(f"Login successful for: {db_user.email}") + return JSONResponse( + status_code=200, + content={ + "access_token": access_token, + "token_type": "bearer", + "user_id": db_user.id + } + ) + + except Exception as e: + print(f"Login error: {str(e)}") + return JSONResponse( + status_code=400, + content={"message": str(e)} + ) + +@app.post("/api/update-profile") +async def update_profile(request: Request, db: Session = Depends(get_db)): + try: + # Get request body + profile_data = await request.json() + + # Get current user from token + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return JSONResponse( + status_code=401, + content={"message": "Not authenticated"} + ) + + token = auth_header.split(' ')[1] + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email = payload.get("sub") + + # Update user profile + user = db.query(UserModel).filter(UserModel.email == email).first() + if not user: + return JSONResponse( + status_code=404, + content={"message": "User not found"} + ) + + # Update user fields + user.age = profile_data.get('age') + user.location = profile_data.get('location') + user.bio = profile_data.get('bio') + user.interests = ','.join(profile_data.get('interests', [])) + + db.commit() + + return JSONResponse( + status_code=200, + content={"message": "Profile updated successfully"} + ) + + except Exception as e: + print(f"Error updating profile: {str(e)}") + return JSONResponse( + status_code=500, + content={"message": str(e)} + ) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/reset_db.py b/reset_db.py new file mode 100644 index 0000000..016f888 --- /dev/null +++ b/reset_db.py @@ -0,0 +1,77 @@ +import os +import bcrypt +import time +from database import Base, engine, SessionLocal, UserModel + +def reset_database(): + print("Starting database reset...") + + # Try to remove the database with retries + for attempt in range(5): # Try 5 times + try: + if os.path.exists("sql_app.db"): + os.remove("sql_app.db") + print("Removed existing database") + break + except PermissionError: + print(f"Database is locked, attempt {attempt + 1} of 5...") + time.sleep(1) # Wait 1 second before retry + except Exception as e: + print(f"Error removing database: {e}") + time.sleep(1) + + try: + # Create new tables + Base.metadata.create_all(bind=engine) + print("Created new tables") + + # Create a session + db = SessionLocal() + + try: + # Create test user + test_password = "test123" + hashed_password = bcrypt.hashpw(test_password.encode('utf-8'), bcrypt.gensalt()) + + test_user = UserModel( + email="test@example.com", + name="Test User", + hashed_password=hashed_password.decode('utf-8'), + age=25, + location="New York", + bio="Test user bio", + interests="reading,music,travel" + ) + + db.add(test_user) + db.commit() + + # Verify user creation + user = db.query(UserModel).filter(UserModel.email == "test@example.com").first() + if user: + print(f"Test user created successfully: {user.email}") + else: + print("Failed to create test user!") + + except Exception as e: + print(f"Error creating test user: {e}") + db.rollback() + finally: + db.close() + + except Exception as e: + print(f"Database error: {e}") + return False + + return True + +if __name__ == "__main__": + print("Starting database initialization...") + + # Make sure no server is running + input("Please make sure the FastAPI server is stopped (press Enter to continue)...") + + if reset_database(): + print("Database reset completed successfully!") + else: + print("Database reset failed!") \ No newline at end of file diff --git a/schemas.py b/schemas.py new file mode 100644 index 0000000..4b9af65 --- /dev/null +++ b/schemas.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +class UserAuth(BaseModel): + email: str + password: str + +class UserCreate(UserAuth): + name: str \ No newline at end of file diff --git a/sql_app.db b/sql_app.db new file mode 100644 index 0000000..9f076d8 Binary files /dev/null and b/sql_app.db differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5d6ec70 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,384 @@ + + +
+ + +