From b395d148f0f8899926a30f0f4bb178974403d720 Mon Sep 17 00:00:00 2001 From: Erik Vroon Date: Fri, 23 Feb 2024 20:41:55 +0100 Subject: [PATCH] Add newtypes for primary keys (#512) Adds type safety --- backend/bracket/logic/planning/matches.py | 9 +- backend/bracket/logic/planning/rounds.py | 5 +- backend/bracket/logic/ranking/elo.py | 21 ++- backend/bracket/logic/scheduling/builder.py | 9 +- .../bracket/logic/scheduling/elimination.py | 3 +- .../scheduling/handle_stage_activation.py | 13 +- .../bracket/logic/scheduling/ladder_teams.py | 9 +- .../bracket/logic/scheduling/round_robin.py | 5 +- .../logic/scheduling/upcoming_matches.py | 7 +- backend/bracket/logic/tournaments.py | 7 +- backend/bracket/models/db/club.py | 3 +- backend/bracket/models/db/court.py | 7 +- backend/bracket/models/db/match.py | 49 +++--- backend/bracket/models/db/player.py | 7 +- backend/bracket/models/db/player_x_team.py | 7 +- backend/bracket/models/db/round.py | 9 +- backend/bracket/models/db/stage.py | 5 +- backend/bracket/models/db/stage_item.py | 9 +- .../bracket/models/db/stage_item_inputs.py | 27 +-- backend/bracket/models/db/team.py | 13 +- backend/bracket/models/db/tournament.py | 7 +- backend/bracket/models/db/user.py | 5 +- backend/bracket/models/db/user_x_club.py | 7 +- backend/bracket/models/db/util.py | 3 +- backend/bracket/routes/auth.py | 9 +- backend/bracket/routes/clubs.py | 5 +- backend/bracket/routes/courts.py | 13 +- backend/bracket/routes/matches.py | 17 +- backend/bracket/routes/players.py | 15 +- backend/bracket/routes/rounds.py | 11 +- backend/bracket/routes/stage_items.py | 22 ++- backend/bracket/routes/stages.py | 19 ++- backend/bracket/routes/teams.py | 15 +- backend/bracket/routes/tournaments.py | 10 +- backend/bracket/routes/users.py | 7 +- backend/bracket/routes/util.py | 21 ++- backend/bracket/sql/clubs.py | 13 +- backend/bracket/sql/courts.py | 11 +- backend/bracket/sql/matches.py | 19 ++- backend/bracket/sql/players.py | 13 +- backend/bracket/sql/rounds.py | 17 +- backend/bracket/sql/shared.py | 5 +- backend/bracket/sql/stage_item_inputs.py | 94 ++++++----- backend/bracket/sql/stage_items.py | 23 ++- backend/bracket/sql/stages.py | 31 ++-- backend/bracket/sql/teams.py | 31 ++-- backend/bracket/sql/tournaments.py | 5 +- backend/bracket/sql/users.py | 19 ++- backend/bracket/sql/validation.py | 35 ++++ backend/bracket/utils/db_init.py | 157 ++++++++++++------ backend/bracket/utils/dummy_records.py | 68 ++++---- backend/bracket/utils/id_types.py | 15 ++ backend/tests/integration_tests/sql.py | 3 +- backend/tests/unit_tests/elo_test.py | 19 ++- backend/tests/unit_tests/swiss_test.py | 25 ++- 55 files changed, 622 insertions(+), 391 deletions(-) create mode 100644 backend/bracket/sql/validation.py create mode 100644 backend/bracket/utils/id_types.py diff --git a/backend/bracket/logic/planning/matches.py b/backend/bracket/logic/planning/matches.py index ac451a98b..38a255fc6 100644 --- a/backend/bracket/logic/planning/matches.py +++ b/backend/bracket/logic/planning/matches.py @@ -16,10 +16,11 @@ ) from bracket.sql.stages import get_full_tournament_details from bracket.sql.tournaments import sql_get_tournament +from bracket.utils.id_types import CourtId, MatchId, TournamentId from bracket.utils.types import assert_some -async def schedule_all_unscheduled_matches(tournament_id: int) -> None: +async def schedule_all_unscheduled_matches(tournament_id: TournamentId) -> None: tournament = await sql_get_tournament(tournament_id) stages = await get_full_tournament_details(tournament_id) courts = await get_all_courts_in_tournament(tournament_id) @@ -81,7 +82,7 @@ class MatchPosition(NamedTuple): async def reorder_matches_for_court( tournament: Tournament, scheduled_matches: list[MatchPosition], - court_id: int, + court_id: CourtId, ) -> None: matches_this_court = sorted( (match_pos for match_pos in scheduled_matches if match_pos.match.court_id == court_id), @@ -104,7 +105,7 @@ async def reorder_matches_for_court( async def handle_match_reschedule( - tournament_id: int, body: MatchRescheduleBody, match_id: int + tournament_id: TournamentId, body: MatchRescheduleBody, match_id: MatchId ) -> None: if body.old_position == body.new_position and body.old_court_id == body.new_court_id: return @@ -143,7 +144,7 @@ async def handle_match_reschedule( await reorder_matches_for_court(tournament, scheduled_matches, body.old_court_id) -async def update_start_times_of_matches(tournament_id: int) -> None: +async def update_start_times_of_matches(tournament_id: TournamentId) -> None: stages = await get_full_tournament_details(tournament_id) tournament = await sql_get_tournament(tournament_id) courts = await get_all_courts_in_tournament(tournament_id) diff --git a/backend/bracket/logic/planning/rounds.py b/backend/bracket/logic/planning/rounds.py index b25d9fae6..7ed46c309 100644 --- a/backend/bracket/logic/planning/rounds.py +++ b/backend/bracket/logic/planning/rounds.py @@ -8,6 +8,7 @@ ) from bracket.sql.stages import get_full_tournament_details from bracket.sql.tournaments import sql_get_tournament +from bracket.utils.id_types import TournamentId from bracket.utils.types import assert_some @@ -36,7 +37,9 @@ def is_round_in_future(round_: RoundWithMatches) -> bool: async def schedule_all_matches_for_swiss_round( - tournament_id: int, active_round: RoundWithMatches, adjust_to_time: datetime_utc | None = None + tournament_id: TournamentId, + 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) diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index 6e9846d89..9445597b1 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -1,6 +1,7 @@ import math from collections import defaultdict from decimal import Decimal +from typing import TypeVar from bracket.database import database from bracket.models.db.match import MatchWithDetailsDefinitive @@ -10,17 +11,21 @@ from bracket.sql.players import get_all_players_in_tournament, update_player_stats from bracket.sql.stages import get_full_tournament_details from bracket.sql.teams import update_team_stats +from bracket.utils.id_types import PlayerId, TeamId, TournamentId from bracket.utils.types import assert_some K = 32 D = 400 +TeamIdOrPlayerId = TypeVar("TeamIdOrPlayerId", bound=PlayerId | TeamId) + + def set_statistics_for_player_or_team( team_index: int, - stats: defaultdict[int, PlayerStatistics], + stats: defaultdict[TeamIdOrPlayerId, PlayerStatistics], match: MatchWithDetailsDefinitive, - team_or_player_id: int, + team_or_player_id: TeamIdOrPlayerId, rating_team1_before: float, rating_team2_before: float, ) -> None: @@ -48,9 +53,9 @@ def set_statistics_for_player_or_team( def determine_ranking_for_stage_items( stage_items: list[StageItemWithRounds], -) -> tuple[defaultdict[int, PlayerStatistics], defaultdict[int, PlayerStatistics]]: - player_x_stats: defaultdict[int, PlayerStatistics] = defaultdict(PlayerStatistics) - team_x_stats: defaultdict[int, PlayerStatistics] = defaultdict(PlayerStatistics) +) -> tuple[defaultdict[PlayerId, PlayerStatistics], defaultdict[TeamId, PlayerStatistics]]: + player_x_stats: defaultdict[PlayerId, PlayerStatistics] = defaultdict(PlayerStatistics) + team_x_stats: defaultdict[TeamId, PlayerStatistics] = defaultdict(PlayerStatistics) matches = [ match for stage_item in stage_items @@ -100,19 +105,19 @@ def determine_ranking_for_stage_items( def determine_team_ranking_for_stage_item( stage_item: StageItemWithRounds, -) -> list[tuple[int, PlayerStatistics]]: +) -> list[tuple[TeamId, PlayerStatistics]]: _, team_ranking = determine_ranking_for_stage_items([stage_item]) return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True) -async def recalculate_ranking_for_tournament_id(tournament_id: int) -> None: +async def recalculate_ranking_for_tournament_id(tournament_id: TournamentId) -> None: stages = await get_full_tournament_details(tournament_id) stage_items = [stage_item for stage in stages for stage_item in stage.stage_items] await recalculate_ranking_for_stage_items(tournament_id, stage_items) async def recalculate_ranking_for_stage_items( - tournament_id: int, stage_items: list[StageItemWithRounds] + tournament_id: TournamentId, stage_items: list[StageItemWithRounds] ) -> None: elo_per_player, elo_per_team = determine_ranking_for_stage_items(stage_items) diff --git a/backend/bracket/logic/scheduling/builder.py b/backend/bracket/logic/scheduling/builder.py index 116d6a264..67dc402c7 100644 --- a/backend/bracket/logic/scheduling/builder.py +++ b/backend/bracket/logic/scheduling/builder.py @@ -18,10 +18,13 @@ from bracket.models.db.util import StageWithStageItems from bracket.sql.rounds import get_next_round_name, sql_create_round from bracket.sql.stage_items import get_stage_item +from bracket.utils.id_types import StageId, TournamentId from bracket.utils.types import assert_some -async def create_rounds_for_new_stage_item(tournament_id: int, stage_item: StageItem) -> None: +async def create_rounds_for_new_stage_item( + tournament_id: TournamentId, stage_item: StageItem +) -> None: rounds_count: int match stage_item.type: case StageType.ROUND_ROBIN: @@ -42,7 +45,7 @@ async def create_rounds_for_new_stage_item(tournament_id: int, stage_item: Stage ) -async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: int) -> None: +async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: TournamentId) -> None: await create_rounds_for_new_stage_item(tournament_id, stage_item) stage_item_with_rounds = await get_stage_item(tournament_id, assert_some(stage_item.id)) @@ -66,7 +69,7 @@ async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: int def determine_available_inputs( - stage_id: int, + stage_id: StageId, teams: list[FullTeamWithPlayers], stages: list[StageWithStageItems], ) -> list[StageItemInputOptionTentative | StageItemInputOptionFinal]: diff --git a/backend/bracket/logic/scheduling/elimination.py b/backend/bracket/logic/scheduling/elimination.py index fc32cc1ac..18951d18a 100644 --- a/backend/bracket/logic/scheduling/elimination.py +++ b/backend/bracket/logic/scheduling/elimination.py @@ -4,6 +4,7 @@ from bracket.sql.matches import sql_create_match from bracket.sql.rounds import get_rounds_for_stage_item from bracket.sql.tournaments import sql_get_tournament +from bracket.utils.id_types import TournamentId from bracket.utils.types import assert_some @@ -70,7 +71,7 @@ def determine_matches_subsequent_round( async def build_single_elimination_stage_item( - tournament_id: int, stage_item: StageItemWithRounds + tournament_id: TournamentId, stage_item: StageItemWithRounds ) -> None: rounds = await get_rounds_for_stage_item(tournament_id, stage_item.id) tournament = await sql_get_tournament(tournament_id) diff --git a/backend/bracket/logic/scheduling/handle_stage_activation.py b/backend/bracket/logic/scheduling/handle_stage_activation.py index dab19afdb..5b6fafb6c 100644 --- a/backend/bracket/logic/scheduling/handle_stage_activation.py +++ b/backend/bracket/logic/scheduling/handle_stage_activation.py @@ -5,15 +5,16 @@ from bracket.sql.matches import sql_get_match, sql_update_team_ids_for_match from bracket.sql.stage_items import get_stage_item from bracket.sql.stages import get_full_tournament_details +from bracket.utils.id_types import MatchId, StageId, StageItemId, TeamId, TournamentId from bracket.utils.types import assert_some async def determine_team_id( - tournament_id: int, - winner_from_stage_item_id: int | None, + tournament_id: TournamentId, + winner_from_stage_item_id: StageItemId | None, winner_position: int | None, - winner_from_match_id: int | None, -) -> int | None: + winner_from_match_id: MatchId | None, +) -> TeamId | None: if winner_from_stage_item_id is not None and winner_position is not None: stage_item = await get_stage_item(tournament_id, winner_from_stage_item_id) assert stage_item is not None @@ -37,7 +38,7 @@ async def determine_team_id( raise ValueError("Unexpected match type") -async def set_team_ids_for_match(tournament_id: int, match: MatchWithDetails) -> None: +async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails) -> None: team1_id = await determine_team_id( tournament_id, match.team1_winner_from_stage_item_id, @@ -54,7 +55,7 @@ async def set_team_ids_for_match(tournament_id: int, match: MatchWithDetails) -> await sql_update_team_ids_for_match(assert_some(match.id), team1_id, team2_id) -async def update_matches_in_activated_stage(tournament_id: int, stage_id: int) -> None: +async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_id: StageId) -> None: [stage] = await get_full_tournament_details(tournament_id, stage_id=stage_id) for stage_item in stage.stage_items: diff --git a/backend/bracket/logic/scheduling/ladder_teams.py b/backend/bracket/logic/scheduling/ladder_teams.py index e9aa38aec..49ab46314 100644 --- a/backend/bracket/logic/scheduling/ladder_teams.py +++ b/backend/bracket/logic/scheduling/ladder_teams.py @@ -12,15 +12,16 @@ ) from bracket.models.db.team import FullTeamWithPlayers from bracket.models.db.util import RoundWithMatches +from bracket.utils.id_types import TeamId from bracket.utils.types import assert_some -def get_draft_round_team_ids(draft_round: RoundWithMatches) -> list[int]: +def get_draft_round_team_ids(draft_round: RoundWithMatches) -> list[TeamId]: return [ - team + team_id for match in draft_round.matches if isinstance(match, MatchWithDetailsDefinitive) - for team in match.team_ids + for team_id in match.team_ids ] @@ -37,7 +38,7 @@ def get_previous_matches_hashes(rounds: list[RoundWithMatches]) -> frozenset[str def get_number_of_teams_played_per_team( - rounds: list[RoundWithMatches], excluded_team_ids: frozenset[int] + rounds: list[RoundWithMatches], excluded_team_ids: frozenset[TeamId] ) -> dict[int, int]: result: dict[int, int] = defaultdict(int) diff --git a/backend/bracket/logic/scheduling/round_robin.py b/backend/bracket/logic/scheduling/round_robin.py index c18604e34..f1560b902 100644 --- a/backend/bracket/logic/scheduling/round_robin.py +++ b/backend/bracket/logic/scheduling/round_robin.py @@ -4,6 +4,7 @@ from bracket.models.db.util import StageItemWithRounds from bracket.sql.matches import sql_create_match from bracket.sql.tournaments import sql_get_tournament +from bracket.utils.id_types import TournamentId from bracket.utils.types import assert_some @@ -35,7 +36,9 @@ def get_round_robin_combinations(team_count: int) -> list[list[tuple[int, int]]] return matches -async def build_round_robin_stage_item(tournament_id: int, stage_item: StageItemWithRounds) -> None: +async def build_round_robin_stage_item( + tournament_id: TournamentId, stage_item: StageItemWithRounds +) -> None: matches = get_round_robin_combinations(len(stage_item.inputs)) tournament = await sql_get_tournament(tournament_id) diff --git a/backend/bracket/logic/scheduling/upcoming_matches.py b/backend/bracket/logic/scheduling/upcoming_matches.py index 1cc2716a0..62b5d5ea0 100644 --- a/backend/bracket/logic/scheduling/upcoming_matches.py +++ b/backend/bracket/logic/scheduling/upcoming_matches.py @@ -7,13 +7,16 @@ from bracket.sql.rounds import get_rounds_for_stage_item from bracket.sql.stages import get_full_tournament_details from bracket.sql.teams import get_teams_with_members +from bracket.utils.id_types import TournamentId from bracket.utils.types import assert_some async def get_upcoming_matches_for_swiss_round( - match_filter: MatchFilter, round_: Round, tournament_id: int + match_filter: MatchFilter, round_: Round, tournament_id: TournamentId ) -> list[SuggestedMatch]: - [stage] = await get_full_tournament_details(tournament_id, stage_item_id=round_.stage_item_id) + [stage] = await get_full_tournament_details( + tournament_id, stage_item_ids={round_.stage_item_id} + ) assert len(stage.stage_items) == 1 [stage_item] = stage.stage_items diff --git a/backend/bracket/logic/tournaments.py b/backend/bracket/logic/tournaments.py index a9fb86ab3..2d108960c 100644 --- a/backend/bracket/logic/tournaments.py +++ b/backend/bracket/logic/tournaments.py @@ -7,22 +7,23 @@ from bracket.sql.stages import get_full_tournament_details, sql_delete_stage from bracket.sql.teams import sql_delete_teams_of_tournament from bracket.sql.tournaments import sql_delete_tournament, sql_get_tournament +from bracket.utils.id_types import TournamentId from bracket.utils.types import assert_some -async def get_tournament_logo_path(tournament_id: int) -> str | None: +async def get_tournament_logo_path(tournament_id: TournamentId) -> str | None: tournament = await sql_get_tournament(tournament_id) logo_path = f"static/{tournament.logo_path}" if tournament.logo_path else None return logo_path if logo_path is not None and await aiofiles.os.path.exists(logo_path) else None -async def delete_tournament_logo(tournament_id: int) -> None: +async def delete_tournament_logo(tournament_id: TournamentId) -> None: logo_path = await get_tournament_logo_path(tournament_id) if logo_path is not None: await aiofiles.os.remove(logo_path) -async def sql_delete_tournament_completely(tournament_id: int) -> None: +async def sql_delete_tournament_completely(tournament_id: TournamentId) -> None: stages = await get_full_tournament_details(tournament_id) await delete_tournament_logo(tournament_id) diff --git a/backend/bracket/models/db/club.py b/backend/bracket/models/db/club.py index 1efad83ef..cfde65da7 100644 --- a/backend/bracket/models/db/club.py +++ b/backend/bracket/models/db/club.py @@ -1,10 +1,11 @@ from heliclockter import datetime_utc from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import ClubId class Club(BaseModelORM): - id: int | None = None + id: ClubId | None = None name: str created: datetime_utc diff --git a/backend/bracket/models/db/court.py b/backend/bracket/models/db/court.py index 89fa023b9..1cf4df4f2 100644 --- a/backend/bracket/models/db/court.py +++ b/backend/bracket/models/db/court.py @@ -1,13 +1,14 @@ from heliclockter import datetime_utc from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import CourtId, TournamentId class Court(BaseModelORM): - id: int | None = None + id: CourtId | None = None name: str created: datetime_utc - tournament_id: int + tournament_id: TournamentId class CourtBody(BaseModelORM): @@ -16,4 +17,4 @@ class CourtBody(BaseModelORM): class CourtToInsert(CourtBody): created: datetime_utc - tournament_id: int + tournament_id: TournamentId diff --git a/backend/bracket/models/db/match.py b/backend/bracket/models/db/match.py index e60bd200a..fd1adecaf 100644 --- a/backend/bracket/models/db/match.py +++ b/backend/bracket/models/db/match.py @@ -6,11 +6,12 @@ from bracket.models.db.court import Court from bracket.models.db.shared import BaseModelORM from bracket.models.db.team import FullTeamWithPlayers, TeamWithPlayers +from bracket.utils.id_types import CourtId, MatchId, PlayerId, RoundId, StageItemId, TeamId from bracket.utils.types import assert_some class MatchBase(BaseModelORM): - id: int | None = None + id: MatchId | None = None created: datetime_utc start_time: datetime_utc | None = None duration_minutes: int @@ -18,10 +19,10 @@ class MatchBase(BaseModelORM): custom_duration_minutes: int | None = None custom_margin_minutes: int | None = None position_in_schedule: int | None = None - round_id: int + round_id: RoundId team1_score: int team2_score: int - court_id: int | None = None + court_id: CourtId | None = None @property def end_time(self) -> datetime_utc: @@ -32,14 +33,14 @@ def end_time(self) -> datetime_utc: class Match(MatchBase): - team1_id: int | None = None - team2_id: int | None = None + team1_id: TeamId | None = None + team2_id: TeamId | 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 + team1_winner_from_stage_item_id: StageItemId | None = None + team2_winner_from_stage_item_id: StageItemId | None = None team2_winner_position: int | None = None - team1_winner_from_match_id: int | None = None - team2_winner_from_match_id: int | None = None + team1_winner_from_match_id: MatchId | None = None + team2_winner_from_match_id: MatchId | None = None def get_winner_index(self) -> int | None: if self.team1_score == self.team2_score: @@ -52,7 +53,7 @@ class MatchWithDetails(Match): court: Court | None = None -def get_match_hash(team_1_id: int | None, team_2_id: int | None) -> str: +def get_match_hash(team_1_id: TeamId | None, team_2_id: TeamId | None) -> str: return f"{team_1_id}-{team_2_id}" @@ -66,7 +67,7 @@ def teams(self) -> list[FullTeamWithPlayers]: return [self.team1, self.team2] @property - def team_ids(self) -> list[int]: + def team_ids(self) -> list[TeamId]: return [assert_some(self.team1.id), assert_some(self.team2.id)] def get_team_ids_hashes(self) -> list[str]: @@ -76,30 +77,30 @@ def get_team_ids_hashes(self) -> list[str]: ] @property - def player_ids(self) -> list[int]: + def player_ids(self) -> list[PlayerId]: return self.team1.player_ids + self.team2.player_ids class MatchBody(BaseModelORM): - round_id: int + round_id: RoundId team1_score: int = 0 team2_score: int = 0 - court_id: int | None = None + court_id: CourtId | None = None custom_duration_minutes: int | None = None custom_margin_minutes: int | None = None class MatchCreateBodyFrontend(BaseModelORM): - round_id: int - court_id: int | None = None - team1_id: int | None = None - team2_id: int | None = None - team1_winner_from_stage_item_id: int | None = None + round_id: RoundId + court_id: CourtId | None = None + team1_id: TeamId | None = None + team2_id: TeamId | None = None + team1_winner_from_stage_item_id: StageItemId | 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 + team1_winner_from_match_id: MatchId | None = None + team2_winner_from_stage_item_id: StageItemId | None = None team2_winner_position: int | None = None - team2_winner_from_match_id: int | None = None + team2_winner_from_match_id: MatchId | None = None class MatchCreateBody(MatchCreateBodyFrontend): @@ -110,9 +111,9 @@ class MatchCreateBody(MatchCreateBodyFrontend): class MatchRescheduleBody(BaseModelORM): - old_court_id: int + old_court_id: CourtId old_position: int - new_court_id: int + new_court_id: CourtId new_position: int diff --git a/backend/bracket/models/db/player.py b/backend/bracket/models/db/player.py index 3918272e5..cbf3189e9 100644 --- a/backend/bracket/models/db/player.py +++ b/backend/bracket/models/db/player.py @@ -4,14 +4,15 @@ from pydantic import Field from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import PlayerId, TournamentId class Player(BaseModelORM): - id: int | None = None + id: PlayerId | None = None active: bool name: str created: datetime_utc - tournament_id: int + tournament_id: TournamentId elo_score: Decimal = Decimal("0.0") swiss_score: Decimal = Decimal("0.0") wins: int = 0 @@ -34,7 +35,7 @@ class PlayerMultiBody(BaseModelORM): class PlayerToInsert(PlayerBody): created: datetime_utc - tournament_id: int + tournament_id: TournamentId elo_score: Decimal = Decimal("1200.0") swiss_score: Decimal wins: int = 0 diff --git a/backend/bracket/models/db/player_x_team.py b/backend/bracket/models/db/player_x_team.py index 6c6ed709c..2b1cddf0a 100644 --- a/backend/bracket/models/db/player_x_team.py +++ b/backend/bracket/models/db/player_x_team.py @@ -1,7 +1,8 @@ from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import PlayerId, PlayerXTeamId, TeamId class PlayerXTeam(BaseModelORM): - id: int | None = None - player_id: int - team_id: int + id: PlayerXTeamId | None = None + player_id: PlayerId + team_id: TeamId diff --git a/backend/bracket/models/db/round.py b/backend/bracket/models/db/round.py index c75efd00b..f1d076aab 100644 --- a/backend/bracket/models/db/round.py +++ b/backend/bracket/models/db/round.py @@ -1,11 +1,12 @@ from heliclockter import datetime_utc from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import RoundId, StageItemId class Round(BaseModelORM): - id: int | None = None - stage_item_id: int + id: RoundId | None = None + stage_item_id: StageItemId created: datetime_utc is_draft: bool is_active: bool = False @@ -20,10 +21,10 @@ class RoundUpdateBody(BaseModelORM): class RoundCreateBody(BaseModelORM): name: str | None = None - stage_item_id: int + stage_item_id: StageItemId class RoundToInsert(RoundUpdateBody): - stage_item_id: int + stage_item_id: StageItemId is_draft: bool = False is_active: bool = False diff --git a/backend/bracket/models/db/stage.py b/backend/bracket/models/db/stage.py index 0b02cc191..b4cc8a569 100644 --- a/backend/bracket/models/db/stage.py +++ b/backend/bracket/models/db/stage.py @@ -3,11 +3,12 @@ from heliclockter import datetime_utc from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import StageId, TournamentId class Stage(BaseModelORM): - id: int | None = None - tournament_id: int + id: StageId | None = None + tournament_id: TournamentId name: str created: datetime_utc is_active: bool diff --git a/backend/bracket/models/db/stage_item.py b/backend/bracket/models/db/stage_item.py index f58caa3e0..c2b28057c 100644 --- a/backend/bracket/models/db/stage_item.py +++ b/backend/bracket/models/db/stage_item.py @@ -6,6 +6,7 @@ from bracket.models.db.shared import BaseModelORM from bracket.models.db.stage_item_inputs import StageItemInputCreateBody +from bracket.utils.id_types import StageId, StageItemId from bracket.utils.types import EnumAutoStr @@ -20,8 +21,8 @@ def supports_dynamic_number_of_rounds(self) -> bool: class StageItemToInsert(BaseModelORM): - id: int | None = None - stage_id: int + id: StageItemId | None = None + stage_id: StageId name: str created: datetime_utc type: StageType @@ -29,7 +30,7 @@ class StageItemToInsert(BaseModelORM): class StageItem(StageItemToInsert): - id: int + id: StageItemId class StageItemUpdateBody(BaseModelORM): @@ -41,7 +42,7 @@ class StageItemActivateNextBody(BaseModelORM): class StageItemCreateBody(BaseModelORM): - stage_id: int + stage_id: StageId name: str | None = None type: StageType team_count: int = Field(ge=2, le=64) diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index 52e340b19..da03a9b0f 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -1,20 +1,21 @@ from pydantic import BaseModel, Field from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import MatchId, StageItemId, StageItemInputId, TeamId, TournamentId class StageItemInputBase(BaseModelORM): - id: int | None = None + id: StageItemInputId | None = None slot: int - tournament_id: int - stage_item_id: int | None = None + tournament_id: TournamentId + stage_item_id: StageItemId | None = None class StageItemInputGeneric(BaseModel): - team_id: int | None = None - winner_from_stage_item_id: int | None = None + team_id: TeamId | None = None + winner_from_stage_item_id: StageItemId | None = None winner_position: int | None = None - winner_from_match_id: int | None = None + winner_from_match_id: MatchId | None = None def __hash__(self) -> int: return ( @@ -28,12 +29,12 @@ def __hash__(self) -> int: class StageItemInputTentative(StageItemInputBase, StageItemInputGeneric): team_id: None = None winner_from_match_id: None = None - winner_from_stage_item_id: int + winner_from_stage_item_id: StageItemId winner_position: int = Field(ge=1) class StageItemInputFinal(StageItemInputBase, StageItemInputGeneric): - team_id: int + team_id: TeamId winner_from_match_id: None = None winner_from_stage_item_id: None = None winner_position: None = None @@ -41,7 +42,7 @@ class StageItemInputFinal(StageItemInputBase, StageItemInputGeneric): class StageItemInputMatch(StageItemInputBase, StageItemInputGeneric): team_id: None = None - winner_from_match_id: int + winner_from_match_id: MatchId winner_from_stage_item_id: None = None winner_position: None = None @@ -51,22 +52,22 @@ class StageItemInputMatch(StageItemInputBase, StageItemInputGeneric): class StageItemInputCreateBodyTentative(BaseModel): slot: int - winner_from_stage_item_id: int + winner_from_stage_item_id: StageItemId winner_position: int = Field(ge=1) class StageItemInputCreateBodyFinal(BaseModel): slot: int - team_id: int + team_id: TeamId StageItemInputCreateBody = StageItemInputCreateBodyTentative | StageItemInputCreateBodyFinal class StageItemInputOptionFinal(BaseModel): - team_id: int + team_id: TeamId class StageItemInputOptionTentative(BaseModel): - winner_from_stage_item_id: int + winner_from_stage_item_id: StageItemId winner_position: int diff --git a/backend/bracket/models/db/team.py b/backend/bracket/models/db/team.py index a79f08a4d..97b50d511 100644 --- a/backend/bracket/models/db/team.py +++ b/backend/bracket/models/db/team.py @@ -11,14 +11,15 @@ from bracket.models.db.player import Player from bracket.models.db.players import START_ELO from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import PlayerId, TeamId, TournamentId from bracket.utils.types import assert_some class Team(BaseModelORM): - id: int | None = None + id: TeamId | None = None created: datetime_utc name: str - tournament_id: int + tournament_id: TournamentId active: bool elo_score: Decimal = Decimal(START_ELO) swiss_score: Decimal = Decimal("0.0") @@ -28,7 +29,7 @@ class Team(BaseModelORM): class TeamWithPlayers(BaseModel): - id: int | None = None + id: TeamId | None = None players: list[Player] elo_score: Decimal = Decimal(START_ELO) swiss_score: Decimal = Decimal("0.0") @@ -38,7 +39,7 @@ class TeamWithPlayers(BaseModel): name: str @property - def player_ids(self) -> list[int]: + def player_ids(self) -> list[PlayerId]: return [assert_some(player.id) for player in self.players] @field_validator("players", mode="before") @@ -79,7 +80,7 @@ class FullTeamWithPlayers(TeamWithPlayers, Team): class TeamBody(BaseModelORM): name: Annotated[str, StringConstraints(min_length=1, max_length=30)] active: bool - player_ids: set[int] + player_ids: set[PlayerId] class TeamMultiBody(BaseModelORM): @@ -90,7 +91,7 @@ class TeamMultiBody(BaseModelORM): class TeamToInsert(BaseModelORM): created: datetime_utc name: str - tournament_id: int + tournament_id: TournamentId active: bool elo_score: Decimal = Decimal("0.0") swiss_score: Decimal = Decimal("0.0") diff --git a/backend/bracket/models/db/tournament.py b/backend/bracket/models/db/tournament.py index 7fe61b88b..4a5ac71cc 100644 --- a/backend/bracket/models/db/tournament.py +++ b/backend/bracket/models/db/tournament.py @@ -2,12 +2,13 @@ from pydantic import Field from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import ClubId, TournamentId from bracket.utils.pydantic import EmptyStrToNone class Tournament(BaseModelORM): - id: int | None = None - club_id: int + id: TournamentId | None = None + club_id: ClubId name: str created: datetime_utc start_time: datetime_utc @@ -32,7 +33,7 @@ class TournamentUpdateBody(BaseModelORM): class TournamentBody(TournamentUpdateBody): - club_id: int + club_id: ClubId class TournamentToInsert(TournamentBody): diff --git a/backend/bracket/models/db/user.py b/backend/bracket/models/db/user.py index ecc3dc775..856fb6635 100644 --- a/backend/bracket/models/db/user.py +++ b/backend/bracket/models/db/user.py @@ -7,13 +7,14 @@ from bracket.models.db.account import UserAccountType from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import UserId if TYPE_CHECKING: from bracket.logic.subscriptions import Subscription class UserBase(BaseModelORM): - id: int | None = None + id: UserId | None = None email: str name: str created: datetime_utc @@ -55,5 +56,5 @@ class UserToRegister(BaseModelORM): class UserInDB(User): - id: int + id: UserId password_hash: str diff --git a/backend/bracket/models/db/user_x_club.py b/backend/bracket/models/db/user_x_club.py index 4dce917eb..2c8a81b9f 100644 --- a/backend/bracket/models/db/user_x_club.py +++ b/backend/bracket/models/db/user_x_club.py @@ -1,6 +1,7 @@ from enum import auto from bracket.models.db.shared import BaseModelORM +from bracket.utils.id_types import ClubId, UserId, UserXClubId from bracket.utils.types import EnumAutoStr @@ -10,7 +11,7 @@ class UserXClubRelation(EnumAutoStr): class UserXClub(BaseModelORM): - id: int | None = None - user_id: int - club_id: int + id: UserXClubId | None = None + user_id: UserId + club_id: ClubId relation: UserXClubRelation diff --git a/backend/bracket/models/db/util.py b/backend/bracket/models/db/util.py index 48848a500..c8125874b 100644 --- a/backend/bracket/models/db/util.py +++ b/backend/bracket/models/db/util.py @@ -11,6 +11,7 @@ from bracket.models.db.stage import Stage from bracket.models.db.stage_item import StageItem, StageType from bracket.models.db.stage_item_inputs import StageItemInput +from bracket.utils.id_types import TeamId from bracket.utils.types import assert_some @@ -23,7 +24,7 @@ def handle_matches(values: list[Match]) -> list[Match]: # type: ignore[misc] return [] return values - def get_team_ids(self) -> set[int]: + def get_team_ids(self) -> set[TeamId]: return { assert_some(team.id) for match in self.matches diff --git a/backend/bracket/routes/auth.py b/backend/bracket/routes/auth.py index 690d59d91..17e8c3c78 100644 --- a/backend/bracket/routes/auth.py +++ b/backend/bracket/routes/auth.py @@ -16,6 +16,7 @@ from bracket.sql.tournaments import sql_get_tournament_by_endpoint_name from bracket.sql.users import get_user, get_user_access_to_club, get_user_access_to_tournament from bracket.utils.db import fetch_all_parsed +from bracket.utils.id_types import ClubId, TournamentId, UserId from bracket.utils.security import verify_password from bracket.utils.types import assert_some @@ -45,7 +46,7 @@ class Token(BaseModel): access_token: str token_type: str - user_id: int + user_id: UserId class TokenData(BaseModel): @@ -98,7 +99,7 @@ async def user_authenticated(token: str = Depends(oauth2_scheme)) -> UserPublic: async def user_authenticated_for_tournament( - tournament_id: int, token: str = Depends(oauth2_scheme) + tournament_id: TournamentId, token: str = Depends(oauth2_scheme) ) -> UserPublic: user = await check_jwt_and_get_user(token) @@ -113,7 +114,7 @@ async def user_authenticated_for_tournament( async def user_authenticated_for_club( - club_id: int, token: str = Depends(oauth2_scheme) + club_id: ClubId, token: str = Depends(oauth2_scheme) ) -> UserPublic: user = await check_jwt_and_get_user(token) @@ -128,7 +129,7 @@ async def user_authenticated_for_club( async def user_authenticated_or_public_dashboard( - tournament_id: int, request: Request + tournament_id: TournamentId, request: Request ) -> UserPublic | None: try: token: str = assert_some(await oauth2_scheme(request)) diff --git a/backend/bracket/routes/clubs.py b/backend/bracket/routes/clubs.py index 7b6b02fb2..b4c7527c2 100644 --- a/backend/bracket/routes/clubs.py +++ b/backend/bracket/routes/clubs.py @@ -8,6 +8,7 @@ from bracket.routes.models import ClubResponse, ClubsResponse, SuccessResponse from bracket.sql.clubs import create_club, get_clubs_for_user_id, sql_delete_club, sql_update_club from bracket.utils.errors import ForeignKey, check_foreign_key_violation +from bracket.utils.id_types import ClubId from bracket.utils.types import assert_some router = APIRouter() @@ -29,7 +30,7 @@ async def create_new_club( @router.delete("/clubs/{club_id}", response_model=SuccessResponse) async def delete_club( - club_id: int, _: UserPublic = Depends(user_authenticated_for_club) + club_id: ClubId, _: UserPublic = Depends(user_authenticated_for_club) ) -> SuccessResponse: try: await sql_delete_club(club_id) @@ -41,6 +42,6 @@ async def delete_club( @router.put("/clubs/{club_id}", response_model=ClubResponse) async def update_club( - club_id: int, club: ClubUpdateBody, _: UserPublic = Depends(user_authenticated_for_club) + club_id: ClubId, club: ClubUpdateBody, _: UserPublic = Depends(user_authenticated_for_club) ) -> ClubResponse: return ClubResponse(data=await sql_update_club(club_id, club)) diff --git a/backend/bracket/routes/courts.py b/backend/bracket/routes/courts.py index 765fc0291..57c614512 100644 --- a/backend/bracket/routes/courts.py +++ b/backend/bracket/routes/courts.py @@ -15,6 +15,7 @@ from bracket.sql.courts import get_all_courts_in_tournament, sql_delete_court, update_court from bracket.sql.stages import get_full_tournament_details from bracket.utils.db import fetch_one_parsed +from bracket.utils.id_types import CourtId, TournamentId from bracket.utils.types import assert_some router = APIRouter() @@ -22,7 +23,7 @@ @router.get("/tournaments/{tournament_id}/courts", response_model=CourtsResponse) async def get_courts( - tournament_id: int, + tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_or_public_dashboard), ) -> CourtsResponse: return CourtsResponse(data=await get_all_courts_in_tournament(tournament_id)) @@ -30,8 +31,8 @@ async def get_courts( @router.put("/tournaments/{tournament_id}/courts/{court_id}", response_model=SingleCourtResponse) async def update_court_by_id( - tournament_id: int, - court_id: int, + tournament_id: TournamentId, + court_id: CourtId, court_body: CourtBody, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SingleCourtResponse: @@ -55,7 +56,9 @@ async def update_court_by_id( @router.delete("/tournaments/{tournament_id}/courts/{court_id}", response_model=SuccessResponse) async def delete_court( - tournament_id: int, court_id: int, _: UserPublic = Depends(user_authenticated_for_tournament) + tournament_id: TournamentId, + court_id: CourtId, + _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: stages = await get_full_tournament_details(tournament_id, no_draft_rounds=False) used_in_matches_count = 0 @@ -79,7 +82,7 @@ async def delete_court( @router.post("/tournaments/{tournament_id}/courts", response_model=SingleCourtResponse) async def create_court( court_body: CourtBody, - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SingleCourtResponse: existing_courts = await get_all_courts_in_tournament(tournament_id) diff --git a/backend/bracket/routes/matches.py b/backend/bracket/routes/matches.py index 8313378b6..79ecd178d 100644 --- a/backend/bracket/routes/matches.py +++ b/backend/bracket/routes/matches.py @@ -26,6 +26,7 @@ from bracket.sql.courts import get_all_courts_in_tournament from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match from bracket.sql.tournaments import sql_get_tournament +from bracket.utils.id_types import MatchId, TournamentId from bracket.utils.types import assert_some router = APIRouter() @@ -36,7 +37,7 @@ response_model=UpcomingMatchesResponse, ) async def get_matches_to_schedule( - tournament_id: int, + tournament_id: TournamentId, elo_diff_threshold: int = 200, iterations: int = 200, only_recommended: bool = False, @@ -61,7 +62,7 @@ async def get_matches_to_schedule( @router.delete("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse) async def delete_match( - tournament_id: int, + tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament), match: Match = Depends(match_dependency), ) -> SuccessResponse: @@ -72,7 +73,7 @@ async def delete_match( @router.post("/tournaments/{tournament_id}/matches", response_model=SingleMatchResponse) async def create_match( - tournament_id: int, + tournament_id: TournamentId, match_body: MatchCreateBodyFrontend, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SingleMatchResponse: @@ -88,7 +89,7 @@ async def create_match( @router.post("/tournaments/{tournament_id}/schedule_matches", response_model=SuccessResponse) async def schedule_matches( - tournament_id: int, + tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: await schedule_all_unscheduled_matches(tournament_id) @@ -99,8 +100,8 @@ async def schedule_matches( "/tournaments/{tournament_id}/matches/{match_id}/reschedule", response_model=SuccessResponse ) async def reschedule_match( - tournament_id: int, - match_id: int, + tournament_id: TournamentId, + match_id: MatchId, body: MatchRescheduleBody, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: @@ -113,7 +114,7 @@ async def reschedule_match( response_model=SuccessResponse, ) async def create_matches_automatically( - tournament_id: int, + tournament_id: TournamentId, elo_diff_threshold: int = 100, iterations: int = 200, only_recommended: bool = False, @@ -169,7 +170,7 @@ async def create_matches_automatically( @router.put("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse) async def update_match_by_id( - tournament_id: int, + tournament_id: TournamentId, match_body: MatchBody, _: UserPublic = Depends(user_authenticated_for_tournament), match: Match = Depends(match_dependency), diff --git a/backend/bracket/routes/players.py b/backend/bracket/routes/players.py index 9391c1d6e..d69e6fc5e 100644 --- a/backend/bracket/routes/players.py +++ b/backend/bracket/routes/players.py @@ -19,6 +19,7 @@ sql_delete_player, ) from bracket.utils.db import fetch_one_parsed +from bracket.utils.id_types import PlayerId, TournamentId from bracket.utils.pagination import PaginationPlayers from bracket.utils.types import assert_some @@ -27,7 +28,7 @@ @router.get("/tournaments/{tournament_id}/players", response_model=PlayersResponse) async def get_players( - tournament_id: int, + tournament_id: TournamentId, not_in_team: bool = False, pagination: PaginationPlayers = Depends(), _: UserPublic = Depends(user_authenticated_for_tournament), @@ -44,8 +45,8 @@ async def get_players( @router.put("/tournaments/{tournament_id}/players/{player_id}", response_model=SinglePlayerResponse) async def update_player_by_id( - tournament_id: int, - player_id: int, + tournament_id: TournamentId, + player_id: PlayerId, player_body: PlayerBody, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SinglePlayerResponse: @@ -70,7 +71,9 @@ async def update_player_by_id( @router.delete("/tournaments/{tournament_id}/players/{player_id}", response_model=SuccessResponse) async def delete_player( - tournament_id: int, player_id: int, _: UserPublic = Depends(user_authenticated_for_tournament) + tournament_id: TournamentId, + player_id: PlayerId, + _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: await sql_delete_player(tournament_id, player_id) return SuccessResponse() @@ -79,7 +82,7 @@ async def delete_player( @router.post("/tournaments/{tournament_id}/players", response_model=SuccessResponse) async def create_single_player( player_body: PlayerBody, - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: existing_players = await get_all_players_in_tournament(tournament_id) @@ -91,7 +94,7 @@ async def create_single_player( @router.post("/tournaments/{tournament_id}/players_multi", response_model=SuccessResponse) async def create_multiple_players( player_body: PlayerMultiBody, - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: player_names = [player.strip() for player in player_body.names.split("\n") if len(player) > 0] diff --git a/backend/bracket/routes/rounds.py b/backend/bracket/routes/rounds.py index 549a82c6c..a6029e139 100644 --- a/backend/bracket/routes/rounds.py +++ b/backend/bracket/routes/rounds.py @@ -22,14 +22,15 @@ from bracket.sql.rounds import get_next_round_name, set_round_active_or_draft, sql_create_round from bracket.sql.stage_items import get_stage_item from bracket.sql.stages import get_full_tournament_details +from bracket.utils.id_types import RoundId, TournamentId router = APIRouter() @router.delete("/tournaments/{tournament_id}/rounds/{round_id}", response_model=SuccessResponse) async def delete_round( - tournament_id: int, - round_id: int, + tournament_id: TournamentId, + round_id: RoundId, _: UserPublic = Depends(user_authenticated_for_tournament), round_with_matches: RoundWithMatches = Depends(round_with_matches_dependency), ) -> SuccessResponse: @@ -50,7 +51,7 @@ async def delete_round( @router.post("/tournaments/{tournament_id}/rounds", response_model=SuccessResponse) async def create_round( - tournament_id: int, + tournament_id: TournamentId, round_body: RoundCreateBody, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: @@ -90,8 +91,8 @@ async def create_round( @router.put("/tournaments/{tournament_id}/rounds/{round_id}", response_model=SuccessResponse) async def update_round_by_id( - tournament_id: int, - round_id: int, + tournament_id: TournamentId, + round_id: RoundId, round_body: RoundUpdateBody, _: UserPublic = Depends(user_authenticated_for_tournament), __: Round = Depends(round_dependency), diff --git a/backend/bracket/routes/stage_items.py b/backend/bracket/routes/stage_items.py index 4beed76b8..be6c14da5 100644 --- a/backend/bracket/routes/stage_items.py +++ b/backend/bracket/routes/stage_items.py @@ -27,9 +27,11 @@ from bracket.sql.rounds import set_round_active_or_draft from bracket.sql.shared import sql_delete_stage_item_with_foreign_keys from bracket.sql.stage_items import ( + get_stage_item, sql_create_stage_item, ) from bracket.sql.stages import get_full_tournament_details +from bracket.utils.id_types import StageItemId, TournamentId router = APIRouter() @@ -38,8 +40,8 @@ "/tournaments/{tournament_id}/stage_items/{stage_item_id}", response_model=SuccessResponse ) async def delete_stage_item( - tournament_id: int, - stage_item_id: int, + tournament_id: TournamentId, + stage_item_id: StageItemId, _: UserPublic = Depends(user_authenticated_for_tournament), stage_item: StageItemWithRounds = Depends(stage_item_dependency), ) -> SuccessResponse: @@ -50,7 +52,7 @@ async def delete_stage_item( @router.post("/tournaments/{tournament_id}/stage_items", response_model=SuccessResponse) async def create_stage_item( - tournament_id: int, + tournament_id: TournamentId, stage_body: StageItemCreateBody, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: @@ -73,12 +75,18 @@ async def create_stage_item( "/tournaments/{tournament_id}/stage_items/{stage_item_id}", response_model=SuccessResponse ) async def update_stage_item( - tournament_id: int, - stage_item_id: int, + tournament_id: TournamentId, + stage_item_id: StageItemId, stage_item_body: StageItemUpdateBody, _: UserPublic = Depends(user_authenticated_for_tournament), stage_item: StageItemWithRounds = Depends(stage_item_dependency), ) -> SuccessResponse: + if await get_stage_item(tournament_id, stage_item_id) is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Could not find all stages", + ) + query = """ UPDATE stage_items SET name = :name @@ -96,8 +104,8 @@ async def update_stage_item( response_model=SuccessResponse, ) async def start_next_round( - tournament_id: int, - stage_item_id: int, + tournament_id: TournamentId, + stage_item_id: StageItemId, active_next_body: StageItemActivateNextBody, stage_item: StageItemWithRounds = Depends(stage_item_dependency), _: UserPublic = Depends(user_authenticated_for_tournament), diff --git a/backend/bracket/routes/stages.py b/backend/bracket/routes/stages.py index 9fe54e188..ea70974d4 100644 --- a/backend/bracket/routes/stages.py +++ b/backend/bracket/routes/stages.py @@ -27,13 +27,14 @@ sql_delete_stage, ) from bracket.sql.teams import get_teams_with_members +from bracket.utils.id_types import StageId, TournamentId router = APIRouter() @router.get("/tournaments/{tournament_id}/stages", response_model=StagesWithStageItemsResponse) async def get_stages( - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_or_public_dashboard), no_draft_rounds: bool = False, ) -> StagesWithStageItemsResponse: @@ -49,8 +50,8 @@ async def get_stages( @router.delete("/tournaments/{tournament_id}/stages/{stage_id}", response_model=SuccessResponse) async def delete_stage( - tournament_id: int, - stage_id: int, + tournament_id: TournamentId, + stage_id: StageId, _: UserPublic = Depends(user_authenticated_for_tournament), stage: StageWithStageItems = Depends(stage_dependency), ) -> SuccessResponse: @@ -74,7 +75,7 @@ async def delete_stage( @router.post("/tournaments/{tournament_id}/stages", response_model=SuccessResponse) async def create_stage( - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: existing_stages = await get_full_tournament_details(tournament_id) @@ -86,8 +87,8 @@ async def create_stage( @router.put("/tournaments/{tournament_id}/stages/{stage_id}", response_model=SuccessResponse) async def update_stage( - tournament_id: int, - stage_id: int, + tournament_id: TournamentId, + stage_id: StageId, stage_body: StageUpdateBody, _: UserPublic = Depends(user_authenticated_for_tournament), stage: Stage = Depends(stage_dependency), # pylint: disable=redefined-builtin @@ -108,7 +109,7 @@ async def update_stage( @router.post("/tournaments/{tournament_id}/stages/activate", response_model=SuccessResponse) async def activate_next_stage( - tournament_id: int, + tournament_id: TournamentId, stage_body: StageActivateBody, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: @@ -130,8 +131,8 @@ async def activate_next_stage( response_model=StageItemInputOptionsResponse, ) async def get_available_inputs( - tournament_id: int, - stage_id: int, + tournament_id: TournamentId, + stage_id: StageId, _: UserPublic = Depends(user_authenticated_for_tournament), stage: Stage = Depends(stage_dependency), ) -> StageItemInputOptionsResponse: diff --git a/backend/bracket/routes/teams.py b/backend/bracket/routes/teams.py index 417d0ebec..f8c8e2ea4 100644 --- a/backend/bracket/routes/teams.py +++ b/backend/bracket/routes/teams.py @@ -27,13 +27,16 @@ sql_delete_team, ) from bracket.utils.db import fetch_one_parsed +from bracket.utils.id_types import PlayerId, TeamId, TournamentId from bracket.utils.pagination import PaginationTeams from bracket.utils.types import assert_some router = APIRouter() -async def update_team_members(team_id: int, tournament_id: int, player_ids: set[int]) -> None: +async def update_team_members( + team_id: TeamId, tournament_id: TournamentId, player_ids: set[PlayerId] +) -> None: [team] = await get_teams_with_members(tournament_id, team_id=team_id) # Add members to the team @@ -56,7 +59,7 @@ async def update_team_members(team_id: int, tournament_id: int, player_ids: set[ @router.get("/tournaments/{tournament_id}/teams", response_model=TeamsWithPlayersResponse) async def get_teams( - tournament_id: int, + tournament_id: TournamentId, pagination: PaginationTeams = Depends(), _: UserPublic = Depends(user_authenticated_or_public_dashboard), ) -> TeamsWithPlayersResponse: @@ -70,7 +73,7 @@ async def get_teams( @router.put("/tournaments/{tournament_id}/teams/{team_id}", response_model=SingleTeamResponse) async def update_team_by_id( - tournament_id: int, + tournament_id: TournamentId, team_body: TeamBody, _: UserPublic = Depends(user_authenticated_for_tournament), team: Team = Depends(team_dependency), @@ -99,7 +102,7 @@ async def update_team_by_id( @router.delete("/tournaments/{tournament_id}/teams/{team_id}", response_model=SuccessResponse) async def delete_team( - tournament_id: int, + tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament), team: FullTeamWithPlayers = Depends(team_with_players_dependency), ) -> SuccessResponse: @@ -127,7 +130,7 @@ async def delete_team( @router.post("/tournaments/{tournament_id}/teams", response_model=SingleTeamResponse) async def create_team( team_to_insert: TeamBody, - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SingleTeamResponse: existing_teams = await get_teams_with_members(tournament_id) @@ -151,7 +154,7 @@ async def create_team( @router.post("/tournaments/{tournament_id}/teams_multi", response_model=SuccessResponse) async def create_multiple_teams( team_body: TeamMultiBody, - tournament_id: int, + tournament_id: TournamentId, user: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: team_names = [team.strip() for team in team_body.names.split("\n") if len(team) > 0] diff --git a/backend/bracket/routes/tournaments.py b/backend/bracket/routes/tournaments.py index 501c3e016..a405851c6 100644 --- a/backend/bracket/routes/tournaments.py +++ b/backend/bracket/routes/tournaments.py @@ -38,6 +38,7 @@ check_foreign_key_violation, check_unique_constraint_violation, ) +from bracket.utils.id_types import TournamentId from bracket.utils.logging import logger from bracket.utils.types import assert_some @@ -51,7 +52,8 @@ @router.get("/tournaments/{tournament_id}", response_model=TournamentResponse) async def get_tournament( - tournament_id: int, user: UserPublic | None = Depends(user_authenticated_or_public_dashboard) + tournament_id: TournamentId, + user: UserPublic | None = Depends(user_authenticated_or_public_dashboard), ) -> TournamentResponse: tournament = await sql_get_tournament(tournament_id) if user is None and not tournament.dashboard_public: @@ -90,7 +92,7 @@ async def get_tournaments( @router.put("/tournaments/{tournament_id}", response_model=SuccessResponse) async def update_tournament_by_id( - tournament_id: int, + tournament_id: TournamentId, tournament_body: TournamentUpdateBody, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> SuccessResponse: @@ -108,7 +110,7 @@ async def update_tournament_by_id( @router.delete("/tournaments/{tournament_id}", response_model=SuccessResponse) async def delete_tournament( - tournament_id: int, _: UserPublic = Depends(user_authenticated_for_tournament) + tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament) ) -> SuccessResponse: try: await sql_delete_tournament(tournament_id) @@ -151,7 +153,7 @@ async def create_tournament( @router.post("/tournaments/{tournament_id}/logo") async def upload_logo( - tournament_id: int, + tournament_id: TournamentId, file: UploadFile | None = None, _: UserPublic = Depends(user_authenticated_for_tournament), ) -> TournamentResponse: diff --git a/backend/bracket/routes/users.py b/backend/bracket/routes/users.py index 6231e50da..0bde70da5 100644 --- a/backend/bracket/routes/users.py +++ b/backend/bracket/routes/users.py @@ -28,6 +28,7 @@ update_user, update_user_password, ) +from bracket.utils.id_types import UserId from bracket.utils.security import hash_password, verify_captcha_token from bracket.utils.types import assert_some @@ -41,7 +42,7 @@ async def get_user(user_public: UserPublic = Depends(user_authenticated)) -> Use @router.get("/users/{user_id}", response_model=UserPublicResponse) async def get_me( - user_id: int, user_public: UserPublic = Depends(user_authenticated) + user_id: UserId, user_public: UserPublic = Depends(user_authenticated) ) -> UserPublicResponse: if user_public.id != user_id: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Can't view details of this user") @@ -51,7 +52,7 @@ async def get_me( @router.put("/users/{user_id}", response_model=UserPublicResponse) async def put_user( - user_id: int, + user_id: UserId, user_to_update: UserToUpdate, user_public: UserPublic = Depends(user_authenticated), ) -> UserPublicResponse: @@ -65,7 +66,7 @@ async def put_user( @router.put("/users/{user_id}/password", response_model=SuccessResponse) async def put_user_password( - user_id: int, + user_id: UserId, user_to_update: UserPasswordToUpdate, user_public: UserPublic = Depends(user_authenticated), ) -> SuccessResponse: diff --git a/backend/bracket/routes/util.py b/backend/bracket/routes/util.py index 93c40082c..fe52dc699 100644 --- a/backend/bracket/routes/util.py +++ b/backend/bracket/routes/util.py @@ -12,9 +12,10 @@ from bracket.sql.stages import get_full_tournament_details from bracket.sql.teams import get_teams_with_members from bracket.utils.db import fetch_one_parsed +from bracket.utils.id_types import MatchId, RoundId, StageId, StageItemId, TeamId, TournamentId -async def round_dependency(tournament_id: int, round_id: int) -> Round: +async def round_dependency(tournament_id: TournamentId, round_id: RoundId) -> Round: round_ = await fetch_one_parsed( database, Round, @@ -30,7 +31,9 @@ async def round_dependency(tournament_id: int, round_id: int) -> Round: return round_ -async def round_with_matches_dependency(tournament_id: int, round_id: int) -> RoundWithMatches: +async def round_with_matches_dependency( + tournament_id: TournamentId, round_id: RoundId +) -> RoundWithMatches: round_ = await get_round_by_id(tournament_id, round_id) if round_ is None: raise HTTPException( @@ -41,7 +44,7 @@ async def round_with_matches_dependency(tournament_id: int, round_id: int) -> Ro return round_ -async def stage_dependency(tournament_id: int, stage_id: int) -> StageWithStageItems: +async def stage_dependency(tournament_id: TournamentId, stage_id: StageId) -> StageWithStageItems: stages = await get_full_tournament_details( tournament_id, no_draft_rounds=False, stage_id=stage_id ) @@ -55,7 +58,9 @@ async def stage_dependency(tournament_id: int, stage_id: int) -> StageWithStageI return stages[0] -async def stage_item_dependency(tournament_id: int, stage_item_id: int) -> StageItemWithRounds: +async def stage_item_dependency( + tournament_id: TournamentId, stage_item_id: StageItemId +) -> StageItemWithRounds: stage_item = await get_stage_item(tournament_id, stage_item_id=stage_item_id) if stage_item is None: @@ -67,7 +72,7 @@ async def stage_item_dependency(tournament_id: int, stage_item_id: int) -> Stage return stage_item -async def match_dependency(tournament_id: int, match_id: int) -> Match: +async def match_dependency(tournament_id: TournamentId, match_id: MatchId) -> Match: match = await fetch_one_parsed( database, Match, @@ -85,7 +90,7 @@ async def match_dependency(tournament_id: int, match_id: int) -> Match: return match -async def team_dependency(tournament_id: int, team_id: int) -> Team: +async def team_dependency(tournament_id: TournamentId, team_id: TeamId) -> Team: team = await fetch_one_parsed( database, Team, @@ -101,7 +106,9 @@ async def team_dependency(tournament_id: int, team_id: int) -> Team: return team -async def team_with_players_dependency(tournament_id: int, team_id: int) -> FullTeamWithPlayers: +async def team_with_players_dependency( + tournament_id: TournamentId, team_id: TeamId +) -> FullTeamWithPlayers: teams_with_members = await get_teams_with_members(tournament_id, team_id=team_id) if len(teams_with_members) < 1: diff --git a/backend/bracket/sql/clubs.py b/backend/bracket/sql/clubs.py index 1969b584b..1db7c5544 100644 --- a/backend/bracket/sql/clubs.py +++ b/backend/bracket/sql/clubs.py @@ -1,9 +1,10 @@ from bracket.database import database from bracket.models.db.club import Club, ClubCreateBody, ClubUpdateBody +from bracket.utils.id_types import ClubId, UserId from bracket.utils.types import assert_some -async def create_club(club: ClubCreateBody, user_id: int) -> Club: +async def create_club(club: ClubCreateBody, user_id: UserId) -> Club: async with database.transaction(): query = """ INSERT INTO clubs (name, created) @@ -28,7 +29,7 @@ async def create_club(club: ClubCreateBody, user_id: int) -> Club: return club_created -async def sql_update_club(club_id: int, club: ClubUpdateBody) -> Club | None: +async def sql_update_club(club_id: ClubId, club: ClubUpdateBody) -> Club | None: query = """ UPDATE clubs SET name = :name @@ -39,7 +40,7 @@ async def sql_update_club(club_id: int, club: ClubUpdateBody) -> Club | None: return Club.model_validate(result) if result is not None else None -async def sql_delete_club(club_id: int) -> None: +async def sql_delete_club(club_id: ClubId) -> None: query = """ DELETE FROM clubs WHERE id = :club_id @@ -47,7 +48,7 @@ async def sql_delete_club(club_id: int) -> None: await database.execute(query=query, values={"club_id": club_id}) -async def todo_sql_remove_user_from_club(club_id: int, user_id: int) -> None: +async def todo_sql_remove_user_from_club(club_id: ClubId, user_id: UserId) -> None: query = """ DELETE FROM users_x_clubs WHERE club_id = :club_id @@ -56,7 +57,7 @@ async def todo_sql_remove_user_from_club(club_id: int, user_id: int) -> None: await database.execute(query=query, values={"club_id": club_id, "user_id": user_id}) -async def get_clubs_for_user_id(user_id: int) -> list[Club]: +async def get_clubs_for_user_id(user_id: UserId) -> list[Club]: query = """ SELECT clubs.* FROM clubs JOIN users_x_clubs uxc on clubs.id = uxc.club_id @@ -66,7 +67,7 @@ async def get_clubs_for_user_id(user_id: int) -> list[Club]: 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: +async def todo_get_club_for_user_id(club_id: ClubId, user_id: UserId) -> Club | None: query = """ SELECT clubs.* FROM clubs JOIN users_x_clubs uxc on clubs.id = uxc.club_id diff --git a/backend/bracket/sql/courts.py b/backend/bracket/sql/courts.py index 723b24949..251ad78ab 100644 --- a/backend/bracket/sql/courts.py +++ b/backend/bracket/sql/courts.py @@ -1,8 +1,9 @@ from bracket.database import database from bracket.models.db.court import Court, CourtBody +from bracket.utils.id_types import CourtId, TournamentId -async def get_all_courts_in_tournament(tournament_id: int) -> list[Court]: +async def get_all_courts_in_tournament(tournament_id: TournamentId) -> list[Court]: query = """ SELECT * FROM courts @@ -13,7 +14,9 @@ async def get_all_courts_in_tournament(tournament_id: int) -> list[Court]: 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]: +async def update_court( + tournament_id: TournamentId, court_id: CourtId, court_body: CourtBody +) -> list[Court]: query = """ UPDATE courts SET name = :name @@ -27,13 +30,13 @@ async def update_court(tournament_id: int, court_id: int, court_body: CourtBody) return [Court.model_validate(dict(x._mapping)) for x in result] -async def sql_delete_court(tournament_id: int, court_id: int) -> None: +async def sql_delete_court(tournament_id: TournamentId, court_id: CourtId) -> None: query = "DELETE FROM courts WHERE id = :court_id AND tournament_id = :tournament_id" await database.fetch_one( query=query, values={"court_id": court_id, "tournament_id": tournament_id} ) -async def sql_delete_courts_of_tournament(tournament_id: int) -> None: +async def sql_delete_courts_of_tournament(tournament_id: TournamentId) -> None: query = "DELETE FROM courts WHERE tournament_id = :tournament_id" await database.fetch_one(query=query, values={"tournament_id": tournament_id}) diff --git a/backend/bracket/sql/matches.py b/backend/bracket/sql/matches.py index fc3994246..dfb8da715 100644 --- a/backend/bracket/sql/matches.py +++ b/backend/bracket/sql/matches.py @@ -5,9 +5,10 @@ from bracket.database import database from bracket.models.db.match import Match, MatchBody, MatchCreateBody from bracket.models.db.tournament import Tournament +from bracket.utils.id_types import CourtId, MatchId, StageItemId, TeamId -async def sql_delete_match(match_id: int) -> None: +async def sql_delete_match(match_id: MatchId) -> None: query = """ DELETE FROM matches WHERE matches.id = :match_id @@ -15,7 +16,7 @@ async def sql_delete_match(match_id: int) -> None: await database.execute(query=query, values={"match_id": match_id}) -async def sql_delete_matches_for_stage_item_id(stage_item_id: int) -> None: +async def sql_delete_matches_for_stage_item_id(stage_item_id: StageItemId) -> None: query = """ DELETE FROM matches WHERE matches.id IN ( @@ -78,7 +79,7 @@ async def sql_create_match(match: MatchCreateBody) -> Match: return Match.model_validate(dict(result._mapping)) -async def sql_update_match(match_id: int, match: MatchBody, tournament: Tournament) -> None: +async def sql_update_match(match_id: MatchId, match: MatchBody, tournament: Tournament) -> None: query = """ UPDATE matches SET round_id = :round_id, @@ -115,7 +116,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 = None + match_id: MatchId, team1_id: TeamId | None, team2_id: TeamId | None = None ) -> None: query = """ UPDATE matches @@ -129,8 +130,8 @@ async def sql_update_team_ids_for_match( async def sql_reschedule_match( - match_id: int, - court_id: int | None, + match_id: MatchId, + court_id: CourtId | None, start_time: datetime_utc, position_in_schedule: int | None, duration_minutes: int, @@ -165,8 +166,8 @@ async def sql_reschedule_match( async def sql_reschedule_match_and_determine_duration_and_margin( - match_id: int, - court_id: int | None, + match_id: MatchId, + court_id: CourtId | None, start_time: datetime_utc, position_in_schedule: int | None, match: Match, @@ -194,7 +195,7 @@ async def sql_reschedule_match_and_determine_duration_and_margin( ) -async def sql_get_match(match_id: int) -> Match: +async def sql_get_match(match_id: MatchId) -> Match: query = """ SELECT * FROM matches diff --git a/backend/bracket/sql/players.py b/backend/bracket/sql/players.py index f2cc15739..d39cc3599 100644 --- a/backend/bracket/sql/players.py +++ b/backend/bracket/sql/players.py @@ -7,12 +7,13 @@ from bracket.models.db.player import Player, PlayerBody, PlayerToInsert from bracket.models.db.players import START_ELO, PlayerStatistics from bracket.schema import players +from bracket.utils.id_types import PlayerId, TournamentId from bracket.utils.pagination import PaginationPlayers from bracket.utils.types import dict_without_none async def get_all_players_in_tournament( - tournament_id: int, + tournament_id: TournamentId, *, not_in_team: bool = False, pagination: PaginationPlayers | None = None, @@ -49,7 +50,7 @@ async def get_all_players_in_tournament( async def get_player_count( - tournament_id: int, + tournament_id: TournamentId, *, not_in_team: bool = False, ) -> int: @@ -64,7 +65,7 @@ async def get_player_count( async def update_player_stats( - tournament_id: int, player_id: int, player_statistics: PlayerStatistics + tournament_id: TournamentId, player_id: PlayerId, player_statistics: PlayerStatistics ) -> None: query = """ UPDATE players @@ -91,19 +92,19 @@ async def update_player_stats( ) -async def sql_delete_player(tournament_id: int, player_id: int) -> None: +async def sql_delete_player(tournament_id: TournamentId, player_id: PlayerId) -> None: query = "DELETE FROM players WHERE id = :player_id AND tournament_id = :tournament_id" await database.fetch_one( query=query, values={"player_id": player_id, "tournament_id": tournament_id} ) -async def sql_delete_players_of_tournament(tournament_id: int) -> None: +async def sql_delete_players_of_tournament(tournament_id: TournamentId) -> None: query = "DELETE FROM players WHERE tournament_id = :tournament_id" await database.fetch_one(query=query, values={"tournament_id": tournament_id}) -async def insert_player(player_body: PlayerBody, tournament_id: int) -> None: +async def insert_player(player_body: PlayerBody, tournament_id: TournamentId) -> None: await database.execute( query=players.insert(), values=PlayerToInsert( diff --git a/backend/bracket/sql/rounds.py b/backend/bracket/sql/rounds.py index c60eb84cc..1af5dff29 100644 --- a/backend/bracket/sql/rounds.py +++ b/backend/bracket/sql/rounds.py @@ -3,15 +3,16 @@ from bracket.models.db.util import RoundWithMatches from bracket.sql.stage_items import get_stage_item from bracket.sql.stages import get_full_tournament_details +from bracket.utils.id_types import RoundId, StageItemId, TournamentId -async def sql_create_round(round_: RoundToInsert) -> int: +async def sql_create_round(round_: RoundToInsert) -> RoundId: query = """ INSERT INTO rounds (created, is_draft, is_active, name, stage_item_id) VALUES (NOW(), :is_draft, :is_active, :name, :stage_item_id) RETURNING id """ - result: int = await database.fetch_val( + result: RoundId = await database.fetch_val( query=query, values={ "name": round_.name, @@ -24,7 +25,7 @@ async def sql_create_round(round_: RoundToInsert) -> int: async def get_rounds_for_stage_item( - tournament_id: int, stage_item_id: int + tournament_id: TournamentId, stage_item_id: StageItemId ) -> list[RoundWithMatches]: stage_item = await get_stage_item(tournament_id, stage_item_id) @@ -36,7 +37,9 @@ async def get_rounds_for_stage_item( return stage_item.rounds -async def get_round_by_id(tournament_id: int, round_id: int) -> RoundWithMatches | None: +async def get_round_by_id( + tournament_id: TournamentId, round_id: RoundId +) -> RoundWithMatches | None: stages = await get_full_tournament_details( tournament_id, no_draft_rounds=False, round_id=round_id ) @@ -50,7 +53,7 @@ async def get_round_by_id(tournament_id: int, round_id: int) -> RoundWithMatches return None -async def get_next_round_name(tournament_id: int, stage_item_id: int) -> str: +async def get_next_round_name(tournament_id: TournamentId, stage_item_id: StageItemId) -> str: query = """ SELECT count(*) FROM rounds JOIN stage_items on stage_items.id = rounds.stage_item_id @@ -66,7 +69,7 @@ async def get_next_round_name(tournament_id: int, stage_item_id: int) -> str: return f"Round {round_count + 1:02d}" -async def sql_delete_rounds_for_stage_item_id(stage_item_id: int) -> None: +async def sql_delete_rounds_for_stage_item_id(stage_item_id: StageItemId) -> None: query = """ DELETE FROM rounds WHERE rounds.stage_item_id = :stage_item_id @@ -75,7 +78,7 @@ async def sql_delete_rounds_for_stage_item_id(stage_item_id: int) -> None: async def set_round_active_or_draft( - round_id: int, tournament_id: int, *, is_active: bool, is_draft: bool + round_id: RoundId, tournament_id: TournamentId, *, is_active: bool, is_draft: bool ) -> None: query = """ UPDATE rounds diff --git a/backend/bracket/sql/shared.py b/backend/bracket/sql/shared.py index abcf0ae11..292d2b29a 100644 --- a/backend/bracket/sql/shared.py +++ b/backend/bracket/sql/shared.py @@ -1,8 +1,9 @@ from bracket.database import database from bracket.sql.stage_items import sql_delete_stage_item +from bracket.utils.id_types import StageItemId -async def sql_delete_stage_item_relations(stage_item_id: int) -> None: +async def sql_delete_stage_item_relations(stage_item_id: StageItemId) -> None: from bracket.sql.matches import sql_delete_matches_for_stage_item_id from bracket.sql.rounds import sql_delete_rounds_for_stage_item_id from bracket.sql.stage_item_inputs import sql_delete_stage_item_inputs @@ -13,7 +14,7 @@ async def sql_delete_stage_item_relations(stage_item_id: int) -> None: await sql_delete_stage_item_inputs(stage_item_id) -async def sql_delete_stage_item_with_foreign_keys(stage_item_id: int) -> None: +async def sql_delete_stage_item_with_foreign_keys(stage_item_id: StageItemId) -> None: from bracket.sql.matches import sql_delete_matches_for_stage_item_id from bracket.sql.rounds import sql_delete_rounds_for_stage_item_id from bracket.sql.stage_item_inputs import sql_delete_stage_item_inputs diff --git a/backend/bracket/sql/stage_item_inputs.py b/backend/bracket/sql/stage_item_inputs.py index 95675a6d9..ca4f57896 100644 --- a/backend/bracket/sql/stage_item_inputs.py +++ b/backend/bracket/sql/stage_item_inputs.py @@ -5,9 +5,10 @@ StageItemInputCreateBodyFinal, StageItemInputCreateBodyTentative, ) +from bracket.utils.id_types import StageItemId, TournamentId -async def sql_delete_stage_item_inputs(stage_item_id: int) -> None: +async def sql_delete_stage_item_inputs(stage_item_id: StageItemId) -> None: query = """ DELETE FROM stage_item_inputs WHERE stage_item_id = :stage_item_id OR winner_from_stage_item_id = :stage_item_id @@ -16,53 +17,54 @@ async def sql_delete_stage_item_inputs(stage_item_id: int) -> None: async def sql_create_stage_item_input( - tournament_id: int, stage_item_id: int, stage_item_input: StageItemInputCreateBody + tournament_id: TournamentId, + stage_item_id: StageItemId, + stage_item_input: StageItemInputCreateBody, ) -> StageItemInputBase: - async with database.transaction(): - query = """ - INSERT INTO stage_item_inputs - ( - slot, - tournament_id, - stage_item_id, - team_id, - winner_from_stage_item_id, - winner_position - ) - VALUES - ( - :slot, - :tournament_id, - :stage_item_id, - :team_id, - :winner_from_stage_item_id, - :winner_position - ) - RETURNING * - """ - result = await database.fetch_one( - query=query, - values={ - "slot": stage_item_input.slot, - "tournament_id": tournament_id, - "stage_item_id": stage_item_id, - "team_id": ( - stage_item_input.team_id - if isinstance(stage_item_input, StageItemInputCreateBodyFinal) - else None - ), - "winner_from_stage_item_id": ( - stage_item_input.winner_from_stage_item_id - if isinstance(stage_item_input, StageItemInputCreateBodyTentative) - else None - ), - "winner_position": ( - stage_item_input.winner_position - if isinstance(stage_item_input, StageItemInputCreateBodyTentative) - else None - ), - }, + query = """ + INSERT INTO stage_item_inputs + ( + slot, + tournament_id, + stage_item_id, + team_id, + winner_from_stage_item_id, + winner_position + ) + VALUES + ( + :slot, + :tournament_id, + :stage_item_id, + :team_id, + :winner_from_stage_item_id, + :winner_position ) + RETURNING * + """ + result = await database.fetch_one( + query=query, + values={ + "slot": stage_item_input.slot, + "tournament_id": tournament_id, + "stage_item_id": stage_item_id, + "team_id": ( + stage_item_input.team_id + if isinstance(stage_item_input, StageItemInputCreateBodyFinal) + else None + ), + "winner_from_stage_item_id": ( + stage_item_input.winner_from_stage_item_id + if isinstance(stage_item_input, StageItemInputCreateBodyTentative) + else None + ), + "winner_position": ( + stage_item_input.winner_position + if isinstance(stage_item_input, StageItemInputCreateBodyTentative) + else None + ), + }, + ) if result is None: raise ValueError("Could not create stage") diff --git a/backend/bracket/sql/stage_items.py b/backend/bracket/sql/stage_items.py index 4f423ce5f..59c1eede4 100644 --- a/backend/bracket/sql/stage_items.py +++ b/backend/bracket/sql/stage_items.py @@ -3,9 +3,12 @@ from bracket.models.db.util import StageItemWithRounds from bracket.sql.stage_item_inputs import sql_create_stage_item_input from bracket.sql.stages import get_full_tournament_details +from bracket.utils.id_types import StageItemId, TournamentId -async def sql_create_stage_item(tournament_id: int, stage_item: StageItemCreateBody) -> StageItem: +async def sql_create_stage_item( + tournament_id: TournamentId, stage_item: StageItemCreateBody +) -> StageItem: async with database.transaction(): query = """ INSERT INTO stage_items (type, stage_id, name, team_count) @@ -33,7 +36,7 @@ async def sql_create_stage_item(tournament_id: int, stage_item: StageItemCreateB return stage_item_result -async def sql_delete_stage_item(stage_item_id: int) -> None: +async def sql_delete_stage_item(stage_item_id: StageItemId) -> None: query = """ DELETE FROM stage_items WHERE stage_items.id = :stage_item_id @@ -41,9 +44,21 @@ async def sql_delete_stage_item(stage_item_id: int) -> None: await database.execute(query=query, values={"stage_item_id": stage_item_id}) -async def get_stage_item(tournament_id: int, stage_item_id: int) -> StageItemWithRounds | None: - stages = await get_full_tournament_details(tournament_id, stage_item_id=stage_item_id) +async def get_stage_item( + tournament_id: TournamentId, stage_item_id: StageItemId +) -> StageItemWithRounds | None: + stages = await get_full_tournament_details(tournament_id, stage_item_ids={stage_item_id}) if len(stages) < 1 or len(stages[0].stage_items) < 1: return None return stages[0].stage_items[0] + + +async def get_stage_items( + tournament_id: TournamentId, stage_item_ids: set[StageItemId] +) -> list[StageItemWithRounds]: + stages = await get_full_tournament_details(tournament_id, stage_item_ids=stage_item_ids) + if len(stages) < 1: + return [] + + return stages[0].stage_items diff --git a/backend/bracket/sql/stages.py b/backend/bracket/sql/stages.py index d3bfc681d..b86ab2bbb 100644 --- a/backend/bracket/sql/stages.py +++ b/backend/bracket/sql/stages.py @@ -3,24 +3,27 @@ from bracket.database import database from bracket.models.db.stage import Stage from bracket.models.db.util import StageWithStageItems +from bracket.utils.id_types import RoundId, StageId, StageItemId, TournamentId from bracket.utils.types import dict_without_none async def get_full_tournament_details( - tournament_id: int, - round_id: int | None = None, - stage_id: int | None = None, - stage_item_id: int | None = None, + tournament_id: TournamentId, + round_id: RoundId | None = None, + stage_id: StageId | None = None, + stage_item_ids: set[StageItemId] | None = None, *, no_draft_rounds: bool = False, ) -> list[StageWithStageItems]: draft_filter = "AND rounds.is_draft IS FALSE" if no_draft_rounds else "" round_filter = "AND rounds.id = :round_id" if round_id is not None else "" stage_filter = "AND stages.id = :stage_id" if stage_id is not None else "" - stage_item_filter = "AND stage_items.id = :stage_item_id" if stage_item_id is not None else "" + stage_item_filter = ( + "AND stage_items.id = any(:stage_item_ids)" if stage_item_ids is not None else "" + ) stage_item_filter_join = ( "LEFT JOIN stage_items on stages.id = stage_items.stage_id" - if stage_item_id is not None + if stage_item_ids is not None else "" ) @@ -102,14 +105,14 @@ async def get_full_tournament_details( "tournament_id": tournament_id, "round_id": round_id, "stage_id": stage_id, - "stage_item_id": stage_item_id, + "stage_item_ids": stage_item_ids, } ) result = await database.fetch_all(query=query, values=values) return [StageWithStageItems.model_validate(dict(x._mapping)) for x in result] -async def sql_delete_stage(tournament_id: int, stage_id: int) -> None: +async def sql_delete_stage(tournament_id: TournamentId, stage_id: StageId) -> None: async with database.transaction(): query = """ DELETE FROM stage_items @@ -127,7 +130,7 @@ async def sql_delete_stage(tournament_id: int, stage_id: int) -> None: ) -async def sql_create_stage(tournament_id: int) -> Stage: +async def sql_create_stage(tournament_id: TournamentId) -> Stage: query = """ INSERT INTO stages (created, is_active, name, tournament_id) VALUES (NOW(), false, :name, :tournament_id) @@ -145,8 +148,8 @@ async def sql_create_stage(tournament_id: int) -> Stage: async def get_next_stage_in_tournament( - tournament_id: int, direction: Literal["next", "previous"] -) -> int | None: + tournament_id: TournamentId, direction: Literal["next", "previous"] +) -> StageId | None: select_query = """ SELECT id FROM stages @@ -181,7 +184,7 @@ async def get_next_stage_in_tournament( AND is_active IS FALSE """ return cast( - int | None, + StageId | None, await database.execute( query=select_query, values={"tournament_id": tournament_id, "direction": direction}, @@ -189,7 +192,9 @@ async def get_next_stage_in_tournament( ) -async def sql_activate_next_stage(new_active_stage_id: int, tournament_id: int) -> None: +async def sql_activate_next_stage( + new_active_stage_id: StageId, tournament_id: TournamentId +) -> None: update_query = """ UPDATE stages SET is_active = (stages.id = :new_active_stage_id) diff --git a/backend/bracket/sql/teams.py b/backend/bracket/sql/teams.py index 2e83e9a48..5b42368e3 100644 --- a/backend/bracket/sql/teams.py +++ b/backend/bracket/sql/teams.py @@ -3,28 +3,37 @@ from bracket.database import database from bracket.models.db.players import PlayerStatistics from bracket.models.db.team import FullTeamWithPlayers, Team +from bracket.utils.id_types import TeamId, TournamentId from bracket.utils.pagination import PaginationTeams from bracket.utils.types import dict_without_none -async def get_team_by_id(team_id: int, tournament_id: int) -> Team | None: +async def get_teams_by_id(team_ids: set[TeamId], tournament_id: TournamentId) -> list[Team]: + if len(team_ids) < 1: + return [] + query = """ SELECT * FROM teams - WHERE id = :team_id + WHERE id = any(:team_ids) AND tournament_id = :tournament_id """ - result = await database.fetch_one( - query=query, values={"team_id": team_id, "tournament_id": tournament_id} + result = await database.fetch_all( + query=query, values={"team_ids": team_ids, "tournament_id": tournament_id} ) - return Team.model_validate(result) if result is not None else None + return [Team.model_validate(team) for team in result] + + +async def get_team_by_id(team_id: TeamId, tournament_id: TournamentId) -> Team | None: + result = await get_teams_by_id({team_id}, tournament_id) + return result[0] if len(result) > 0 else None async def get_teams_with_members( - tournament_id: int, + tournament_id: TournamentId, *, only_active_teams: bool = False, - team_id: int | None = None, + team_id: TeamId | None = None, pagination: PaginationTeams | None = None, ) -> list[FullTeamWithPlayers]: active_team_filter = "AND teams.active IS TRUE" if only_active_teams else "" @@ -66,7 +75,7 @@ async def get_teams_with_members( async def get_team_count( - tournament_id: int, + tournament_id: TournamentId, *, only_active_teams: bool = False, ) -> int: @@ -82,7 +91,7 @@ async def get_team_count( async def update_team_stats( - tournament_id: int, team_id: int, team_statistics: PlayerStatistics + tournament_id: TournamentId, team_id: TeamId, team_statistics: PlayerStatistics ) -> None: query = """ UPDATE teams @@ -109,13 +118,13 @@ async def update_team_stats( ) -async def sql_delete_team(tournament_id: int, team_id: int) -> None: +async def sql_delete_team(tournament_id: TournamentId, team_id: TeamId) -> None: query = "DELETE FROM teams WHERE id = :team_id AND tournament_id = :tournament_id" await database.fetch_one( query=query, values={"team_id": team_id, "tournament_id": tournament_id} ) -async def sql_delete_teams_of_tournament(tournament_id: int) -> None: +async def sql_delete_teams_of_tournament(tournament_id: TournamentId) -> None: query = "DELETE FROM teams WHERE tournament_id = :tournament_id" await database.fetch_one(query=query, values={"tournament_id": tournament_id}) diff --git a/backend/bracket/sql/tournaments.py b/backend/bracket/sql/tournaments.py index 00afeb494..35248b97d 100644 --- a/backend/bracket/sql/tournaments.py +++ b/backend/bracket/sql/tournaments.py @@ -2,9 +2,10 @@ from bracket.database import database from bracket.models.db.tournament import Tournament +from bracket.utils.id_types import TournamentId -async def sql_get_tournament(tournament_id: int) -> Tournament: +async def sql_get_tournament(tournament_id: TournamentId) -> Tournament: query = """ SELECT * FROM tournaments @@ -45,7 +46,7 @@ async def sql_get_tournaments( return [Tournament.model_validate(x) for x in result] -async def sql_delete_tournament(tournament_id: int) -> None: +async def sql_delete_tournament(tournament_id: TournamentId) -> None: query = """ DELETE FROM tournaments WHERE id = :tournament_id diff --git a/backend/bracket/sql/users.py b/backend/bracket/sql/users.py index ef024fe47..7f5f0a500 100644 --- a/backend/bracket/sql/users.py +++ b/backend/bracket/sql/users.py @@ -6,10 +6,11 @@ from bracket.sql.clubs import get_clubs_for_user_id, sql_delete_club from bracket.sql.tournaments import sql_get_tournaments from bracket.utils.db import fetch_one_parsed +from bracket.utils.id_types import ClubId, TournamentId, UserId from bracket.utils.types import assert_some -async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> bool: +async def get_user_access_to_tournament(tournament_id: TournamentId, user_id: UserId) -> bool: query = """ SELECT DISTINCT t.id FROM users_x_clubs @@ -20,7 +21,7 @@ async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> boo return tournament_id in {tournament.id for tournament in result} # type: ignore[attr-defined] -async def get_which_clubs_has_user_access_to(user_id: int) -> set[int]: +async def get_which_clubs_has_user_access_to(user_id: UserId) -> set[ClubId]: query = """ SELECT club_id FROM users_x_clubs @@ -30,11 +31,11 @@ async def get_which_clubs_has_user_access_to(user_id: int) -> set[int]: return {club.club_id for club in result} # type: ignore[attr-defined] -async def get_user_access_to_club(club_id: int, user_id: int) -> bool: +async def get_user_access_to_club(club_id: ClubId, user_id: UserId) -> bool: return club_id in await get_which_clubs_has_user_access_to(user_id) -async def update_user(user_id: int, user: UserToUpdate) -> None: +async def update_user(user_id: UserId, user: UserToUpdate) -> None: query = """ UPDATE users SET name = :name, email = :email @@ -45,7 +46,7 @@ async def update_user(user_id: int, user: UserToUpdate) -> None: ) -async def update_user_account_type(user_id: int, account_type: UserAccountType) -> None: +async def update_user_account_type(user_id: UserId, account_type: UserAccountType) -> None: query = """ UPDATE users SET account_type = :account_type @@ -56,7 +57,7 @@ async def update_user_account_type(user_id: int, account_type: UserAccountType) ) -async def update_user_password(user_id: int, password_hash: str) -> None: +async def update_user_password(user_id: UserId, password_hash: str) -> None: query = """ UPDATE users SET password_hash = :password_hash @@ -65,7 +66,7 @@ async def update_user_password(user_id: int, password_hash: str) -> None: await database.execute(query=query, values={"user_id": user_id, "password_hash": password_hash}) -async def get_user_by_id(user_id: int) -> UserPublic | None: +async def get_user_by_id(user_id: UserId) -> UserPublic | None: query = """ SELECT * FROM users @@ -105,7 +106,7 @@ async def create_user(user: User) -> User: return User.model_validate(dict(assert_some(result)._mapping)) -async def delete_user(user_id: int) -> None: +async def delete_user(user_id: UserId) -> None: query = """ DELETE FROM users WHERE id = :user_id @@ -127,7 +128,7 @@ async def get_user(email: str) -> UserInDB | None: return await fetch_one_parsed(database, UserInDB, users.select().where(users.c.email == email)) -async def delete_user_and_owned_clubs(user_id: int) -> None: +async def delete_user_and_owned_clubs(user_id: UserId) -> None: for club in await get_clubs_for_user_id(user_id): club_id = assert_some(club.id) diff --git a/backend/bracket/sql/validation.py b/backend/bracket/sql/validation.py new file mode 100644 index 000000000..c68cd839a --- /dev/null +++ b/backend/bracket/sql/validation.py @@ -0,0 +1,35 @@ +from fastapi import HTTPException +from starlette import status + +from bracket.models.db.stage_item import StageItemCreateBody +from bracket.models.db.stage_item_inputs import ( + StageItemInputCreateBodyFinal, + StageItemInputCreateBodyTentative, +) +from bracket.sql.stage_items import get_stage_items +from bracket.sql.teams import get_teams_by_id +from bracket.utils.id_types import TournamentId + + +async def todo_check_inputs_belong_to_tournament( + stage_body: StageItemCreateBody, tournament_id: TournamentId +) -> None: + teams = { + input_.team_id + for input_ in stage_body.inputs + if isinstance(input_, StageItemInputCreateBodyFinal) + } + teams_fetched = await get_teams_by_id(teams, tournament_id) + + stage_items = { + input_.winner_from_stage_item_id + for input_ in stage_body.inputs + if isinstance(input_, StageItemInputCreateBodyTentative) + } + stage_items_fetched = await get_stage_items(tournament_id, stage_items) + + if len(teams) != len(teams_fetched) or len(stage_items) != len(stage_items_fetched): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Could not find all team ids or stages", + ) diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 5b78a96bf..8bd89ecbe 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -1,5 +1,5 @@ import random -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from heliclockter import datetime_utc @@ -70,6 +70,16 @@ DUMMY_TOURNAMENT, DUMMY_USER, ) +from bracket.utils.id_types import ( + ClubId, + CourtId, + PlayerId, + PlayerXTeamId, + StageId, + TeamId, + TournamentId, + UserId, +) from bracket.utils.logging import logger from bracket.utils.security import hash_password from bracket.utils.types import BaseModelT, assert_some @@ -77,8 +87,10 @@ if TYPE_CHECKING: from sqlalchemy import Table +T = TypeVar("T", bound=int) + -async def create_admin_user() -> int: +async def create_admin_user() -> UserId: assert config.admin_email assert config.admin_password @@ -94,7 +106,7 @@ async def create_admin_user() -> int: return assert_some(user.id) -async def init_db_when_empty() -> int | None: +async def init_db_when_empty() -> UserId | None: table_count = await database.fetch_val( "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" ) @@ -111,7 +123,7 @@ async def init_db_when_empty() -> int | None: return None -async def sql_create_dev_db() -> int: +async def sql_create_dev_db() -> UserId: # TODO: refactor into smaller functions # pylint: disable=too-many-statements assert environment is not Environment.PRODUCTION @@ -137,101 +149,136 @@ async def sql_create_dev_db() -> int: StageItem: stage_items, } - async def insert_dummy(obj_to_insert: BaseModelT, update_data: dict[str, Any] = {}) -> int: + async def insert_dummy( + obj_to_insert: BaseModelT, id_type: type[T], update_data: dict[str, Any] = {} + ) -> T: record_id, _ = await insert_generic( database, obj_to_insert.model_copy(update=update_data), table_lookup[type(obj_to_insert)], type(obj_to_insert), ) - return record_id + return id_type(record_id) - user_id_1 = await insert_dummy(DUMMY_USER) - club_id_1 = await insert_dummy(DUMMY_CLUB) + user_id_1 = await insert_dummy(DUMMY_USER, UserId) + club_id_1 = await insert_dummy(DUMMY_CLUB, ClubId) await insert_dummy( - UserXClub(user_id=user_id_1, club_id=club_id_1, relation=UserXClubRelation.OWNER) + UserXClub(user_id=user_id_1, club_id=club_id_1, relation=UserXClubRelation.OWNER), int ) if real_user_id is not None: await insert_dummy( - UserXClub(user_id=real_user_id, club_id=club_id_1, relation=UserXClubRelation.OWNER) + UserXClub(user_id=real_user_id, club_id=club_id_1, relation=UserXClubRelation.OWNER), + int, ) - tournament_id_1 = await insert_dummy(DUMMY_TOURNAMENT, {"club_id": club_id_1}) - stage_id_1 = await insert_dummy(DUMMY_STAGE1, {"tournament_id": tournament_id_1}) - stage_id_2 = await insert_dummy(DUMMY_STAGE2, {"tournament_id": tournament_id_1}) + tournament_id_1 = await insert_dummy(DUMMY_TOURNAMENT, TournamentId, {"club_id": club_id_1}) + stage_id_1 = await insert_dummy(DUMMY_STAGE1, StageId, {"tournament_id": tournament_id_1}) + stage_id_2 = await insert_dummy(DUMMY_STAGE2, StageId, {"tournament_id": tournament_id_1}) - team_id_1 = await insert_dummy(DUMMY_TEAM1, {"tournament_id": tournament_id_1}) - team_id_2 = await insert_dummy(DUMMY_TEAM2, {"tournament_id": tournament_id_1}) - team_id_3 = await insert_dummy(DUMMY_TEAM3, {"tournament_id": tournament_id_1}) - team_id_4 = await insert_dummy(DUMMY_TEAM4, {"tournament_id": tournament_id_1}) + team_id_1 = await insert_dummy(DUMMY_TEAM1, TeamId, {"tournament_id": tournament_id_1}) + team_id_2 = await insert_dummy(DUMMY_TEAM2, TeamId, {"tournament_id": tournament_id_1}) + team_id_3 = await insert_dummy(DUMMY_TEAM3, TeamId, {"tournament_id": tournament_id_1}) + team_id_4 = await insert_dummy(DUMMY_TEAM4, TeamId, {"tournament_id": tournament_id_1}) team_id_5 = await insert_dummy( - DUMMY_TEAM4, {"name": "Team 5", "tournament_id": tournament_id_1} + DUMMY_TEAM4, TeamId, {"name": "Team 5", "tournament_id": tournament_id_1} ) team_id_6 = await insert_dummy( - DUMMY_TEAM4, {"name": "Team 6", "tournament_id": tournament_id_1} + DUMMY_TEAM4, TeamId, {"name": "Team 6", "tournament_id": tournament_id_1} ) team_id_7 = await insert_dummy( - DUMMY_TEAM4, {"name": "Team 7", "tournament_id": tournament_id_1} + DUMMY_TEAM4, TeamId, {"name": "Team 7", "tournament_id": tournament_id_1} ) team_id_8 = await insert_dummy( - DUMMY_TEAM4, {"name": "Team 8", "tournament_id": tournament_id_1} + DUMMY_TEAM4, TeamId, {"name": "Team 8", "tournament_id": tournament_id_1} ) - player_id_1 = await insert_dummy(DUMMY_PLAYER1, {"tournament_id": tournament_id_1}) - player_id_2 = await insert_dummy(DUMMY_PLAYER2, {"tournament_id": tournament_id_1}) - player_id_3 = await insert_dummy(DUMMY_PLAYER3, {"tournament_id": tournament_id_1}) - player_id_4 = await insert_dummy(DUMMY_PLAYER4, {"tournament_id": tournament_id_1}) - player_id_5 = await insert_dummy(DUMMY_PLAYER5, {"tournament_id": tournament_id_1}) - player_id_6 = await insert_dummy(DUMMY_PLAYER6, {"tournament_id": tournament_id_1}) - player_id_7 = await insert_dummy(DUMMY_PLAYER7, {"tournament_id": tournament_id_1}) - player_id_8 = await insert_dummy(DUMMY_PLAYER8, {"tournament_id": tournament_id_1}) + player_id_1 = await insert_dummy(DUMMY_PLAYER1, PlayerId, {"tournament_id": tournament_id_1}) + player_id_2 = await insert_dummy(DUMMY_PLAYER2, PlayerId, {"tournament_id": tournament_id_1}) + player_id_3 = await insert_dummy(DUMMY_PLAYER3, PlayerId, {"tournament_id": tournament_id_1}) + player_id_4 = await insert_dummy(DUMMY_PLAYER4, PlayerId, {"tournament_id": tournament_id_1}) + player_id_5 = await insert_dummy(DUMMY_PLAYER5, PlayerId, {"tournament_id": tournament_id_1}) + player_id_6 = await insert_dummy(DUMMY_PLAYER6, PlayerId, {"tournament_id": tournament_id_1}) + player_id_7 = await insert_dummy(DUMMY_PLAYER7, PlayerId, {"tournament_id": tournament_id_1}) + player_id_8 = await insert_dummy(DUMMY_PLAYER8, PlayerId, {"tournament_id": tournament_id_1}) player_id_9 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 09", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 09", "tournament_id": tournament_id_1} ) player_id_10 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 10", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 10", "tournament_id": tournament_id_1} ) player_id_11 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 11", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 11", "tournament_id": tournament_id_1} ) player_id_12 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 12", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 12", "tournament_id": tournament_id_1} ) player_id_13 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 13", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 13", "tournament_id": tournament_id_1} ) player_id_14 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 14", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 14", "tournament_id": tournament_id_1} ) player_id_15 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 15", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 15", "tournament_id": tournament_id_1} ) player_id_16 = await insert_dummy( - DUMMY_PLAYER8, {"name": "Player 16", "tournament_id": tournament_id_1} + DUMMY_PLAYER8, PlayerId, {"name": "Player 16", "tournament_id": tournament_id_1} ) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_1, "team_id": team_id_1}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_2, "team_id": team_id_1}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_3, "team_id": team_id_2}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_4, "team_id": team_id_2}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_5, "team_id": team_id_3}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_6, "team_id": team_id_3}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_7, "team_id": team_id_4}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_8, "team_id": team_id_4}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_9, "team_id": team_id_5}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_10, "team_id": team_id_5}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_11, "team_id": team_id_6}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_12, "team_id": team_id_6}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_13, "team_id": team_id_7}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_14, "team_id": team_id_7}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_15, "team_id": team_id_8}) - await insert_dummy(DUMMY_PLAYER_X_TEAM, {"player_id": player_id_16, "team_id": team_id_8}) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_1, "team_id": team_id_1} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_2, "team_id": team_id_1} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_3, "team_id": team_id_2} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_4, "team_id": team_id_2} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_5, "team_id": team_id_3} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_6, "team_id": team_id_3} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_7, "team_id": team_id_4} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_8, "team_id": team_id_4} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_9, "team_id": team_id_5} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_10, "team_id": team_id_5} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_11, "team_id": team_id_6} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_12, "team_id": team_id_6} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_13, "team_id": team_id_7} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_14, "team_id": team_id_7} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_15, "team_id": team_id_8} + ) + await insert_dummy( + DUMMY_PLAYER_X_TEAM, PlayerXTeamId, {"player_id": player_id_16, "team_id": team_id_8} + ) - await insert_dummy(DUMMY_COURT1, {"tournament_id": tournament_id_1}) - await insert_dummy(DUMMY_COURT2, {"tournament_id": tournament_id_1}) + await insert_dummy(DUMMY_COURT1, CourtId, {"tournament_id": tournament_id_1}) + await insert_dummy(DUMMY_COURT2, CourtId, {"tournament_id": tournament_id_1}) stage_item_1 = await sql_create_stage_item( tournament_id_1, diff --git a/backend/bracket/utils/dummy_records.py b/backend/bracket/utils/dummy_records.py index dcfc1b22d..9682b76e6 100644 --- a/backend/bracket/utils/dummy_records.py +++ b/backend/bracket/utils/dummy_records.py @@ -14,6 +14,16 @@ from bracket.models.db.team import Team from bracket.models.db.tournament import Tournament from bracket.models.db.user import User +from bracket.utils.id_types import ( + ClubId, + CourtId, + PlayerId, + RoundId, + StageId, + StageItemId, + TeamId, + TournamentId, +) from bracket.utils.security import hash_password DUMMY_MOCK_TIME = datetime_utc(2022, 1, 11, 4, 32, 11, tzinfo=ZoneInfo("UTC")) @@ -27,7 +37,7 @@ ) DUMMY_TOURNAMENT = Tournament( - club_id=DB_PLACEHOLDER_ID, + club_id=ClubId(DB_PLACEHOLDER_ID), name="Some Cool Tournament", created=DUMMY_MOCK_TIME, start_time=DUMMY_MOCK_TIME, @@ -41,21 +51,21 @@ ) DUMMY_STAGE1 = Stage( - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, is_active=True, name="Group Stage", ) DUMMY_STAGE2 = Stage( - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, is_active=False, name="Knockout Stage", ) DUMMY_STAGE_ITEM1 = StageItemToInsert( - stage_id=DB_PLACEHOLDER_ID, + stage_id=StageId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, type=StageType.ROUND_ROBIN, team_count=4, @@ -63,7 +73,7 @@ ) DUMMY_STAGE_ITEM2 = StageItemToInsert( - stage_id=DB_PLACEHOLDER_ID, + stage_id=StageId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, type=StageType.ROUND_ROBIN, team_count=4, @@ -71,7 +81,7 @@ ) DUMMY_STAGE_ITEM3 = StageItemToInsert( - stage_id=DB_PLACEHOLDER_ID, + stage_id=StageId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, type=StageType.SINGLE_ELIMINATION, team_count=4, @@ -79,21 +89,21 @@ ) DUMMY_ROUND1 = Round( - stage_item_id=DB_PLACEHOLDER_ID, + stage_item_id=StageItemId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, is_draft=False, name="Round 1", ) DUMMY_ROUND2 = Round( - stage_item_id=DB_PLACEHOLDER_ID, + stage_item_id=StageItemId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, is_draft=True, name="Round 2", ) DUMMY_ROUND3 = Round( - stage_item_id=DB_PLACEHOLDER_ID, + stage_item_id=StageItemId(DB_PLACEHOLDER_ID), created=DUMMY_MOCK_TIME, is_draft=False, name="Round 3", @@ -102,12 +112,12 @@ DUMMY_MATCH1 = Match( created=DUMMY_MOCK_TIME, start_time=DUMMY_MOCK_TIME, - round_id=DB_PLACEHOLDER_ID, - team1_id=DB_PLACEHOLDER_ID, - team2_id=DB_PLACEHOLDER_ID, + round_id=RoundId(DB_PLACEHOLDER_ID), + team1_id=TeamId(DB_PLACEHOLDER_ID), + team2_id=TeamId(DB_PLACEHOLDER_ID), team1_score=11, team2_score=22, - court_id=DB_PLACEHOLDER_ID, + court_id=CourtId(DB_PLACEHOLDER_ID), team1_winner_from_stage_item_id=None, team1_winner_position=None, team1_winner_from_match_id=None, @@ -132,28 +142,28 @@ DUMMY_TEAM1 = Team( created=DUMMY_MOCK_TIME, name="Team 1", - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), active=True, ) DUMMY_TEAM2 = Team( created=DUMMY_MOCK_TIME, name="Team 2", - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), active=True, ) DUMMY_TEAM3 = Team( created=DUMMY_MOCK_TIME, name="Team 3", - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), active=True, ) DUMMY_TEAM4 = Team( created=DUMMY_MOCK_TIME, name="Team 4", - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), active=True, ) @@ -162,71 +172,71 @@ name="Player 01", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER2 = Player( name="Player 02", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER3 = Player( name="Player 03", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER4 = Player( name="Player 04", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER5 = Player( name="Player 05", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER6 = Player( name="Player 06", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER7 = Player( name="Player 07", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER8 = Player( name="Player 08", active=True, created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_PLAYER_X_TEAM = PlayerXTeam( - player_id=DB_PLACEHOLDER_ID, - team_id=DB_PLACEHOLDER_ID, + player_id=PlayerId(DB_PLACEHOLDER_ID), + team_id=TeamId(DB_PLACEHOLDER_ID), ) DUMMY_COURT1 = Court( name="Court 1", created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) DUMMY_COURT2 = Court( name="Court 2", created=DUMMY_MOCK_TIME, - tournament_id=DB_PLACEHOLDER_ID, + tournament_id=TournamentId(DB_PLACEHOLDER_ID), ) diff --git a/backend/bracket/utils/id_types.py b/backend/bracket/utils/id_types.py new file mode 100644 index 000000000..3ce10c11b --- /dev/null +++ b/backend/bracket/utils/id_types.py @@ -0,0 +1,15 @@ +from typing import NewType + +ClubId = NewType("ClubId", int) +TournamentId = NewType("TournamentId", int) +StageId = NewType("StageId", int) +StageItemId = NewType("StageItemId", int) +StageItemInputId = NewType("StageItemInputId", int) +MatchId = NewType("MatchId", int) +RoundId = NewType("RoundId", int) +TeamId = NewType("TeamId", int) +PlayerId = NewType("PlayerId", int) +CourtId = NewType("CourtId", int) +UserId = NewType("UserId", int) +PlayerXTeamId = NewType("PlayerXTeamId", int) +UserXClubId = NewType("UserXClubId", int) diff --git a/backend/tests/integration_tests/sql.py b/backend/tests/integration_tests/sql.py index 3c504e128..45ae60d2d 100644 --- a/backend/tests/integration_tests/sql.py +++ b/backend/tests/integration_tests/sql.py @@ -33,6 +33,7 @@ ) from bracket.utils.db import insert_generic from bracket.utils.dummy_records import DUMMY_CLUB, DUMMY_TOURNAMENT +from bracket.utils.id_types import TeamId from bracket.utils.types import BaseModelT, assert_some from tests.integration_tests.mocks import get_mock_token, get_mock_user from tests.integration_tests.models import AuthContext @@ -92,7 +93,7 @@ async def inserted_player(player: Player) -> AsyncIterator[Player]: @asynccontextmanager -async def inserted_player_in_team(player: Player, team_id: int) -> AsyncIterator[Player]: +async def inserted_player_in_team(player: Player, team_id: TeamId) -> AsyncIterator[Player]: async with inserted_generic(player, players, Player) as row_inserted: async with inserted_generic( PlayerXTeam(player_id=assert_some(row_inserted.id), team_id=team_id), diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 1ff233868..32387c4e6 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -13,11 +13,12 @@ DUMMY_PLAYER2, DUMMY_STAGE_ITEM1, ) +from bracket.utils.id_types import RoundId, StageItemId, TeamId, TournamentId def test_elo_calculation() -> None: round_ = RoundWithMatches( - stage_item_id=1, + stage_item_id=StageItemId(1), created=DUMMY_MOCK_TIME, is_draft=False, is_active=False, @@ -26,8 +27,8 @@ def test_elo_calculation() -> None: MatchWithDetailsDefinitive( created=DUMMY_MOCK_TIME, start_time=DUMMY_MOCK_TIME, - team1_id=1, - team2_id=1, + team1_id=TeamId(1), + team2_id=TeamId(1), team1_winner_from_stage_item_id=None, team1_winner_position=None, team1_winner_from_match_id=None, @@ -36,7 +37,7 @@ def test_elo_calculation() -> None: team2_winner_from_match_id=None, team1_score=3, team2_score=4, - round_id=1, + round_id=RoundId(1), court_id=None, court=None, duration_minutes=10, @@ -45,9 +46,9 @@ def test_elo_calculation() -> None: custom_margin_minutes=None, position_in_schedule=0, team1=FullTeamWithPlayers( - id=3, + id=TeamId(3), name="Dummy team 1", - tournament_id=1, + tournament_id=TournamentId(1), active=True, created=DUMMY_MOCK_TIME, players=[DUMMY_PLAYER1.model_copy(update={"id": 1})], @@ -58,9 +59,9 @@ def test_elo_calculation() -> None: losses=DUMMY_PLAYER1.losses, ), team2=FullTeamWithPlayers( - id=4, + id=TeamId(4), name="Dummy team 2", - tournament_id=1, + tournament_id=TournamentId(1), active=True, created=DUMMY_MOCK_TIME, players=[DUMMY_PLAYER2.model_copy(update={"id": 2})], @@ -75,7 +76,7 @@ def test_elo_calculation() -> None: ) stage_item = StageItemWithRounds( **DUMMY_STAGE_ITEM1.model_dump(exclude={"id"}), - id=-1, + id=StageItemId(-1), inputs=[], rounds=[round_], ) diff --git a/backend/tests/unit_tests/swiss_test.py b/backend/tests/unit_tests/swiss_test.py index 03031e8fe..7f03a4c68 100644 --- a/backend/tests/unit_tests/swiss_test.py +++ b/backend/tests/unit_tests/swiss_test.py @@ -14,6 +14,7 @@ DUMMY_TEAM3, DUMMY_TEAM4, ) +from bracket.utils.id_types import StageItemId, TeamId from tests.integration_tests.mocks import MOCK_NOW MATCH_FILTER = MatchFilter(elo_diff_threshold=50, iterations=100, limit=20, only_recommended=False) @@ -24,7 +25,7 @@ def test_no_draft_round() -> None: get_possible_upcoming_matches_for_swiss(MATCH_FILTER, [], []) -def get_team(team: Team, team_id: int) -> FullTeamWithPlayers: +def get_team(team: Team, team_id: TeamId) -> FullTeamWithPlayers: return FullTeamWithPlayers( id=team_id, **team.model_dump(exclude={"id"}), @@ -44,20 +45,30 @@ def get_match( def test_constraints() -> None: - 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) + team1 = get_team( + DUMMY_TEAM1.model_copy(update={"elo_score": Decimal("1125.0")}), team_id=TeamId(-1) + ) + team2 = get_team( + DUMMY_TEAM2.model_copy(update={"elo_score": Decimal("1175.0")}), team_id=TeamId(-2) + ) + team3 = get_team( + DUMMY_TEAM3.model_copy(update={"elo_score": Decimal("1200.0")}), team_id=TeamId(-3) + ) + team4 = get_team( + DUMMY_TEAM4.model_copy(update={"elo_score": Decimal("1250.0")}), team_id=TeamId(-4) + ) rounds = [ RoundWithMatches( matches=[get_match(DUMMY_MATCH1, team1, team2)], is_draft=False, - stage_item_id=-1, + stage_item_id=StageItemId(-1), name="R1", created=MOCK_NOW, ), - RoundWithMatches(matches=[], is_draft=True, stage_item_id=-1, name="R2", created=MOCK_NOW), + RoundWithMatches( + matches=[], is_draft=True, stage_item_id=StageItemId(-1), name="R2", created=MOCK_NOW + ), ] teams = [team1, team2, team3, team4] result = get_possible_upcoming_matches_for_swiss(MATCH_FILTER, rounds, teams)