hiresense-api / backend /services /leetcode_analyzer.py
hemish22's picture
Super-squash branch 'main' using huggingface_hub
953f021
Raw History Blame Contribute Delete
10.5 kB
"""
HireSense AI — LeetCode Analyzer Service
Fetches problem-solving stats from LeetCode GraphQL endpoint.
"""
import requests
from typing import Optional
class LeetCodeAnalyzer:
"""Analyzes a LeetCode profile using the public GraphQL API."""
GRAPHQL_URL = "https://leetcode.com/graphql"
PROFILE_QUERY = """
query getUserProfile($username: String!) {
matchedUser(username: $username) {
username
submitStatsGlobal {
acSubmissionNum {
difficulty
count
}
}
profile {
ranking
reputation
starRating
}
userCalendar {
streak
totalActiveDays
}
}
}
"""
def __init__(self):
self.headers = {
"Content-Type": "application/json",
"Referer": "https://leetcode.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
}
def analyze(self, username: str) -> dict:
"""
Analyze a LeetCode user profile.
Returns dict with: username, profile, problems, scores, details, explanations
"""
user_data = self._fetch_user_data(username)
if user_data.get("error"):
return {
"username": username,
"user_not_found": True,
"error": user_data["error"],
"problems": {"total_solved": 0, "easy": 0, "medium": 0, "hard": 0},
"scores": {"problem_solving": 0, "difficulty_balance": 0, "overall": 0},
"explanations": {
"problem_solving": "User not found on LeetCode",
"difficulty_balance": "N/A",
},
}
matched = user_data.get("matchedUser", {})
if not matched:
return {
"username": username,
"user_not_found": True,
"error": f"LeetCode user '{username}' not found",
"problems": {"total_solved": 0, "easy": 0, "medium": 0, "hard": 0},
"scores": {"problem_solving": 0, "difficulty_balance": 0, "overall": 0},
"explanations": {
"problem_solving": "User not found on LeetCode",
"difficulty_balance": "N/A",
},
}
# Parse problem stats
problems = self._parse_problem_stats(matched)
# Parse profile
profile_data = matched.get("profile", {}) or {}
calendar_data = matched.get("userCalendar", {}) or {}
# Calculate scores
ps_score, ps_expl = self._calculate_problem_solving_score(problems)
db_score, db_expl = self._calculate_difficulty_balance_score(problems)
# Overall = weighted average
overall = ps_score * 0.65 + db_score * 0.35
return {
"username": username,
"profile": {
"ranking": profile_data.get("ranking", 0),
"reputation": profile_data.get("reputation", 0),
"star_rating": profile_data.get("starRating"),
},
"problems": problems,
"scores": {
"problem_solving": round(ps_score, 1),
"difficulty_balance": round(db_score, 1),
"overall": round(overall, 1),
},
"details": {
"easy_percentage": round(
(problems["easy"] / max(problems["total_solved"], 1)) * 100, 1
),
"medium_percentage": round(
(problems["medium"] / max(problems["total_solved"], 1)) * 100, 1
),
"hard_percentage": round(
(problems["hard"] / max(problems["total_solved"], 1)) * 100, 1
),
"streak": calendar_data.get("streak", 0),
"total_active_days": calendar_data.get("totalActiveDays", 0),
},
"explanations": {
"problem_solving": ps_expl,
"difficulty_balance": db_expl,
},
}
def _fetch_user_data(self, username: str) -> dict:
"""Fetch user data from LeetCode GraphQL endpoint."""
try:
payload = {
"query": self.PROFILE_QUERY,
"variables": {"username": username},
}
resp = requests.post(
self.GRAPHQL_URL,
json=payload,
headers=self.headers,
timeout=15,
)
if resp.status_code != 200:
return {"error": f"LeetCode API returned status {resp.status_code}"}
data = resp.json()
if "errors" in data:
error_msg = data["errors"][0].get("message", "Unknown GraphQL error")
return {"error": f"LeetCode GraphQL error: {error_msg}"}
return data.get("data", {})
except requests.Timeout:
return {"error": "LeetCode API request timed out"}
except requests.RequestException as e:
return {"error": f"LeetCode API error: {str(e)}"}
except (ValueError, KeyError) as e:
return {"error": f"Failed to parse LeetCode response: {str(e)}"}
def _parse_problem_stats(self, matched_user: dict) -> dict:
"""Parse submission stats into problem counts."""
problems = {"total_solved": 0, "easy": 0, "medium": 0, "hard": 0}
submit_stats = matched_user.get("submitStatsGlobal", {}) or {}
ac_submissions = submit_stats.get("acSubmissionNum", []) or []
for item in ac_submissions:
difficulty = item.get("difficulty", "").lower()
count = item.get("count", 0)
if difficulty == "all":
problems["total_solved"] = count
elif difficulty == "easy":
problems["easy"] = count
elif difficulty == "medium":
problems["medium"] = count
elif difficulty == "hard":
problems["hard"] = count
# If 'All' wasn't explicit, sum the difficulties
if problems["total_solved"] == 0:
problems["total_solved"] = problems["easy"] + problems["medium"] + problems["hard"]
return problems
def _calculate_problem_solving_score(self, problems: dict) -> tuple:
"""
Score based on number and difficulty of problems solved.
Weighted: easy=1pt, medium=3pt, hard=5pt.
"""
total = problems["total_solved"]
if total == 0:
return 0.0, "No problems solved on LeetCode"
weighted = problems["easy"] * 1 + problems["medium"] * 3 + problems["hard"] * 5
if weighted >= 500:
score = 90 + min(10, (weighted - 500) // 100)
explanation = (
f"Exceptional problem solver: {total} problems solved "
f"({problems['easy']}E/{problems['medium']}M/{problems['hard']}H). "
f"Weighted score: {weighted} points."
)
elif weighted >= 200:
score = 70 + min(20, (weighted - 200) // 20)
explanation = (
f"Strong problem solver: {total} problems "
f"({problems['easy']}E/{problems['medium']}M/{problems['hard']}H). "
f"Good depth in algorithmic challenges."
)
elif weighted >= 50:
score = 40 + min(30, (weighted - 50) // 6)
explanation = (
f"Developing skills: {total} problems solved "
f"({problems['easy']}E/{problems['medium']}M/{problems['hard']}H). "
f"Building a solid algorithmic foundation."
)
elif weighted > 0:
score = 15 + min(25, weighted)
explanation = (
f"Getting started: {total} problems solved. "
f"More practice needed to demonstrate strong problem-solving ability."
)
else:
score = 5
explanation = "Minimal LeetCode activity."
return min(score, 100), explanation
def _calculate_difficulty_balance_score(self, problems: dict) -> tuple:
"""
Score based on the mix of easy/medium/hard problems.
Ideal mix: some easy, mostly medium, meaningful hard.
"""
total = problems["total_solved"]
if total == 0:
return 0.0, "No problems solved — cannot assess difficulty balance"
easy_pct = problems["easy"] / total
medium_pct = problems["medium"] / total
hard_pct = problems["hard"] / total
# Ideal: 20-40% easy, 40-50% medium, 15-30% hard
if hard_pct >= 0.15 and medium_pct >= 0.35:
score = 80 + min(20, int(hard_pct * 50))
explanation = (
f"Excellent difficulty balance: {int(easy_pct*100)}% easy, "
f"{int(medium_pct*100)}% medium, {int(hard_pct*100)}% hard. "
f"Actively challenging themselves with hard problems."
)
elif medium_pct >= 0.3 and hard_pct >= 0.05:
score = 55 + min(25, int(medium_pct * 40))
explanation = (
f"Good balance: {int(easy_pct*100)}% easy, "
f"{int(medium_pct*100)}% medium, {int(hard_pct*100)}% hard. "
f"Could benefit from attempting more hard problems."
)
elif easy_pct > 0.7:
score = 20 + min(15, int(medium_pct * 50))
explanation = (
f"Heavily skewed toward easy problems ({int(easy_pct*100)}%). "
f"Should attempt more medium and hard challenges to demonstrate growth."
)
elif medium_pct >= 0.3:
score = 45 + min(20, int(medium_pct * 30))
explanation = (
f"Moderate balance: {int(easy_pct*100)}% easy, "
f"{int(medium_pct*100)}% medium, {int(hard_pct*100)}% hard. "
f"Decent mix but room for harder challenges."
)
else:
score = 30
explanation = (
f"Limited diversity: {int(easy_pct*100)}% easy, "
f"{int(medium_pct*100)}% medium, {int(hard_pct*100)}% hard. "
f"Recommend attempting a broader range of difficulties."
)
return min(score, 100), explanation