Skip to content

Commit 44c0634

Browse files
guillaumetaverniershaihhBrzuszek Maëlmaximeroucherfoucblg
authored
Flappy Bird Module (#109)
### Description Please explain the changes you made here. ### Checklist - [x] Created tests which fail without the change (if possible) - [x] All tests passing - [ ] Extended the documentation, if necessary --------- Co-authored-by: shaihh <[email protected]> Co-authored-by: Brzuszek Maël <[email protected]> Co-authored-by: Maxime Roucher <[email protected]> Co-authored-by: Foucauld Bellanger <[email protected]>
1 parent 2fd8740 commit 44c0634

File tree

7 files changed

+380
-0
lines changed

7 files changed

+380
-0
lines changed

app/modules/flappybird/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import logging
2+
3+
from sqlalchemy import and_, func, select
4+
from sqlalchemy.exc import IntegrityError
5+
from sqlalchemy.ext.asyncio import AsyncSession
6+
from sqlalchemy.orm import selectinload
7+
8+
from app.modules.flappybird import models_flappybird
9+
10+
hyperion_logger = logging.getLogger("hyperion.error")
11+
12+
13+
async def get_flappybird_score_leaderboard(
14+
db: AsyncSession,
15+
skip: int,
16+
limit: int,
17+
) -> list[models_flappybird.FlappyBirdScore]:
18+
"""Return the flappybird leaderboard scores from postion skip to skip+limit"""
19+
subquery = (
20+
select(
21+
func.max(models_flappybird.FlappyBirdScore.value).label("max_score"),
22+
models_flappybird.FlappyBirdScore.user_id,
23+
)
24+
.group_by(models_flappybird.FlappyBirdScore.user_id)
25+
.alias("subquery")
26+
)
27+
28+
result = await db.execute(
29+
select(models_flappybird.FlappyBirdScore)
30+
.join(
31+
subquery,
32+
and_(
33+
models_flappybird.FlappyBirdScore.user_id == subquery.c.user_id,
34+
models_flappybird.FlappyBirdScore.value == subquery.c.max_score,
35+
),
36+
)
37+
.options(selectinload(models_flappybird.FlappyBirdScore.user))
38+
.order_by(models_flappybird.FlappyBirdScore.value.desc())
39+
.offset(skip)
40+
.limit(limit),
41+
)
42+
return list(result.scalars().all())
43+
44+
45+
async def get_flappybird_personal_best_by_user_id(
46+
db: AsyncSession,
47+
user_id: str,
48+
) -> models_flappybird.FlappyBirdScore | None:
49+
"""Return the flappybird PB in the leaderboard by user_id"""
50+
51+
personal_best_result = await db.execute(
52+
select(models_flappybird.FlappyBirdScore)
53+
.where(models_flappybird.FlappyBirdScore.user_id == user_id)
54+
.order_by(models_flappybird.FlappyBirdScore.value.desc())
55+
.limit(1),
56+
)
57+
return personal_best_result.scalar()
58+
59+
60+
async def get_flappybird_score_position(
61+
db: AsyncSession,
62+
score_value: int,
63+
) -> int | None:
64+
"""Return the position in the leaderboard of a given score value"""
65+
subquery = (
66+
select(
67+
func.max(models_flappybird.FlappyBirdScore.value).label("max_score"),
68+
models_flappybird.FlappyBirdScore.user_id,
69+
)
70+
.group_by(models_flappybird.FlappyBirdScore.user_id)
71+
.alias("subquery")
72+
)
73+
74+
result = await db.execute(
75+
select(func.count())
76+
.select_from(subquery)
77+
.where(subquery.c.max_score >= score_value),
78+
)
79+
80+
return result.scalar()
81+
82+
83+
async def create_flappybird_score(
84+
db: AsyncSession,
85+
flappybird_score: models_flappybird.FlappyBirdScore,
86+
) -> models_flappybird.FlappyBirdScore:
87+
"""Add a FlappyBirdScore in database"""
88+
db.add(flappybird_score)
89+
try:
90+
await db.commit()
91+
return flappybird_score
92+
except IntegrityError as error:
93+
await db.rollback()
94+
raise ValueError(error)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import uuid
2+
from datetime import UTC, datetime
3+
4+
from fastapi import Depends, HTTPException
5+
from sqlalchemy.ext.asyncio import AsyncSession
6+
7+
from app.core import models_core
8+
from app.core.groups.groups_type import GroupType
9+
from app.core.module import Module
10+
from app.dependencies import get_db, is_user_a_member
11+
from app.modules.flappybird import (
12+
cruds_flappybird,
13+
models_flappybird,
14+
schemas_flappybird,
15+
)
16+
17+
module = Module(
18+
root="flappybird",
19+
tag="Flappy Bird",
20+
default_allowed_groups_ids=[GroupType.student],
21+
)
22+
23+
24+
@module.router.get(
25+
"/flappybird/scores",
26+
response_model=list[schemas_flappybird.FlappyBirdScoreInDB],
27+
status_code=200,
28+
)
29+
async def get_flappybird_score(
30+
skip: int = 0,
31+
limit: int = 10,
32+
db: AsyncSession = Depends(get_db),
33+
):
34+
"""Return the leaderboard score of the skip...limit"""
35+
leaderboard = await cruds_flappybird.get_flappybird_score_leaderboard(
36+
db=db,
37+
skip=skip,
38+
limit=limit,
39+
)
40+
return leaderboard
41+
42+
43+
@module.router.get(
44+
"/flappybird/scores/me",
45+
status_code=200,
46+
response_model=schemas_flappybird.FlappyBirdScoreCompleteFeedBack,
47+
)
48+
async def get_current_user_flappybird_personal_best(
49+
db: AsyncSession = Depends(get_db),
50+
user: models_core.CoreUser = Depends(is_user_a_member),
51+
):
52+
user_personal_best_table = (
53+
await cruds_flappybird.get_flappybird_personal_best_by_user_id(
54+
db=db,
55+
user_id=user.id,
56+
)
57+
)
58+
59+
if user_personal_best_table is None:
60+
raise HTTPException(
61+
status_code=404,
62+
detail="Not found",
63+
)
64+
65+
position = await cruds_flappybird.get_flappybird_score_position(
66+
db=db,
67+
score_value=user_personal_best_table.value,
68+
)
69+
if position is None:
70+
raise HTTPException(
71+
status_code=404,
72+
detail="Not found",
73+
)
74+
user_personal_best = schemas_flappybird.FlappyBirdScoreCompleteFeedBack(
75+
value=user_personal_best_table.value,
76+
user=user_personal_best_table.user,
77+
creation_time=user_personal_best_table.creation_time,
78+
position=position,
79+
)
80+
81+
return user_personal_best
82+
83+
84+
@module.router.post(
85+
"/flappybird/scores",
86+
response_model=schemas_flappybird.FlappyBirdScoreBase,
87+
status_code=201,
88+
)
89+
async def create_flappybird_score(
90+
flappybird_score: schemas_flappybird.FlappyBirdScoreBase,
91+
user: models_core.CoreUser = Depends(is_user_a_member),
92+
db: AsyncSession = Depends(get_db),
93+
):
94+
# Currently, flappybird_score is a schema instance
95+
# To add it to the database, we need to create a model
96+
97+
# We need to generate a new UUID for the score
98+
score_id = uuid.uuid4()
99+
# And get the current date and time
100+
creation_time = datetime.now(UTC)
101+
102+
db_flappybird_score = models_flappybird.FlappyBirdScore(
103+
id=score_id,
104+
user_id=user.id,
105+
value=flappybird_score.value,
106+
creation_time=creation_time,
107+
# We add all informations contained in the schema
108+
)
109+
try:
110+
return await cruds_flappybird.create_flappybird_score(
111+
flappybird_score=db_flappybird_score,
112+
db=db,
113+
)
114+
except ValueError as error:
115+
raise HTTPException(status_code=400, detail=str(error))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from datetime import datetime
2+
3+
from sqlalchemy import ForeignKey, String
4+
from sqlalchemy.orm import Mapped, mapped_column, relationship
5+
6+
from app.core.models_core import CoreUser
7+
from app.types.sqlalchemy import Base, PrimaryKey
8+
9+
10+
class FlappyBirdScore(Base):
11+
__tablename__ = "flappy-bird_score"
12+
13+
# id: Mapped[str] = mapped_column(String, primary_key=True, index=True)
14+
id: Mapped[PrimaryKey]
15+
user_id: Mapped[str] = mapped_column(
16+
String,
17+
ForeignKey("core_user.id"),
18+
nullable=False,
19+
)
20+
user: Mapped[CoreUser] = relationship("CoreUser")
21+
value: Mapped[int]
22+
creation_time: Mapped[datetime]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
from pydantic import BaseModel
5+
6+
from app.core.schemas_core import CoreUserSimple
7+
8+
9+
class FlappyBirdScoreBase(BaseModel):
10+
value: int
11+
12+
13+
class FlappyBirdScore(FlappyBirdScoreBase):
14+
user: CoreUserSimple
15+
creation_time: datetime
16+
17+
18+
class FlappyBirdScoreInDB(FlappyBirdScore):
19+
id: uuid.UUID
20+
user_id: str
21+
22+
23+
class FlappyBirdScoreCompleteFeedBack(FlappyBirdScore):
24+
"""
25+
A score, with it's position in the best players leaderboard
26+
"""
27+
28+
position: int
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""flappybird
2+
3+
Create Date: 2024-05-14 12:08:30.761420
4+
"""
5+
6+
from collections.abc import Sequence
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from pytest_alembic import MigrationContext
11+
12+
import sqlalchemy as sa
13+
from alembic import op
14+
15+
from app.types.sqlalchemy import TZDateTime
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = "e98026d51884"
19+
down_revision: str | None = "e22bfe152f72"
20+
branch_labels: str | Sequence[str] | None = None
21+
depends_on: str | Sequence[str] | None = None
22+
23+
24+
def upgrade() -> None:
25+
# ### commands auto generated by Alembic - please adjust! ###
26+
op.create_table(
27+
"flappy-bird_score",
28+
sa.Column("id", sa.Uuid(), nullable=False),
29+
sa.Column("user_id", sa.String(), nullable=False),
30+
sa.Column("value", sa.Integer(), nullable=False),
31+
sa.Column("creation_time", TZDateTime(), nullable=False),
32+
sa.ForeignKeyConstraint(["user_id"], ["core_user.id"]),
33+
sa.PrimaryKeyConstraint("id"),
34+
)
35+
# ### end Alembic commands ###
36+
37+
38+
def downgrade() -> None:
39+
# ### commands auto generated by Alembic - please adjust! ###
40+
op.drop_table("flappy-bird_score")
41+
# ### end Alembic commands ###
42+
43+
44+
def pre_test_upgrade(
45+
alembic_runner: "MigrationContext",
46+
alembic_connection: sa.Connection,
47+
) -> None:
48+
pass
49+
50+
51+
def test_upgrade(
52+
alembic_runner: "MigrationContext",
53+
alembic_connection: sa.Connection,
54+
) -> None:
55+
pass

tests/test_flappybird.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import uuid
2+
from datetime import UTC, datetime
3+
4+
import pytest_asyncio
5+
6+
from app.core import models_core
7+
from app.core.groups.groups_type import GroupType
8+
from app.modules.flappybird import models_flappybird
9+
from tests.commons import (
10+
add_object_to_db,
11+
client,
12+
create_api_access_token,
13+
create_user_with_groups,
14+
event_loop, # noqa
15+
)
16+
17+
flappybird_score: models_flappybird.FlappyBirdScore | None = None
18+
user: models_core.CoreUser | None = None
19+
token: str = ""
20+
21+
22+
@pytest_asyncio.fixture(scope="module", autouse=True)
23+
async def init_objects():
24+
global user
25+
user = await create_user_with_groups([GroupType.student])
26+
27+
global token
28+
token = create_api_access_token(user=user)
29+
30+
global flappybird_score
31+
flappybird_score = models_flappybird.FlappyBirdScore(
32+
id=uuid.uuid4(),
33+
user_id=user.id,
34+
user=user,
35+
value=25,
36+
creation_time=datetime.now(UTC),
37+
)
38+
39+
await add_object_to_db(flappybird_score)
40+
41+
42+
def test_get_flappybird_score():
43+
response = client.get(
44+
"/flappybird/scores/",
45+
headers={"Authorization": f"Bearer {token}"},
46+
)
47+
assert response.status_code == 200
48+
49+
50+
def test_get_current_user_flappybird_personal_best():
51+
response = client.get(
52+
"/flappybird/scores/me/",
53+
headers={"Authorization": f"Bearer {token}"},
54+
)
55+
assert response.status_code == 200
56+
57+
58+
def test_create_flappybird_score():
59+
response = client.post(
60+
"/flappybird/scores",
61+
json={
62+
"value": "26",
63+
},
64+
headers={"Authorization": f"Bearer {token}"},
65+
)
66+
assert response.status_code == 201

0 commit comments

Comments
 (0)