diff --git a/engineapi/engineapi/actions.py b/engineapi/engineapi/actions.py index 5c38101a..375e1468 100644 --- a/engineapi/engineapi/actions.py +++ b/engineapi/engineapi/actions.py @@ -11,7 +11,7 @@ from hexbytes import HexBytes import requests # type: ignore from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Session -from sqlalchemy import func, text, or_ +from sqlalchemy import func, text, or_, Subquery from sqlalchemy.engine import Row from web3 import Web3 from web3.types import ChecksumAddress @@ -24,6 +24,7 @@ from .models import ( DropperClaim, Leaderboard, LeaderboardScores, + LeaderboardVersion, ) from . import signatures from .settings import ( @@ -91,6 +92,10 @@ class LeaderboardConfigAlreadyInactive(Exception): pass +class LeaderboardVersionNotFound(Exception): + pass + + BATCH_SIGNATURE_PAGE_SIZE = 500 logger = logging.getLogger(__name__) @@ -959,6 +964,25 @@ def refetch_drop_signatures( return claimant_objects +def leaderboard_version_filter( + db_session: Session, + leaderboard_id: uuid.UUID, + version_number: Optional[int] = None, +) -> Union[Subquery, int]: + # Subquery to get the latest version number for the given leaderboard + if not version_number: + latest_version = ( + db_session.query(func.max(LeaderboardVersion.version_number)).filter( + LeaderboardVersion.leaderboard_id == leaderboard_id, + LeaderboardVersion.published == True, + ) + ).scalar_subquery() + else: + latest_version = version_number + + return latest_version + + def get_leaderboard_total_count(db_session: Session, leaderboard_id) -> int: """ Get the total number of claimants in the leaderboard @@ -971,12 +995,17 @@ def get_leaderboard_total_count(db_session: Session, leaderboard_id) -> int: def get_leaderboard_info( - db_session: Session, leaderboard_id: uuid.UUID + db_session: Session, leaderboard_id: uuid.UUID, version_number: Optional[int] = None ) -> Row[Tuple[uuid.UUID, str, str, int, Optional[datetime]]]: """ Get the leaderboard from the database with users count """ + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) leaderboard = ( db_session.query( Leaderboard.id, @@ -990,6 +1019,15 @@ def get_leaderboard_info( LeaderboardScores.leaderboard_id == Leaderboard.id, isouter=True, ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == Leaderboard.id, + isouter=True, + ) + .filter( + LeaderboardVersion.published == True, + LeaderboardVersion.version_number == latest_version, + ) .filter(Leaderboard.id == leaderboard_id) .group_by(Leaderboard.id, Leaderboard.title, Leaderboard.description) .one() @@ -1078,19 +1116,44 @@ def get_leaderboards( def get_position( - db_session: Session, leaderboard_id, address, window_size, limit: int, offset: int + db_session: Session, + leaderboard_id, + address, + window_size, + limit: int, + offset: int, + version_number: Optional[int] = None, ) -> List[Row[Tuple[str, int, int, int, Any]]]: """ - Return position by address with window size """ - query = db_session.query( - LeaderboardScores.address, - LeaderboardScores.score, - LeaderboardScores.points_data.label("points_data"), - func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), - func.row_number().over(order_by=LeaderboardScores.score.desc()).label("number"), - ).filter(LeaderboardScores.leaderboard_id == leaderboard_id) + + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) + + query = ( + db_session.query( + LeaderboardScores.address, + LeaderboardScores.score, + LeaderboardScores.points_data.label("points_data"), + func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), + func.row_number() + .over(order_by=LeaderboardScores.score.desc()) + .label("number"), + ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id, + ) + .filter( + LeaderboardVersion.published == True, + LeaderboardVersion.version_number == latest_version, + ) + .filter(LeaderboardScores.leaderboard_id == leaderboard_id) + ) ranked_leaderboard = query.cte(name="ranked_leaderboard") @@ -1130,11 +1193,25 @@ def get_position( def get_leaderboard_positions( - db_session: Session, leaderboard_id, limit: int, offset: int + db_session: Session, + leaderboard_id, + limit: int, + offset: int, + version_number: Optional[int] = None, ) -> List[Row[Tuple[uuid.UUID, str, int, str, int]]]: """ Get the leaderboard positions """ + + # get public leaderboard scores with max version + + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) + + # Main query query = ( db_session.query( LeaderboardScores.id, @@ -1143,8 +1220,13 @@ def get_leaderboard_positions( LeaderboardScores.points_data, func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id, + ) .filter(LeaderboardScores.leaderboard_id == leaderboard_id) - .order_by(text("rank asc, id asc")) + .filter(LeaderboardVersion.published == True) + .filter(LeaderboardVersion.version_number == latest_version) ) if limit: @@ -1157,18 +1239,35 @@ def get_leaderboard_positions( def get_qurtiles( - db_session: Session, leaderboard_id + db_session: Session, leaderboard_id, version_number: Optional[int] = None ) -> Tuple[Row[Tuple[str, float, int]], ...]: """ Get the leaderboard qurtiles https://docs.sqlalchemy.org/en/14/core/functions.html#sqlalchemy.sql.functions.percentile_disc """ - query = db_session.query( - LeaderboardScores.address, - LeaderboardScores.score, - func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), - ).filter(LeaderboardScores.leaderboard_id == leaderboard_id) + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) + + query = ( + db_session.query( + LeaderboardScores.address, + LeaderboardScores.score, + func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), + ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id, + ) + .filter( + LeaderboardVersion.published == True, + LeaderboardVersion.version_number == latest_version, + ) + .filter(LeaderboardScores.leaderboard_id == leaderboard_id) + ) ranked_leaderboard = query.cte(name="ranked_leaderboard") @@ -1192,17 +1291,37 @@ def get_qurtiles( return q1, q2, q3 -def get_ranks(db_session: Session, leaderboard_id) -> List[Row[Tuple[int, int, int]]]: +def get_ranks( + db_session: Session, leaderboard_id, version_number: Optional[int] = None +) -> List[Row[Tuple[int, int, int]]]: """ Get the leaderboard rank buckets(rank, size, score) """ - query = db_session.query( - LeaderboardScores.id, - LeaderboardScores.address, - LeaderboardScores.score, - LeaderboardScores.points_data, - func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), - ).filter(LeaderboardScores.leaderboard_id == leaderboard_id) + + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) + + query = ( + db_session.query( + LeaderboardScores.id, + LeaderboardScores.address, + LeaderboardScores.score, + LeaderboardScores.points_data, + func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), + ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id, + ) + .filter( + LeaderboardVersion.published == True, + LeaderboardVersion.version_number == latest_version, + ) + .filter(LeaderboardScores.leaderboard_id == leaderboard_id) + ) ranked_leaderboard = query.cte(name="ranked_leaderboard") @@ -1220,10 +1339,18 @@ def get_rank( rank: int, limit: Optional[int] = None, offset: Optional[int] = None, + version_number: Optional[int] = None, ) -> List[Row[Tuple[uuid.UUID, str, int, str, int]]]: """ Get bucket in leaderboard by rank """ + + latest_version = leaderboard_version_filter( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version_number, + ) + query = ( db_session.query( LeaderboardScores.id, @@ -1232,6 +1359,14 @@ def get_rank( LeaderboardScores.points_data, func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), ) + .join( + LeaderboardVersion, + LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id, + ) + .filter( + LeaderboardVersion.published == True, + LeaderboardVersion.version_number == latest_version, + ) .filter(LeaderboardScores.leaderboard_id == leaderboard_id) .order_by(text("rank asc, id asc")) ) @@ -1377,7 +1512,7 @@ def add_scores( db_session: Session, leaderboard_id: uuid.UUID, scores: List[Score], - overwrite: bool = False, + version_number: int, normalize_addresses: bool = True, ): """ @@ -1397,16 +1532,6 @@ def add_scores( raise DuplicateLeaderboardAddressError("Dublicated addresses", duplicates) - if overwrite: - db_session.query(LeaderboardScores).filter( - LeaderboardScores.leaderboard_id == leaderboard_id - ).delete() - try: - db_session.commit() - except: - db_session.rollback() - raise LeaderboardDeleteScoresError("Error deleting leaderboard scores") - for score in scores: leaderboard_scores.append( { @@ -1414,6 +1539,7 @@ def add_scores( "address": normalizer_fn(score.address), "score": score.score, "points_data": score.points_data, + "leaderboard_version_number": version_number, } ) @@ -1675,3 +1801,137 @@ def check_leaderboard_resource_permissions( return True return False + + +def get_leaderboard_version( + db_session: Session, leaderboard_id: uuid.UUID, version_number: int +) -> LeaderboardVersion: + """ + Get the leaderboard version by id + """ + return ( + db_session.query(LeaderboardVersion) + .filter(LeaderboardVersion.leaderboard_id == leaderboard_id) + .filter(LeaderboardVersion.version_number == version_number) + .one() + ) + + +def create_leaderboard_version( + db_session: Session, + leaderboard_id: uuid.UUID, + version_number: Optional[int] = None, + publish: bool = False, +) -> LeaderboardVersion: + """ + Create a leaderboard version + """ + + if version_number is None: + latest_version_result = ( + db_session.query(func.max(LeaderboardVersion.version_number)) + .filter(LeaderboardVersion.leaderboard_id == leaderboard_id) + .one() + ) + + latest_version = latest_version_result[0] + + if latest_version is None: + version_number = 0 + else: + version_number = latest_version + 1 + + leaderboard_version = LeaderboardVersion( + leaderboard_id=leaderboard_id, + version_number=version_number, + published=publish, + ) + + db_session.add(leaderboard_version) + db_session.commit() + + return leaderboard_version + + +def change_publish_leaderboard_version_status( + db_session: Session, leaderboard_id: uuid.UUID, version_number: int, published: bool +) -> LeaderboardVersion: + """ + Publish a leaderboard version + """ + leaderboard_version = ( + db_session.query(LeaderboardVersion) + .filter(LeaderboardVersion.leaderboard_id == leaderboard_id) + .filter(LeaderboardVersion.version_number == version_number) + .one() + ) + + leaderboard_version.published = published + + db_session.commit() + + return leaderboard_version + + +def get_leaderboard_versions( + db_session: Session, leaderboard_id: uuid.UUID +) -> List[LeaderboardVersion]: + """ + Get all leaderboard versions + """ + return ( + db_session.query(LeaderboardVersion) + .filter(LeaderboardVersion.leaderboard_id == leaderboard_id) + .all() + ) + + +def delete_leaderboard_version( + db_session: Session, leaderboard_id: uuid.UUID, version_number: int +) -> LeaderboardVersion: + """ + Delete a leaderboard version + """ + leaderboard_version = ( + db_session.query(LeaderboardVersion) + .filter(LeaderboardVersion.leaderboard_id == leaderboard_id) + .filter(LeaderboardVersion.version_number == version_number) + .one() + ) + + db_session.delete(leaderboard_version) + db_session.commit() + + return leaderboard_version + + +def get_leaderboard_version_scores( + db_session: Session, + leaderboard_id: uuid.UUID, + version_number: int, + limit: int, + offset: int, +) -> List[LeaderboardScores]: + """ + Get the leaderboard scores by version number + """ + + query = ( + db_session.query( + LeaderboardScores.id, + LeaderboardScores.address.label("address"), + LeaderboardScores.score.label("score"), + LeaderboardScores.points_data.label("points_data"), + func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"), + ) + .filter(LeaderboardScores.leaderboard_id == leaderboard_id) + .filter(LeaderboardScores.leaderboard_version_number == version_number) + ) + + if limit: + query = query.limit(limit) + + if offset: + query = query.offset(offset) + + return query diff --git a/engineapi/engineapi/data.py b/engineapi/engineapi/data.py index 8a33d619..acf1fa6e 100644 --- a/engineapi/engineapi/data.py +++ b/engineapi/engineapi/data.py @@ -442,3 +442,11 @@ class LeaderboardConfigUpdate(BaseModel): query_name: Optional[str] = None params: Dict[str, int] normalize_addresses: Optional[bool] = None + + +class LeaderboardVersion(BaseModel): + leaderboard_id: UUID + version: int + published: bool + created_at: datetime + updated_at: datetime diff --git a/engineapi/engineapi/routes/leaderboard.py b/engineapi/engineapi/routes/leaderboard.py index c0e08cb8..f9bc4ef1 100644 --- a/engineapi/engineapi/routes/leaderboard.py +++ b/engineapi/engineapi/routes/leaderboard.py @@ -94,6 +94,7 @@ async def leaderboard( limit: int = Query(10), offset: int = Query(0), db_session: Session = Depends(db.yield_db_session), + version: Optional[str] = Query(None, description="Version of the leaderboard."), ) -> List[data.LeaderboardPosition]: """ Returns the leaderboard positions. @@ -112,7 +113,7 @@ async def leaderboard( raise EngineHTTPException(status_code=500, detail="Internal server error") leaderboard_positions = actions.get_leaderboard_positions( - db_session, leaderboard_id, limit, offset + db_session, leaderboard_id, limit, offset, version ) result = [ data.LeaderboardPosition( @@ -604,10 +605,6 @@ async def leaderboard_push_scores( scores: List[data.Score] = Body( ..., description="Scores to put to the leaderboard." ), - overwrite: bool = Query( - False, - description="If enabled, this will delete all current scores and replace them with the new scores provided.", - ), normalize_addresses: bool = Query( True, description="Normalize addresses to checksum." ), @@ -635,13 +632,22 @@ async def leaderboard_push_scores( status_code=403, detail="You don't have access to this leaderboard." ) + try: + new_version = actions.create_leaderboard_version( + db_session=db_session, + leaderboard_id=leaderboard_id, + ) + except Exception as e: + logger.error(f"Error while creating leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + try: leaderboard_points = actions.add_scores( db_session=db_session, leaderboard_id=leaderboard_id, scores=scores, - overwrite=overwrite, normalize_addresses=normalize_addresses, + version_number=new_version.version_number, ) except actions.DuplicateLeaderboardAddressError as e: raise EngineHTTPException( @@ -658,6 +664,17 @@ async def leaderboard_push_scores( logger.error(f"Score update failed with error: {e}") raise EngineHTTPException(status_code=500, detail="Score update failed.") + try: + actions.change_publish_leaderboard_version_status( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=new_version.version_number, + published=True, + ) + except Exception as e: + logger.error(f"Error while updating leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + result = [ data.LeaderboardScore( leaderboard_id=score["leaderboard_id"], @@ -881,3 +898,420 @@ async def leaderboard_config_deactivate( raise EngineHTTPException(status_code=500, detail="Internal server error") return True + + +@app.get( + "/{leaderboard_id}/versions", + response_model=List[data.LeaderboardVersion], + tags=["Authorized Endpoints"], +) +async def leaderboard_versions_list( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> List[data.LeaderboardVersion]: + """ + Get leaderboard versions list. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, + leaderboard_id=leaderboard_id, + token=token, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard version." + ) + + try: + leaderboard_versions = actions.get_leaderboard_versions( + db_session=db_session, + leaderboard_id=leaderboard_id, + ) + except Exception as e: + logger.error(f"Error while getting leaderboard versions list: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + result = [ + data.LeaderboardVersion( + leaderboard_id=version.leaderboard_id, + version=version.version_number, + published=version.published, + created_at=version.created_at, + updated_at=version.updated_at, + ) + for version in leaderboard_versions + ] + + return result + + +@app.get( + "/{leaderboard_id}/versions/{version}", + response_model=data.LeaderboardVersion, + tags=["Authorized Endpoints"], +) +async def leaderboard_version_handler( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Path(..., description="Version of the leaderboard."), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> data.LeaderboardVersion: + """ + Get leaderboard version. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, + leaderboard_id=leaderboard_id, + token=token, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard." + ) + + try: + leaderboard_version = actions.get_leaderboard_version( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + except Exception as e: + logger.error(f"Error while getting leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + return data.LeaderboardVersion( + leaderboard_id=leaderboard_version.leaderboard_id, + version=leaderboard_version.version_number, + published=leaderboard_version.published, + created_at=leaderboard_version.created_at, + updated_at=leaderboard_version.updated_at, + ) + + +@app.post( + "/{leaderboard_id}/versions", + response_model=data.LeaderboardVersion, + tags=["Authorized Endpoints"], +) +async def create_leaderboard_version( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Query(..., description="Version of the leaderboard."), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> data.LeaderboardVersion: + """ + Create leaderboard version. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, + leaderboard_id=leaderboard_id, + token=token, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard." + ) + + try: + leaderboard_version = actions.create_leaderboard_version( + db_session=db_session, + leaderboard_id=leaderboard_id, + version=version, + ) + except BugoutResponseException as e: + raise EngineHTTPException(status_code=e.status_code, detail=e.detail) + except actions.LeaderboardConfigNotFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard config not found.", + ) + except Exception as e: + logger.error(f"Error while creating leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + return leaderboard_version + + +@app.put( + "/{leaderboard_id}/versions/{version}", + response_model=data.LeaderboardVersion, + tags=["Authorized Endpoints"], +) +async def update_leaderboard_version_handler( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Path(..., description="Version of the leaderboard."), + publish: bool = Query( + False, + description="If enabled, this will publish the leaderboard version.", + ), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> data.LeaderboardVersion: + """ + Update leaderboard version. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, + leaderboard_id=leaderboard_id, + token=token, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard version." + ) + + try: + leaderboard_version = actions.change_publish_leaderboard_version_status( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version, + published=publish, + ) + except Exception as e: + logger.error(f"Error while updating leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + return data.LeaderboardVersion( + leaderboard_id=leaderboard_version.leaderboard_id, + version=leaderboard_version.version_number, + published=leaderboard_version.published, + created_at=leaderboard_version.created_at, + updated_at=leaderboard_version.updated_at, + ) + + +@app.delete( + "/{leaderboard_id}/versions/{version}", + response_model=data.LeaderboardVersion, + tags=["Authorized Endpoints"], +) +async def delete_leaderboard_version_handler( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Path(..., description="Version of the leaderboard."), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> data.LeaderboardVersion: + """ + Delete leaderboard version. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, leaderboard_id=leaderboard_id, token=token + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard version." + ) + + try: + leaderboard_version = actions.delete_leaderboard_version( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + except Exception as e: + logger.error(f"Error while deleting leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + return data.LeaderboardVersion( + leaderboard_id=leaderboard_version.leaderboard_id, + version=leaderboard_version.version_number, + published=leaderboard_version.published, + created_at=leaderboard_version.created_at, + updated_at=leaderboard_version.updated_at, + ) + + +@app.get( + "/{leaderboard_id}/versions/{version}/scores", + response_model=List[data.LeaderboardPosition], + tags=["Authorized Endpoints"], +) +async def leaderboard_version_scores_handler( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Path(..., description="Version of the leaderboard."), + limit: int = Query(10), + offset: int = Query(0), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> List[data.LeaderboardPosition]: + """ + Get leaderboard version scores. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, leaderboard_id=leaderboard_id, token=token + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard version." + ) + + try: + leaderboard_version_scores = actions.get_leaderboard_version_scores( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version, + limit=limit, + offset=offset, + ) + except Exception as e: + logger.error(f"Error while getting leaderboard version scores: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + result = [ + data.LeaderboardPosition( + address=score.address, + score=score.score, + rank=score.rank, + points_data=score.points_data, + ) + for score in leaderboard_version_scores + ] + + return result + + +@app.put( + "/{leaderboard_id}/versions/{version}/scores", + response_model=List[data.LeaderboardScore], + tags=["Authorized Endpoints"], +) +async def leaderboard_version_push_scores_handler( + request: Request, + leaderboard_id: UUID = Path(..., description="Leaderboard ID"), + version: int = Path(..., description="Version of the leaderboard."), + scores: List[data.Score] = Body( + ..., description="Scores to put to the leaderboard version." + ), + normalize_addresses: bool = Query( + True, description="Normalize addresses to checksum." + ), + db_session: Session = Depends(db.yield_db_session), + Authorization: str = AuthHeader, +) -> List[data.LeaderboardScore]: + """ + Put the leaderboard version to the database. + """ + token = request.state.token + try: + access = actions.check_leaderboard_resource_permissions( + db_session=db_session, leaderboard_id=leaderboard_id, token=token + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + + if not access: + raise EngineHTTPException( + status_code=403, detail="You don't have access to this leaderboard version." + ) + + try: + leaderboard_version = actions.get_leaderboard_version( + db_session=db_session, + leaderboard_id=leaderboard_id, + version_number=version, + ) + except NoResultFound as e: + raise EngineHTTPException( + status_code=404, + detail="Leaderboard version not found.", + ) + except Exception as e: + logger.error(f"Error while getting leaderboard version: {e}") + raise EngineHTTPException(status_code=500, detail="Internal server error") + + try: + leaderboard_points = actions.add_scores( + db_session=db_session, + leaderboard_id=leaderboard_id, + scores=scores, + normalize_addresses=normalize_addresses, + version_number=leaderboard_version.version_number, + ) + except actions.DuplicateLeaderboardAddressError as e: + raise EngineHTTPException( + status_code=409, + detail=f"Duplicates in push to database is disallowed.\n List of duplicates:{e.duplicates}.\n Please handle duplicates manualy.", + ) + except Exception as e: + logger.error(f"Score update failed with error: {e}") + raise EngineHTTPException(status_code=500, detail="Score update failed.") + + result = [ + data.LeaderboardScore( + leaderboard_id=score["leaderboard_id"], + address=score["address"], + score=score["score"], + points_data=score["points_data"], + ) + for score in leaderboard_points + ] + + return result