diff --git a/app/Controllers/User/leetcode_controller.py b/app/Controllers/User/leetcode_controller.py deleted file mode 100644 index 7f0bda7..0000000 --- a/app/Controllers/User/leetcode_controller.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import Optional -from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException, Response -from sqlmodel import Session - -from db import get_session -from Settings.logging_config import setup_logging -from Services.User.leetcode_service import LeetCodeService -from Entities.leetcode_entity import ( - ReadLeetcode, - CreateLeetcodeBadge, - CreateLeetcodeTag, - ReadLeetcodeBadge, - ReadLeetcodeTag, -) - -logger = setup_logging() - -router = APIRouter(prefix="/Dijkstra/v1/leetcode", tags=["Leetcode"]) - - -@router.post("/sync/{profile_id}/{lc_username}", response_model=ReadLeetcode) -def sync_leetcode(profile_id: UUID, lc_username: str, session: Session = Depends(get_session)): - service = LeetCodeService(session) - logger.info(f"Syncing LeetCode data profile_id={profile_id} username={lc_username}") - return service.create_or_update_from_api(profile_id, lc_username) - - -@router.get("/id/{leetcode_id}", response_model=ReadLeetcode) -def get_leetcode(leetcode_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - model = service.get(leetcode_id) - if not model: - raise HTTPException(status_code=404, detail="LeetCode record not found") - return model - - -@router.get("/{github_username}", response_model=ReadLeetcode) -def get_leetcode_by_github_username(github_username: str, session: Session = Depends(get_session)): - service = LeetCodeService(session) - logger.info(f"Fetching LeetCode data for GitHub username: {github_username}") - model = service.get_by_github_username(github_username) - if not model: - raise HTTPException(status_code=404, detail="LeetCode record not found for user") - return model - - -@router.get("/profile/{profile_id}", response_model=ReadLeetcode) -def get_leetcode_by_profile(profile_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - model = service.get_by_profile(profile_id) - if not model: - raise HTTPException(status_code=404, detail="LeetCode record not found for profile") - return model - - -@router.delete("/{leetcode_id}", status_code=204) -def delete_leetcode(leetcode_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - service.delete(leetcode_id) - return Response(status_code=204) - - -# ----------------------------- Badges --------------------------------- -@router.post("/{leetcode_id}/badges", response_model=ReadLeetcodeBadge) -def create_badge(leetcode_id: UUID, payload: CreateLeetcodeBadge, session: Session = Depends(get_session)): - if payload.leetcode_id != leetcode_id: - raise HTTPException(status_code=400, detail="leetcode_id mismatch") - service = LeetCodeService(session) - return service.add_badge(payload) - - -@router.get("/{leetcode_id}/badges", response_model=list[ReadLeetcodeBadge]) -def list_badges(leetcode_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - return service.list_badges(leetcode_id) - - -@router.delete("/badges/{badge_id}", status_code=204) -def delete_badge(badge_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - service.delete_badge(badge_id) - return Response(status_code=204) - - -# ----------------------------- Tags ----------------------------------- -@router.post("/{leetcode_id}/tags", response_model=ReadLeetcodeTag) -def create_tag(leetcode_id: UUID, payload: CreateLeetcodeTag, session: Session = Depends(get_session)): - if payload.leetcode_id != leetcode_id: - raise HTTPException(status_code=400, detail="leetcode_id mismatch") - service = LeetCodeService(session) - return service.add_tag(payload) - - -@router.get("/{leetcode_id}/tags", response_model=list[ReadLeetcodeTag]) -def list_tags(leetcode_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - return service.list_tags(leetcode_id) - - -@router.delete("/tags/{tag_id}", status_code=204) -def delete_tag(tag_id: UUID, session: Session = Depends(get_session)): - service = LeetCodeService(session) - service.delete_tag(tag_id) - return Response(status_code=204) diff --git a/app/Controllers/error_handlers.py b/app/Controllers/error_handlers.py index f7bb4fd..55a7469 100644 --- a/app/Controllers/error_handlers.py +++ b/app/Controllers/error_handlers.py @@ -12,9 +12,6 @@ EducationNotFound, GitHubUsernameAlreadyExists, GitHubUsernameNotFound, - LeetcodeBadgeNotFound, - LeetcodeNotFound, - LeetcodeTagNotFound, LinksAlreadyExists, LinksNotFound, LocationNotFound, @@ -132,36 +129,6 @@ async def work_experience_not_found_handler(request: Request, exc: WorkExperienc status=404 ) - @app.exception_handler(LeetcodeNotFound) - async def leetcode_not_found_handler(request: Request, exc: LeetcodeNotFound): - logger.warning(f"LeetCode not found: {exc.leetcode_id}") - raise_api_error( - code=ErrorCodes.USER_LEETCODE_NF_A01, - error="LeetCode record not found", - detail=str(exc), - status=404 - ) - - @app.exception_handler(LeetcodeBadgeNotFound) - async def leetcode_badge_not_found_handler(request: Request, exc: LeetcodeBadgeNotFound): - logger.warning(f"LeetCode badge not found: {exc.badge_id}") - raise_api_error( - code=ErrorCodes.USER_LEETCODE_NF_A02, - error="LeetCode badge not found", - detail=str(exc), - status=404 - ) - - @app.exception_handler(LeetcodeTagNotFound) - async def leetcode_tag_not_found_handler(request: Request, exc: LeetcodeTagNotFound): - logger.warning(f"LeetCode tag not found: {exc.tag_id}") - raise_api_error( - code=ErrorCodes.USER_LEETCODE_NF_A03, - error="LeetCode tag not found", - detail=str(exc), - status=404 - ) - @app.exception_handler(CertificationNotFound) async def certification_not_found_handler(request: Request, exc: CertificationNotFound): logger.warning(f"Certificate not found: {exc.certificate_id}") diff --git a/app/Entities/leetcode_entity.py b/app/Entities/leetcode_entity.py deleted file mode 100644 index 579bb50..0000000 --- a/app/Entities/leetcode_entity.py +++ /dev/null @@ -1,196 +0,0 @@ -from typing import List, Optional -from uuid import UUID -from datetime import datetime -from pydantic import BaseModel, field_validator - -from Schema.SQL.Models.models import LeetcodeTagCategory -from Schema.SQL.Enums.enums import Tools - - -# ------------------------------------------------------------------------- -# Leetcode DTOs -# ------------------------------------------------------------------------- -class CreateLeetcode(BaseModel): - profile_id: UUID - lc_username: Optional[str] = None - real_name: Optional[str] = None - about_me: Optional[str] = None - school: Optional[str] = None - websites: Optional[str] = None - country: Optional[str] = None - company: Optional[str] = None - job_title: Optional[str] = None - skill_tags: Optional[List[Tools]] = None - ranking: Optional[int] = None - avatar: Optional[str] = None - reputation: Optional[int] = None - solution_count: Optional[int] = None - total_problems_solved: Optional[int] = None - easy_problems_solved: Optional[int] = None - medium_problems_solved: Optional[int] = None - hard_problems_solved: Optional[int] = None - language_problem_count: Optional[List[str]] = None - attended_contests: Optional[int] = None - competition_rating: Optional[float] = None - global_ranking: Optional[int] = None - total_participants: Optional[int] = None - top_percentage: Optional[float] = None - competition_badge: Optional[str] = None - - @field_validator('profile_id') - def profile_id_must_be_present(cls, v): - if not v: - raise ValueError('profile_id is required') - return v - - @field_validator('lc_username') - def lc_username_trim(cls, v): - return v.strip() if v else v - - -class UpdateLeetcode(BaseModel): - lc_username: Optional[str] = None - real_name: Optional[str] = None - about_me: Optional[str] = None - school: Optional[str] = None - websites: Optional[str] = None - country: Optional[str] = None - company: Optional[str] = None - job_title: Optional[str] = None - skill_tags: Optional[List[Tools]] = None - ranking: Optional[int] = None - avatar: Optional[str] = None - reputation: Optional[int] = None - solution_count: Optional[int] = None - total_problems_solved: Optional[int] = None - easy_problems_solved: Optional[int] = None - medium_problems_solved: Optional[int] = None - hard_problems_solved: Optional[int] = None - language_problem_count: Optional[List[str]] = None - attended_contests: Optional[int] = None - competition_rating: Optional[float] = None - global_ranking: Optional[int] = None - total_participants: Optional[int] = None - top_percentage: Optional[float] = None - competition_badge: Optional[str] = None - - @field_validator('lc_username') - def lc_username_trim(cls, v): - if v is not None and not v.strip(): - raise ValueError('lc_username cannot be empty') - return v.strip() if v else v - - -class ReadLeetcode(BaseModel): - id: UUID - profile_id: UUID - lc_username: Optional[str] - real_name: Optional[str] - about_me: Optional[str] - school: Optional[str] - websites: Optional[str] - country: Optional[str] - company: Optional[str] - job_title: Optional[str] - skill_tags: Optional[List[Tools]] - ranking: Optional[int] - avatar: Optional[str] - reputation: Optional[int] - solution_count: Optional[int] - total_problems_solved: Optional[int] - easy_problems_solved: Optional[int] - medium_problems_solved: Optional[int] - hard_problems_solved: Optional[int] - language_problem_count: Optional[List[str]] - attended_contests: Optional[int] - competition_rating: Optional[float] - global_ranking: Optional[int] - total_participants: Optional[int] - top_percentage: Optional[float] - competition_badge: Optional[str] - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - - -# ------------------------------------------------------------------------- -# Leetcode Badges DTOs -# ------------------------------------------------------------------------- -class CreateLeetcodeBadge(BaseModel): - leetcode_id: UUID - name: Optional[str] = None - icon: Optional[str] = None - hover_text: Optional[str] = None - - @field_validator('leetcode_id') - def leetcode_id_must_be_present(cls, v): - if not v: - raise ValueError('leetcode_id is required') - return v - - -class UpdateLeetcodeBadge(BaseModel): - name: Optional[str] = None - icon: Optional[str] = None - hover_text: Optional[str] = None - - -class ReadLeetcodeBadge(BaseModel): - id: UUID - leetcode_id: UUID - name: Optional[str] - icon: Optional[str] - hover_text: Optional[str] - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - - -# ------------------------------------------------------------------------- -# Leetcode Tags DTOs -# ------------------------------------------------------------------------- -class CreateLeetcodeTag(BaseModel): - leetcode_id: UUID - tag_category: Optional[LeetcodeTagCategory] = None - tag_name: Optional[str] = None - problems_solved: Optional[int] = None - - @field_validator('leetcode_id') - def leetcode_id_must_be_present(cls, v): - if not v: - raise ValueError('leetcode_id is required') - return v - - -class UpdateLeetcodeTag(BaseModel): - tag_category: Optional[LeetcodeTagCategory] = None - tag_name: Optional[str] = None - problems_solved: Optional[int] = None - - -class ReadLeetcodeTag(BaseModel): - id: UUID - leetcode_id: UUID - tag_category: Optional[LeetcodeTagCategory] - tag_name: Optional[str] - problems_solved: Optional[int] - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - - -# ------------------------------------------------------------------------- -# Aggregated Read DTO including related badges & tags -# ------------------------------------------------------------------------- -class ReadLeetcodeWithRelations(ReadLeetcode): - badges: Optional[List[ReadLeetcodeBadge]] = None - tags: Optional[List[ReadLeetcodeTag]] = None - - class Config: - from_attributes = True diff --git a/app/Repository/User/leetcode_repository.py b/app/Repository/User/leetcode_repository.py deleted file mode 100644 index f3c6eaa..0000000 --- a/app/Repository/User/leetcode_repository.py +++ /dev/null @@ -1,241 +0,0 @@ -from typing import List, Optional -from uuid import UUID -from sqlmodel import Session, select -from sqlalchemy import asc, desc -from sqlalchemy.exc import SQLAlchemyError - -from Schema.SQL.Models.models import Leetcode, LeetcodeBadges, LeetcodeTags, LeetcodeTagCategory - - -class LeetcodeRepository: - def __init__(self, session: Session): - self.session = session - - # ------------------------------------------------------------------ - # Core Leetcode - # ------------------------------------------------------------------ - def create(self, leetcode: Leetcode) -> Leetcode: - try: - self.session.add(leetcode) - self.session.commit() - self.session.refresh(leetcode) - return leetcode - except SQLAlchemyError: - self.session.rollback() - raise - - def get(self, leetcode_id: UUID) -> Optional[Leetcode]: - statement = select(Leetcode).where(Leetcode.id == leetcode_id) - return self.session.exec(statement).first() - - def get_by_profile_id(self, profile_id: UUID) -> Optional[Leetcode]: - statement = select(Leetcode).where(Leetcode.profile_id == profile_id) - return self.session.exec(statement).first() - - def get_by_username(self, lc_username: str) -> Optional[Leetcode]: - statement = select(Leetcode).where(Leetcode.lc_username == lc_username) - return self.session.exec(statement).first() - - def list( - self, - skip: int = 0, - limit: int = 20, - sort_by: str = "created_at", - order: str = "desc", - profile_id: Optional[UUID] = None, - lc_username: Optional[str] = None, - country: Optional[str] = None, - company: Optional[str] = None, - min_total_solved: Optional[int] = None, - max_total_solved: Optional[int] = None, - min_rating: Optional[float] = None, - max_rating: Optional[float] = None, - ) -> List[Leetcode]: - statement = select(Leetcode) - - # Filtering - if profile_id: - statement = statement.where(Leetcode.profile_id == profile_id) - if lc_username: - statement = statement.where(Leetcode.lc_username.ilike(f"%{lc_username}%")) - if country: - statement = statement.where(Leetcode.country.ilike(f"%{country}%")) - if company: - statement = statement.where(Leetcode.company.ilike(f"%{company}%")) - if min_total_solved is not None: - statement = statement.where(Leetcode.total_problems_solved >= min_total_solved) - if max_total_solved is not None: - statement = statement.where(Leetcode.total_problems_solved <= max_total_solved) - if min_rating is not None: - statement = statement.where(Leetcode.competition_rating >= min_rating) - if max_rating is not None: - statement = statement.where(Leetcode.competition_rating <= max_rating) - - # Sorting - sort_column = getattr(Leetcode, sort_by, Leetcode.created_at) - if order.lower() == "desc": - statement = statement.order_by(desc(sort_column)) - else: - statement = statement.order_by(asc(sort_column)) - - # Pagination - statement = statement.offset(skip).limit(limit) - return self.session.exec(statement).all() - - def update(self, leetcode: Leetcode) -> Leetcode: - try: - self.session.add(leetcode) - self.session.commit() - self.session.refresh(leetcode) - return leetcode - except SQLAlchemyError: - self.session.rollback() - raise - - def delete(self, leetcode: Leetcode): - try: - self.session.delete(leetcode) - self.session.commit() - except SQLAlchemyError: - self.session.rollback() - raise - - def delete_by_id(self, leetcode_id: UUID) -> bool: - """Delete a Leetcode record by its ID. Returns True if deleted, False if not found.""" - record = self.get(leetcode_id) - if not record: - return False - self.delete(record) - return True - - -# ------------------------------------------------------------------ -# Badge Repository -# ------------------------------------------------------------------ -class LeetcodeBadgeRepository: - def __init__(self, session: Session): - self.session = session - - def create(self, badge: LeetcodeBadges) -> LeetcodeBadges: - try: - self.session.add(badge) - self.session.commit() - self.session.refresh(badge) - return badge - except SQLAlchemyError: - self.session.rollback() - raise - - def get(self, badge_id: UUID) -> Optional[LeetcodeBadges]: - statement = select(LeetcodeBadges).where(LeetcodeBadges.id == badge_id) - return self.session.exec(statement).first() - - def list( - self, - leetcode_id: Optional[UUID] = None, - name: Optional[str] = None, - skip: int = 0, - limit: int = 50, - sort_by: str = "created_at", - order: str = "desc", - ) -> List[LeetcodeBadges]: - statement = select(LeetcodeBadges) - if leetcode_id: - statement = statement.where(LeetcodeBadges.leetcode_id == leetcode_id) - if name: - statement = statement.where(LeetcodeBadges.name.ilike(f"%{name}%")) - - sort_column = getattr(LeetcodeBadges, sort_by, LeetcodeBadges.created_at) - if order.lower() == "desc": - statement = statement.order_by(desc(sort_column)) - else: - statement = statement.order_by(asc(sort_column)) - statement = statement.offset(skip).limit(limit) - return self.session.exec(statement).all() - - def update(self, badge: LeetcodeBadges) -> LeetcodeBadges: - try: - self.session.add(badge) - self.session.commit() - self.session.refresh(badge) - return badge - except SQLAlchemyError: - self.session.rollback() - raise - - def delete(self, badge: LeetcodeBadges): - try: - self.session.delete(badge) - self.session.commit() - except SQLAlchemyError: - self.session.rollback() - raise - - -# ------------------------------------------------------------------ -# Tag Repository -# ------------------------------------------------------------------ -class LeetcodeTagRepository: - def __init__(self, session: Session): - self.session = session - - def create(self, tag: LeetcodeTags) -> LeetcodeTags: - try: - self.session.add(tag) - self.session.commit() - self.session.refresh(tag) - return tag - except SQLAlchemyError: - self.session.rollback() - raise - - def get(self, tag_id: UUID) -> Optional[LeetcodeTags]: - statement = select(LeetcodeTags).where(LeetcodeTags.id == tag_id) - return self.session.exec(statement).first() - - def list( - self, - leetcode_id: Optional[UUID] = None, - tag_category: Optional[LeetcodeTagCategory] = None, - tag_name: Optional[str] = None, - min_solved: Optional[int] = None, - max_solved: Optional[int] = None, - skip: int = 0, - limit: int = 100, - sort_by: str = "created_at", - order: str = "desc", - ) -> List[LeetcodeTags]: - statement = select(LeetcodeTags) - if leetcode_id: - statement = statement.where(LeetcodeTags.leetcode_id == leetcode_id) - if tag_category: - statement = statement.where(LeetcodeTags.tag_category == tag_category) - if tag_name: - statement = statement.where(LeetcodeTags.tag_name.ilike(f"%{tag_name}%")) - if min_solved is not None: - statement = statement.where(LeetcodeTags.problems_solved >= min_solved) - if max_solved is not None: - statement = statement.where(LeetcodeTags.problems_solved <= max_solved) - - sort_column = getattr(LeetcodeTags, sort_by, LeetcodeTags.created_at) - statement = statement.order_by(desc(sort_column) if order.lower() == "desc" else asc(sort_column)) - statement = statement.offset(skip).limit(limit) - return self.session.exec(statement).all() - - def update(self, tag: LeetcodeTags) -> LeetcodeTags: - try: - self.session.add(tag) - self.session.commit() - self.session.refresh(tag) - return tag - except SQLAlchemyError: - self.session.rollback() - raise - - def delete(self, tag: LeetcodeTags): - try: - self.session.delete(tag) - self.session.commit() - except SQLAlchemyError: - self.session.rollback() - raise diff --git a/app/Schema/SQL/Enums/enums.py b/app/Schema/SQL/Enums/enums.py index 3f039ff..21f93f9 100644 --- a/app/Schema/SQL/Enums/enums.py +++ b/app/Schema/SQL/Enums/enums.py @@ -99,12 +99,6 @@ class SchoolType(str, Enum): COURSE = "COURSE" BOOTCAMP = "BOOTCAMP" -# LEETCODE_TAG_CATEGORY -class LeetcodeTagCategory(str, Enum): - FUNDAMENTAL = "FUNDAMENTAL" - INTERMEDIATE = "INTERMEDIATE" - ADVANCED = "ADVANCED" - # PROJECT_LEVEL class ProjectLevel(str, Enum): USER_PROJECT = "USER_PROJECT" diff --git a/app/Schema/SQL/Models/models.py b/app/Schema/SQL/Models/models.py index 117fba6..5fa5475 100644 --- a/app/Schema/SQL/Models/models.py +++ b/app/Schema/SQL/Models/models.py @@ -9,7 +9,7 @@ from Schema.SQL.Enums.enums import ( Difficulty, ProjectLevel, Rank, SchoolType, Tools, WorkLocationType, EmploymentType, Currency, Cause, CertificationType, Domain, - LeetcodeTagCategory, Status, TestScoreType, Degree, SkillCategory + Status, TestScoreType, Degree, SkillCategory ) # Base class with UUID PK and timestamps @@ -86,7 +86,6 @@ class Profile(UUIDBaseTable, table=True): volunteering: List["Volunteering"] = Relationship(back_populates="profile_rel") publications: List["Publications"] = Relationship(back_populates="profile_rel") projects: List["Projects"] = Relationship(back_populates="profile_rel") - leetcode: Optional["Leetcode"] = Relationship(back_populates="profile_rel") documents: List["Document"] = Relationship(back_populates="profile_rel") github: Optional["Github"] = Relationship(back_populates="profile_rel") posts_saved: List["PostsSaved"] = Relationship(back_populates="profile_rel") @@ -353,76 +352,6 @@ class Projects(UUIDBaseTable, table=True): profile_rel: Profile = Relationship(back_populates="projects") owner_rel: "Github" = Relationship(back_populates="projects") -# ------------------------------------------------------------------------- -# Leetcode model -# ------------------------------------------------------------------------- -class Leetcode(UUIDBaseTable, table=True): - __tablename__ = "Leetcode" - - profile_id: UUID = Field(foreign_key="Profile.id", nullable=False) - lc_username: Optional[str] = None - real_name: Optional[str] = None - about_me: Optional[str] = None - school: Optional[str] = None - websites: Optional[str] = None - country: Optional[str] = None - company: Optional[str] = None - job_title: Optional[str] = None - - skill_tags: Optional[List[Tools]] = Field( - sa_column=Column(ARRAY(SQLEnum(Tools, name="TOOLS"))) - ) - ranking: Optional[int] = None - avatar: Optional[str] = None - reputation: Optional[int] = None - solution_count: Optional[int] = None - total_problems_solved: Optional[int] = None - easy_problems_solved: Optional[int] = None - medium_problems_solved: Optional[int] = None - hard_problems_solved: Optional[int] = None - language_problem_count: Optional[List[dict]] = Field(sa_column=Column(ARRAY(JSONB))) - attended_contests: Optional[int] = None - competition_rating: Optional[float] = None - global_ranking: Optional[int] = None - total_participants: Optional[int] = None - top_percentage: Optional[float] = None - competition_badge: Optional[str] = None - - # Relationships - profile_rel: Profile = Relationship(back_populates="leetcode") - badges: List["LeetcodeBadges"] = Relationship(back_populates="leetcode_rel") - tags: List["LeetcodeTags"] = Relationship(back_populates="leetcode_rel") - -# ------------------------------------------------------------------------- -# LeetcodeBadges model -# ------------------------------------------------------------------------- -class LeetcodeBadges(UUIDBaseTable, table=True): - __tablename__ = "LeetcodeBadges" - - leetcode_id: UUID = Field(foreign_key="Leetcode.id", nullable=False) - name: Optional[str] = None - icon: Optional[str] = None - hover_text: Optional[str] = None - - # Relationships - leetcode_rel: Leetcode = Relationship(back_populates="badges") - -# ------------------------------------------------------------------------- -# LeetcodeTags model -# ------------------------------------------------------------------------- -class LeetcodeTags(UUIDBaseTable, table=True): - __tablename__ = "LeetcodeTags" - - leetcode_id: UUID = Field(foreign_key="Leetcode.id", nullable=False) - tag_category: Optional[LeetcodeTagCategory] = Field( - sa_column=Column(SQLEnum(LeetcodeTagCategory, name="LEETCODE_TAG_CATEGORY")) - ) - tag_name: Optional[str] = None - problems_solved: Optional[int] = None - - # Relationships - leetcode_rel: Leetcode = Relationship(back_populates="tags") - # ------------------------------------------------------------------------- # Github model # ------------------------------------------------------------------------- diff --git a/app/Services/User/leetcode_service.py b/app/Services/User/leetcode_service.py index 431a8c2..833b218 100644 --- a/app/Services/User/leetcode_service.py +++ b/app/Services/User/leetcode_service.py @@ -1,31 +1,9 @@ -from typing import Optional, List, Dict, Any -from uuid import UUID +from typing import Dict, Any import requests -from sqlmodel import Session from Settings.logging_config import setup_logging from Config.constants import LEETCODE_API from Config.queries import lc_query -from Schema.SQL.Models.models import Leetcode, LeetcodeBadges, LeetcodeTags -from Schema.SQL.Enums.enums import Tools -from Repository.User.leetcode_repository import ( - LeetcodeRepository, - LeetcodeBadgeRepository, - LeetcodeTagRepository, -) -from Entities.leetcode_entity import ( - CreateLeetcodeBadge, - CreateLeetcodeTag, - ReadLeetcodeBadge, - ReadLeetcodeTag, -) -from Utils.error_codes import ErrorCodes -from Utils.errors import raise_api_error -from Utils.Exceptions.user_exceptions import ( - LeetcodeNotFound, - LeetcodeBadgeNotFound, - LeetcodeTagNotFound, -) logger = setup_logging() @@ -58,180 +36,3 @@ def getAllLeetcodeData(userName: str) -> Dict[str, Any]: except Exception as e: logger.exception("LeetCode API fetch failed") return {"error": str(e)} - - - def __init__(self, session: Session): - self.session = session - self.repo = LeetcodeRepository(session) - self.badge_repo = LeetcodeBadgeRepository(session) - self.tag_repo = LeetcodeTagRepository(session) - - # Basic read helpers (kept small to align with minimal service design) - def get(self, leetcode_id: UUID) -> Optional[Leetcode]: - return self.repo.get(leetcode_id) - - def get_by_profile(self, profile_id: UUID) -> Optional[Leetcode]: - return self.repo.get_by_profile_id(profile_id) - - def get_by_github_username(self, github_username: str) -> Optional[Leetcode]: - """Get Leetcode data by GitHub username""" - from Services.User.profile_service import ProfileService - - profile_service = ProfileService(self.session) - profile_id = profile_service.get_profile_id_by_github_username(github_username) - return self.get_by_profile(profile_id) - - def delete(self, leetcode_id: UUID) -> bool: - deleted = self.repo.delete_by_id(leetcode_id) - if not deleted: - raise LeetcodeNotFound(leetcode_id) - return True - - # Badge operations ------------------------------------------------- - def add_badge(self, dto: CreateLeetcodeBadge) -> LeetcodeBadges: - badge = LeetcodeBadges(**dto.dict()) - return self.badge_repo.create(badge) - - def list_badges(self, leetcode_id: UUID) -> List[LeetcodeBadges]: - return self.badge_repo.list(leetcode_id=leetcode_id) - - def delete_badge(self, badge_id: UUID) -> bool: - badge = self.badge_repo.get(badge_id) - if not badge: - raise LeetcodeBadgeNotFound(badge_id) - self.badge_repo.delete(badge) - return True - - # Tag operations --------------------------------------------------- - def add_tag(self, dto: CreateLeetcodeTag) -> LeetcodeTags: - tag = LeetcodeTags(**dto.dict()) - return self.tag_repo.create(tag) - - def list_tags(self, leetcode_id: UUID) -> List[LeetcodeTags]: - return self.tag_repo.list(leetcode_id=leetcode_id) - - def delete_tag(self, tag_id: UUID) -> bool: - tag = self.tag_repo.get(tag_id) - if not tag: - raise LeetcodeTagNotFound(tag_id) - self.tag_repo.delete(tag) - return True - - def create_or_update_from_api(self, profile_id: UUID, lc_username: str) -> Leetcode: - if not lc_username: - raise ValueError("LeetCode username is required") - payload = self._fetch_api(lc_username) - if "error" in payload: - raise_api_error( - code=ErrorCodes.USER_LEETCODE_SRV_A02, - error="Failed to fetch from LeetCode API", - detail=str(payload["error"]), - status=502, - ) - - data = payload.get("profile") or {} - profile_node = (data.get("profile") or {}) - contest = payload.get("contestRanking") or {} - - ac_nums = (data.get("submitStatsGlobal") or {}).get("acSubmissionNum", []) - def diff_count(name: str) -> Optional[int]: - for item in ac_nums: - if item.get("difficulty") == name: - return item.get("count") - return None - - total = diff_count("All") - easy = diff_count("Easy") - medium = diff_count("Medium") - hard = diff_count("Hard") - - raw_tags = data.get("skillTags") or [] - skill_tags: Optional[List[Tools]] = [] - for t in raw_tags: - try: - skill_tags.append(Tools(t)) - except Exception: - continue - if not skill_tags: - skill_tags = None - - lps = data.get("languageProblemsSolved", []) or [] - lang_counts: Optional[List[Dict[str, Any]]] = [ - {"language": x.get("language"), "problemsSolved": x.get("problemsSolved")} - for x in lps if x.get("language") - ] or None - - existing = self.repo.get_by_profile_id(profile_id) - if existing: - existing.lc_username = data.get("username") - existing.real_name = profile_node.get("realName") - existing.about_me = profile_node.get("aboutMe") - existing.school = profile_node.get("school") - existing.websites = ",".join(profile_node.get("websites", [])) if isinstance(profile_node.get("websites"), list) else profile_node.get("websites") - existing.country = profile_node.get("countryName") - existing.company = profile_node.get("company") - existing.job_title = profile_node.get("jobTitle") - existing.skill_tags = skill_tags - existing.ranking = data.get("ranking") - existing.avatar = profile_node.get("userAvatar") or data.get("avatar") - existing.reputation = data.get("reputation") - existing.solution_count = data.get("solutionCount") - existing.total_problems_solved = total - existing.easy_problems_solved = easy - existing.medium_problems_solved = medium - existing.hard_problems_solved = hard - existing.language_problem_count = lang_counts - existing.attended_contests = contest.get("attendedContests") or contest.get("attendedContestsCount") - existing.competition_rating = contest.get("rating") - existing.global_ranking = contest.get("globalRanking") - existing.total_participants = contest.get("totalParticipants") - existing.top_percentage = contest.get("topPercentage") - existing.competition_badge = (contest.get("badge") or {}).get("name") if isinstance(contest.get("badge"), dict) else None - return self.repo.update(existing) - - model = Leetcode( - profile_id=profile_id, - lc_username=data.get("username"), - real_name=profile_node.get("realName"), - about_me=profile_node.get("aboutMe"), - school=profile_node.get("school"), - websites=",".join(profile_node.get("websites", [])) if isinstance(profile_node.get("websites"), list) else profile_node.get("websites"), - country=profile_node.get("countryName"), - company=profile_node.get("company"), - job_title=profile_node.get("jobTitle"), - skill_tags=skill_tags, - ranking=data.get("ranking"), - avatar=profile_node.get("userAvatar") or data.get("avatar"), - reputation=data.get("reputation"), - solution_count=data.get("solutionCount"), - total_problems_solved=total, - easy_problems_solved=easy, - medium_problems_solved=medium, - hard_problems_solved=hard, - language_problem_count=lang_counts, - attended_contests=contest.get("attendedContests") or contest.get("attendedContestsCount"), - competition_rating=contest.get("rating"), - global_ranking=contest.get("globalRanking"), - total_participants=contest.get("totalParticipants"), - top_percentage=contest.get("topPercentage"), - competition_badge=(contest.get("badge") or {}).get("name") if isinstance(contest.get("badge"), dict) else None, - ) - return self.repo.create(model) - - def _fetch_api(self, username: str) -> Dict[str, Any]: - try: - resp = requests.post( - LEETCODE_API, - json={"query": lc_query, "variables": {"username": username}}, - timeout=20, - ) - data = resp.json() - if "errors" in data: - return {"error": data["errors"]} - return { - "profile": data.get("data", {}).get("matchedUser"), - "contestRanking": data.get("data", {}).get("userContestRanking"), - } - except Exception as e: - logger.exception("LeetCode API fetch failed") - return {"error": str(e)} diff --git a/app/Utils/Exceptions/user_exceptions.py b/app/Utils/Exceptions/user_exceptions.py index 918c545..476aa43 100644 --- a/app/Utils/Exceptions/user_exceptions.py +++ b/app/Utils/Exceptions/user_exceptions.py @@ -38,21 +38,6 @@ def __init__(self, github_username): super().__init__(f"User with GitHub username '{github_username}' already exists.") self.github_username = github_username -class LeetcodeNotFound(ServiceError): - def __init__(self, leetcode_id): - super().__init__(f"LeetCode record with ID {leetcode_id} does not exist.") - self.leetcode_id = leetcode_id - -class LeetcodeBadgeNotFound(ServiceError): - def __init__(self, badge_id): - super().__init__(f"LeetCode badge with ID {badge_id} does not exist.") - self.badge_id = badge_id - -class LeetcodeTagNotFound(ServiceError): - def __init__(self, tag_id): - super().__init__(f"LeetCode tag with ID {tag_id} does not exist.") - self.tag_id = tag_id - class CertificationNotFound(ServiceError): def __init__(self, certification_id=None): if certification_id is None: diff --git a/app/Utils/error_codes.py b/app/Utils/error_codes.py index 90440ae..de17219 100644 --- a/app/Utils/error_codes.py +++ b/app/Utils/error_codes.py @@ -228,19 +228,6 @@ class ErrorCodes: # Not found errors USER_WORKEXP_NF_A01 = "USER-WORKEXP-NF-A01" # Work experience not found - # ----------------------------- - # Users → LeetCode - # ----------------------------- - USER_LEETCODE_DB_A01 = "USER-LEETCODE-DB-A01" # Failure inserting leetcode record - USER_LEETCODE_DB_A02 = "USER-LEETCODE-DB-A02" # Failure updating leetcode record - USER_LEETCODE_DB_A03 = "USER-LEETCODE-DB-A03" # Failure deleting leetcode record - USER_LEETCODE_SRV_A01 = "USER-LEETCODE-SRV-A01" # Generic service error - USER_LEETCODE_SRV_A02 = "USER-LEETCODE-SRV-A02" # External API fetch failure - USER_LEETCODE_VAL_A01 = "USER-LEETCODE-VAL-A01" # Invalid input - USER_LEETCODE_NF_A01 = "USER-LEETCODE-NF-A01" # Leetcode record not found - USER_LEETCODE_NF_A02 = "USER-LEETCODE-NF-A02" # Badge not found - USER_LEETCODE_NF_A03 = "USER-LEETCODE-NF-A03" # Tag not found - # ----------------------------- # Users → Certificate # ----------------------------- diff --git a/app/main.py b/app/main.py index e3bb402..3c52a8a 100644 --- a/app/main.py +++ b/app/main.py @@ -8,7 +8,6 @@ dijkstra_certificate_controller, document_controller, education_controller, - leetcode_controller, links_controller, profile_controller, projects_controller, @@ -61,7 +60,6 @@ def on_shutdown(): app.include_router(workexperience_controller.router) app.include_router(location_controller.router) app.include_router(profile_controller.router) -app.include_router(leetcode_controller.router) app.include_router(dijkstra_certificate_controller.router) app.include_router(certifications_controller.router) app.include_router(document_controller.router) diff --git a/app/migrations/versions/58d213ac2cce_baseline_schema.py b/app/migrations/versions/58d213ac2cce_baseline_schema.py index 23ba521..0f08ee4 100644 --- a/app/migrations/versions/58d213ac2cce_baseline_schema.py +++ b/app/migrations/versions/58d213ac2cce_baseline_schema.py @@ -328,38 +328,6 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('user_name') ) - op.create_table('Leetcode', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('profile_id', sa.Uuid(), nullable=False), - sa.Column('lc_username', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('real_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('about_me', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('school', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('websites', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('country', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('company', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('job_title', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('skill_tags', sa.ARRAY(sa.Enum('JAVA', 'C', 'CPP', 'PYTHON', 'CSHARP', 'RUST', 'JAVASCRIPT', 'TYPESCRIPT', 'GO', 'GROOVY', 'RUBY', 'PHP', 'SWIFT', 'REACTJS', 'ANGULARJS', 'NEXTJS', 'VUEJS', 'SVELTE', 'NODEJS', 'DJANGO', 'FLASK', 'SPRINGBOOT', 'GIT', 'MARKDOWN', 'DOCKER', 'KUBERNETES', 'HTML', 'CSS', 'POSTMAN', 'FIREBASE', 'SUPABASE', 'AWS', 'AZURE', 'GCP', 'HEROKU', 'DIGITALOCEAN', 'VERCEL', 'RAILWAY', 'NETLIFY', 'JENKINS', 'REDIS', 'MONGODB', 'MYSQL', 'MSSQL', 'POSTGRESQL', 'SQLITE', 'ELASTICSEARCH', 'KAFKA', 'RABBITMQ', 'GRAPHQL', 'COUCHDB', 'CASSANDRA', name='TOOLS')), nullable=True), - sa.Column('ranking', sa.Integer(), nullable=True), - sa.Column('avatar', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('reputation', sa.Integer(), nullable=True), - sa.Column('solution_count', sa.Integer(), nullable=True), - sa.Column('total_problems_solved', sa.Integer(), nullable=True), - sa.Column('easy_problems_solved', sa.Integer(), nullable=True), - sa.Column('medium_problems_solved', sa.Integer(), nullable=True), - sa.Column('hard_problems_solved', sa.Integer(), nullable=True), - sa.Column('language_problem_count', sa.ARRAY(postgresql.JSONB(astext_type=sa.Text())), nullable=True), - sa.Column('attended_contests', sa.Integer(), nullable=True), - sa.Column('competition_rating', sa.Float(), nullable=True), - sa.Column('global_ranking', sa.Integer(), nullable=True), - sa.Column('total_participants', sa.Integer(), nullable=True), - sa.Column('top_percentage', sa.Float(), nullable=True), - sa.Column('competition_badge', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.ForeignKeyConstraint(['profile_id'], ['Profile.id'], ), - sa.PrimaryKeyConstraint('id') - ) op.create_table('PostComments', sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), @@ -483,28 +451,6 @@ def upgrade() -> None: sa.ForeignKeyConstraint(['profile_id'], ['Profile.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_table('LeetcodeBadges', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('leetcode_id', sa.Uuid(), nullable=False), - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('icon', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('hover_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.ForeignKeyConstraint(['leetcode_id'], ['Leetcode.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('LeetcodeTags', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('leetcode_id', sa.Uuid(), nullable=False), - sa.Column('tag_category', sa.Enum('FUNDAMENTAL', 'INTERMEDIATE', 'ADVANCED', name='LEETCODE_TAG_CATEGORY'), nullable=True), - sa.Column('tag_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('problems_solved', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['leetcode_id'], ['Leetcode.id'], ), - sa.PrimaryKeyConstraint('id') - ) op.create_table('Projects', sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), diff --git a/app/migrations/versions/cd7f1a1cc5ba_mig_1_10_02_2025.py b/app/migrations/versions/cd7f1a1cc5ba_mig_1_10_02_2025.py deleted file mode 100644 index 9f216cb..0000000 --- a/app/migrations/versions/cd7f1a1cc5ba_mig_1_10_02_2025.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Mig 1 - 10.02.2025 - -Revision ID: cd7f1a1cc5ba -Revises: 58d213ac2cce -Create Date: 2026-02-10 14:45:00.155333 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'cd7f1a1cc5ba' -down_revision: Union[str, Sequence[str], None] = '58d213ac2cce' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ###