Merge pull request #966 from moonstream-to/leaderboard-versions

Leaderboard versions
pull/975/head
Neeraj Kashyap 2023-11-21 11:03:11 -08:00 zatwierdzone przez GitHub
commit 44a79272df
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
9 zmienionych plików z 1323 dodań i 104 usunięć

Wyświetl plik

@ -1,43 +1,9 @@
# lootbox
## `client`
Use lootboxes in your game economy with ready to use contracts
This repository contains a lightweight Python client for the Engine API.
## Deployment
Deployment with local signer server
To use, for example, with Leaderboard API:
```bash
MOONSTREAM_SIGNING_SERVER_IP=127.0.0.1 ./dev.sh
```
## Run frontend
Do from root directory workspace directory:
Engine:
Run dev
```
yarn workspace engine run dev
```
Build
```
yarn workspace engine run build
```
Player:
Run dev
```
yarn workspace player run dev
```
Build
```
yarn workspace player run build
python -m client.leaderboards -h
```

Wyświetl plik

@ -0,0 +1,154 @@
"""Added leaderboard_versions table and corresponding constraints
Revision ID: cc80e886e153
Revises: 040f2dfde5a5
Create Date: 2023-11-08 16:16:39.265150
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "cc80e886e153"
down_revision = "040f2dfde5a5"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"leaderboard_versions",
sa.Column("leaderboard_id", sa.UUID(), nullable=False),
sa.Column("version_number", sa.DECIMAL(), nullable=False),
sa.Column("published", sa.Boolean(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("TIMEZONE('utc', statement_timestamp())"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("TIMEZONE('utc', statement_timestamp())"),
nullable=False,
),
sa.ForeignKeyConstraint(
["leaderboard_id"],
["leaderboards.id"],
name=op.f("fk_leaderboard_versions_leaderboard_id_leaderboards"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"leaderboard_id", "version_number", name=op.f("pk_leaderboard_versions")
),
sa.UniqueConstraint(
"leaderboard_id",
"version_number",
name=op.f("uq_leaderboard_versions_leaderboard_id"),
),
)
op.create_index(
op.f("ix_leaderboard_versions_created_at"),
"leaderboard_versions",
["created_at"],
unique=False,
)
op.add_column(
"leaderboard_scores",
sa.Column("leaderboard_version_number", sa.DECIMAL(), nullable=True),
)
op.drop_constraint(
"uq_leaderboard_scores_leaderboard_id", "leaderboard_scores", type_="unique"
)
op.create_unique_constraint(
op.f("uq_leaderboard_scores_leaderboard_id"),
"leaderboard_scores",
["leaderboard_id", "address", "leaderboard_version_number"],
)
op.drop_constraint(
"fk_leaderboard_scores_leaderboard_id_leaderboards",
"leaderboard_scores",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_leaderboard_scores_leaderboard_id_leaderboard_versions"),
"leaderboard_scores",
"leaderboard_versions",
["leaderboard_id", "leaderboard_version_number"],
["leaderboard_id", "version_number"],
ondelete="CASCADE",
)
# ### end Alembic commands ###
# Insert version 0 for all existing leaderboards
op.execute(
"""
INSERT INTO leaderboard_versions (leaderboard_id, version_number, published)
SELECT id, 0, true FROM leaderboards
"""
)
# Set the leaderboard_version_number for all existing scores to the version 0
op.execute(
"""
UPDATE leaderboard_scores SET leaderboard_version_number = 0
"""
)
# Alter leaderboard_scores to make leaderboard_version_number non-nullable
op.alter_column(
"leaderboard_scores",
"leaderboard_version_number",
nullable=False,
)
def downgrade():
op.execute(
"""
WITH latest_version_for_leaderboard AS (
SELECT leaderboard_id, MAX(version_number) AS latest_version
FROM leaderboard_versions WHERE published = true
GROUP BY leaderboard_id
)
DELETE FROM leaderboard_scores WHERE
(leaderboard_id, leaderboard_version_number) NOT IN (
SELECT
leaderboard_id,
latest_version AS leaderboard_version_number
FROM
latest_version_for_leaderboard
)
"""
)
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
op.f("fk_leaderboard_scores_leaderboard_id_leaderboard_versions"),
"leaderboard_scores",
type_="foreignkey",
)
op.create_foreign_key(
"fk_leaderboard_scores_leaderboard_id_leaderboards",
"leaderboard_scores",
"leaderboards",
["leaderboard_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
op.f("uq_leaderboard_scores_leaderboard_id"),
"leaderboard_scores",
type_="unique",
)
op.create_unique_constraint(
"uq_leaderboard_scores_leaderboard_id",
"leaderboard_scores",
["leaderboard_id", "address"],
)
op.drop_column("leaderboard_scores", "leaderboard_version_number")
op.drop_index(
op.f("ix_leaderboard_versions_created_at"), table_name="leaderboard_versions"
)
op.drop_table("leaderboard_versions")
# ### end Alembic commands ###

Wyświetl plik

Wyświetl plik

@ -0,0 +1,234 @@
import argparse
import json
import os
import sys
from typing import Optional
import uuid
import requests
LEADERBOARD_API_URL = os.environ.get(
"LEADERBOARD_API_URL", "http://localhost:7191/leaderboard/"
)
def moonstream_access_token(value: Optional[str]) -> uuid.UUID:
if value is None:
value = os.environ.get("MOONSTREAM_ACCESS_TOKEN")
if value is None:
raise ValueError(
"Moonstream access token is required: either via -A/--authorization, or via the MOONSTREAM_ACCESS_TOKEN environment variable"
)
try:
value_uuid = uuid.UUID(value)
except Exception:
raise ValueError("Moonstream access token must be a valid UUID")
return value_uuid
def requires_authorization(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-A",
"--authorization",
type=moonstream_access_token,
required=False,
default=os.environ.get("MOONSTREAM_ACCESS_TOKEN"),
help="Moonstream API access token (if not provided, must be specified using the MOONSTREAM_ACCESS_TOKEN environment variable)",
)
def handle_get(args: argparse.Namespace) -> None:
url = LEADERBOARD_API_URL
params = {
"leaderboard_id": str(args.id),
"limit": str(args.limit),
"offset": str(args.offset),
}
if args.version is not None:
params["version"] = str(args.version)
response = requests.get(url, params=params)
response.raise_for_status()
print(json.dumps(response.json()))
def handle_create(args: argparse.Namespace) -> None:
url = LEADERBOARD_API_URL
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {str(args.authorization)}",
}
body = {
"title": args.title,
"description": args.description,
}
response = requests.post(url, headers=headers, json=body)
response.raise_for_status()
print(json.dumps(response.json()))
def handle_versions(args: argparse.Namespace) -> None:
url = f"{LEADERBOARD_API_URL}{args.id}/versions"
headers = {
"Authorization": f"Bearer {str(args.authorization)}",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
print(json.dumps(response.json()))
def handle_create_version(args: argparse.Namespace) -> None:
url = f"{LEADERBOARD_API_URL}{args.id}/versions"
headers = {
"Authorization": f"Bearer {str(args.authorization)}",
"Content-Type": "application/json",
}
body = {
"publish": args.publish,
}
response = requests.post(url, headers=headers, json=body)
response.raise_for_status()
print(json.dumps(response.json()))
def handle_publish(args: argparse.Namespace) -> None:
url = f"{LEADERBOARD_API_URL}{args.id}/versions/{args.version}"
headers = {
"Authorization": f"Bearer {str(args.authorization)}",
"Content-Type": "application/json",
}
body = {
"publish": args.publish,
}
response = requests.put(url, headers=headers, json=body)
response.raise_for_status()
print(json.dumps(response.json()))
def handle_upload_scores(args: argparse.Namespace) -> None:
url = f"{LEADERBOARD_API_URL}{args.id}/scores"
if args.version is not None:
url = f"{LEADERBOARD_API_URL}{args.id}/versions/{args.version}/scores"
params = {
"overwrite": "true",
"normalize_addresses": "false",
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {str(args.authorization)}",
}
if args.scores is None:
args.scores = sys.stdin
with args.scores as ifp:
body = json.load(ifp)
response = requests.put(url, headers=headers, params=params, json=body)
response.raise_for_status()
print(json.dumps(response.json()))
def generate_cli() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="HTTP client for Leaderboard API")
parser.set_defaults(func=lambda _: parser.print_help())
subparsers = parser.add_subparsers()
# GET /leaderboard/?leaderboard_id=<id>&limit=<limit>&offset=<offset>&version=<version>
get_parser = subparsers.add_parser("get")
get_parser.add_argument("-i", "--id", type=uuid.UUID, required=True)
get_parser.add_argument("-l", "--limit", type=int, default=10)
get_parser.add_argument("-o", "--offset", type=int, default=0)
get_parser.add_argument("-v", "--version", type=int, default=None)
get_parser.set_defaults(func=handle_get)
# POST /leaderboard/
create_parser = subparsers.add_parser("create")
create_parser.add_argument(
"-t", "--title", type=str, required=True, help="Title for leaderboard"
)
create_parser.add_argument(
"-d",
"--description",
type=str,
required=False,
default="",
help="Description for leaderboard",
)
requires_authorization(create_parser)
create_parser.set_defaults(func=handle_create)
# GET /leaderboard/<id>/versions
versions_parser = subparsers.add_parser("versions")
versions_parser.add_argument("-i", "--id", type=uuid.UUID, required=True)
requires_authorization(versions_parser)
versions_parser.set_defaults(func=handle_versions)
# POST /leaderboard/<id>/versions
create_version_parser = subparsers.add_parser("create-version")
create_version_parser.add_argument("-i", "--id", type=uuid.UUID, required=True)
create_version_parser.add_argument(
"--publish",
action="store_true",
help="Set this flag to publish the version immediately upon creation",
)
requires_authorization(create_version_parser)
create_version_parser.set_defaults(func=handle_create_version)
# PUT /leaderboard/<id>/versions/<version>
publish_parser = subparsers.add_parser("publish")
publish_parser.add_argument("-i", "--id", type=uuid.UUID, required=True)
publish_parser.add_argument("-v", "--version", type=int, required=True)
publish_parser.add_argument(
"--publish", action="store_true", help="Set to publish, leave to unpublish"
)
requires_authorization(publish_parser)
publish_parser.set_defaults(func=handle_publish)
# PUT /leaderboard/<id>/scores and PUT /leaderboard/<id>/versions/<version>/scores
upload_scores_parser = subparsers.add_parser("upload-scores")
upload_scores_parser.add_argument("-i", "--id", type=uuid.UUID, required=True)
upload_scores_parser.add_argument(
"-v",
"--version",
type=int,
required=False,
default=None,
help="Specify a version to upload scores to (if not specified a new version is created)",
)
upload_scores_parser.add_argument(
"-s",
"--scores",
type=argparse.FileType("r"),
required=False,
default=None,
help="Path to scores file. If not provided, reads from stdin.",
)
upload_scores_parser.set_defaults(func=handle_upload_scores)
requires_authorization(upload_scores_parser)
return parser
if __name__ == "__main__":
parser = generate_cli()
args = parser.parse_args()
args.func(args)

Wyświetl plik

@ -0,0 +1,10 @@
[
{
"address": "0x0000000000000000000000000000000000000000",
"score": 19,
"points_data": {
"secondary_score_1": 7,
"secondary_score_2": 29
}
}
]

Wyświetl plik

@ -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_, and_, 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,24 +964,71 @@ def refetch_drop_signatures(
return claimant_objects
def get_leaderboard_total_count(db_session: Session, leaderboard_id) -> int:
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 version_number is None:
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, version_number: Optional[int] = None
) -> int:
"""
Get the total number of claimants in the leaderboard
Get the total number of position in the leaderboard
"""
return (
db_session.query(LeaderboardScores)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
.count()
latest_version = leaderboard_version_filter(
db_session=db_session,
leaderboard_id=leaderboard_id,
version_number=version_number,
)
total_count = (
db_session.query(func.count(LeaderboardScores.id))
.join(
LeaderboardVersion,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.filter(
LeaderboardVersion.published == True,
LeaderboardVersion.version_number == latest_version,
)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
).scalar()
return total_count
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,10 +1042,22 @@ def get_leaderboard_info(
LeaderboardScores.leaderboard_id == Leaderboard.id,
isouter=True,
)
.join(
LeaderboardVersion,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
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()
)
).one()
return leaderboard
@ -1078,19 +1142,48 @@ 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,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.filter(
LeaderboardVersion.published == True,
LeaderboardVersion.version_number == latest_version,
)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
)
ranked_leaderboard = query.cte(name="ranked_leaderboard")
@ -1130,11 +1223,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 +1250,17 @@ def get_leaderboard_positions(
LeaderboardScores.points_data,
func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"),
)
.join(
LeaderboardVersion,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.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 +1273,39 @@ 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,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.filter(
LeaderboardVersion.published == True,
LeaderboardVersion.version_number == latest_version,
)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
)
ranked_leaderboard = query.cte(name="ranked_leaderboard")
@ -1192,17 +1329,41 @@ 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,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.filter(
LeaderboardVersion.published == True,
LeaderboardVersion.version_number == latest_version,
)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
)
ranked_leaderboard = query.cte(name="ranked_leaderboard")
@ -1220,10 +1381,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 +1401,18 @@ def get_rank(
LeaderboardScores.points_data,
func.rank().over(order_by=LeaderboardScores.score.desc()).label("rank"),
)
.join(
LeaderboardVersion,
and_(
LeaderboardVersion.leaderboard_id == LeaderboardScores.leaderboard_id,
LeaderboardVersion.version_number
== LeaderboardScores.leaderboard_version_number,
),
)
.filter(
LeaderboardVersion.published == True,
LeaderboardVersion.version_number == latest_version,
)
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
.order_by(text("rank asc, id asc"))
)
@ -1377,7 +1558,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 +1578,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,13 +1585,18 @@ def add_scores(
"address": normalizer_fn(score.address),
"score": score.score,
"points_data": score.points_data,
"leaderboard_version_number": version_number,
}
)
insert_statement = insert(LeaderboardScores).values(leaderboard_scores)
result_stmt = insert_statement.on_conflict_do_update(
index_elements=[LeaderboardScores.address, LeaderboardScores.leaderboard_id],
index_elements=[
LeaderboardScores.address,
LeaderboardScores.leaderboard_id,
LeaderboardScores.leaderboard_version_number,
],
set_=dict(
score=insert_statement.excluded.score,
points_data=insert_statement.excluded.points_data,
@ -1436,7 +1612,7 @@ def add_scores(
return leaderboard_scores
# leadrboard access actions
# leaderboard access actions
def create_leaderboard_resource(
@ -1675,3 +1851,159 @@ 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
def delete_previous_versions(
db_session: Session,
leaderboard_id: uuid.UUID,
threshold_version_number: int,
) -> int:
"""
Delete old leaderboard versions
"""
versions_to_delete = (
db_session.query(LeaderboardVersion)
.filter(LeaderboardVersion.leaderboard_id == leaderboard_id)
.filter(LeaderboardVersion.version_number < threshold_version_number)
)
num_deleted = versions_to_delete.delete(synchronize_session=False)
db_session.commit()
return num_deleted

Wyświetl plik

@ -442,3 +442,15 @@ 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
class LeaderboardVersionRequest(BaseModel):
publish: bool

Wyświetl plik

@ -13,6 +13,7 @@ from sqlalchemy import (
MetaData,
String,
UniqueConstraint,
ForeignKeyConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.ext.compiler import compiles
@ -357,9 +358,45 @@ class Leaderboard(Base): # type: ignore
)
class LeaderboardVersion(Base): # type: ignore
__tablename__ = "leaderboard_versions"
__table_args__ = (UniqueConstraint("leaderboard_id", "version_number"),)
leaderboard_id = Column(
UUID(as_uuid=True),
ForeignKey("leaderboards.id", ondelete="CASCADE"),
primary_key=True,
nullable=False,
)
version_number = Column(DECIMAL, primary_key=True, nullable=False)
published = Column(Boolean, default=False, nullable=False)
created_at = Column(
DateTime(timezone=True),
server_default=utcnow(),
nullable=False,
index=True,
)
updated_at = Column(
DateTime(timezone=True),
server_default=utcnow(),
onupdate=utcnow(),
nullable=False,
)
class LeaderboardScores(Base): # type: ignore
__tablename__ = "leaderboard_scores"
__table_args__ = (UniqueConstraint("leaderboard_id", "address"),)
__table_args__ = (
UniqueConstraint("leaderboard_id", "address", "leaderboard_version_number"),
ForeignKeyConstraint(
["leaderboard_id", "leaderboard_version_number"],
[
"leaderboard_versions.leaderboard_id",
"leaderboard_versions.version_number",
],
ondelete="CASCADE",
),
)
id = Column(
UUID(as_uuid=True),
@ -370,7 +407,10 @@ class LeaderboardScores(Base): # type: ignore
)
leaderboard_id = Column(
UUID(as_uuid=True),
ForeignKey("leaderboards.id", ondelete="CASCADE"),
nullable=False,
)
leaderboard_version_number = Column(
DECIMAL,
nullable=False,
)
address = Column(VARCHAR(256), nullable=False, index=True)

Wyświetl plik

@ -48,7 +48,7 @@ AuthHeader = Header(
)
leaderboad_whitelist = {
leaderboard_whitelist = {
f"/leaderboard/{DOCS_TARGET_PATH}": "GET",
"/leaderboard/openapi.json": "GET",
"/leaderboard/info": "GET",
@ -76,7 +76,7 @@ app = FastAPI(
)
app.add_middleware(ExtractBearerTokenMiddleware, whitelist=leaderboad_whitelist)
app.add_middleware(ExtractBearerTokenMiddleware, whitelist=leaderboard_whitelist)
app.add_middleware(
CORSMiddleware,
@ -87,13 +87,19 @@ app.add_middleware(
)
@app.get("", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"])
@app.get(
"",
response_model=List[data.LeaderboardPosition],
tags=["Public Endpoints"],
include_in_schema=False,
)
@app.get("/", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"])
async def leaderboard(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
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 +118,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(
@ -128,7 +134,10 @@ async def leaderboard(
@app.post(
"", response_model=data.LeaderboardCreatedResponse, tags=["Authorized Endpoints"]
"",
response_model=data.LeaderboardCreatedResponse,
tags=["Authorized Endpoints"],
include_in_schema=False,
)
@app.post(
"/", response_model=data.LeaderboardCreatedResponse, tags=["Authorized Endpoints"]
@ -346,6 +355,7 @@ async def get_leaderboards(
)
async def count_addresses(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
db_session: Session = Depends(db.yield_db_session),
) -> data.CountAddressesResponse:
"""
@ -364,7 +374,7 @@ async def count_addresses(
logger.error(f"Error while getting leaderboard: {e}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
count = actions.get_leaderboard_total_count(db_session, leaderboard_id)
count = actions.get_leaderboard_total_count(db_session, leaderboard_id, version)
return data.CountAddressesResponse(count=count)
@ -375,12 +385,13 @@ async def count_addresses(
async def leadeboard_info(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
db_session: Session = Depends(db.yield_db_session),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
) -> data.LeaderboardInfoResponse:
"""
Returns leaderboard info.
"""
try:
leaderboard = actions.get_leaderboard_info(db_session, leaderboard_id)
leaderboard = actions.get_leaderboard_info(db_session, leaderboard_id, version)
except NoResultFound as e:
raise EngineHTTPException(
status_code=404,
@ -434,6 +445,7 @@ async def get_scores_changes(
async def quartiles(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
db_session: Session = Depends(db.yield_db_session),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
) -> data.QuartilesResponse:
"""
Returns the quartiles of the leaderboard.
@ -451,7 +463,7 @@ async def quartiles(
raise EngineHTTPException(status_code=500, detail="Internal server error")
try:
q1, q2, q3 = actions.get_qurtiles(db_session, leaderboard_id)
q1, q2, q3 = actions.get_qurtiles(db_session, leaderboard_id, version)
except actions.LeaderboardIsEmpty:
raise EngineHTTPException(status_code=204, detail="Leaderboard is empty.")
@ -480,6 +492,7 @@ async def position(
normalize_addresses: bool = Query(
True, description="Normalize addresses to checksum."
),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
db_session: Session = Depends(db.yield_db_session),
) -> List[data.LeaderboardPosition]:
"""
@ -503,7 +516,13 @@ async def position(
address = Web3.toChecksumAddress(address)
positions = actions.get_position(
db_session, leaderboard_id, address, window_size, limit, offset
db_session,
leaderboard_id,
address,
window_size,
limit,
offset,
version,
)
results = [
@ -527,6 +546,7 @@ async def rank(
rank: int = Query(1, description="Rank to get."),
limit: Optional[int] = Query(None),
offset: Optional[int] = Query(None),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
db_session: Session = Depends(db.yield_db_session),
) -> List[data.LeaderboardPosition]:
"""
@ -546,7 +566,12 @@ async def rank(
raise EngineHTTPException(status_code=500, detail="Internal server error")
leaderboard_rank = actions.get_rank(
db_session, leaderboard_id, rank, limit=limit, offset=offset
db_session,
leaderboard_id,
rank,
limit=limit,
offset=offset,
version_number=version,
)
results = [
data.LeaderboardPosition(
@ -563,6 +588,7 @@ async def rank(
@app.get("/ranks", response_model=List[data.RanksResponse], tags=["Public Endpoints"])
async def ranks(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
version: Optional[int] = Query(None, description="Version of the leaderboard."),
db_session: Session = Depends(db.yield_db_session),
) -> List[data.RanksResponse]:
"""
@ -581,7 +607,7 @@ async def ranks(
logger.error(f"Error while getting leaderboard: {e}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
ranks = actions.get_ranks(db_session, leaderboard_id)
ranks = actions.get_ranks(db_session, leaderboard_id, version)
results = [
data.RanksResponse(
score=rank.score,
@ -604,10 +630,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 +657,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 +689,27 @@ 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")
try:
actions.delete_previous_versions(
db_session=db_session,
leaderboard_id=leaderboard_id,
threshold_version_number=new_version.version_number,
)
except Exception as e:
logger.error(f"Error while deleting leaderboard versions: {e}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
result = [
data.LeaderboardScore(
leaderboard_id=score["leaderboard_id"],
@ -881,3 +933,422 @@ 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"),
db_session: Session = Depends(db.yield_db_session),
request_body: data.LeaderboardVersionRequest = Body(
...,
description="JSON object specifying whether to publish or unpublish version.",
),
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:
new_version = actions.create_leaderboard_version(
db_session=db_session,
leaderboard_id=leaderboard_id,
publish=request_body.publish,
)
except Exception as e:
logger.error(f"Error while creating leaderboard version: {e}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
return data.LeaderboardVersion(
leaderboard_id=new_version.leaderboard_id,
version=new_version.version_number,
published=new_version.published,
created_at=new_version.created_at,
updated_at=new_version.updated_at,
)
@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."),
request_body: data.LeaderboardVersionRequest = Body(
...,
description="JSON object specifying whether to publish or unpublish 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=request_body.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