From 61611066cdd26440d8a0a06fd33ac9fd8ae20635 Mon Sep 17 00:00:00 2001 From: Erik Vroon Date: Fri, 9 Feb 2024 11:51:14 +0100 Subject: [PATCH] Pydantic v2 migration (#252) Upgrade Pydantic to V2. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backend.yml | 2 + backend/Pipfile | 12 +- backend/bracket/config.py | 28 ++--- backend/bracket/database.py | 23 +++- backend/bracket/logic/planning/matches.py | 6 +- backend/bracket/logic/planning/rounds.py | 4 +- backend/bracket/logic/ranking/elo.py | 2 +- backend/bracket/logic/scheduling/builder.py | 8 +- backend/bracket/models/db/match.py | 58 ++++----- backend/bracket/models/db/round.py | 2 +- backend/bracket/models/db/shared.py | 16 +-- backend/bracket/models/db/stage_item.py | 8 +- .../bracket/models/db/stage_item_inputs.py | 12 +- backend/bracket/models/db/team.py | 9 +- backend/bracket/models/db/tournament.py | 6 +- backend/bracket/models/db/util.py | 10 +- backend/bracket/routes/auth.py | 8 +- backend/bracket/routes/courts.py | 4 +- backend/bracket/routes/matches.py | 2 +- backend/bracket/routes/models.py | 3 +- backend/bracket/routes/players.py | 2 +- backend/bracket/routes/rounds.py | 2 +- backend/bracket/routes/teams.py | 14 ++- backend/bracket/routes/tournaments.py | 6 +- backend/bracket/sql/clubs.py | 8 +- backend/bracket/sql/courts.py | 4 +- backend/bracket/sql/matches.py | 10 +- backend/bracket/sql/players.py | 6 +- backend/bracket/sql/stage_item_inputs.py | 2 +- backend/bracket/sql/stage_items.py | 2 +- backend/bracket/sql/stages.py | 4 +- backend/bracket/sql/teams.py | 4 +- backend/bracket/sql/tournaments.py | 6 +- backend/bracket/sql/users.py | 10 +- backend/bracket/utils/conversion.py | 7 +- backend/bracket/utils/db.py | 16 ++- backend/bracket/utils/db_init.py | 2 +- backend/pyproject.toml | 3 + .../api/activate_next_stage_test.py | 18 +-- .../tests/integration_tests/api/auth_test.py | 4 +- .../tests/integration_tests/api/clubs_test.py | 2 +- .../integration_tests/api/courts_test.py | 14 +-- .../integration_tests/api/inputs_test.py | 8 +- .../integration_tests/api/matches_test.py | 110 +++++++++--------- .../integration_tests/api/players_test.py | 18 +-- .../api/rescheduling_matches_test.py | 18 +-- .../integration_tests/api/rounds_test.py | 24 ++-- .../api/scheduling_matches_test.py | 14 ++- backend/tests/integration_tests/api/shared.py | 2 +- .../integration_tests/api/stage_items_test.py | 20 ++-- .../integration_tests/api/stages_test.py | 36 +++--- .../tests/integration_tests/api/teams_test.py | 12 +- .../integration_tests/api/tournaments_test.py | 20 ++-- .../tests/integration_tests/api/users_test.py | 2 +- backend/tests/integration_tests/sql.py | 4 +- backend/tests/unit_tests/elo_test.py | 7 +- backend/tests/unit_tests/swiss_test.py | 12 +- 57 files changed, 361 insertions(+), 315 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 06bfeced..0f065fbb 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -42,6 +42,8 @@ jobs: - name: Upload coverage report to Codecov uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Run mypy run: pipenv run mypy --version && pipenv run mypy . diff --git a/backend/Pipfile b/backend/Pipfile index 2f0e9d01..f982f63b 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -9,15 +9,16 @@ aiopg = ">=1.4.0" alembic = ">=1.9.1" bcrypt = "4.1.2" click = ">=8.1.3" -databases = {extras = ["asyncpg"], version = ">=0.7.0"} +databases = {extras = ["asyncpg"], version = "<=0.7.0"} fastapi = ">=0.88.0" fastapi-cache2 = ">=0.2.0" fastapi-sso = ">=0.6.4" gunicorn = ">=20.1.0" -heliclockter = ">=1.0.4" +heliclockter = ">=1.3.0" parameterized = ">=0.8.1" passlib = ">=1.7.4" -pydantic = "<2.0.0" +pydantic = "<3.0.0" +pydantic_settings = "2.1.0" pyjwt = ">=2.6.0" python-dotenv = ">=0.21.0" python-multipart = ">=0.0.5" @@ -32,9 +33,10 @@ uvicorn = ">=0.20.0" [dev-packages] aioresponses = ">=0.7.4" mypy = ">=1.3.1" +pluggy = "<=1.3.0" pylint = ">=2.15.10" -pytest = "8.0.0" -pytest-asyncio = "0.20.3" +pytest = "<=7.4.3" +pytest-asyncio = "<=0.21.1" pytest-cov = ">=4.0.0" pytest-xdist = ">=3.2.1" ruff = ">=0.0.292" diff --git a/backend/bracket/config.py b/backend/bracket/config.py index e2ddea82..0ba302e9 100644 --- a/backend/bracket/config.py +++ b/backend/bracket/config.py @@ -1,9 +1,11 @@ import logging import os from enum import auto +from typing import Annotated import sentry_sdk -from pydantic import BaseSettings, PostgresDsn +from pydantic import Field, PostgresDsn +from pydantic_settings import BaseSettings, SettingsConfigDict from bracket.utils.types import EnumAutoStr @@ -39,31 +41,29 @@ class Config(BaseSettings): class CIConfig(Config): - class Config: - env_file = "ci.env" + model_config = SettingsConfigDict(env_file="ci.env") class DevelopmentConfig(Config): - admin_email = "test@example.org" - admin_password = "aeGhoe1ahng2Aezai0Dei6Aih6dieHoo" - allow_insecure_http_sso = True - jwt_secret = "7495204c062787f257b12d03b88d80da1d338796a6449666eb634c9efbbf5fa7" + admin_email: Annotated[str, Field("test@example.org")] + admin_password: Annotated[str, Field("aeGhoe1ahng2Aezai0Dei6Aih6dieHoo")] + allow_insecure_http_sso: Annotated[bool, Field(True)] + jwt_secret: Annotated[ + str, Field("7495204c062787f257b12d03b88d80da1d338796a6449666eb634c9efbbf5fa7") + ] - class Config: - env_file = "dev.env" + model_config = SettingsConfigDict(env_file="dev.env") class ProductionConfig(Config): - class Config: - env_file = "prod.env" + model_config = SettingsConfigDict(env_file="prod.env") class DemoConfig(Config): - class Config: - env_file = "demo.env" + model_config = SettingsConfigDict(env_file="demo.env") -environment = Environment(os.getenv("ENVIRONMENT", "CI").upper()) +environment = Environment(os.getenv("ENVIRONMENT", "DEVELOPMENT").upper()) config: Config match environment: diff --git a/backend/bracket/database.py b/backend/bracket/database.py index b21106aa..fc65b76f 100644 --- a/backend/bracket/database.py +++ b/backend/bracket/database.py @@ -1,8 +1,27 @@ +from typing import Any + import sqlalchemy from databases import Database +from heliclockter import datetime_utc from bracket.config import config -database = Database(config.pg_dsn) -engine = sqlalchemy.create_engine(config.pg_dsn) +def datetime_decoder(value: str) -> datetime_utc: + value = value.split(".")[0].replace("+00", "+00:00") + return datetime_utc.fromisoformat(value) + + +async def asyncpg_init(connection: Any) -> None: + for timestamp_type in ("timestamp", "timestamptz"): + await connection.set_type_codec( + timestamp_type, + encoder=datetime_utc.isoformat, + decoder=datetime_decoder, + schema="pg_catalog", + ) + + +database = Database(str(config.pg_dsn), init=asyncpg_init) + +engine = sqlalchemy.create_engine(str(config.pg_dsn)) diff --git a/backend/bracket/logic/planning/matches.py b/backend/bracket/logic/planning/matches.py index 26862b89..f8e32141 100644 --- a/backend/bracket/logic/planning/matches.py +++ b/backend/bracket/logic/planning/matches.py @@ -103,7 +103,7 @@ async def todo_schedule_all_matches(tournament_id: int) -> None: matches_per_team: dict[int | None, list[Match]] = defaultdict(list) matches_to_schedule = [ - match.copy(update={"court_id": None, "position_in_schedule": None}) + match.model_copy(update={"court_id": None, "position_in_schedule": None}) for stage in stages for stage_item in stage.stage_items for round_ in stage_item.rounds @@ -156,7 +156,7 @@ async def iterative_scheduling( start_time = tournament.start_time position_in_schedule = 0 - updated_match = match.copy( + updated_match = match.model_copy( update={ "start_time": start_time, "position_in_schedule": position_in_schedule, @@ -238,7 +238,7 @@ async def handle_match_reschedule( ) scheduled_matches.append( MatchPosition( - match=match_pos.match.copy(update={"court_id": body.new_court_id}), + match=match_pos.match.model_copy(update={"court_id": body.new_court_id}), position=body.new_position + offset, ) ) diff --git a/backend/bracket/logic/planning/rounds.py b/backend/bracket/logic/planning/rounds.py index 221930bc..b25d9fae 100644 --- a/backend/bracket/logic/planning/rounds.py +++ b/backend/bracket/logic/planning/rounds.py @@ -36,7 +36,7 @@ def get_active_and_next_rounds( async def schedule_all_matches_for_swiss_round( - tournament_id: int, active_round: RoundWithMatches, adjust_to_time: datetime_utc | None + tournament_id: int, active_round: RoundWithMatches, adjust_to_time: datetime_utc | None = None ) -> None: courts = await get_all_courts_in_tournament(tournament_id) stages = await get_full_tournament_details(tournament_id) @@ -72,7 +72,7 @@ async def schedule_all_matches_for_swiss_round( ) if timing_difference_minutes != 0: - last_match_adjusted = last_match.match.copy( + last_match_adjusted = last_match.match.model_copy( update={ "custom_margin_minutes": last_match.match.margin_minutes + timing_difference_minutes diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index 0486355a..6e9846d8 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -129,5 +129,5 @@ async def recalculate_ranking_for_stage_items( query=players.update().where( (players.c.id == player.id) & (players.c.tournament_id == tournament_id) ), - values=PlayerStatistics().dict(), + values=PlayerStatistics().model_dump(), ) diff --git a/backend/bracket/logic/scheduling/builder.py b/backend/bracket/logic/scheduling/builder.py index a172d8e4..fa42f6fe 100644 --- a/backend/bracket/logic/scheduling/builder.py +++ b/backend/bracket/logic/scheduling/builder.py @@ -18,7 +18,6 @@ from bracket.models.db.stage_item_inputs import ( ) from bracket.models.db.team import FullTeamWithPlayers from bracket.models.db.util import StageWithStageItems -from bracket.schema import rounds from bracket.sql.rounds import get_next_round_name from bracket.sql.stage_items import get_stage_item from bracket.utils.types import assert_some @@ -39,12 +38,15 @@ async def create_rounds_for_new_stage_item(tournament_id: int, stage_item: Stage now = datetime_utc.now() for _ in range(rounds_count): await database.execute( - query=rounds.insert(), + query=""" + INSERT INTO rounds (created, stage_item_id, name, is_draft, is_active) + VALUES (:created, :stage_item_id, :name, :is_draft, :is_active) + """, values=RoundToInsert( created=now, stage_item_id=assert_some(stage_item.id), name=await get_next_round_name(tournament_id, assert_some(stage_item.id)), - ).dict(), + ).model_dump(), ) diff --git a/backend/bracket/models/db/match.py b/backend/bracket/models/db/match.py index ea984f78..e60bd200 100644 --- a/backend/bracket/models/db/match.py +++ b/backend/bracket/models/db/match.py @@ -12,16 +12,16 @@ from bracket.utils.types import assert_some class MatchBase(BaseModelORM): id: int | None = None created: datetime_utc - start_time: datetime_utc | None + start_time: datetime_utc | None = None duration_minutes: int margin_minutes: int - custom_duration_minutes: int | None - custom_margin_minutes: int | None - position_in_schedule: int | None + custom_duration_minutes: int | None = None + custom_margin_minutes: int | None = None + position_in_schedule: int | None = None round_id: int team1_score: int team2_score: int - court_id: int | None + court_id: int | None = None @property def end_time(self) -> datetime_utc: @@ -32,14 +32,14 @@ class MatchBase(BaseModelORM): class Match(MatchBase): - team1_id: int | None - team2_id: int | None - team1_winner_position: int | None - team1_winner_from_stage_item_id: int | None - team2_winner_from_stage_item_id: int | None - team2_winner_position: int | None - team1_winner_from_match_id: int | None - team2_winner_from_match_id: int | None + team1_id: int | None = None + team2_id: int | None = None + team1_winner_position: int | None = None + team1_winner_from_stage_item_id: int | None = None + team2_winner_from_stage_item_id: int | None = None + team2_winner_position: int | None = None + team1_winner_from_match_id: int | None = None + team2_winner_from_match_id: int | None = None def get_winner_index(self) -> int | None: if self.team1_score == self.team2_score: @@ -49,7 +49,7 @@ class Match(MatchBase): class MatchWithDetails(Match): - court: Court | None + court: Court | None = None def get_match_hash(team_1_id: int | None, team_2_id: int | None) -> str: @@ -59,7 +59,7 @@ def get_match_hash(team_1_id: int | None, team_2_id: int | None) -> str: class MatchWithDetailsDefinitive(Match): team1: FullTeamWithPlayers team2: FullTeamWithPlayers - court: Court | None + court: Court | None = None @property def teams(self) -> list[FullTeamWithPlayers]: @@ -84,29 +84,29 @@ class MatchBody(BaseModelORM): round_id: int team1_score: int = 0 team2_score: int = 0 - court_id: int | None - custom_duration_minutes: int | None - custom_margin_minutes: int | None + court_id: int | None = None + custom_duration_minutes: int | None = None + custom_margin_minutes: int | None = None class MatchCreateBodyFrontend(BaseModelORM): round_id: int - court_id: int | None - team1_id: int | None - team2_id: int | None - team1_winner_from_stage_item_id: int | None - team1_winner_position: int | None - team1_winner_from_match_id: int | None - team2_winner_from_stage_item_id: int | None - team2_winner_position: int | None - team2_winner_from_match_id: int | None + court_id: int | None = None + team1_id: int | None = None + team2_id: int | None = None + team1_winner_from_stage_item_id: int | None = None + team1_winner_position: int | None = None + team1_winner_from_match_id: int | None = None + team2_winner_from_stage_item_id: int | None = None + team2_winner_position: int | None = None + team2_winner_from_match_id: int | None = None class MatchCreateBody(MatchCreateBodyFrontend): duration_minutes: int margin_minutes: int - custom_duration_minutes: int | None - custom_margin_minutes: int | None + custom_duration_minutes: int | None = None + custom_margin_minutes: int | None = None class MatchRescheduleBody(BaseModelORM): diff --git a/backend/bracket/models/db/round.py b/backend/bracket/models/db/round.py index 41943b20..7976fe13 100644 --- a/backend/bracket/models/db/round.py +++ b/backend/bracket/models/db/round.py @@ -19,7 +19,7 @@ class RoundUpdateBody(BaseModelORM): class RoundCreateBody(BaseModelORM): - name: str | None + name: str | None = None stage_item_id: int diff --git a/backend/bracket/models/db/shared.py b/backend/bracket/models/db/shared.py index c31495bc..df45b442 100644 --- a/backend/bracket/models/db/shared.py +++ b/backend/bracket/models/db/shared.py @@ -1,19 +1,11 @@ -from datetime import datetime from typing import Any -from heliclockter import datetime_utc -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class BaseModelORM(BaseModel): - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) - def dict(self, **kwargs: Any) -> Any: + def model_dump(self, **kwargs: Any) -> Any: kwargs["exclude_none"] = True - result = super().dict(**kwargs) - for k, v in result.items(): - if isinstance(v, datetime_utc): - result[k] = datetime.fromisoformat(v.isoformat()) - - return result + return super().model_dump(**kwargs) diff --git a/backend/bracket/models/db/stage_item.py b/backend/bracket/models/db/stage_item.py index d3a4a235..f58caa3e 100644 --- a/backend/bracket/models/db/stage_item.py +++ b/backend/bracket/models/db/stage_item.py @@ -2,7 +2,7 @@ from enum import auto from typing import Any from heliclockter import datetime_utc -from pydantic import Field, root_validator +from pydantic import Field, model_validator from bracket.models.db.shared import BaseModelORM from bracket.models.db.stage_item_inputs import StageItemInputCreateBody @@ -37,12 +37,12 @@ class StageItemUpdateBody(BaseModelORM): class StageItemActivateNextBody(BaseModelORM): - adjust_to_time: datetime_utc | None + adjust_to_time: datetime_utc | None = None class StageItemCreateBody(BaseModelORM): stage_id: int - name: str | None + name: str | None = None type: StageType team_count: int = Field(ge=2, le=64) inputs: list[StageItemInputCreateBody] @@ -50,7 +50,7 @@ class StageItemCreateBody(BaseModelORM): def get_name_or_default_name(self) -> str: return self.name if self.name is not None else self.type.value.replace("_", " ").title() - @root_validator + @model_validator(mode="before") def handle_inputs_length(cls, values: Any) -> Any: if ("inputs" in values and "team_count" in values) and ( len(values["inputs"]) != values["team_count"] diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index a7b20193..52e340b1 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -4,17 +4,17 @@ from bracket.models.db.shared import BaseModelORM class StageItemInputBase(BaseModelORM): - id: int | None + id: int | None = None slot: int tournament_id: int - stage_item_id: int | None + stage_item_id: int | None = None class StageItemInputGeneric(BaseModel): - team_id: int | None - winner_from_stage_item_id: int | None - winner_position: int | None - winner_from_match_id: int | None + team_id: int | None = None + winner_from_stage_item_id: int | None = None + winner_position: int | None = None + winner_from_match_id: int | None = None def __hash__(self) -> int: return ( diff --git a/backend/bracket/models/db/team.py b/backend/bracket/models/db/team.py index db08a863..a79f08a4 100644 --- a/backend/bracket/models/db/team.py +++ b/backend/bracket/models/db/team.py @@ -3,9 +3,10 @@ from __future__ import annotations # ruff: noqa: TCH001,TCH002 import json from decimal import Decimal +from typing import Annotated from heliclockter import datetime_utc -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, StringConstraints, field_validator from bracket.models.db.player import Player from bracket.models.db.players import START_ELO @@ -40,7 +41,7 @@ class TeamWithPlayers(BaseModel): def player_ids(self) -> list[int]: return [assert_some(player.id) for player in self.players] - @validator("players", pre=True) + @field_validator("players", mode="before") def handle_players(values: list[Player]) -> list[Player]: # type: ignore[misc] if isinstance(values, str): values_json = json.loads(values) @@ -76,9 +77,9 @@ class FullTeamWithPlayers(TeamWithPlayers, Team): class TeamBody(BaseModelORM): - name: str = Field(..., min_length=1, max_length=30) + name: Annotated[str, StringConstraints(min_length=1, max_length=30)] active: bool - player_ids: list[int] = Field(..., unique_items=True) + player_ids: set[int] class TeamMultiBody(BaseModelORM): diff --git a/backend/bracket/models/db/tournament.py b/backend/bracket/models/db/tournament.py index f5d5c201..3f525474 100644 --- a/backend/bracket/models/db/tournament.py +++ b/backend/bracket/models/db/tournament.py @@ -13,8 +13,8 @@ class Tournament(BaseModelORM): duration_minutes: int = Field(..., ge=1) margin_minutes: int = Field(..., ge=0) dashboard_public: bool - dashboard_endpoint: str | None - logo_path: str | None + dashboard_endpoint: str | None = None + logo_path: str | None = None players_can_be_in_multiple_teams: bool auto_assign_courts: bool @@ -23,7 +23,7 @@ class TournamentUpdateBody(BaseModelORM): start_time: datetime_utc name: str dashboard_public: bool - dashboard_endpoint: str | None + dashboard_endpoint: str | None = None players_can_be_in_multiple_teams: bool auto_assign_courts: bool duration_minutes: int = Field(..., ge=1) diff --git a/backend/bracket/models/db/util.py b/backend/bracket/models/db/util.py index 6974ca2f..48848a50 100644 --- a/backend/bracket/models/db/util.py +++ b/backend/bracket/models/db/util.py @@ -4,7 +4,7 @@ from __future__ import annotations import json from typing import Any -from pydantic import root_validator, validator +from pydantic import field_validator, model_validator from bracket.models.db.match import Match, MatchWithDetails, MatchWithDetailsDefinitive from bracket.models.db.round import Round @@ -17,7 +17,7 @@ from bracket.utils.types import assert_some class RoundWithMatches(Round): matches: list[MatchWithDetailsDefinitive | MatchWithDetails] - @validator("matches", pre=True) + @field_validator("matches", mode="before") def handle_matches(values: list[Match]) -> list[Match]: # type: ignore[misc] if values == [None]: return [] @@ -37,7 +37,7 @@ class StageItemWithRounds(StageItem): inputs: list[StageItemInput] type_name: str - @root_validator(pre=True) + @model_validator(mode="before") def fill_type_name(cls, values: Any) -> Any: match values["type"]: case str() as type_: @@ -47,7 +47,7 @@ class StageItemWithRounds(StageItem): return values - @validator("rounds", "inputs", pre=True) + @field_validator("rounds", "inputs", mode="before") def handle_empty_list_elements(values: list[Any] | None) -> list[Any]: # type: ignore[misc] if values is None: return [] @@ -57,7 +57,7 @@ class StageItemWithRounds(StageItem): class StageWithStageItems(Stage): stage_items: list[StageItemWithRounds] - @validator("stage_items", pre=True) + @field_validator("stage_items", mode="before") def handle_stage_items(values: list[StageItemWithRounds]) -> list[StageItemWithRounds]: # type: ignore[misc] if isinstance(values, str): values_json = json.loads(values) diff --git a/backend/bracket/routes/auth.py b/backend/bracket/routes/auth.py index b594c669..eafcfc16 100644 --- a/backend/bracket/routes/auth.py +++ b/backend/bracket/routes/auth.py @@ -86,7 +86,7 @@ async def check_jwt_and_get_user(token: str) -> UserPublic | None: if user is None: return None - return UserPublic.parse_obj(user.dict()) + return UserPublic.model_validate(user.model_dump()) async def user_authenticated(token: str = Depends(oauth2_scheme)) -> UserPublic: @@ -98,7 +98,7 @@ async def user_authenticated(token: str = Depends(oauth2_scheme)) -> UserPublic: headers={"WWW-Authenticate": "Bearer"}, ) - return UserPublic.parse_obj(user.dict()) + return UserPublic.model_validate(user.model_dump()) async def user_authenticated_for_tournament( @@ -113,7 +113,7 @@ async def user_authenticated_for_tournament( headers={"WWW-Authenticate": "Bearer"}, ) - return UserPublic.parse_obj(user.dict()) + return UserPublic.model_validate(user.model_dump()) async def user_authenticated_for_club( @@ -128,7 +128,7 @@ async def user_authenticated_for_club( headers={"WWW-Authenticate": "Bearer"}, ) - return UserPublic.parse_obj(user.dict()) + return UserPublic.model_validate(user.model_dump()) async def user_authenticated_or_public_dashboard( diff --git a/backend/bracket/routes/courts.py b/backend/bracket/routes/courts.py index d4730c5b..765fc029 100644 --- a/backend/bracket/routes/courts.py +++ b/backend/bracket/routes/courts.py @@ -88,10 +88,10 @@ async def create_court( last_record_id = await database.execute( query=courts.insert(), values=CourtToInsert( - **court_body.dict(), + **court_body.model_dump(), created=datetime_utc.now(), tournament_id=tournament_id, - ).dict(), + ).model_dump(), ) return SingleCourtResponse( data=assert_some( diff --git a/backend/bracket/routes/matches.py b/backend/bracket/routes/matches.py index 30fd02a3..0d1d4dfd 100644 --- a/backend/bracket/routes/matches.py +++ b/backend/bracket/routes/matches.py @@ -79,7 +79,7 @@ async def create_match( ) -> SingleMatchResponse: tournament = await sql_get_tournament(tournament_id) body_with_durations = MatchCreateBody( - **match_body.dict(), + **match_body.model_dump(), duration_minutes=tournament.duration_minutes, margin_minutes=tournament.margin_minutes, ) diff --git a/backend/bracket/routes/models.py b/backend/bracket/routes/models.py index 93e104e1..99a30dd2 100644 --- a/backend/bracket/routes/models.py +++ b/backend/bracket/routes/models.py @@ -1,7 +1,6 @@ from typing import Generic, TypeVar from pydantic import BaseModel -from pydantic.generics import GenericModel from bracket.models.db.club import Club from bracket.models.db.court import Court @@ -24,7 +23,7 @@ class SuccessResponse(BaseModel): success: bool = True -class DataResponse(GenericModel, Generic[DataT]): +class DataResponse(BaseModel, Generic[DataT]): data: DataT diff --git a/backend/bracket/routes/players.py b/backend/bracket/routes/players.py index 5d8bf89c..a05c3034 100644 --- a/backend/bracket/routes/players.py +++ b/backend/bracket/routes/players.py @@ -36,7 +36,7 @@ async def update_player_by_id( query=players.update().where( (players.c.id == player_id) & (players.c.tournament_id == tournament_id) ), - values=player_body.dict(), + values=player_body.model_dump(), ) return SinglePlayerResponse( data=assert_some( diff --git a/backend/bracket/routes/rounds.py b/backend/bracket/routes/rounds.py index 2e34eba2..aa6570ca 100644 --- a/backend/bracket/routes/rounds.py +++ b/backend/bracket/routes/rounds.py @@ -84,7 +84,7 @@ async def create_round( created=datetime_utc.now(), stage_item_id=round_body.stage_item_id, name=await get_next_round_name(tournament_id, round_body.stage_item_id), - ).dict(), + ).model_dump(), ) await set_round_active_or_draft(round_id, tournament_id, is_active=False, is_draft=True) diff --git a/backend/bracket/routes/teams.py b/backend/bracket/routes/teams.py index cc4d1769..9a336b22 100644 --- a/backend/bracket/routes/teams.py +++ b/backend/bracket/routes/teams.py @@ -22,7 +22,7 @@ from bracket.utils.types import assert_some router = APIRouter() -async def update_team_members(team_id: int, tournament_id: int, player_ids: list[int]) -> None: +async def update_team_members(team_id: int, tournament_id: int, player_ids: set[int]) -> None: [team] = await get_teams_with_members(tournament_id, team_id=team_id) # Add members to the team @@ -47,7 +47,9 @@ async def update_team_members(team_id: int, tournament_id: int, player_ids: list async def get_teams( tournament_id: int, _: UserPublic = Depends(user_authenticated_or_public_dashboard) ) -> TeamsWithPlayersResponse: - return TeamsWithPlayersResponse.parse_obj({"data": await get_teams_with_members(tournament_id)}) + return TeamsWithPlayersResponse.model_validate( + {"data": await get_teams_with_members(tournament_id)} + ) @router.put("/tournaments/{tournament_id}/teams/{team_id}", response_model=SingleTeamResponse) @@ -61,7 +63,7 @@ async def update_team_by_id( query=teams.update().where( (teams.c.id == team.id) & (teams.c.tournament_id == tournament_id) ), - values=team_body.dict(exclude={"player_ids"}), + values=team_body.model_dump(exclude={"player_ids"}), ) await update_team_members(assert_some(team.id), tournament_id, team_body.player_ids) await recalculate_ranking_for_tournament_id(tournament_id) @@ -118,10 +120,10 @@ async def create_team( last_record_id = await database.execute( query=teams.insert(), values=TeamToInsert( - **team_to_insert.dict(exclude={"player_ids"}), + **team_to_insert.model_dump(exclude={"player_ids"}), created=datetime_utc.now(), tournament_id=tournament_id, - ).dict(), + ).model_dump(), ) await update_team_members(last_record_id, tournament_id, team_to_insert.player_ids) @@ -148,7 +150,7 @@ async def create_multiple_teams( active=team_body.active, created=datetime_utc.now(), tournament_id=tournament_id, - ).dict(), + ).model_dump(), ) return SuccessResponse() diff --git a/backend/bracket/routes/tournaments.py b/backend/bracket/routes/tournaments.py index 84b3c305..a564d165 100644 --- a/backend/bracket/routes/tournaments.py +++ b/backend/bracket/routes/tournaments.py @@ -81,7 +81,7 @@ async def update_tournament_by_id( ) -> SuccessResponse: await database.execute( query=tournaments.update().where(tournaments.c.id == tournament_id), - values=tournament_body.dict(), + values=tournament_body.model_dump(), ) await update_start_times_of_matches(tournament_id) return SuccessResponse() @@ -115,9 +115,9 @@ async def create_tournament( await database.execute( query=tournaments.insert(), values=TournamentToInsert( - **tournament_to_insert.dict(), + **tournament_to_insert.model_dump(), created=datetime_utc.now(), - ).dict(), + ).model_dump(), ) return SuccessResponse() diff --git a/backend/bracket/sql/clubs.py b/backend/bracket/sql/clubs.py index b7cc279d..1969b584 100644 --- a/backend/bracket/sql/clubs.py +++ b/backend/bracket/sql/clubs.py @@ -14,7 +14,7 @@ async def create_club(club: ClubCreateBody, user_id: int) -> Club: if result is None: raise ValueError("Could not create club") - club_created = Club.parse_obj(result._mapping) + club_created = Club.model_validate(dict(result._mapping)) query_many_to_many = """ INSERT INTO users_x_clubs (club_id, user_id, relation) @@ -36,7 +36,7 @@ async def sql_update_club(club_id: int, club: ClubUpdateBody) -> Club | None: RETURNING * """ result = await database.fetch_one(query=query, values={"name": club.name, "club_id": club_id}) - return Club.parse_obj(result) if result is not None else None + return Club.model_validate(result) if result is not None else None async def sql_delete_club(club_id: int) -> None: @@ -63,7 +63,7 @@ async def get_clubs_for_user_id(user_id: int) -> list[Club]: WHERE uxc.user_id = :user_id """ results = await database.fetch_all(query=query, values={"user_id": user_id}) - return [Club.parse_obj(result._mapping) for result in results] + return [Club.model_validate(dict(result._mapping)) for result in results] async def todo_get_club_for_user_id(club_id: int, user_id: int) -> Club | None: @@ -74,4 +74,4 @@ async def todo_get_club_for_user_id(club_id: int, user_id: int) -> Club | None: AND club_id = :club_id """ result = await database.fetch_one(query=query, values={"user_id": user_id, "club_id": club_id}) - return Club.parse_obj(result._mapping) if result is not None else None + return Club.model_validate(dict(result._mapping)) if result is not None else None diff --git a/backend/bracket/sql/courts.py b/backend/bracket/sql/courts.py index fff35825..723b2494 100644 --- a/backend/bracket/sql/courts.py +++ b/backend/bracket/sql/courts.py @@ -10,7 +10,7 @@ async def get_all_courts_in_tournament(tournament_id: int) -> list[Court]: ORDER BY name """ result = await database.fetch_all(query=query, values={"tournament_id": tournament_id}) - return [Court.parse_obj(x._mapping) for x in result] + return [Court.model_validate(dict(x._mapping)) for x in result] async def update_court(tournament_id: int, court_id: int, court_body: CourtBody) -> list[Court]: @@ -24,7 +24,7 @@ async def update_court(tournament_id: int, court_id: int, court_body: CourtBody) query=query, values={"tournament_id": tournament_id, "court_id": court_id, "name": court_body.name}, ) - return [Court.parse_obj(x._mapping) for x in result] + return [Court.model_validate(dict(x._mapping)) for x in result] async def sql_delete_court(tournament_id: int, court_id: int) -> None: diff --git a/backend/bracket/sql/matches.py b/backend/bracket/sql/matches.py index 11921dce..fc399424 100644 --- a/backend/bracket/sql/matches.py +++ b/backend/bracket/sql/matches.py @@ -70,12 +70,12 @@ async def sql_create_match(match: MatchCreateBody) -> Match: ) RETURNING * """ - result = await database.fetch_one(query=query, values=match.dict()) + result = await database.fetch_one(query=query, values=match.model_dump()) if result is None: raise ValueError("Could not create stage") - return Match.parse_obj(result._mapping) + return Match.model_validate(dict(result._mapping)) async def sql_update_match(match_id: int, match: MatchBody, tournament: Tournament) -> None: @@ -107,7 +107,7 @@ async def sql_update_match(match_id: int, match: MatchBody, tournament: Tourname query=query, values={ "match_id": match_id, - **match.dict(), + **match.model_dump(), "duration_minutes": duration_minutes, "margin_minutes": margin_minutes, }, @@ -115,7 +115,7 @@ async def sql_update_match(match_id: int, match: MatchBody, tournament: Tourname async def sql_update_team_ids_for_match( - match_id: int, team1_id: int | None, team2_id: int | None + match_id: int, team1_id: int | None, team2_id: int | None = None ) -> None: query = """ UPDATE matches @@ -205,4 +205,4 @@ async def sql_get_match(match_id: int) -> Match: if result is None: raise ValueError("Could not create stage") - return Match.parse_obj(result._mapping) + return Match.model_validate(dict(result._mapping)) diff --git a/backend/bracket/sql/players.py b/backend/bracket/sql/players.py index 3f02ce62..9eb90e38 100644 --- a/backend/bracket/sql/players.py +++ b/backend/bracket/sql/players.py @@ -20,7 +20,7 @@ async def get_all_players_in_tournament( query += "AND players.team_id IS NULL" result = await database.fetch_all(query=query, values={"tournament_id": tournament_id}) - return [Player.parse_obj(x._mapping) for x in result] + return [Player.model_validate(dict(x._mapping)) for x in result] async def update_player_stats( @@ -67,10 +67,10 @@ async def insert_player(player_body: PlayerBody, tournament_id: int) -> None: await database.execute( query=players.insert(), values=PlayerToInsert( - **player_body.dict(), + **player_body.model_dump(), created=datetime_utc.now(), tournament_id=tournament_id, elo_score=Decimal(START_ELO), swiss_score=Decimal("0.0"), - ).dict(), + ).model_dump(), ) diff --git a/backend/bracket/sql/stage_item_inputs.py b/backend/bracket/sql/stage_item_inputs.py index e393bab3..95675a6d 100644 --- a/backend/bracket/sql/stage_item_inputs.py +++ b/backend/bracket/sql/stage_item_inputs.py @@ -67,4 +67,4 @@ async def sql_create_stage_item_input( if result is None: raise ValueError("Could not create stage") - return StageItemInputBase.parse_obj(result._mapping) + return StageItemInputBase.model_validate(dict(result._mapping)) diff --git a/backend/bracket/sql/stage_items.py b/backend/bracket/sql/stage_items.py index 0bbdf7fe..4f423ce5 100644 --- a/backend/bracket/sql/stage_items.py +++ b/backend/bracket/sql/stage_items.py @@ -25,7 +25,7 @@ async def sql_create_stage_item(tournament_id: int, stage_item: StageItemCreateB if result is None: raise ValueError("Could not create stage") - stage_item_result = StageItem.parse_obj(result._mapping) + stage_item_result = StageItem.model_validate(dict(result._mapping)) for input_ in stage_item.inputs: await sql_create_stage_item_input(tournament_id, stage_item_result.id, input_) diff --git a/backend/bracket/sql/stages.py b/backend/bracket/sql/stages.py index 95dc86a0..d3bfc681 100644 --- a/backend/bracket/sql/stages.py +++ b/backend/bracket/sql/stages.py @@ -106,7 +106,7 @@ async def get_full_tournament_details( } ) result = await database.fetch_all(query=query, values=values) - return [StageWithStageItems.parse_obj(x._mapping) for x in result] + return [StageWithStageItems.model_validate(dict(x._mapping)) for x in result] async def sql_delete_stage(tournament_id: int, stage_id: int) -> None: @@ -141,7 +141,7 @@ async def sql_create_stage(tournament_id: int) -> Stage: if result is None: raise ValueError("Could not create stage") - return Stage.parse_obj(result._mapping) + return Stage.model_validate(dict(result._mapping)) async def get_next_stage_in_tournament( diff --git a/backend/bracket/sql/teams.py b/backend/bracket/sql/teams.py index e841aa49..abf8da70 100644 --- a/backend/bracket/sql/teams.py +++ b/backend/bracket/sql/teams.py @@ -14,7 +14,7 @@ async def get_team_by_id(team_id: int, tournament_id: int) -> Team | None: result = await database.fetch_one( query=query, values={"team_id": team_id, "tournament_id": tournament_id} ) - return Team.parse_obj(result._mapping) if result is not None else None + return Team.model_validate(dict(result._mapping)) if result is not None else None async def get_teams_with_members( @@ -37,7 +37,7 @@ async def get_teams_with_members( """ values = dict_without_none({"tournament_id": tournament_id, "team_id": team_id}) result = await database.fetch_all(query=query, values=values) - return [FullTeamWithPlayers.parse_obj(x._mapping) for x in result] + return [FullTeamWithPlayers.model_validate(dict(x._mapping)) for x in result] async def update_team_stats( diff --git a/backend/bracket/sql/tournaments.py b/backend/bracket/sql/tournaments.py index 316061cc..42e4e302 100644 --- a/backend/bracket/sql/tournaments.py +++ b/backend/bracket/sql/tournaments.py @@ -23,7 +23,7 @@ async def sql_get_tournament(tournament_id: int) -> Tournament: """ result = await database.fetch_one(query=query, values={"tournament_id": tournament_id}) assert result is not None - return Tournament.parse_obj(result._mapping) + return Tournament.model_validate(dict(result._mapping)) async def sql_get_tournament_by_endpoint_name(endpoint_name: str) -> Tournament: @@ -35,7 +35,7 @@ async def sql_get_tournament_by_endpoint_name(endpoint_name: str) -> Tournament: """ result = await database.fetch_one(query=query, values={"endpoint_name": endpoint_name}) assert result is not None - return Tournament.parse_obj(result._mapping) + return Tournament.model_validate(dict(result._mapping)) async def sql_get_tournaments( @@ -54,7 +54,7 @@ async def sql_get_tournaments( params = {**params, "endpoint_name": endpoint_name} result = await database.fetch_all(query=query, values=params) - return [Tournament.parse_obj(x._mapping) for x in result] + return [Tournament.model_validate(dict(x._mapping)) for x in result] async def sql_delete_tournament(tournament_id: int) -> None: diff --git a/backend/bracket/sql/users.py b/backend/bracket/sql/users.py index 9dd03954..a3ae1beb 100644 --- a/backend/bracket/sql/users.py +++ b/backend/bracket/sql/users.py @@ -1,5 +1,3 @@ -from datetime import datetime - from bracket.database import database from bracket.models.db.user import User, UserInDB, UserPublic, UserToUpdate from bracket.schema import users @@ -61,7 +59,7 @@ async def get_user_by_id(user_id: int) -> UserPublic | None: WHERE id = :user_id """ result = await database.fetch_one(query=query, values={"user_id": user_id}) - return UserPublic.parse_obj(result._mapping) if result is not None else None + return UserPublic.model_validate(dict(result._mapping)) if result is not None else None async def get_expired_demo_users() -> list[UserPublic]: @@ -72,7 +70,7 @@ async def get_expired_demo_users() -> list[UserPublic]: AND created <= NOW() - INTERVAL '30 minutes' """ result = await database.fetch_all(query=query) - return [UserPublic.parse_obj(demo_user._mapping) for demo_user in result] + return [UserPublic.model_validate(demo_user._mapping) for demo_user in result] async def create_user(user: User) -> User: @@ -87,11 +85,11 @@ async def create_user(user: User) -> User: "password_hash": user.password_hash, "name": user.name, "email": user.email, - "created": datetime.fromisoformat(user.created.isoformat()), + "created": user.created, "account_type": user.account_type.value, }, ) - return User.parse_obj(assert_some(result)._mapping) + return User.model_validate(dict(assert_some(result)._mapping)) async def delete_user(user_id: int) -> None: diff --git a/backend/bracket/utils/conversion.py b/backend/bracket/utils/conversion.py index 7ba62b45..5b0852b7 100644 --- a/backend/bracket/utils/conversion.py +++ b/backend/bracket/utils/conversion.py @@ -1,6 +1,7 @@ from collections.abc import Mapping from typing import Any +from heliclockter import datetime_tz from pydantic import BaseModel from bracket.utils.types import EnumAutoStr @@ -9,7 +10,9 @@ from bracket.utils.types import EnumAutoStr def _map_to_str(value: Any) -> Any: match value: case EnumAutoStr(): - return value.name + return value.value + case datetime_tz(): + return value.isoformat() return value @@ -17,4 +20,4 @@ def to_string_mapping(obj: BaseModel) -> Mapping[str, Any]: """ Turns a pydantic object into a string mapping to be used as database query """ - return {key: _map_to_str(value) for key, value in obj.dict().items()} + return {key: _map_to_str(value) for key, value in obj.model_dump(exclude_none=True).items()} diff --git a/backend/bracket/utils/db.py b/backend/bracket/utils/db.py index 71a92fcb..ea5cd8ab 100644 --- a/backend/bracket/utils/db.py +++ b/backend/bracket/utils/db.py @@ -2,6 +2,7 @@ from databases import Database from sqlalchemy import Table from sqlalchemy.sql import Select +from bracket.config import Environment, environment from bracket.utils.conversion import to_string_mapping from bracket.utils.logging import logger from bracket.utils.types import BaseModelT, assert_some @@ -11,7 +12,7 @@ async def fetch_one_parsed( database: Database, model: type[BaseModelT], query: Select ) -> BaseModelT | None: record = await database.fetch_one(query) - return model.parse_obj(record._mapping) if record is not None else None + return model.model_validate(dict(record._mapping)) if record is not None else None async def fetch_one_parsed_certain( @@ -24,21 +25,24 @@ async def fetch_all_parsed( database: Database, model: type[BaseModelT], query: Select ) -> list[BaseModelT]: records = await database.fetch_all(query) - return [model.parse_obj(record._mapping) for record in records] + return [model.model_validate(dict(record._mapping)) for record in records] async def insert_generic( database: Database, data_model: BaseModelT, table: Table, return_type: type[BaseModelT] ) -> tuple[int, BaseModelT]: + assert environment is not Environment.PRODUCTION, "Below code can allow SQL injection" try: - last_record_id: int = await database.execute( - query=table.insert(), - values=to_string_mapping(data_model), # type: ignore[arg-type] + mapping = to_string_mapping(data_model) + values = ", ".join([f"'{x}'" for x in mapping.values()]) + query = ( + f"INSERT INTO {table.name} ({', '.join(mapping.keys())}) VALUES ({values}) RETURNING *" ) + last_record_id: int = await database.execute(query) row_inserted = await fetch_one_parsed( database, return_type, table.select().where(table.c.id == last_record_id) ) - assert isinstance(row_inserted, return_type) + assert isinstance(row_inserted, return_type), f"Unexpected type: {row_inserted}" return last_record_id, row_inserted except Exception: logger.exception(f"Could not insert {type(data_model).__name__}") diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 2becb2d0..c0a9bd37 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -136,7 +136,7 @@ async def sql_create_dev_db() -> int: async def insert_dummy(obj_to_insert: BaseModelT, update_data: dict[str, Any] = {}) -> int: record_id, _ = await insert_generic( database, - obj_to_insert.copy(update=update_data), + obj_to_insert.model_copy(update=update_data), table_lookup[type(obj_to_insert)], type(obj_to_insert), ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 69236b0c..54702f14 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,6 +9,9 @@ filterwarnings = [ 'ignore:The SelectBase.c and SelectBase.columns attributes are deprecated.*:DeprecationWarning', 'ignore:pkg_resources is deprecated as an API.*:DeprecationWarning', 'ignore:Deprecated call to `pkg_resources.declare_namespace(.*)`.*:DeprecationWarning', + 'ignore:.*:pytest.PytestDeprecationWarning', + 'ignore:.*pytest-asyncio detected an unclosed event loop.*:DeprecationWarning', + 'ignore:.*The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning', ] [tool.mypy] diff --git a/backend/tests/integration_tests/api/activate_next_stage_test.py b/backend/tests/integration_tests/api/activate_next_stage_test.py index 268ec980..4625373e 100644 --- a/backend/tests/integration_tests/api/activate_next_stage_test.py +++ b/backend/tests/integration_tests/api/activate_next_stage_test.py @@ -33,24 +33,26 @@ async def test_activate_next_stage( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_court(DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_court( + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) + ), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_1, inserted_stage( - DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_2, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_1, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_2, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_3, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_4, ): tournament_id = assert_some(auth_context.tournament.id) @@ -112,7 +114,7 @@ async def test_activate_next_stage( assert match1.team2.id == team_inserted_2.id await sql_update_match( assert_some(match1.id), - MatchBody(**match1.copy(update={"team2_score": 42}).dict()), + MatchBody(**match1.model_copy(update={"team2_score": 42}).model_dump()), auth_context.tournament, ) diff --git a/backend/tests/integration_tests/api/auth_test.py b/backend/tests/integration_tests/api/auth_test.py index d6e87580..99d94b71 100644 --- a/backend/tests/integration_tests/api/auth_test.py +++ b/backend/tests/integration_tests/api/auth_test.py @@ -65,7 +65,7 @@ async def test_auth_on_protected_endpoint(startup_and_shutdown_uvicorn_server: N "id": user_inserted.id, "email": user_inserted.email, "name": user_inserted.name, - "created": "2200-01-01T00:00:00+00:00", + "created": "2200-01-01T00:00:00Z", "account_type": UserAccountType.REGULAR.value, } } @@ -83,7 +83,7 @@ async def test_not_authenticated_for_tournament( ) -> None: async with inserted_club(DUMMY_CLUB) as club_inserted: async with inserted_tournament( - DUMMY_TOURNAMENT.copy(update={"club_id": club_inserted.id}) + DUMMY_TOURNAMENT.model_copy(update={"club_id": club_inserted.id}) ) as tournament_inserted: response = JsonDict( await send_auth_request( diff --git a/backend/tests/integration_tests/api/clubs_test.py b/backend/tests/integration_tests/api/clubs_test.py index bccd2a92..d4c62022 100644 --- a/backend/tests/integration_tests/api/clubs_test.py +++ b/backend/tests/integration_tests/api/clubs_test.py @@ -14,7 +14,7 @@ async def test_clubs_endpoint( assert await send_auth_request(HTTPMethod.GET, "clubs", auth_context, {}) == { "data": [ { - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "id": auth_context.club.id, "name": "Some Cool Club", } diff --git a/backend/tests/integration_tests/api/courts_test.py b/backend/tests/integration_tests/api/courts_test.py index 51bd2873..2e8975ce 100644 --- a/backend/tests/integration_tests/api/courts_test.py +++ b/backend/tests/integration_tests/api/courts_test.py @@ -13,15 +13,15 @@ async def test_courts_endpoint( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court_inserted: assert await send_tournament_request(HTTPMethod.GET, "courts", auth_context, {}) == { "data": [ { - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "id": court_inserted.id, "name": "Court 1", "tournament_id": auth_context.tournament.id, @@ -43,10 +43,10 @@ async def test_delete_court( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court_inserted: assert ( await send_tournament_request( @@ -62,10 +62,10 @@ async def test_update_court( ) -> None: body = {"name": "Some new name"} async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court_inserted: response = await send_tournament_request( HTTPMethod.PUT, f"courts/{court_inserted.id}", auth_context, json=body diff --git a/backend/tests/integration_tests/api/inputs_test.py b/backend/tests/integration_tests/api/inputs_test.py index 608c3c3d..489001be 100644 --- a/backend/tests/integration_tests/api/inputs_test.py +++ b/backend/tests/integration_tests/api/inputs_test.py @@ -20,15 +20,15 @@ async def test_available_inputs( ) -> None: async with ( inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted, inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_1, # inserted_stage( - # DUMMY_STAGE2.copy(update={'tournament_id': auth_context.tournament.id}) + # DUMMY_STAGE2.model_copy(update={'tournament_id': auth_context.tournament.id}) # ) as stage_inserted_2, - inserted_stage_item(DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted_1.id})), + inserted_stage_item(DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted_1.id})), ): response = await send_tournament_request( HTTPMethod.GET, f"stages/{stage_inserted_1.id}/available_inputs", auth_context diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index d2f71052..79090c4d 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from bracket.database import database from bracket.models.db.match import Match from bracket.models.db.stage_item import StageType @@ -37,22 +39,22 @@ async def test_create_match( ) -> None: async with ( inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team1_inserted, inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court1_inserted, inserted_team( - DUMMY_TEAM2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team2_inserted, ): body = { @@ -74,25 +76,25 @@ async def test_delete_match( ) -> None: async with ( inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team1_inserted, inserted_team( - DUMMY_TEAM2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team2_inserted, inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court1_inserted, inserted_match( - DUMMY_MATCH1.copy( + DUMMY_MATCH1.model_copy( update={ "round_id": round_inserted.id, "team1_id": team1_inserted.id, @@ -116,25 +118,25 @@ async def test_update_match( ) -> None: async with ( inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team1_inserted, inserted_team( - DUMMY_TEAM2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team2_inserted, inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court1_inserted, inserted_match( - DUMMY_MATCH1.copy( + DUMMY_MATCH1.model_copy( update={ "round_id": round_inserted.id, "team1_id": team1_inserted.id, @@ -177,7 +179,7 @@ async def test_upcoming_matches_endpoint( ) -> None: async with ( inserted_stage( - DUMMY_STAGE1.copy( + DUMMY_STAGE1.model_copy( update={ "is_active": True, "tournament_id": auth_context.tournament.id, @@ -185,10 +187,12 @@ async def test_upcoming_matches_endpoint( ) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id, "type": StageType.SWISS}) + DUMMY_STAGE_ITEM1.model_copy( + update={"stage_id": stage_inserted.id, "type": StageType.SWISS} + ) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy( + DUMMY_ROUND1.model_copy( update={ "is_draft": True, "stage_item_id": stage_item_inserted.id, @@ -196,36 +200,36 @@ async def test_upcoming_matches_endpoint( ) ) as round_inserted, inserted_team( - DUMMY_TEAM1.copy( - update={"tournament_id": auth_context.tournament.id, "elo_score": 1150} + DUMMY_TEAM1.model_copy( + update={"tournament_id": auth_context.tournament.id, "elo_score": Decimal("1150.0")} ) ) as team1_inserted, inserted_team( - DUMMY_TEAM2.copy( - update={"tournament_id": auth_context.tournament.id, "elo_score": 1350} + DUMMY_TEAM2.model_copy( + update={"tournament_id": auth_context.tournament.id, "elo_score": Decimal("1350.0")} ) ) as team2_inserted, inserted_player_in_team( - DUMMY_PLAYER1.copy( - update={"elo_score": 1100, "tournament_id": auth_context.tournament.id} + DUMMY_PLAYER1.model_copy( + update={"elo_score": Decimal("1100.0"), "tournament_id": auth_context.tournament.id} ), assert_some(team1_inserted.id), ) as player_inserted_1, inserted_player_in_team( - DUMMY_PLAYER2.copy( - update={"elo_score": 1300, "tournament_id": auth_context.tournament.id} + DUMMY_PLAYER2.model_copy( + update={"elo_score": Decimal("1300.0"), "tournament_id": auth_context.tournament.id} ), assert_some(team2_inserted.id), ) as player_inserted_2, inserted_player_in_team( - DUMMY_PLAYER3.copy( - update={"elo_score": 1200, "tournament_id": auth_context.tournament.id} + DUMMY_PLAYER3.model_copy( + update={"elo_score": Decimal("1200.0"), "tournament_id": auth_context.tournament.id} ), assert_some(team1_inserted.id), ) as player_inserted_3, inserted_player_in_team( - DUMMY_PLAYER4.copy( - update={"elo_score": 1400, "tournament_id": auth_context.tournament.id} + DUMMY_PLAYER4.model_copy( + update={"elo_score": Decimal("1400.0"), "tournament_id": auth_context.tournament.id} ), assert_some(team2_inserted.id), ) as player_inserted_4, @@ -244,10 +248,10 @@ async def test_upcoming_matches_endpoint( "id": player_inserted_1.id, "active": True, "name": "Player 01", - "created": "2022-01-11T04:32:11+00:00", + "created": "2022-01-11T04:32:11Z", "tournament_id": auth_context.tournament.id, - "elo_score": 1100, - "swiss_score": 0, + "elo_score": "1100", + "swiss_score": "0", "wins": 0, "draws": 0, "losses": 0, @@ -256,17 +260,17 @@ async def test_upcoming_matches_endpoint( "id": player_inserted_3.id, "active": True, "name": "Player 03", - "created": "2022-01-11T04:32:11+00:00", + "created": "2022-01-11T04:32:11Z", "tournament_id": auth_context.tournament.id, - "elo_score": 1200, - "swiss_score": 0, + "elo_score": "1200", + "swiss_score": "0", "wins": 0, "draws": 0, "losses": 0, }, ], - "swiss_score": 0.0, - "elo_score": 1150.0, + "swiss_score": "0.0", + "elo_score": "1150.0", "wins": 0, "draws": 0, "losses": 0, @@ -279,10 +283,10 @@ async def test_upcoming_matches_endpoint( "id": player_inserted_2.id, "active": True, "name": "Player 02", - "created": "2022-01-11T04:32:11+00:00", + "created": "2022-01-11T04:32:11Z", "tournament_id": auth_context.tournament.id, - "elo_score": 1300, - "swiss_score": 0, + "elo_score": "1300", + "swiss_score": "0", "wins": 0, "draws": 0, "losses": 0, @@ -291,23 +295,23 @@ async def test_upcoming_matches_endpoint( "id": player_inserted_4.id, "active": True, "name": "Player 04", - "created": "2022-01-11T04:32:11+00:00", + "created": "2022-01-11T04:32:11Z", "tournament_id": auth_context.tournament.id, - "elo_score": 1400, - "swiss_score": 0, + "elo_score": "1400", + "swiss_score": "0", "wins": 0, "draws": 0, "losses": 0, }, ], - "swiss_score": 0.0, - "elo_score": 1350.0, + "swiss_score": "0.0", + "elo_score": "1350.0", "wins": 0, "draws": 0, "losses": 0, }, - "elo_diff": 200, - "swiss_diff": 0, + "elo_diff": "200", + "swiss_diff": "0", "is_recommended": True, "player_behind_schedule_count": 0, } diff --git a/backend/tests/integration_tests/api/players_test.py b/backend/tests/integration_tests/api/players_test.py index 6d4c385b..773a9ef5 100644 --- a/backend/tests/integration_tests/api/players_test.py +++ b/backend/tests/integration_tests/api/players_test.py @@ -13,19 +13,19 @@ async def test_players_endpoint( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_player( - DUMMY_PLAYER1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_PLAYER1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as player_inserted: assert await send_tournament_request(HTTPMethod.GET, "players", auth_context, {}) == { "data": [ { - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "id": player_inserted.id, "active": True, - "elo_score": 0.0, - "swiss_score": 0.0, + "elo_score": "0.0", + "swiss_score": "0.0", "wins": 0, "draws": 0, "losses": 0, @@ -60,10 +60,10 @@ async def test_delete_player( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_player( - DUMMY_PLAYER1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_PLAYER1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as player_inserted: assert ( await send_tournament_request( @@ -79,10 +79,10 @@ async def test_update_player( ) -> None: body = {"name": "Some new name", "active": True} async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): async with inserted_player( - DUMMY_PLAYER1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_PLAYER1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as player_inserted: response = await send_tournament_request( HTTPMethod.PUT, f"players/{player_inserted.id}", auth_context, json=body diff --git a/backend/tests/integration_tests/api/rescheduling_matches_test.py b/backend/tests/integration_tests/api/rescheduling_matches_test.py index cca5dee6..e967ffa6 100644 --- a/backend/tests/integration_tests/api/rescheduling_matches_test.py +++ b/backend/tests/integration_tests/api/rescheduling_matches_test.py @@ -31,28 +31,28 @@ async def test_reschedule_match( ) -> None: async with ( inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team1_inserted, inserted_team( - DUMMY_TEAM2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team2_inserted, inserted_court( - DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court1_inserted, inserted_court( - DUMMY_COURT2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_COURT2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as court2_inserted, inserted_match( - DUMMY_MATCH1.copy( + DUMMY_MATCH1.model_copy( update={ "round_id": round_inserted.id, "team1_id": team1_inserted.id, @@ -73,7 +73,7 @@ async def test_reschedule_match( HTTPMethod.POST, f"matches/{match_inserted.id}/reschedule", auth_context, - json=body.dict(), + json=body.model_dump(), ) == SUCCESS_RESPONSE ) diff --git a/backend/tests/integration_tests/api/rounds_test.py b/backend/tests/integration_tests/api/rounds_test.py index 2efe779b..cdc428f7 100644 --- a/backend/tests/integration_tests/api/rounds_test.py +++ b/backend/tests/integration_tests/api/rounds_test.py @@ -20,12 +20,14 @@ async def test_create_round( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id, "type": StageType.SWISS}) + DUMMY_STAGE_ITEM1.model_copy( + update={"stage_id": stage_inserted.id, "type": StageType.SWISS} + ) ) as stage_item_inserted, ): assert ( @@ -44,15 +46,15 @@ async def test_delete_round( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, ): assert ( @@ -69,15 +71,15 @@ async def test_update_round( ) -> None: body = {"name": "Some new name", "is_draft": True, "is_active": False} async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, ): assert ( diff --git a/backend/tests/integration_tests/api/scheduling_matches_test.py b/backend/tests/integration_tests/api/scheduling_matches_test.py index 61150222..983e4a62 100644 --- a/backend/tests/integration_tests/api/scheduling_matches_test.py +++ b/backend/tests/integration_tests/api/scheduling_matches_test.py @@ -32,21 +32,23 @@ async def test_schedule_all_matches( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_court(DUMMY_COURT1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_court( + DUMMY_COURT1.model_copy(update={"tournament_id": auth_context.tournament.id}) + ), inserted_stage( - DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_1, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_1, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_2, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_3, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_4, ): tournament_id = assert_some(auth_context.tournament.id) diff --git a/backend/tests/integration_tests/api/shared.py b/backend/tests/integration_tests/api/shared.py index 9eef81bb..b4c42a4f 100644 --- a/backend/tests/integration_tests/api/shared.py +++ b/backend/tests/integration_tests/api/shared.py @@ -14,7 +14,7 @@ from bracket.utils.http import HTTPMethod from bracket.utils.types import JsonDict from tests.integration_tests.models import AuthContext -SUCCESS_RESPONSE = SuccessResponse().dict() +SUCCESS_RESPONSE = SuccessResponse().model_dump() def find_free_port() -> int: diff --git a/backend/tests/integration_tests/api/stage_items_test.py b/backend/tests/integration_tests/api/stage_items_test.py index 9e34414a..d36181f0 100644 --- a/backend/tests/integration_tests/api/stage_items_test.py +++ b/backend/tests/integration_tests/api/stage_items_test.py @@ -27,19 +27,19 @@ async def test_create_stage_item( ) -> None: async with ( inserted_stage( - DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_1, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_1, inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted_2, ): assert team_inserted_1.id and team_inserted_2.id inputs = [ - StageItemInputCreateBodyFinal(slot=1, team_id=team_inserted_1.id).dict(), - StageItemInputCreateBodyFinal(slot=2, team_id=team_inserted_2.id).dict(), + StageItemInputCreateBodyFinal(slot=1, team_id=team_inserted_1.id).model_dump(), + StageItemInputCreateBodyFinal(slot=2, team_id=team_inserted_2.id).model_dump(), ] assert ( await send_tournament_request( @@ -65,12 +65,12 @@ async def test_delete_stage_item( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted_1, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted_1.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted_1.id}) ) as stage_item_inserted, ): assert ( @@ -88,10 +88,10 @@ async def test_update_stage_item( body = {"name": "Optimus"} async with ( inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, ): assert ( diff --git a/backend/tests/integration_tests/api/stages_test.py b/backend/tests/integration_tests/api/stages_test.py index 833fc4fe..c879f67d 100644 --- a/backend/tests/integration_tests/api/stages_test.py +++ b/backend/tests/integration_tests/api/stages_test.py @@ -32,15 +32,15 @@ async def test_stages_endpoint( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext, with_auth: bool ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, inserted_stage_item( - DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id}) + DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id}) ) as stage_item_inserted, inserted_round( - DUMMY_ROUND1.copy(update={"stage_item_id": stage_item_inserted.id}) + DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id}) ) as round_inserted, ): if with_auth: @@ -55,7 +55,7 @@ async def test_stages_endpoint( { "id": stage_inserted.id, "tournament_id": auth_context.tournament.id, - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "is_active": True, "name": "Group Stage", "stage_items": [ @@ -63,14 +63,14 @@ async def test_stages_endpoint( "id": stage_item_inserted.id, "stage_id": stage_inserted.id, "name": "Group A", - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "type": "ROUND_ROBIN", "team_count": 4, "rounds": [ { "id": round_inserted.id, "stage_item_id": stage_item_inserted.id, - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "is_draft": False, "is_active": False, "name": "Round 1", @@ -90,7 +90,7 @@ async def test_create_stage( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ): assert ( await send_tournament_request( @@ -109,9 +109,9 @@ async def test_delete_stage( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, ): assert ( @@ -128,11 +128,11 @@ async def test_update_stage( ) -> None: body = {"name": "Optimus"} async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), inserted_stage( - DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as stage_inserted, - inserted_stage_item(DUMMY_STAGE_ITEM1.copy(update={"stage_id": stage_inserted.id})), + inserted_stage_item(DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})), ): assert ( await send_tournament_request( @@ -152,9 +152,13 @@ async def test_activate_stage( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with ( - inserted_team(DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id})), - inserted_stage(DUMMY_STAGE1.copy(update={"tournament_id": auth_context.tournament.id})), - inserted_stage(DUMMY_STAGE2.copy(update={"tournament_id": auth_context.tournament.id})), + inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})), + inserted_stage( + DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id}) + ), + inserted_stage( + DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id}) + ), ): assert ( await send_tournament_request( diff --git a/backend/tests/integration_tests/api/teams_test.py b/backend/tests/integration_tests/api/teams_test.py index b3d5017f..c94b390f 100644 --- a/backend/tests/integration_tests/api/teams_test.py +++ b/backend/tests/integration_tests/api/teams_test.py @@ -13,19 +13,19 @@ async def test_teams_endpoint( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted: assert await send_tournament_request(HTTPMethod.GET, "teams", auth_context, {}) == { "data": [ { "active": True, - "created": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "id": team_inserted.id, "name": "Team 1", "players": [], "tournament_id": team_inserted.tournament_id, - "elo_score": 1200.0, - "swiss_score": 0.0, + "elo_score": "1200.0", + "swiss_score": "0.0", "wins": 0, "draws": 0, "losses": 0, @@ -58,7 +58,7 @@ async def test_delete_team( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted: assert ( await send_tournament_request( @@ -74,7 +74,7 @@ async def test_update_team( ) -> None: body = {"name": "Some new name", "active": True, "player_ids": []} async with inserted_team( - DUMMY_TEAM1.copy(update={"tournament_id": auth_context.tournament.id}) + DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id}) ) as team_inserted: response = await send_tournament_request( HTTPMethod.PUT, f"teams/{team_inserted.id}", auth_context, None, body diff --git a/backend/tests/integration_tests/api/tournaments_test.py b/backend/tests/integration_tests/api/tournaments_test.py index 1f5c4a9b..7b02da9e 100644 --- a/backend/tests/integration_tests/api/tournaments_test.py +++ b/backend/tests/integration_tests/api/tournaments_test.py @@ -21,9 +21,10 @@ async def test_tournaments_endpoint( { "id": auth_context.tournament.id, "club_id": auth_context.club.id, - "created": DUMMY_MOCK_TIME.isoformat(), - "start_time": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), + "start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "name": "Some Cool Tournament", + "logo_path": None, "dashboard_public": True, "dashboard_endpoint": "cool-tournament", "players_can_be_in_multiple_teams": True, @@ -44,8 +45,9 @@ async def test_tournament_endpoint( "data": { "id": auth_context.tournament.id, "club_id": auth_context.club.id, - "created": DUMMY_MOCK_TIME.isoformat(), - "start_time": DUMMY_MOCK_TIME.isoformat(), + "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), + "start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), + "logo_path": None, "name": "Some Cool Tournament", "dashboard_public": True, "dashboard_endpoint": "cool-tournament", @@ -62,7 +64,7 @@ async def test_create_tournament( ) -> None: body = { "name": "Some new name", - "start_time": DUMMY_MOCK_TIME.isoformat(), + "start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "club_id": auth_context.club.id, "dashboard_public": False, "players_can_be_in_multiple_teams": True, @@ -82,7 +84,7 @@ async def test_update_tournament( ) -> None: body = { "name": "Some new name", - "start_time": DUMMY_MOCK_TIME.isoformat(), + "start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "dashboard_public": False, "players_can_be_in_multiple_teams": True, "auto_assign_courts": True, @@ -108,11 +110,13 @@ async def test_delete_tournament( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: async with inserted_tournament( - DUMMY_TOURNAMENT.copy(update={"club_id": auth_context.club.id}) + DUMMY_TOURNAMENT.model_copy(update={"club_id": auth_context.club.id}) ) as tournament_inserted: assert ( await send_tournament_request( - HTTPMethod.DELETE, "", auth_context.copy(update={"tournament": tournament_inserted}) + HTTPMethod.DELETE, + "", + auth_context.model_copy(update={"tournament": tournament_inserted}), ) == SUCCESS_RESPONSE ) diff --git a/backend/tests/integration_tests/api/users_test.py b/backend/tests/integration_tests/api/users_test.py index aa30fdf8..77ef5e16 100644 --- a/backend/tests/integration_tests/api/users_test.py +++ b/backend/tests/integration_tests/api/users_test.py @@ -18,7 +18,7 @@ async def test_users_endpoint( ) == { "data": { "email": auth_context.user.email, - "created": "2200-01-01T00:00:00+00:00", + "created": "2200-01-01T00:00:00Z", "id": auth_context.user.id, "name": "Donald Duck", "account_type": UserAccountType.REGULAR.value, diff --git a/backend/tests/integration_tests/sql.py b/backend/tests/integration_tests/sql.py index 614c8608..3c504e12 100644 --- a/backend/tests/integration_tests/sql.py +++ b/backend/tests/integration_tests/sql.py @@ -111,7 +111,7 @@ async def inserted_stage(stage: Stage) -> AsyncIterator[Stage]: @asynccontextmanager async def inserted_stage_item(stage_item: StageItemToInsert) -> AsyncIterator[StageItem]: async with inserted_generic(stage_item, stage_items, StageItem) as row_inserted: - yield StageItem(**row_inserted.dict()) + yield StageItem(**row_inserted.model_dump()) @asynccontextmanager @@ -140,7 +140,7 @@ async def inserted_auth_context() -> AsyncIterator[AuthContext]: inserted_user(mock_user) as user_inserted, inserted_club(DUMMY_CLUB) as club_inserted, inserted_tournament( - DUMMY_TOURNAMENT.copy(update={"club_id": club_inserted.id}) + DUMMY_TOURNAMENT.model_copy(update={"club_id": club_inserted.id}) ) as tournament_inserted, inserted_user_x_club( UserXClub( diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 56c9c576..1ff23386 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -50,7 +50,7 @@ def test_elo_calculation() -> None: tournament_id=1, active=True, created=DUMMY_MOCK_TIME, - players=[DUMMY_PLAYER1.copy(update={"id": 1})], + players=[DUMMY_PLAYER1.model_copy(update={"id": 1})], elo_score=DUMMY_PLAYER1.elo_score, swiss_score=DUMMY_PLAYER1.swiss_score, wins=DUMMY_PLAYER1.wins, @@ -63,7 +63,7 @@ def test_elo_calculation() -> None: tournament_id=1, active=True, created=DUMMY_MOCK_TIME, - players=[DUMMY_PLAYER2.copy(update={"id": 2})], + players=[DUMMY_PLAYER2.model_copy(update={"id": 2})], elo_score=DUMMY_PLAYER2.elo_score, swiss_score=DUMMY_PLAYER2.swiss_score, wins=DUMMY_PLAYER2.wins, @@ -74,9 +74,10 @@ def test_elo_calculation() -> None: ], ) stage_item = StageItemWithRounds( - **DUMMY_STAGE_ITEM1.copy(update={"rounds": [round_]}).dict(), + **DUMMY_STAGE_ITEM1.model_dump(exclude={"id"}), id=-1, inputs=[], + rounds=[round_], ) player_stats, team_stats = determine_ranking_for_stage_items([stage_item]) assert player_stats == { diff --git a/backend/tests/unit_tests/swiss_test.py b/backend/tests/unit_tests/swiss_test.py index f646a772..03031e8f 100644 --- a/backend/tests/unit_tests/swiss_test.py +++ b/backend/tests/unit_tests/swiss_test.py @@ -27,7 +27,7 @@ def test_no_draft_round() -> None: def get_team(team: Team, team_id: int) -> FullTeamWithPlayers: return FullTeamWithPlayers( id=team_id, - **team.dict(), + **team.model_dump(exclude={"id"}), players=[], ) @@ -36,7 +36,7 @@ def get_match( match: Match, team1: FullTeamWithPlayers, team2: FullTeamWithPlayers ) -> MatchWithDetailsDefinitive: return MatchWithDetailsDefinitive( - **match.copy(update={"team1_id": team1.id, "team2_id": team2.id}).dict(), + **match.model_copy(update={"team1_id": team1.id, "team2_id": team2.id}).model_dump(), team1=team1, team2=team2, court=None, @@ -44,10 +44,10 @@ def get_match( def test_constraints() -> None: - team1 = get_team(DUMMY_TEAM1.copy(update={"elo_score": 1125}), team_id=-1) - team2 = get_team(DUMMY_TEAM2.copy(update={"elo_score": 1175}), team_id=-2) - team3 = get_team(DUMMY_TEAM3.copy(update={"elo_score": 1200}), team_id=-3) - team4 = get_team(DUMMY_TEAM4.copy(update={"elo_score": 1250}), team_id=-4) + team1 = get_team(DUMMY_TEAM1.model_copy(update={"elo_score": Decimal("1125.0")}), team_id=-1) + team2 = get_team(DUMMY_TEAM2.model_copy(update={"elo_score": Decimal("1175.0")}), team_id=-2) + team3 = get_team(DUMMY_TEAM3.model_copy(update={"elo_score": Decimal("1200.0")}), team_id=-3) + team4 = get_team(DUMMY_TEAM4.model_copy(update={"elo_score": Decimal("1250.0")}), team_id=-4) rounds = [ RoundWithMatches(