Spaces:
Runtime error
Runtime error
Download backend/services/leetcode_analyzer.py from hemish22/hiresense-api: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/hemish22/hiresense-api/resolve/main/backend/services/leetcode_analyzer.py
- Command line
-
hf download hf://spaces/hemish22/hiresense-api/backend/services/leetcode_analyzer.py
-
curl -L -o leetcode_analyzer.py https://huggingface.co/spaces/hemish22/hiresense-api/resolve/main/backend/services/leetcode_analyzer.py
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 | |