From 67c0a8d4626d9c9b09832be49adf1f485a28c5f3 Mon Sep 17 00:00:00 2001 From: adrunkhuman <16039109+adrunkhuman@users.noreply.github.com> Date: Sat, 23 May 2026 15:49:04 +0200 Subject: [PATCH 1/3] refactor: consolidate prediction parsing --- AGENTS.md | 2 +- tests/test_admin_service.py | 4 +- tests/test_prediction_parser.py | 209 ++++++------------ .../commands/admin_panel/result_modals.py | 4 +- typer_bot/services/admin_service.py | 6 +- typer_bot/utils/__init__.py | 4 - typer_bot/utils/prediction_parser.py | 107 ++------- 7 files changed, 92 insertions(+), 244 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 719151e..4431341 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ You are working on `TyperBot`, a Discord bot for football prediction leagues. - **Thread Predictions:** Users can post predictions in public threads under fixture announcements. - **Rate Limiting:** Thread predictions are rate-limited to 1 per second per user. Cooldown entries auto-expire after 1 hour. - **Async:** All database ops must be async (`aiosqlite`). -- **Parsing:** Use `utils.prediction_parser.parse_line_predictions` for all score parsing. Do NOT write ad-hoc regex. +- **Parsing:** Use `utils.prediction_parser.parse_prediction_lines` for all score parsing. Do NOT write ad-hoc regex. - **Logging:** Use `typer_bot.utils.logger.setup_logging()` early. Do not use `print()`. - **Timezones:** All datetime operations use timezone-aware objects. Use `utils.timezone.now()` instead of `datetime.now()`. Configure via `TZ` env var (default: `UTC`). - **Permissions:** Bot requires `Send Messages`, `Send Messages in Threads`, `Read Message History`, `Add Reactions`, `Create Public Threads`, and `Use Slash Commands`. diff --git a/tests/test_admin_service.py b/tests/test_admin_service.py index 9fd4e18..4ebcf70 100644 --- a/tests/test_admin_service.py +++ b/tests/test_admin_service.py @@ -242,7 +242,7 @@ async def test_replace_prediction_preserves_original_timing_metadata( fixture, updated_prediction, recalculation = await service.replace_prediction( fixture_id, "user-1", - "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2", + "Team E - Team F 0-2\nTeam A - Team B 2-1\nTeam C - Team D 1-1", "admin-1", "111111", ) @@ -388,7 +388,7 @@ async def test_correct_results_recalculates_fixture_scores_and_standings( fixture, results, recalculation = await service.correct_results( fixture1_id, - "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2", + "Team E - Team F 0-2\nTeam A - Team B 2-1\nTeam C - Team D 1-1", "111111", ) diff --git a/tests/test_prediction_parser.py b/tests/test_prediction_parser.py index 5bd02b5..a81060b 100644 --- a/tests/test_prediction_parser.py +++ b/tests/test_prediction_parser.py @@ -4,124 +4,22 @@ ascii_username, format_predictions_preview, format_standings, - parse_line_predictions, parse_prediction_lines, - parse_predictions, ) -class TestParsePredictions: - """Test suite for parse_predictions function.""" - - def test_basic_scores(self): - """Basic hyphen-separated scores.""" - predictions, errors = parse_predictions("2-1 1-0 2-2", expected_count=3) - assert predictions == ["2-1", "1-0", "2-2"] - assert not errors - - def test_colon_separators(self): - """Scores with colon separators.""" - predictions, errors = parse_predictions("2:1 1:0 2:2", expected_count=3) - assert predictions == ["2-1", "1-0", "2-2"] - assert not errors - - def test_mixed_separators(self): - """Mixed hyphen and colon separators.""" - predictions, errors = parse_predictions("2-1 1:0 2-2", expected_count=3) - assert predictions == ["2-1", "1-0", "2-2"] - assert not errors - - def test_extra_spaces(self): - """Scores with extra spaces around separators.""" - predictions, errors = parse_predictions("2 - 1 1 : 0", expected_count=2) - assert predictions == ["2-1", "1-0"] - assert not errors - - def test_comma_separation(self): - """Comma-separated scores.""" - predictions, errors = parse_predictions("2-1, 1-0, 2-2", expected_count=3) - assert predictions == ["2-1", "1-0", "2-2"] - assert not errors - - def test_random_newlines(self): - """Scores with random newlines mixed in.""" - predictions, errors = parse_predictions("2-1\n\n1-0\n2-2\n", expected_count=3) - assert predictions == ["2-1", "1-0", "2-2"] - assert not errors - - def test_leading_trailing_whitespace(self): - """Input with leading/trailing whitespace and indentation.""" - predictions, errors = parse_predictions(" 2-1 1-0 ", expected_count=2) - assert predictions == ["2-1", "1-0"] - assert not errors - - def test_double_digit_scores(self): - """Double-digit scores like 10-0.""" - predictions, errors = parse_predictions("10-0 0-10 12-12", expected_count=3) - assert predictions == ["10-0", "0-10", "12-12"] - assert not errors - - def test_wrong_count_error(self): - """Error when count doesn't match expected.""" - predictions, errors = parse_predictions("2-1 1-0", expected_count=3) - assert predictions == ["2-1", "1-0"] - assert len(errors) == 1 - assert "Expected 3 scores, found 2" in errors[0] - - def test_no_valid_scores(self): - """Input with no valid score patterns.""" - predictions, errors = parse_predictions("hello world test", expected_count=3) - assert predictions == [] - assert len(errors) == 1 - - -class TestParseLinePredictions: - """Test suite for parse_line_predictions function.""" - - def test_basic_line_parsing(self): - """Basic line-by-line parsing with game context.""" - input_text = "Team A vs Team B 2-1\nTeam C vs Team D 1-0" +class TestParsePredictionLines: + def test_full_prediction_maps_by_game_name(self): games = ["Team A vs Team B", "Team C vs Team D"] - predictions, errors = parse_line_predictions(input_text, games) - assert predictions == ["2-1", "1-0"] - assert not errors - - def test_score_at_end_of_line(self): - """Score must be at end of line - trailing text causes parse failure.""" - # Score at end works - input_text = "Team A vs Team B 2-1\nTeam B 1-0" - games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) - assert predictions == ["2-1", "1-0"] - assert not errors - - def test_trailing_text_fails(self): - """Text after score fails - parser expects score at line end.""" - input_text = "Team A 2-1 some comment\nTeam B 1-0" - games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) - assert predictions == ["1-0"] # Only second line parses - assert len(errors) == 1 - assert "Could not find score" in errors[0] - - def test_leading_indentation(self): - """Lines with leading whitespace/indentation.""" - input_text = " Team A 2-1\n Team B 1-0" - games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) - assert predictions == ["2-1", "1-0"] - assert not errors + predictions, game_indexes, errors = parse_prediction_lines( + "Team A vs Team B 2-1\nTeam C vs Team D 1-0", + games, + ) - def test_colon_separators_in_lines(self): - """Lines with colon separators.""" - input_text = "Team A 2:1\nTeam B 1:0" - games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) assert predictions == ["2-1", "1-0"] - assert not errors - + assert game_indexes == [0, 1] + assert errors == [] -class TestParsePredictionLines: def test_partial_mapping_by_game_name(self): games = ["Team A - Team B", "Team C - Team D", "Team E - Team F"] input_text = "Team C - Team D 1-1\nTeam E - Team F 0-2" @@ -144,6 +42,25 @@ def test_full_prediction_falls_back_to_positional_matching(self): assert game_indexes == [0, 1] assert errors == [] + def test_score_at_end_of_line(self): + games = ["Team A", "Team B"] + predictions, game_indexes, errors = parse_prediction_lines("Team A 2-1\nTeam B 1-0", games) + + assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] + assert errors == [] + + def test_trailing_text_fails(self): + games = ["Team A", "Team B"] + predictions, game_indexes, errors = parse_prediction_lines( + "Team A 2-1 some comment\nTeam B 1-0", games + ) + + assert predictions == [] + assert game_indexes == [] + assert len(errors) == 1 + assert "Could not find score" in errors[0] + def test_partial_requires_game_names(self): games = ["Team A - Team B", "Team C - Team D"] predictions, game_indexes, errors = parse_prediction_lines("2-1", games, allow_partial=True) @@ -175,125 +92,135 @@ def test_partial_mapping_preserves_team_names_starting_with_number(self): assert errors == [] def test_mixed_separators_in_lines(self): - """Lines with mixed separators.""" input_text = "Team A 2-1\nTeam B 1:0\nTeam C 2-2" games = ["Team A", "Team B", "Team C"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0", "2-2"] + assert game_indexes == [0, 1, 2] assert not errors + def test_double_digit_scores(self): + games = ["Team A", "Team B"] + predictions, game_indexes, errors = parse_prediction_lines( + "Team A 10-0\nTeam B 0:12", games + ) + + assert predictions == ["10-0", "0-12"] + assert game_indexes == [0, 1] + assert errors == [] + def test_missing_score_in_line(self): - """Line without a valid score at the end.""" input_text = "Team A 2-1\nTeam B no score here" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) - assert predictions == ["2-1"] + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) + assert predictions == [] + assert game_indexes == [] assert len(errors) == 1 assert "Could not find score" in errors[0] def test_wrong_line_count_error(self): - """Error when line count doesn't match game count.""" input_text = "Team A 2-1" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == [] + assert game_indexes == [] assert len(errors) == 1 assert "Expected 2 predictions, found 1" in errors[0] def test_extra_whitespace_in_lines(self): - """Lines with extra internal whitespace.""" input_text = "Team A 2 - 1\nTeam B 1 : 0 " games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] assert not errors def test_nullified_game_lowercase_x(self): - """Parse 'x' as nullified game marker.""" input_text = "Team A 2-1\nTeam B x" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "x"] + assert game_indexes == [0, 1] assert not errors def test_nullified_game_uppercase_x(self): - """Parse 'X' as nullified game marker (case insensitive).""" input_text = "Team A 2-1\nTeam B X" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "x"] + assert game_indexes == [0, 1] assert not errors def test_mixed_scores_and_nullified(self): - """Mix of regular scores and nullified games.""" input_text = "Team A 2-1\nTeam B x\nTeam C 0-0\nTeam D X" games = ["Team A", "Team B", "Team C", "Team D"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "x", "0-0", "x"] + assert game_indexes == [0, 1, 2, 3] assert not errors def test_nullified_with_whitespace(self): - """Nullified marker with trailing whitespace.""" input_text = "Team A x \nTeam B X " games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["x", "x"] + assert game_indexes == [0, 1] assert not errors def test_comma_separated_predictions(self): - """Comma-separated predictions.""" input_text = "Team A 2-1, Team B 1-0" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] assert not errors def test_mixed_comma_and_newline(self): - """Mixed comma and newline delimiters.""" input_text = "Team A 2-1, Team B 1-0\nTeam C 2-2" games = ["Team A", "Team B", "Team C"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0", "2-2"] + assert game_indexes == [0, 1, 2] assert not errors def test_comma_with_extra_whitespace(self): - """Comma-separated with extra whitespace.""" input_text = "Team A 2-1 , Team B 1-0 , Team C 2-2" games = ["Team A", "Team B", "Team C"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0", "2-2"] + assert game_indexes == [0, 1, 2] assert not errors def test_trailing_comma(self): - """Trailing comma should be handled gracefully.""" input_text = "Team A 2-1, Team B 1-0," games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] assert not errors def test_multiple_commas(self): - """Multiple consecutive commas should not create empty segments.""" input_text = "Team A 2-1,, Team B 1-0" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] assert not errors def test_comma_with_nullified_games(self): - """Comma-separated with nullified games.""" input_text = "Team A 2-1, Team B x, Team C 1-0" games = ["Team A", "Team B", "Team C"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "x", "1-0"] + assert game_indexes == [0, 1, 2] assert not errors def test_comma_with_colon_separator(self): - """Comma-separated with colon score separators.""" input_text = "Team A 2:1, Team B 1:0" games = ["Team A", "Team B"] - predictions, errors = parse_line_predictions(input_text, games) + predictions, game_indexes, errors = parse_prediction_lines(input_text, games) assert predictions == ["2-1", "1-0"] + assert game_indexes == [0, 1] assert not errors diff --git a/typer_bot/commands/admin_panel/result_modals.py b/typer_bot/commands/admin_panel/result_modals.py index 1d10d6e..1adb3a5 100644 --- a/typer_bot/commands/admin_panel/result_modals.py +++ b/typer_bot/commands/admin_panel/result_modals.py @@ -5,7 +5,7 @@ import discord from typer_bot.database import Database -from typer_bot.utils import get_admin_permission_error, parse_line_predictions +from typer_bot.utils import get_admin_permission_error, parse_prediction_lines from .base import _format_prediction_line @@ -107,7 +107,7 @@ async def on_submit(self, interaction: discord.Interaction): await interaction.response.send_message(permission_error, ephemeral=True) return - results, errors = parse_line_predictions(self.results_input.value, self.fixture["games"]) + results, _, errors = parse_prediction_lines(self.results_input.value, self.fixture["games"]) if errors: await interaction.response.send_message("\n".join(errors), ephemeral=True) return diff --git a/typer_bot/services/admin_service.py b/typer_bot/services/admin_service.py index adac30c..fcfb06a 100644 --- a/typer_bot/services/admin_service.py +++ b/typer_bot/services/admin_service.py @@ -11,7 +11,7 @@ PredictionDisappearedError, PredictionNotFoundError, ) -from typer_bot.utils import parse_line_predictions +from typer_bot.utils import parse_prediction_lines @dataclass(slots=True) @@ -190,7 +190,7 @@ async def replace_prediction( if existing_prediction is None: raise PredictionNotFoundError - predictions, errors = parse_line_predictions(prediction_lines, fixture["games"]) + predictions, _, errors = parse_prediction_lines(prediction_lines, fixture["games"]) if errors: raise ValueError("\n".join(errors)) @@ -265,7 +265,7 @@ async def correct_results( if fixture is None: raise FixtureNotFoundError - results, errors = parse_line_predictions(results_lines, fixture["games"]) + results, _, errors = parse_prediction_lines(results_lines, fixture["games"]) if errors: raise ValueError("\n".join(errors)) diff --git a/typer_bot/utils/__init__.py b/typer_bot/utils/__init__.py index 914e0af..401c893 100644 --- a/typer_bot/utils/__init__.py +++ b/typer_bot/utils/__init__.py @@ -15,9 +15,7 @@ format_fixture_results, format_predictions_preview, format_standings, - parse_line_predictions, parse_prediction_lines, - parse_predictions, ) from .prediction_submission import PredictionSubmission, build_prediction_submission from .scoring import ( @@ -38,9 +36,7 @@ "is_admin", "is_admin_member", "is_configured_admin", - "parse_predictions", "parse_prediction_lines", - "parse_line_predictions", "format_fixture_results", "format_predictions_preview", "format_standings", diff --git a/typer_bot/utils/prediction_parser.py b/typer_bot/utils/prediction_parser.py index fc0d485..d54f1ae 100644 --- a/typer_bot/utils/prediction_parser.py +++ b/typer_bot/utils/prediction_parser.py @@ -16,12 +16,22 @@ def parse_prediction_lines( *, allow_partial: bool = False, ) -> tuple[list[str], list[int], list[str]]: - """Parse prediction lines and map them to specific fixture rows. + """Parse prediction lines and map them to fixture rows. - Each non-empty line must end with a score like ``2:0`` or ``2-1``. When the - fixture label at the start of the line matches one of the provided games, the - prediction is mapped to that exact game row. Full submissions can still fall - back to positional matching for backward compatibility. + Each non-empty line must end with a score like ``2:0``/``2-1``, or cancelled + marker ``x``. Scores are normalized to ``home-away``. When a line starts with + an exact fixture label from ``games``, that prediction is mapped to the matching + fixture row. Otherwise, full submissions fall back to positional matching when + line count equals fixture count. Partial submissions must include fixture names. + + Args: + input_text: Raw text using newline or comma separators. + games: Fixture rows used for mapping and count validation. + allow_partial: Whether fewer predictions than fixture rows are accepted. + + Returns: + A tuple of fixture-ordered predictions, mapped fixture indexes, and errors. + Any parse or mapping error returns empty predictions and indexes. """ normalized = input_text.replace(",", "\n") lines = [line.strip() for line in normalized.split("\n") if line.strip()] @@ -37,7 +47,7 @@ def parse_prediction_lines( match = re.search(r"(\d+)\s*[-:]\s*(\d+)\s*$", stripped) if not match and not is_cancelled: errors.append( - f"Line {line_number}: Could not find score (expected format: '2:0' or '2-1')" + f"Line {line_number}: Could not find score (expected format: '2:0' or '2-1', or 'x' for cancelled games)" ) continue @@ -87,91 +97,6 @@ def parse_prediction_lines( return ordered_predictions, ordered_indexes, [] -def parse_predictions(input_text: str, expected_count: int = 9) -> tuple[list[str], list[str]]: - """Parse predictions from user input. - - Format agnostic: "2-1 1-0", "2:1, 1:0", "2 - 1". - Returns: (valid_predictions, errors) - """ - logger.debug(f"Parsing predictions: expected={expected_count}, input_length={len(input_text)}") - - normalized = input_text.replace(",", " ") - pattern = r"\s*(\d+)\s*[-:]\s*(\d+)\s*" - - predictions = [] - errors = [] - - matches = list(re.finditer(pattern, normalized)) - logger.debug(f"Found {len(matches)} score matches in input") - - for match in matches: - home = match.group(1) - away = match.group(2) - predictions.append(f"{home}-{away}") - - if len(predictions) != expected_count: - error_msg = f"Expected {expected_count} scores, found {len(predictions)}" - logger.warning(f"Prediction count mismatch: {error_msg}") - errors.append(error_msg) - else: - logger.debug(f"Successfully parsed {len(predictions)} predictions") - - return predictions, errors - - -def parse_line_predictions(input_text: str, games: list[str]) -> tuple[list[str], list[str]]: - """Parse predictions from user input with game context. - - Accepts newline-separated or comma-separated predictions. Each segment should - contain a score at the end in format like ``2:0`` or ``2-1``. Cancelled or - voided fixtures can be marked with ``x`` and numeric scores are normalized to - ``home-away`` output regardless of the input separator. - - Args: - input_text: Raw input text (supports commas or newlines as delimiters) - games: Fixture game list used to validate line count and build errors - - Returns: (valid_predictions, errors) - """ - normalized = input_text.replace(",", "\n") - lines = [line.strip() for line in normalized.split("\n") if line.strip()] - - logger.debug(f"Parsing line predictions: {len(lines)} lines, {len(games)} games") - - predictions = [] - errors = [] - - if len(lines) != len(games): - error_msg = f"Expected {len(games)} predictions, found {len(lines)}" - logger.warning(f"Line count mismatch: {error_msg}") - errors.append(error_msg) - return predictions, errors - - for i, line in enumerate(lines): - stripped = line.strip() - - if re.search(r"[xX]\s*$", stripped): - predictions.append("x") - logger.debug(f"Line {i + 1}: Parsed nullified game (x)") - continue - - match = re.search(r"(\d+)\s*[-:]\s*(\d+)\s*$", stripped) - if match: - home_score = match.group(1) - away_score = match.group(2) - predictions.append(f"{home_score}-{away_score}") - logger.debug(f"Line {i + 1}: Parsed {home_score}-{away_score}") - else: - error_msg = f"Line {i + 1}: Could not find score (expected format: '2:0' or '2-1', or 'x' for cancelled games)" - logger.warning(f"Parse error on line {i + 1}: '{line[:50]}...'") - errors.append(error_msg) - - if not errors: - logger.debug(f"Successfully parsed all {len(predictions)} line predictions") - - return predictions, errors - - def ascii_username(username: str, max_len: int = 20) -> str: """Filter username to ASCII-only for reliable alignment in Discord code blocks.""" ascii_only = "".join(c for c in username if ord(c) < 128) From 7169afd4de4ea06884ac8689057c87e0e6826781 Mon Sep 17 00:00:00 2001 From: adrunkhuman <16039109+adrunkhuman@users.noreply.github.com> Date: Sat, 23 May 2026 16:08:13 +0200 Subject: [PATCH 2/3] refactor: extract calculation posting --- AGENTS.md | 1 + .../test_fixture_panel_results_actions.py | 128 ++++++++--------- tests/test_calculation_posting.py | 130 ++++++++++++++++++ typer_bot/commands/admin_commands.py | 77 ----------- .../commands/admin_panel/unified_actions.py | 8 +- typer_bot/services/__init__.py | 2 + typer_bot/services/calculation_posting.py | 100 ++++++++++++++ 7 files changed, 294 insertions(+), 152 deletions(-) create mode 100644 tests/test_calculation_posting.py create mode 100644 typer_bot/services/calculation_posting.py diff --git a/AGENTS.md b/AGENTS.md index 4431341..35bb43f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,7 @@ guild_config ( - `typer_bot/commands/admin_panel/`: Admin panel UI views, selects, and modals split out of `admin_commands.py`. - `typer_bot/handlers/thread_prediction_handler.py`: Thread-based prediction processing (on_message) plus thread prediction cooldown state. - `typer_bot/commands/admin_commands.py`: `/admin` command surface and orchestration for admin workflows, including admin calculation cooldown state. +- `typer_bot/services/calculation_posting.py`: Post-calculation side effects: best-effort DB backup, league-channel publishing, and admin interaction responses. - `typer_bot/utils/config.py`: Centralized configuration (data paths via env vars). - `typer_bot/utils/prediction_parser.py`: Central logic for parsing "2-1" or "2:1" strings. - `typer_bot/utils/scoring.py`: Point calculation using season scoring rules. diff --git a/tests/admin_panel/test_fixture_panel_results_actions.py b/tests/admin_panel/test_fixture_panel_results_actions.py index 71cec1c..cd365c4 100644 --- a/tests/admin_panel/test_fixture_panel_results_actions.py +++ b/tests/admin_panel/test_fixture_panel_results_actions.py @@ -4,6 +4,7 @@ import discord import pytest +import typer_bot.commands.admin_panel.unified_actions as unified_actions from tests.admin_panel_helpers import get_button as _get_button from tests.admin_panel_helpers import has_button as _has_button from typer_bot.commands.admin_panel import ( @@ -74,6 +75,7 @@ async def test_unified_panel_calculate_scores_button_posts_results( admin_cog, mock_interaction_admin, sample_games, + monkeypatch, ): fixture_id = await admin_cog.db.create_fixture( "111111", 45, sample_games, datetime.now(UTC) + timedelta(days=1) @@ -86,18 +88,10 @@ async def test_unified_panel_calculate_scores_button_posts_results( ["2-1", "1-1", "0-2"], False, ) - command_channel = MagicMock(spec=discord.TextChannel) - command_channel.id = 999999 - command_channel.send = AsyncMock() - league_channel = MagicMock(spec=discord.TextChannel) - league_channel.id = 123456 - league_channel.send = AsyncMock() - mock_interaction_admin.channel = command_channel - admin_cog.bot.get_channel.return_value = None - admin_cog.bot.fetch_channel = AsyncMock(return_value=league_channel) mock_interaction_admin.message = MagicMock() mock_interaction_admin.message.edit = AsyncMock() - admin_cog._create_backup = AsyncMock() + post_calculation_result = AsyncMock() + monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) view = UnifiedAdminPanelView( admin_cog.db, @@ -118,74 +112,24 @@ async def test_unified_panel_calculate_scores_button_posts_results( admin_cog.get_calculate_cooldown("111111", str(mock_interaction_admin.user.id)) is not None ) - league_channel.send.assert_awaited_once() - command_channel.send.assert_not_awaited() - assert ( - "Week 45 results calculated and posted to the league channel" - in mock_interaction_admin.response_sent[-1]["content"] + post_calculation_result.assert_awaited_once() + assert post_calculation_result.call_args.args[:3] == ( + admin_cog.bot, + admin_cog.db, + mock_interaction_admin, ) - assert "User One" in league_channel.send.call_args.args[0] assert view.selection.fixture_label == "Week 45 [CLOSED]" assert _has_button(view, "Calculate Scores") is False assert _has_button(view, "Delete Fixture") is False assert mock_interaction_admin.message.edit.await_count == 1 - @pytest.mark.asyncio - async def test_unified_panel_calculate_scores_button_rejects_unavailable_league_channel( - self, - admin_cog, - mock_interaction_admin, - sample_games, - ): - fixture_id = await admin_cog.db.create_fixture( - "111111", 46, sample_games, datetime.now(UTC) + timedelta(days=1) - ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( - fixture_id, - "111", - "User One", - ["2-1", "1-1", "0-2"], - False, - ) - command_channel = MagicMock(spec=discord.TextChannel) - command_channel.send = AsyncMock() - mock_interaction_admin.channel = command_channel - admin_cog.bot.get_channel.return_value = None - admin_cog.bot.fetch_channel = AsyncMock( - side_effect=discord.InvalidData("unknown channel type") - ) - mock_interaction_admin.message = MagicMock() - mock_interaction_admin.message.edit = AsyncMock() - admin_cog._create_backup = AsyncMock() - - view = UnifiedAdminPanelView( - admin_cog.db, - admin_cog.service, - str(mock_interaction_admin.user.id), - "111111", - admin_commands=admin_cog, - bot=admin_cog.bot, - ) - await view.load_fixture_options() - view.fixture_select._values = [str(fixture_id)] - await view.fixture_select.callback(mock_interaction_admin) - - calculate_button = _get_button(view, "Calculate Scores") - await calculate_button.callback(mock_interaction_admin) - - command_channel.send.assert_not_awaited() - assert ( - "configured league channel is unavailable" - in mock_interaction_admin.response_sent[-1]["content"].lower() - ) - @pytest.mark.asyncio async def test_stale_calculate_scores_button_refreshes_when_fixture_already_scored( self, admin_cog, mock_interaction_admin, sample_games, + monkeypatch, ): fixture_id = await admin_cog.db.create_fixture( "111111", 45, sample_games, datetime.now(UTC) + timedelta(days=1) @@ -200,8 +144,8 @@ async def test_stale_calculate_scores_button_refreshes_when_fixture_already_scor ) mock_interaction_admin.message = MagicMock() mock_interaction_admin.message.edit = AsyncMock() - admin_cog._create_backup = AsyncMock() - admin_cog._post_calculation_to_channel = AsyncMock() + post_calculation_result = AsyncMock() + monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) view = UnifiedAdminPanelView( admin_cog.db, admin_cog.service, @@ -219,8 +163,7 @@ async def test_stale_calculate_scores_button_refreshes_when_fixture_already_scor await stale_button.callback(mock_interaction_admin) assert "no longer open" in mock_interaction_admin.response_sent[-1]["content"] - admin_cog._create_backup.assert_not_awaited() - admin_cog._post_calculation_to_channel.assert_not_awaited() + post_calculation_result.assert_not_awaited() assert view.selection.fixture_label == "Week 45 [CLOSED]" assert _has_button(view, "Calculate Scores") is False assert _has_button(view, "Delete Fixture") is False @@ -232,6 +175,7 @@ async def test_unified_panel_calculate_scores_button_rejects_active_cooldown( admin_cog, mock_interaction_admin, sample_games, + monkeypatch, ): fixture_id = await admin_cog.db.create_fixture( "111111", 47, sample_games, datetime.now(UTC) + timedelta(days=1) @@ -241,6 +185,8 @@ async def test_unified_panel_calculate_scores_button_rejects_active_cooldown( "111111", str(mock_interaction_admin.user.id), current_time=now().timestamp() ) admin_cog.service.calculate_fixture_scores = AsyncMock() + post_calculation_result = AsyncMock() + monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) view = UnifiedAdminPanelView( admin_cog.db, @@ -258,6 +204,7 @@ async def test_unified_panel_calculate_scores_button_rejects_active_cooldown( await calculate_button.callback(mock_interaction_admin) assert "Please wait" in mock_interaction_admin.response_sent[-1]["content"] + post_calculation_result.assert_not_awaited() @pytest.mark.asyncio async def test_unified_panel_calculate_scores_button_handles_service_error( @@ -265,13 +212,14 @@ async def test_unified_panel_calculate_scores_button_handles_service_error( admin_cog, mock_interaction_admin, sample_games, + monkeypatch, ): fixture_id = await admin_cog.db.create_fixture( "111111", 48, sample_games, datetime.now(UTC) + timedelta(days=1) ) await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) - admin_cog._create_backup = AsyncMock() - + post_calculation_result = AsyncMock() + monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) view = UnifiedAdminPanelView( admin_cog.db, admin_cog.service, @@ -291,6 +239,42 @@ async def test_unified_panel_calculate_scores_button_handles_service_error( mock_interaction_admin.response_sent[-1]["content"] == "No predictions found for this fixture" ) + post_calculation_result.assert_not_awaited() + + @pytest.mark.asyncio + async def test_unified_panel_calculate_scores_button_requires_bot_context( + self, + admin_cog, + mock_interaction_admin, + sample_games, + monkeypatch, + ): + fixture_id = await admin_cog.db.create_fixture( + "111111", 49, sample_games, datetime.now(UTC) + timedelta(days=1) + ) + await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + admin_cog.service.calculate_fixture_scores = AsyncMock() + post_calculation_result = AsyncMock() + monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) + + view = UnifiedAdminPanelView( + admin_cog.db, + admin_cog.service, + str(mock_interaction_admin.user.id), + "111111", + admin_commands=admin_cog, + bot=None, + ) + await view.load_fixture_options() + view.fixture_select._values = [str(fixture_id)] + await view.fixture_select.callback(mock_interaction_admin) + + calculate_button = _get_button(view, "Calculate Scores") + await calculate_button.callback(mock_interaction_admin) + + assert "unavailable" in mock_interaction_admin.response_sent[-1]["content"] + admin_cog.service.calculate_fixture_scores.assert_not_awaited() + post_calculation_result.assert_not_awaited() @pytest.mark.asyncio async def test_unified_panel_post_results_button_opens_confirmation( diff --git a/tests/test_calculation_posting.py b/tests/test_calculation_posting.py new file mode 100644 index 0000000..5b0ad0f --- /dev/null +++ b/tests/test_calculation_posting.py @@ -0,0 +1,130 @@ +from unittest.mock import AsyncMock, MagicMock + +import discord +import pytest + +import typer_bot.services.calculation_posting as calculation_posting +from typer_bot.services.admin_service import FixtureScoreResult + + +def _score_result(sample_games: list[str]) -> FixtureScoreResult: + return FixtureScoreResult( + fixture={"games": sample_games, "week_number": 7}, + results=["2-1", "1-1", "0-2"], + predictions=[], + scores=[], + standings=[ + { + "user_id": "111", + "user_name": "User One", + "total_points": 7, + "total_exact": 2, + "total_correct": 1, + } + ], + last_fixture=None, + ) + + +def _bot_with_executor() -> MagicMock: + bot = MagicMock(spec=discord.Client) + bot.loop = MagicMock() + + async def run_in_executor(_executor, callback): + return callback() + + bot.loop.run_in_executor = AsyncMock(side_effect=run_in_executor) + return bot + + +@pytest.mark.asyncio +async def test_post_calculation_result_posts_to_configured_league_channel( + database, + mock_interaction_admin, + sample_games, + monkeypatch, +): + bot = _bot_with_executor() + channel = MagicMock(spec=discord.TextChannel) + channel.send = AsyncMock() + bot.get_channel.return_value = channel + monkeypatch.setattr(calculation_posting, "create_backup", MagicMock(return_value="backup.sql")) + monkeypatch.setattr(calculation_posting, "cleanup_old_backups", MagicMock(return_value=0)) + + await calculation_posting.post_calculation_result( + bot, database, mock_interaction_admin, _score_result(sample_games) + ) + + channel.send.assert_awaited_once() + assert "Week 7 Results" in channel.send.call_args.args[0] + assert "User One" in channel.send.call_args.args[0] + assert "posted to the league channel" in mock_interaction_admin.response_sent[-1]["content"] + + +@pytest.mark.asyncio +async def test_post_calculation_result_posts_when_backup_fails( + database, + mock_interaction_admin, + sample_games, + monkeypatch, +): + bot = _bot_with_executor() + channel = MagicMock(spec=discord.TextChannel) + channel.send = AsyncMock() + bot.get_channel.return_value = channel + monkeypatch.setattr( + calculation_posting, + "create_backup", + MagicMock(side_effect=RuntimeError("backup failed")), + ) + + await calculation_posting.post_calculation_result( + bot, database, mock_interaction_admin, _score_result(sample_games) + ) + + channel.send.assert_awaited_once() + assert "posted to the league channel" in mock_interaction_admin.response_sent[-1]["content"] + + +@pytest.mark.asyncio +async def test_post_calculation_result_reports_send_failure( + database, + mock_interaction_admin, + sample_games, + monkeypatch, +): + bot = _bot_with_executor() + channel = MagicMock(spec=discord.TextChannel) + channel.send = AsyncMock(side_effect=RuntimeError("discord unavailable")) + bot.get_channel.return_value = channel + monkeypatch.setattr(calculation_posting, "create_backup", MagicMock(return_value="backup.sql")) + monkeypatch.setattr(calculation_posting, "cleanup_old_backups", MagicMock(return_value=0)) + + await calculation_posting.post_calculation_result( + bot, database, mock_interaction_admin, _score_result(sample_games) + ) + + assert "failed to post" in mock_interaction_admin.response_sent[-1]["content"] + + +@pytest.mark.asyncio +async def test_post_calculation_result_reports_unavailable_league_channel( + database, + mock_interaction_admin, + sample_games, + monkeypatch, +): + bot = _bot_with_executor() + bot.get_channel.return_value = None + bot.fetch_channel = AsyncMock(side_effect=discord.InvalidData("unknown channel type")) + monkeypatch.setattr(calculation_posting, "create_backup", MagicMock(return_value="backup.sql")) + monkeypatch.setattr(calculation_posting, "cleanup_old_backups", MagicMock(return_value=0)) + + await calculation_posting.post_calculation_result( + bot, database, mock_interaction_admin, _score_result(sample_games) + ) + + assert ( + "configured league channel is unavailable" + in mock_interaction_admin.response_sent[-1]["content"].lower() + ) diff --git a/typer_bot/commands/admin_commands.py b/typer_bot/commands/admin_commands.py index e21dc95..c2cf489 100644 --- a/typer_bot/commands/admin_commands.py +++ b/typer_bot/commands/admin_commands.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from datetime import timedelta from typing import cast @@ -15,23 +14,16 @@ ) from typer_bot.database import Database from typer_bot.services import AdminService -from typer_bot.services.admin_service import FixtureScoreResult from typer_bot.utils import ( SETUP_REQUIRED_MESSAGE, - format_fixture_results, - format_standings, get_admin_permission_error, has_setup_permission, now, ) -from typer_bot.utils.config import BACKUP_DIR -from typer_bot.utils.db_backup import cleanup_old_backups, create_backup CALCULATE_COOLDOWN = 30.0 COOLDOWN_ENTRY_EXPIRY = timedelta(hours=1) -logger = logging.getLogger(__name__) - def _is_everyone_role(role: discord.Role, guild_id: int | None) -> bool: is_default = getattr(role, "is_default", None) @@ -336,75 +328,6 @@ def cleanup_expired_state(self) -> int: self._calculate_cooldowns.pop(key, None) return len(expired) - async def _create_backup(self) -> None: - try: - await self.bot.loop.run_in_executor( - None, lambda: create_backup(self.db.db_path, BACKUP_DIR) - ) - await self.bot.loop.run_in_executor( - None, lambda: cleanup_old_backups(BACKUP_DIR, keep=10) - ) - except Exception as exc: - logger.warning(f"Backup failed but calculation succeeded: {exc}") - - async def _post_calculation_to_channel( - self, - interaction: discord.Interaction, - score_result: FixtureScoreResult, - ) -> None: - if interaction.guild_id is None: - await interaction.response.send_message( - "Scores calculated but could not resolve this server.", ephemeral=True - ) - return - - config = await self.db.get_guild_config(str(interaction.guild_id)) - channel = None - if config is not None: - try: - channel_id = int(config["league_channel_id"]) - except (TypeError, ValueError): - channel_id = None - if channel_id is not None: - channel = self.bot.get_channel(channel_id) - if channel is None: - fetch_channel = getattr(self.bot, "fetch_channel", None) - if fetch_channel is not None: - try: - channel = await fetch_channel(channel_id) - except discord.DiscordException: - channel = None - - if not isinstance(channel, discord.TextChannel): - await interaction.response.send_message( - "Scores calculated but the configured league channel is unavailable.", - ephemeral=True, - ) - return - - results_section = format_fixture_results( - score_result.fixture["games"], - score_result.results, - score_result.fixture["week_number"], - ) - message = ( - results_section - + "\n\n" - + format_standings(score_result.standings, score_result.last_fixture) - ) - - try: - await channel.send(message) - await interaction.response.send_message( - f"Week {score_result.fixture['week_number']} results calculated and posted to the league channel!", - ephemeral=True, - ) - except Exception as exc: - logger.error(f"Failed to post results to channel: {exc}") - await interaction.response.send_message( - "Scores calculated but failed to post to channel.", ephemeral=True - ) - admin = app_commands.Group( name="admin", description="Open the admin panel and manage fixtures/results" ) diff --git a/typer_bot/commands/admin_panel/unified_actions.py b/typer_bot/commands/admin_panel/unified_actions.py index 00a1682..2373ba5 100644 --- a/typer_bot/commands/admin_panel/unified_actions.py +++ b/typer_bot/commands/admin_panel/unified_actions.py @@ -6,6 +6,7 @@ import discord +from typer_bot.services import post_calculation_result from typer_bot.utils import format_standings, get_admin_permission_error, has_setup_permission, now from .modals import CreateFixtureModal, EnterResultsModal @@ -366,7 +367,7 @@ async def callback(self, interaction: discord.Interaction): return admin_commands = self.parent_view.admin_commands - if admin_commands is None: + if admin_commands is None or self.parent_view.bot is None: await interaction.response.send_message( "Calculate Scores is unavailable in this context.", ephemeral=True ) @@ -396,8 +397,9 @@ async def callback(self, interaction: discord.Interaction): admin_commands.record_calculate_cooldown( self.parent_view.guild_id, user_id, current_time=current_time ) - await admin_commands._create_backup() - await admin_commands._post_calculation_to_channel(interaction, score_result) + await post_calculation_result( + self.parent_view.bot, self.parent_view.db, interaction, score_result + ) await self._refresh_parent_panel(fixture_id) await self._edit_parent_message(interaction) diff --git a/typer_bot/services/__init__.py b/typer_bot/services/__init__.py index 881a3b0..07658cb 100644 --- a/typer_bot/services/__init__.py +++ b/typer_bot/services/__init__.py @@ -1,6 +1,7 @@ """Shared service-layer helpers.""" from .admin_service import AdminService +from .calculation_posting import post_calculation_result from .errors import ( AdminFlowError, FixtureNotFoundError, @@ -16,4 +17,5 @@ "NoPredictionsSavedError", "PredictionDisappearedError", "PredictionNotFoundError", + "post_calculation_result", ] diff --git a/typer_bot/services/calculation_posting.py b/typer_bot/services/calculation_posting.py new file mode 100644 index 0000000..9278a40 --- /dev/null +++ b/typer_bot/services/calculation_posting.py @@ -0,0 +1,100 @@ +"""Post-calculation publishing helpers.""" + +from __future__ import annotations + +import logging + +import discord +from discord.ext import commands + +from typer_bot.database import Database +from typer_bot.services.admin_service import FixtureScoreResult +from typer_bot.utils import format_fixture_results, format_standings +from typer_bot.utils.config import BACKUP_DIR +from typer_bot.utils.db_backup import cleanup_old_backups, create_backup + +logger = logging.getLogger(__name__) + + +async def post_calculation_result( + bot: commands.Bot | discord.Client, + db: Database, + interaction: discord.Interaction, + score_result: FixtureScoreResult, +) -> None: + """Run best-effort DB backup, then publish fixture results and standings. + + The interaction response must still be unused. Backup failures are logged but + do not fail score calculation; posting failures are reported ephemerally to + the admin. + """ + await _create_backup(bot, db.db_path) + await _post_calculation_to_channel(bot, db, interaction, score_result) + + +async def _create_backup(bot: commands.Bot | discord.Client, db_path: str) -> None: + try: + await bot.loop.run_in_executor(None, lambda: create_backup(db_path, BACKUP_DIR)) + await bot.loop.run_in_executor(None, lambda: cleanup_old_backups(BACKUP_DIR, keep=10)) + except Exception as exc: + logger.warning(f"Backup failed but calculation succeeded: {exc}") + + +async def _post_calculation_to_channel( + bot: commands.Bot | discord.Client, + db: Database, + interaction: discord.Interaction, + score_result: FixtureScoreResult, +) -> None: + if interaction.guild_id is None: + await interaction.response.send_message( + "Scores calculated but could not resolve this server.", ephemeral=True + ) + return + + config = await db.get_guild_config(str(interaction.guild_id)) + channel = None + if config is not None: + try: + channel_id = int(config["league_channel_id"]) + except (TypeError, ValueError): + channel_id = None + if channel_id is not None: + channel = bot.get_channel(channel_id) + if channel is None: + fetch_channel = getattr(bot, "fetch_channel", None) + if fetch_channel is not None: + try: + channel = await fetch_channel(channel_id) + except discord.DiscordException: + channel = None + + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message( + "Scores calculated but the configured league channel is unavailable.", + ephemeral=True, + ) + return + + results_section = format_fixture_results( + score_result.fixture["games"], + score_result.results, + score_result.fixture["week_number"], + ) + message = ( + results_section + + "\n\n" + + format_standings(score_result.standings, score_result.last_fixture) + ) + + try: + await channel.send(message) + await interaction.response.send_message( + f"Week {score_result.fixture['week_number']} results calculated and posted to the league channel!", + ephemeral=True, + ) + except Exception as exc: + logger.error(f"Failed to post results to channel: {exc}") + await interaction.response.send_message( + "Scores calculated but failed to post to channel.", ephemeral=True + ) From a51f208db8d391fc752981e2022ce0f202a0202d Mon Sep 17 00:00:00 2001 From: adrunkhuman <16039109+adrunkhuman@users.noreply.github.com> Date: Sat, 23 May 2026 16:44:10 +0200 Subject: [PATCH 3/3] refactor: expose database repositories --- .../test_fixture_panel_jump_to_week.py | 10 +- .../test_fixture_panel_pending_partials.py | 22 +- .../test_fixture_panel_results_actions.py | 50 ++-- .../test_fixture_panel_scoring_rules.py | 32 +-- .../admin_panel/test_fixture_panel_seasons.py | 10 +- .../test_fixture_panel_selection.py | 40 +-- tests/conftest.py | 14 +- tests/database/conftest.py | 6 +- tests/database/test_fixtures.py | 138 +++++----- tests/database/test_guild_config.py | 18 +- tests/database/test_predictions.py | 72 ++--- tests/database/test_schema_validation.py | 34 +-- tests/database/test_scores.py | 244 +++++++++-------- tests/database/test_seasons.py | 214 +++++++-------- tests/test_admin_commands.py | 16 +- tests/test_admin_panel_command.py | 24 +- tests/test_admin_panel_modals.py | 168 ++++++------ tests/test_admin_panel_predictions.py | 68 ++--- tests/test_admin_panel_results.py | 96 +++---- tests/test_admin_service.py | 174 ++++++------- tests/test_bot.py | 42 +-- tests/test_integration.py | 170 ++++++------ tests/test_seed_test_data.py | 48 ++-- tests/test_thread_prediction_handler.py | 46 ++-- tests/user_commands/test_fixtures_command.py | 10 +- tests/user_commands/test_help_command.py | 2 +- .../test_my_predictions_command.py | 16 +- tests/user_commands/test_predict_command.py | 157 ++++++----- tests/user_commands/test_standings_command.py | 12 +- typer_bot/bot.py | 8 +- typer_bot/commands/admin_commands.py | 6 +- typer_bot/commands/admin_panel/base.py | 4 +- .../commands/admin_panel/fixture_modals.py | 12 +- typer_bot/commands/admin_panel/fixtures.py | 8 +- .../commands/admin_panel/partial_review.py | 4 +- .../admin_panel/prediction_actions.py | 12 +- .../commands/admin_panel/prediction_modals.py | 2 +- typer_bot/commands/admin_panel/predictions.py | 10 +- .../commands/admin_panel/result_modals.py | 2 +- typer_bot/commands/admin_panel/results.py | 10 +- typer_bot/commands/admin_panel/unified.py | 20 +- .../commands/admin_panel/unified_actions.py | 38 ++- typer_bot/commands/user_commands.py | 38 +-- typer_bot/database/connection.py | 246 ++---------------- typer_bot/database/fixtures.py | 8 +- typer_bot/database/predictions.py | 12 +- typer_bot/database/results.py | 4 +- typer_bot/database/scores.py | 2 +- typer_bot/dev/seed_test_data.py | 16 +- .../handlers/thread_prediction_handler.py | 6 +- typer_bot/services/admin_service.py | 84 +++--- typer_bot/services/calculation_posting.py | 2 +- typer_bot/utils/logger.py | 10 +- typer_bot/utils/permissions.py | 6 +- 54 files changed, 1237 insertions(+), 1286 deletions(-) diff --git a/tests/admin_panel/test_fixture_panel_jump_to_week.py b/tests/admin_panel/test_fixture_panel_jump_to_week.py index 26f1859..72773e4 100644 --- a/tests/admin_panel/test_fixture_panel_jump_to_week.py +++ b/tests/admin_panel/test_fixture_panel_jump_to_week.py @@ -20,11 +20,13 @@ async def test_unified_panel_jump_to_week_reaches_older_open_fixture( deadline = datetime.now(UTC) + timedelta(days=1) first_fixture_id = None for week in range(1, 28): - fixture_id = await admin_cog.db.create_fixture("111111", week, sample_games, deadline) + fixture_id = await admin_cog.db.fixtures.create_fixture( + "111111", week, sample_games, deadline + ) if week == 1: first_fixture_id = fixture_id assert first_fixture_id is not None - await admin_cog.db.save_results(first_fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(first_fixture_id, ["1-0", "1-1", "0-0"]) view = UnifiedAdminPanelView( admin_cog.db, @@ -83,8 +85,8 @@ async def test_unified_panel_jump_to_week_rejects_duplicate_open_weeks( sample_games, ): deadline = datetime.now(UTC) + timedelta(days=1) - await admin_cog.db.create_fixture("111111", 5, sample_games, deadline) - await admin_cog.db.create_fixture("111111", 5, sample_games, deadline) + await admin_cog.db.fixtures.create_fixture("111111", 5, sample_games, deadline) + await admin_cog.db.fixtures.create_fixture("111111", 5, sample_games, deadline) view = UnifiedAdminPanelView( admin_cog.db, diff --git a/tests/admin_panel/test_fixture_panel_pending_partials.py b/tests/admin_panel/test_fixture_panel_pending_partials.py index cdd4951..f3650cf 100644 --- a/tests/admin_panel/test_fixture_panel_pending_partials.py +++ b/tests/admin_panel/test_fixture_panel_pending_partials.py @@ -36,10 +36,10 @@ async def test_unified_panel_shows_review_pending_button_when_pending_partials_e mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 55, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -68,10 +68,10 @@ async def test_unified_panel_hides_other_guild_pending_partials( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "guild-2", 55, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -100,11 +100,11 @@ async def test_unified_panel_review_pending_button_jumps_to_pending_submission( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 56, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -141,13 +141,13 @@ async def test_unified_panel_review_pending_button_cycles_pending_submissions( mock_interaction_admin, sample_games, ): - fixture_a = await admin_cog.db.create_fixture( + fixture_a = await admin_cog.db.fixtures.create_fixture( "111111", 57, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture_b = await admin_cog.db.create_fixture( + fixture_b = await admin_cog.db.fixtures.create_fixture( "111111", 58, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_a, "111", "User One", @@ -156,7 +156,7 @@ async def test_unified_panel_review_pending_button_cycles_pending_submissions( predicted_game_indexes=[1, 2], pending_partial_approval=True, ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_b, "222", "User Two", diff --git a/tests/admin_panel/test_fixture_panel_results_actions.py b/tests/admin_panel/test_fixture_panel_results_actions.py index cd365c4..54c370e 100644 --- a/tests/admin_panel/test_fixture_panel_results_actions.py +++ b/tests/admin_panel/test_fixture_panel_results_actions.py @@ -23,7 +23,7 @@ async def test_unified_panel_enter_results_button_opens_modal( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 44, sample_games, datetime.now(UTC) + timedelta(days=1) ) view = UnifiedAdminPanelView( @@ -50,10 +50,10 @@ async def test_unified_panel_hides_enter_results_button_after_results_are_saved( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 46, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = UnifiedAdminPanelView( admin_cog.db, admin_cog.service, @@ -77,11 +77,11 @@ async def test_unified_panel_calculate_scores_button_posts_results( sample_games, monkeypatch, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 45, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -131,11 +131,11 @@ async def test_stale_calculate_scores_button_refreshes_when_fixture_already_scor sample_games, monkeypatch, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 45, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -158,7 +158,7 @@ async def test_stale_calculate_scores_button_refreshes_when_fixture_already_scor view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) stale_button = _get_button(view, "Calculate Scores") - await admin_cog.db.recalculate_fixture_scores(fixture_id) + await admin_cog.db.scores.recalculate_fixture_scores(fixture_id) await stale_button.callback(mock_interaction_admin) @@ -177,10 +177,10 @@ async def test_unified_panel_calculate_scores_button_rejects_active_cooldown( sample_games, monkeypatch, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 47, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) admin_cog.record_calculate_cooldown( "111111", str(mock_interaction_admin.user.id), current_time=now().timestamp() ) @@ -214,10 +214,10 @@ async def test_unified_panel_calculate_scores_button_handles_service_error( sample_games, monkeypatch, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 48, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) post_calculation_result = AsyncMock() monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) view = UnifiedAdminPanelView( @@ -249,10 +249,10 @@ async def test_unified_panel_calculate_scores_button_requires_bot_context( sample_games, monkeypatch, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 49, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) admin_cog.service.calculate_fixture_scores = AsyncMock() post_calculation_result = AsyncMock() monkeypatch.setattr(unified_actions, "post_calculation_result", post_calculation_result) @@ -290,7 +290,7 @@ async def test_unified_panel_post_results_button_opens_confirmation( mock_interaction_admin.channel = command_channel admin_cog.bot.get_channel.return_value = None admin_cog.bot.fetch_channel = AsyncMock(return_value=league_channel) - admin_cog.db.get_last_fixture_scores = AsyncMock( + admin_cog.db.scores.get_last_fixture_scores = AsyncMock( return_value={ "week_number": 1, "games": ["A - B"], @@ -306,7 +306,7 @@ async def test_unified_panel_post_results_button_opens_confirmation( ], } ) - admin_cog.db.get_standings = AsyncMock( + admin_cog.db.scores.get_standings = AsyncMock( return_value=[ { "user_id": "123", @@ -347,13 +347,13 @@ async def test_unified_panel_post_results_only_previews_current_guild_scores( mock_interaction_admin.channel = channel admin_cog.bot.get_channel.return_value = channel deadline = datetime.now(UTC) - timedelta(days=1) - current_fixture_id = await admin_cog.db.create_fixture( + current_fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], deadline ) - other_fixture_id = await admin_cog.db.create_fixture( + other_fixture_id = await admin_cog.db.fixtures.create_fixture( "guild-2", 2, ["Team C - Team D"], deadline ) - await admin_cog.db.save_scores( + await admin_cog.db.scores.save_scores( current_fixture_id, [ { @@ -365,7 +365,7 @@ async def test_unified_panel_post_results_only_previews_current_guild_scores( } ], ) - await admin_cog.db.save_scores( + await admin_cog.db.scores.save_scores( other_fixture_id, [ { @@ -399,8 +399,8 @@ async def test_unified_panel_post_results_button_rejects_unavailable_league_chan admin_cog, mock_interaction_admin, ): - admin_cog.db.get_last_fixture_scores = AsyncMock(return_value={"scores": []}) - admin_cog.db.get_standings = AsyncMock(return_value=[]) + admin_cog.db.scores.get_last_fixture_scores = AsyncMock(return_value={"scores": []}) + admin_cog.db.scores.get_standings = AsyncMock(return_value=[]) admin_cog.bot.get_channel.return_value = None admin_cog.bot.fetch_channel = AsyncMock( side_effect=discord.InvalidData("unknown channel type") @@ -431,7 +431,7 @@ async def test_unified_panel_post_results_button_rejects_missing_scores( channel = MagicMock(spec=discord.TextChannel) channel.id = mock_interaction_admin.channel.id mock_interaction_admin.channel = channel - admin_cog.db.get_last_fixture_scores = AsyncMock(return_value=None) + admin_cog.db.scores.get_last_fixture_scores = AsyncMock(return_value=None) view = UnifiedAdminPanelView( admin_cog.db, diff --git a/tests/admin_panel/test_fixture_panel_scoring_rules.py b/tests/admin_panel/test_fixture_panel_scoring_rules.py index 2372cb5..adb2155 100644 --- a/tests/admin_panel/test_fixture_panel_scoring_rules.py +++ b/tests/admin_panel/test_fixture_panel_scoring_rules.py @@ -61,7 +61,7 @@ async def test_scoring_rules_modal_updates_active_season_rules( await modal.on_submit(mock_interaction_admin) - assert await admin_cog.db.get_active_scoring_rules("111111") == { + assert await admin_cog.db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 5, "correct_outcome_points": 2, "wrong_outcome_points": 1, @@ -86,7 +86,7 @@ async def test_scoring_rules_button_refreshes_modal_defaults( bot=admin_cog.bot, ) await view.load_fixture_options() - await admin_cog.db.update_active_scoring_rules("111111", {"exact_score_points": 5}) + await admin_cog.db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) scoring_button = _get_button(view, "Scoring Rules") await scoring_button.callback(mock_interaction_admin) @@ -115,18 +115,18 @@ async def test_scoring_rules_modal_rejects_stale_season_submit( modal.outcome_input._value = "2" modal.wrong_input._value = "1" modal.late_input._value = "1" - await admin_cog.db.start_new_season("111111", "Next Season") + await admin_cog.db.seasons.start_new_season("111111", "Next Season") await modal.on_submit(mock_interaction_admin) assert "active season changed" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_active_scoring_rules("111111") == { + assert await admin_cog.db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 3, "correct_outcome_points": 1, "wrong_outcome_points": 0, "late_prediction_points": 0, } - seasons = await admin_cog.db.get_seasons("111111") + seasons = await admin_cog.db.seasons.get_seasons("111111") assert seasons[0]["status"] == "archived" assert seasons[0]["scoring_rules"] == old_season_rules @@ -159,7 +159,7 @@ async def test_scoring_rules_modal_rejects_non_owner( await modal.on_submit(outsider) assert "permission" in outsider.response_sent[-1]["content"] - assert await admin_cog.db.get_active_scoring_rules("111111") == { + assert await admin_cog.db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 3, "correct_outcome_points": 1, "wrong_outcome_points": 0, @@ -192,7 +192,7 @@ async def test_scoring_rules_modal_rechecks_admin_permission( await modal.on_submit(mock_interaction_admin) assert "no longer have permission" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_active_scoring_rules("111111") == { + assert await admin_cog.db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 3, "correct_outcome_points": 1, "wrong_outcome_points": 0, @@ -206,11 +206,11 @@ async def test_scoring_rules_modal_blocks_changes_after_scores_exist( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-0"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-0"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -233,12 +233,12 @@ async def test_scoring_rules_modal_blocks_changes_after_scores_exist( modal.outcome_input._value = "2" modal.wrong_input._value = "1" modal.late_input._value = "1" - await admin_cog.db.recalculate_fixture_scores(fixture_id) + await admin_cog.db.scores.recalculate_fixture_scores(fixture_id) await modal.on_submit(mock_interaction_admin) assert "Cannot change scoring rules" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_active_scoring_rules("111111") == { + assert await admin_cog.db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 3, "correct_outcome_points": 1, "wrong_outcome_points": 0, @@ -277,11 +277,11 @@ async def test_stale_scoring_rules_button_refreshes_when_scores_now_exist( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 45, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -298,7 +298,7 @@ async def test_stale_scoring_rules_button_refreshes_when_scores_now_exist( ) await view.load_fixture_options() stale_button = _get_button(view, "Scoring Rules") - await admin_cog.db.recalculate_fixture_scores(fixture_id) + await admin_cog.db.scores.recalculate_fixture_scores(fixture_id) await stale_button.callback(mock_interaction_admin) diff --git a/tests/admin_panel/test_fixture_panel_seasons.py b/tests/admin_panel/test_fixture_panel_seasons.py index 6354df4..4921acc 100644 --- a/tests/admin_panel/test_fixture_panel_seasons.py +++ b/tests/admin_panel/test_fixture_panel_seasons.py @@ -59,7 +59,7 @@ async def test_new_season_modal_blocks_open_fixtures( mock_interaction_admin, sample_games, ): - await admin_cog.db.create_fixture( + await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) view = UnifiedAdminPanelView( @@ -79,7 +79,7 @@ async def test_new_season_modal_blocks_open_fixtures( await modal.on_submit(mock_interaction_admin) assert "Close all open fixtures" in mock_interaction_admin.response_sent[-1]["content"] - assert (await admin_cog.db.get_active_season("111111"))["name"] == "Default Season" + assert (await admin_cog.db.seasons.get_active_season("111111"))["name"] == "Default Season" @pytest.mark.asyncio async def test_new_season_modal_starts_season_and_refreshes_panel( @@ -88,10 +88,10 @@ async def test_new_season_modal_starts_season_and_refreshes_panel( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_scores( + await admin_cog.db.scores.save_scores( fixture_id, [ { @@ -118,7 +118,7 @@ async def test_new_season_modal_starts_season_and_refreshes_panel( modal.name_input._value = "2026/27" await modal.on_submit(mock_interaction_admin) - _new_fixture_id, new_week = await admin_cog.db.create_next_fixture( + _new_fixture_id, new_week = await admin_cog.db.fixtures.create_next_fixture( "111111", sample_games, datetime.now(UTC) + timedelta(days=1) ) diff --git a/tests/admin_panel/test_fixture_panel_selection.py b/tests/admin_panel/test_fixture_panel_selection.py index ec145e2..5f22665 100644 --- a/tests/admin_panel/test_fixture_panel_selection.py +++ b/tests/admin_panel/test_fixture_panel_selection.py @@ -1,5 +1,5 @@ from datetime import UTC, datetime, timedelta -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest @@ -24,7 +24,7 @@ async def test_fixture_button_populates_open_fixture_options( mock_interaction_admin, sample_games, ): - await admin_cog.db.create_fixture( + await admin_cog.db.fixtures.create_fixture( "111111", 4, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -54,10 +54,10 @@ async def test_admin_fixture_selectors_only_show_current_guild( sample_games, ): deadline = datetime.now(UTC) + timedelta(days=1) - current_guild_fixture_id = await admin_cog.db.create_fixture( + current_guild_fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, deadline ) - other_guild_fixture_id = await admin_cog.db.create_fixture( + other_guild_fixture_id = await admin_cog.db.fixtures.create_fixture( "guild-2", 2, sample_games, deadline ) @@ -80,7 +80,7 @@ async def test_fixture_panel_delete_button_enables_after_fixture_selection( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 5, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -109,7 +109,7 @@ async def test_fixture_panel_delete_confirmation_shows_games( sample_games, ): """Deletion confirmation must show game list so admin can verify the right fixture.""" - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 6, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -142,7 +142,7 @@ async def test_unified_panel_hides_contextual_actions_until_fixture_selection( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) view = UnifiedAdminPanelView( @@ -166,7 +166,7 @@ async def test_unified_panel_hides_contextual_actions_until_fixture_selection( assert _has_button(view, "Correct Results") is False assert _has_button(view, "Delete Fixture") is True - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) @@ -174,7 +174,7 @@ async def test_unified_panel_hides_contextual_actions_until_fixture_selection( assert _has_button(view, "Calculate Scores") is True assert _has_button(view, "Correct Results") is True - await admin_cog.db.save_scores( + await admin_cog.db.scores.save_scores( fixture_id, [ { @@ -202,18 +202,24 @@ async def test_fixture_panel_delete_confirm_shows_error_on_db_failure( sample_games, ): """Silent DB failures surface as a visible error instead of timing out the interaction.""" - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 7, sample_games, datetime.now(UTC) + timedelta(days=1) ) db_mock = AsyncMock(spec=Database) - db_mock.get_guild_config.return_value = { - "admin_role_id": str( - mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id).roles[0].id - ), - "league_channel_id": "123456", - } - db_mock.delete_fixture.side_effect = RuntimeError("DB locked") + db_mock.guild_config = MagicMock() + db_mock.guild_config.get_guild_config = AsyncMock( + return_value={ + "admin_role_id": str( + mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id) + .roles[0] + .id + ), + "league_channel_id": "123456", + } + ) + db_mock.fixtures = MagicMock() + db_mock.fixtures.delete_fixture = AsyncMock(side_effect=RuntimeError("DB locked")) confirm_view = DeleteConfirmView( db_mock, diff --git a/tests/conftest.py b/tests/conftest.py index 0242ac4..0915544 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ def mock_bot(): async def database(temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.upsert_guild_config("111111", str(MockRole("admin").id), "123456") + await db.guild_config.upsert_guild_config("111111", str(MockRole("admin").id), "123456") yield db @@ -209,9 +209,11 @@ def sample_predictions(): @pytest.fixture async def fixture_with_thread(database, sample_games): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) - await database.update_fixture_announcement(fixture_id, message_id="789012", channel_id="123456") - fixture = await database.get_fixture_by_id(fixture_id, "111111") + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.update_fixture_announcement( + fixture_id, message_id="789012", channel_id="123456" + ) + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "111111") yield fixture @@ -219,8 +221,8 @@ async def fixture_with_thread(database, sample_games): async def fixture_with_dm(database, sample_games): """Fixture for DM prediction tests (no thread needed).""" deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) - fixture = await database.get_fixture_by_id(fixture_id, "111111") + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "111111") yield fixture diff --git a/tests/database/conftest.py b/tests/database/conftest.py index 844ee66..ff690fa 100644 --- a/tests/database/conftest.py +++ b/tests/database/conftest.py @@ -26,13 +26,15 @@ async def prediction_db(temp_db_path): @pytest.fixture async def open_fixture_id(prediction_db): deadline = datetime.now(UTC) + timedelta(hours=1) - return await prediction_db.create_fixture("111111", 1, ["A - B", "C - D"], deadline) + return await prediction_db.fixtures.create_fixture("111111", 1, ["A - B", "C - D"], deadline) @pytest.fixture async def closed_fixture_id(prediction_db): deadline = datetime.now(UTC) + timedelta(hours=1) - fixture_id = await prediction_db.create_fixture("111111", 2, ["A - B", "C - D"], deadline) + fixture_id = await prediction_db.fixtures.create_fixture( + "111111", 2, ["A - B", "C - D"], deadline + ) async with aiosqlite.connect(prediction_db.db_path) as conn: await conn.execute("UPDATE fixtures SET status = 'closed' WHERE id = ?", (fixture_id,)) await conn.commit() diff --git a/tests/database/test_fixtures.py b/tests/database/test_fixtures.py index 310d476..da54c40 100644 --- a/tests/database/test_fixtures.py +++ b/tests/database/test_fixtures.py @@ -14,7 +14,7 @@ async def test_get_max_week_number_empty_db(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - result = await db.get_max_week_number("111111") + result = await db.fixtures.get_max_week_number("111111") assert result == 0 @pytest.mark.asyncio @@ -22,11 +22,11 @@ async def test_get_max_week_number_with_fixtures(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) - await db.create_fixture("111111", 3, ["Team C - Team D"], datetime.now(UTC)) - await db.create_fixture("111111", 5, ["Team E - Team F"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 3, ["Team C - Team D"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 5, ["Team E - Team F"], datetime.now(UTC)) - result = await db.get_max_week_number("111111") + result = await db.fixtures.get_max_week_number("111111") assert result == 5 @pytest.mark.asyncio @@ -34,8 +34,10 @@ async def test_get_max_week_number_closed_fixtures(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 10, ["Team A - Team B"], datetime.now(UTC)) - await db.save_scores( + fixture_id = await db.fixtures.create_fixture( + "111111", 10, ["Team A - Team B"], datetime.now(UTC) + ) + await db.scores.save_scores( fixture_id, [ { @@ -48,23 +50,23 @@ async def test_get_max_week_number_closed_fixtures(self, temp_db_path): ], ) - await db.create_fixture("111111", 5, ["Team C - Team D"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 5, ["Team C - Team D"], datetime.now(UTC)) - result = await db.get_max_week_number("111111") + result = await db.fixtures.get_max_week_number("111111") assert result == 10 @pytest.mark.asyncio async def test_get_max_week_number_is_active_season_scoped(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.create_fixture("111111", 10, ["Team A - Team B"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 10, ["Team A - Team B"], datetime.now(UTC)) await start_new_active_season(temp_db_path, "111111") - assert await db.get_max_week_number("111111") == 0 + assert await db.fixtures.get_max_week_number("111111") == 0 - await db.create_fixture("111111", 1, ["Team C - Team D"], datetime.now(UTC)) + await db.fixtures.create_fixture("111111", 1, ["Team C - Team D"], datetime.now(UTC)) - assert await db.get_max_week_number("111111") == 1 + assert await db.fixtures.get_max_week_number("111111") == 1 class TestOpenFixturesQueries: @@ -74,22 +76,22 @@ async def test_get_open_fixtures_returns_all_open_ordered(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_week_2 = await db.create_fixture( + fixture_week_2 = await db.fixtures.create_fixture( "111111", 2, ["Team C - Team D"], datetime.now(UTC) ) - fixture_week_1 = await db.create_fixture( + fixture_week_1 = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - fixture_week_3 = await db.create_fixture( + fixture_week_3 = await db.fixtures.create_fixture( "111111", 3, ["Team E - Team F"], datetime.now(UTC) ) # Close week 3 fixture so only weeks 1 and 2 remain open - await db.save_scores(fixture_week_3, []) + await db.scores.save_scores(fixture_week_3, []) - await db.create_fixture("guild-2", 1, ["Other A - Other B"], datetime.now(UTC)) + await db.fixtures.create_fixture("guild-2", 1, ["Other A - Other B"], datetime.now(UTC)) - open_fixtures = await db.get_open_fixtures("111111") + open_fixtures = await db.fixtures.get_open_fixtures("111111") open_ids = [fixture["id"] for fixture in open_fixtures] open_weeks = [fixture["week_number"] for fixture in open_fixtures] @@ -102,16 +104,16 @@ async def test_get_open_fixture_by_week_ignores_closed_fixtures(self, temp_db_pa db = Database(temp_db_path) await db.initialize() - open_fixture_id = await db.create_fixture( + open_fixture_id = await db.fixtures.create_fixture( "111111", 7, ["Team A - Team B"], datetime.now(UTC) ) - closed_fixture_id = await db.create_fixture( + closed_fixture_id = await db.fixtures.create_fixture( "111111", 8, ["Team C - Team D"], datetime.now(UTC) ) - await db.save_scores(closed_fixture_id, []) + await db.scores.save_scores(closed_fixture_id, []) - open_fixture = await db.get_open_fixture_by_week("111111", 7) - closed_fixture = await db.get_open_fixture_by_week("111111", 8) + open_fixture = await db.fixtures.get_open_fixture_by_week("111111", 7) + closed_fixture = await db.fixtures.get_open_fixture_by_week("111111", 8) assert open_fixture is not None assert open_fixture["id"] == open_fixture_id @@ -122,55 +124,59 @@ async def test_week_and_recent_fixture_queries_are_guild_scoped(self, temp_db_pa db = Database(temp_db_path) await db.initialize() - guild_one_week = await db.create_fixture( + guild_one_week = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - guild_two_week = await db.create_fixture( + guild_two_week = await db.fixtures.create_fixture( "guild-2", 1, ["Team C - Team D"], datetime.now(UTC) ) - assert (await db.get_fixture_by_week("111111", 1))["id"] == guild_one_week - assert (await db.get_fixture_by_week("guild-2", 1))["id"] == guild_two_week - assert [fixture["id"] for fixture in await db.get_recent_fixtures("111111")] == [ + assert (await db.fixtures.get_fixture_by_week("111111", 1))["id"] == guild_one_week + assert (await db.fixtures.get_fixture_by_week("guild-2", 1))["id"] == guild_two_week + assert [fixture["id"] for fixture in await db.fixtures.get_recent_fixtures("111111")] == [ guild_one_week ] - assert await db.get_fixture_by_id(guild_two_week, "111111") is None + assert await db.fixtures.get_fixture_by_id(guild_two_week, "111111") is None @pytest.mark.asyncio async def test_delete_fixture_can_require_guild_ownership(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("guild-2", 1, ["Team A - Team B"], datetime.now(UTC)) + fixture_id = await db.fixtures.create_fixture( + "guild-2", 1, ["Team A - Team B"], datetime.now(UTC) + ) - assert await db.delete_fixture(fixture_id, "111111") is False - assert await db.get_fixture_by_id(fixture_id, "guild-2") is not None + assert await db.fixtures.delete_fixture(fixture_id, "111111") is False + assert await db.fixtures.get_fixture_by_id(fixture_id, "guild-2") is not None - assert await db.delete_fixture(fixture_id, "guild-2") is True - assert await db.get_fixture_by_id(fixture_id, "guild-2") is None + assert await db.fixtures.delete_fixture(fixture_id, "guild-2") is True + assert await db.fixtures.get_fixture_by_id(fixture_id, "guild-2") is None @pytest.mark.asyncio async def test_get_prediction_requires_fixture_guild_ownership(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("guild-2", 1, ["Team A - Team B"], datetime.now(UTC)) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + fixture_id = await db.fixtures.create_fixture( + "guild-2", 1, ["Team A - Team B"], datetime.now(UTC) + ) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - assert await db.get_prediction(fixture_id, "user-1", "111111") is None - assert await db.get_prediction(fixture_id, "user-1", "guild-2") is not None + assert await db.predictions.get_prediction(fixture_id, "user-1", "111111") is None + assert await db.predictions.get_prediction(fixture_id, "user-1", "guild-2") is not None @pytest.mark.asyncio async def test_pending_partial_predictions_are_guild_scoped(self, temp_db_path): db = Database(temp_db_path) await db.initialize() deadline = datetime.now(UTC) - timedelta(hours=1) - guild_one_fixture_id = await db.create_fixture( + guild_one_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B", "Team C - Team D"], deadline ) - guild_two_fixture_id = await db.create_fixture( + guild_two_fixture_id = await db.fixtures.create_fixture( "guild-2", 1, ["Team E - Team F", "Team G - Team H"], deadline ) - await db.save_prediction( + await db.predictions.save_prediction( guild_one_fixture_id, "guild-one-user", "Guild One", @@ -179,7 +185,7 @@ async def test_pending_partial_predictions_are_guild_scoped(self, temp_db_path): predicted_game_indexes=[0], pending_partial_approval=True, ) - await db.save_prediction( + await db.predictions.save_prediction( guild_two_fixture_id, "guild-two-user", "Guild Two", @@ -189,8 +195,8 @@ async def test_pending_partial_predictions_are_guild_scoped(self, temp_db_path): pending_partial_approval=True, ) - guild_one_pending = await db.get_pending_partial_predictions("111111") - guild_two_pending = await db.get_pending_partial_predictions("guild-2") + guild_one_pending = await db.predictions.get_pending_partial_predictions("111111") + guild_two_pending = await db.predictions.get_pending_partial_predictions("guild-2") assert [prediction["user_id"] for prediction in guild_one_pending] == ["guild-one-user"] assert [prediction["user_id"] for prediction in guild_two_pending] == ["guild-two-user"] @@ -201,19 +207,19 @@ async def test_create_next_fixture_allocates_incrementing_weeks(self, temp_db_pa db = Database(temp_db_path) await db.initialize() - fixture_one_id, week_one = await db.create_next_fixture( + fixture_one_id, week_one = await db.fixtures.create_next_fixture( "111111", ["Team A - Team B"], datetime.now(UTC), ) - fixture_two_id, week_two = await db.create_next_fixture( + fixture_two_id, week_two = await db.fixtures.create_next_fixture( "111111", ["Team C - Team D"], datetime.now(UTC), ) - fixture_one = await db.get_fixture_by_id(fixture_one_id, "111111") - fixture_two = await db.get_fixture_by_id(fixture_two_id, "111111") + fixture_one = await db.fixtures.get_fixture_by_id(fixture_one_id, "111111") + fixture_two = await db.fixtures.get_fixture_by_id(fixture_two_id, "111111") assert week_one == 1 assert week_two == 2 @@ -227,14 +233,14 @@ async def test_created_fixtures_store_guild_ownership(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture( + fixture_id = await db.fixtures.create_fixture( "guild-2", 4, ["Team A - Team B"], datetime.now(UTC), ) - fixture = await db.get_fixture_by_id(fixture_id, "guild-2") + fixture = await db.fixtures.get_fixture_by_id(fixture_id, "guild-2") assert fixture is not None assert fixture["guild_id"] == "guild-2" @@ -244,7 +250,7 @@ async def test_create_fixture_rejects_missing_guild_id(self, temp_db_path): await db.initialize() with pytest.raises(ValueError, match="guild_id is required"): - await db.create_fixture("", 1, ["Team A - Team B"], datetime.now(UTC)) + await db.fixtures.create_fixture("", 1, ["Team A - Team B"], datetime.now(UTC)) @pytest.mark.asyncio async def test_create_next_fixture_rejects_missing_guild_id(self, temp_db_path): @@ -252,30 +258,30 @@ async def test_create_next_fixture_rejects_missing_guild_id(self, temp_db_path): await db.initialize() with pytest.raises(ValueError, match="guild_id is required"): - await db.create_next_fixture("", ["Team A - Team B"], datetime.now(UTC)) + await db.fixtures.create_next_fixture("", ["Team A - Team B"], datetime.now(UTC)) @pytest.mark.asyncio async def test_create_next_fixture_allocates_weeks_per_guild(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - _, guild_one_week = await db.create_next_fixture( + _, guild_one_week = await db.fixtures.create_next_fixture( "111111", ["Team A - Team B"], datetime.now(UTC), ) - _, guild_two_week = await db.create_next_fixture( + _, guild_two_week = await db.fixtures.create_next_fixture( "guild-2", ["Team C - Team D"], datetime.now(UTC), ) - guild_one_second_id = await db.create_fixture( + guild_one_second_id = await db.fixtures.create_fixture( "111111", 2, ["Team E - Team F"], datetime.now(UTC), ) - await db.create_fixture( + await db.fixtures.create_fixture( "guild-2", 9, ["Team G - Team H"], @@ -284,10 +290,10 @@ async def test_create_next_fixture_allocates_weeks_per_guild(self, temp_db_path) assert guild_one_week == 1 assert guild_two_week == 1 - assert await db.get_max_week_number("111111") == 2 - assert await db.get_max_week_number("guild-2") == 9 + assert await db.fixtures.get_max_week_number("111111") == 2 + assert await db.fixtures.get_max_week_number("guild-2") == 9 - guild_one_second = await db.get_fixture_by_id(guild_one_second_id, "111111") + guild_one_second = await db.fixtures.get_fixture_by_id(guild_one_second_id, "111111") assert guild_one_second is not None assert guild_one_second["guild_id"] == "111111" @@ -299,8 +305,8 @@ async def test_concurrent_calls_allocate_distinct_week_numbers(self, temp_db_pat await db.initialize() created = await asyncio.gather( - db.create_next_fixture("111111", ["A - B"], datetime.now(UTC)), - db.create_next_fixture("111111", ["C - D"], datetime.now(UTC)), + db.fixtures.create_next_fixture("111111", ["A - B"], datetime.now(UTC)), + db.fixtures.create_next_fixture("111111", ["C - D"], datetime.now(UTC)), ) fixture_ids = [fixture_id for fixture_id, _week in created] @@ -308,7 +314,9 @@ async def test_concurrent_calls_allocate_distinct_week_numbers(self, temp_db_pat assert weeks == [1, 2] - fixtures = [await db.get_fixture_by_id(fixture_id, "111111") for fixture_id in fixture_ids] + fixtures = [ + await db.fixtures.get_fixture_by_id(fixture_id, "111111") for fixture_id in fixture_ids + ] assert all(fixture is not None for fixture in fixtures) assert sorted(fixture["week_number"] for fixture in fixtures if fixture is not None) == [ 1, @@ -322,7 +330,7 @@ async def test_empty_games_column_returns_empty_list(self, temp_db_path): """Empty games column must deserialize to [] not [''] (split artefact).""" db = Database(temp_db_path) await db.initialize() - season = await db.get_or_create_active_season("111111") + season = await db.seasons.get_or_create_active_season("111111") async with aiosqlite.connect(temp_db_path) as conn: await conn.execute( @@ -331,6 +339,6 @@ async def test_empty_games_column_returns_empty_list(self, temp_db_path): ) await conn.commit() - fixture = await db.get_current_fixture("111111") + fixture = await db.fixtures.get_current_fixture("111111") assert fixture is not None assert fixture["games"] == [] diff --git a/tests/database/test_guild_config.py b/tests/database/test_guild_config.py index 66d40b0..7be5cf1 100644 --- a/tests/database/test_guild_config.py +++ b/tests/database/test_guild_config.py @@ -9,15 +9,15 @@ async def test_guild_config_persists_and_updates(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - assert await db.get_guild_config("111111") is None + assert await db.guild_config.get_guild_config("111111") is None - await db.upsert_guild_config("111111", "role-1", "channel-1") - config = await db.get_guild_config("111111") + await db.guild_config.upsert_guild_config("111111", "role-1", "channel-1") + config = await db.guild_config.get_guild_config("111111") assert config["admin_role_id"] == "role-1" assert config["league_channel_id"] == "channel-1" - await db.upsert_guild_config("111111", "role-2", "channel-2") - updated = await db.get_guild_config("111111") + await db.guild_config.upsert_guild_config("111111", "role-2", "channel-2") + updated = await db.guild_config.get_guild_config("111111") assert updated["admin_role_id"] == "role-2" assert updated["league_channel_id"] == "channel-2" @@ -26,10 +26,10 @@ async def test_guild_config_is_per_guild(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.upsert_guild_config("111111", "role-1", "channel-1") - await db.upsert_guild_config("222222", "role-2", "channel-2") + await db.guild_config.upsert_guild_config("111111", "role-1", "channel-1") + await db.guild_config.upsert_guild_config("222222", "role-2", "channel-2") - guild_one = await db.get_guild_config("111111") - guild_two = await db.get_guild_config("222222") + guild_one = await db.guild_config.get_guild_config("111111") + guild_two = await db.guild_config.get_guild_config("222222") assert guild_one["admin_role_id"] == "role-1" assert guild_two["admin_role_id"] == "role-2" diff --git a/tests/database/test_predictions.py b/tests/database/test_predictions.py index 0261c96..94b1237 100644 --- a/tests/database/test_predictions.py +++ b/tests/database/test_predictions.py @@ -15,35 +15,41 @@ class TestTrySavePrediction: async def test_saved_when_fixture_open_and_no_prior_prediction( self, prediction_db, open_fixture_id ): - result = await prediction_db.try_save_prediction( + result = await prediction_db.predictions.try_save_prediction( open_fixture_id, "u1", "User", ["2-1", "0-0"] ) assert result == SaveResult.SAVED - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction is not None assert prediction["predictions"] == ["2-1", "0-0"] @pytest.mark.asyncio async def test_duplicate_when_prior_prediction_exists(self, prediction_db, open_fixture_id): - await prediction_db.try_save_prediction(open_fixture_id, "u1", "User", ["2-1", "0-0"]) - result = await prediction_db.try_save_prediction( + await prediction_db.predictions.try_save_prediction( + open_fixture_id, "u1", "User", ["2-1", "0-0"] + ) + result = await prediction_db.predictions.try_save_prediction( open_fixture_id, "u1", "User", ["3-0", "1-1"] ) assert result == SaveResult.DUPLICATE - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction["predictions"] == ["2-1", "0-0"] @pytest.mark.asyncio async def test_fixture_closed_returns_fixture_closed(self, prediction_db, closed_fixture_id): - result = await prediction_db.try_save_prediction( + result = await prediction_db.predictions.try_save_prediction( closed_fixture_id, "u1", "User", ["2-1", "0-0"] ) assert result == SaveResult.FIXTURE_CLOSED @pytest.mark.asyncio async def test_no_row_written_on_fixture_closed(self, prediction_db, closed_fixture_id): - await prediction_db.try_save_prediction(closed_fixture_id, "u1", "User", ["2-1", "0-0"]) - prediction = await prediction_db.get_prediction(closed_fixture_id, "u1", "111111") + await prediction_db.predictions.try_save_prediction( + closed_fixture_id, "u1", "User", ["2-1", "0-0"] + ) + prediction = await prediction_db.predictions.get_prediction( + closed_fixture_id, "u1", "111111" + ) assert prediction is None @pytest.mark.asyncio @@ -54,7 +60,7 @@ async def test_fixture_closed_checked_before_duplicate(self, prediction_db, clos (closed_fixture_id,), ) await conn.commit() - result = await prediction_db.try_save_prediction( + result = await prediction_db.predictions.try_save_prediction( closed_fixture_id, "u1", "User", ["3-0", "1-1"] ) assert result == SaveResult.FIXTURE_CLOSED @@ -64,7 +70,7 @@ async def test_concurrent_writers_allow_only_one_prediction( self, prediction_db, open_fixture_id ): async def save(user_name, predictions): - return await prediction_db.try_save_prediction( + return await prediction_db.predictions.try_save_prediction( open_fixture_id, "u1", user_name, @@ -102,7 +108,7 @@ class TestPredictionSaveMetadata: async def test_save_paths_preserve_non_default_metadata( self, prediction_db, open_fixture_id, method_name ): - save = getattr(prediction_db, method_name) + save = getattr(prediction_db.predictions, method_name) result = await save( open_fixture_id, @@ -118,7 +124,7 @@ async def test_save_paths_preserve_non_default_metadata( if method_name != "save_prediction": assert result == SaveResult.SAVED - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction is not None assert prediction["user_name"] == "User" assert prediction["predictions"] == ["1-1"] @@ -134,52 +140,56 @@ class TestSavePredictionGuarded: @pytest.mark.asyncio async def test_saved_when_fixture_open(self, prediction_db, open_fixture_id): - result = await prediction_db.save_prediction_guarded( + result = await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "User", ["2-1", "0-0"] ) assert result == SaveResult.SAVED - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction["predictions"] == ["2-1", "0-0"] @pytest.mark.asyncio async def test_fixture_closed_blocks_write(self, prediction_db, closed_fixture_id): - result = await prediction_db.save_prediction_guarded( + result = await prediction_db.predictions.save_prediction_guarded( closed_fixture_id, "u1", "User", ["2-1", "0-0"] ) assert result == SaveResult.FIXTURE_CLOSED - prediction = await prediction_db.get_prediction(closed_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction( + closed_fixture_id, "u1", "111111" + ) assert prediction is None @pytest.mark.asyncio async def test_allows_overwrite_of_existing_prediction(self, prediction_db, open_fixture_id): - await prediction_db.save_prediction_guarded(open_fixture_id, "u1", "User", ["2-1", "0-0"]) - result = await prediction_db.save_prediction_guarded( + await prediction_db.predictions.save_prediction_guarded( + open_fixture_id, "u1", "User", ["2-1", "0-0"] + ) + result = await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "User", ["3-0", "1-1"] ) assert result == SaveResult.SAVED - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction["predictions"] == ["3-0", "1-1"] @pytest.mark.asyncio async def test_updates_user_name_on_resubmission(self, prediction_db, open_fixture_id): - await prediction_db.save_prediction_guarded( + await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "OldName", ["2-1", "0-0"] ) - await prediction_db.save_prediction_guarded( + await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "NewName", ["3-0", "1-1"] ) - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction["user_name"] == "NewName" @pytest.mark.asyncio async def test_resubmission_clears_admin_and_waiver_metadata( self, prediction_db, open_fixture_id ): - await prediction_db.save_prediction_guarded( + await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "User", ["2-1", "0-0"], True ) - await prediction_db.set_late_penalty_waiver(open_fixture_id, "u1", True) - await prediction_db.admin_update_prediction( + await prediction_db.predictions.set_late_penalty_waiver(open_fixture_id, "u1", True) + await prediction_db.predictions.admin_update_prediction( open_fixture_id, "u1", ["2-0", "1-1"], "admin-1" ) async with aiosqlite.connect(prediction_db.db_path) as conn: @@ -189,12 +199,12 @@ async def test_resubmission_clears_admin_and_waiver_metadata( ) await conn.commit() - result = await prediction_db.save_prediction_guarded( + result = await prediction_db.predictions.save_prediction_guarded( open_fixture_id, "u1", "User", ["3-0", "1-1"], False ) assert result == SaveResult.SAVED - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert prediction is not None assert prediction["late_penalty_waived"] == 0 assert prediction["admin_edited_at"] is None @@ -207,7 +217,7 @@ class TestPartialApprovalPending: async def test_clearing_pending_does_not_clear_late_status( self, prediction_db, open_fixture_id ): - await prediction_db.save_prediction( + await prediction_db.predictions.save_prediction( open_fixture_id, "u1", "User", @@ -218,7 +228,7 @@ async def test_clearing_pending_does_not_clear_late_status( predictions = PredictionRepository(prediction_db.db_path) updated = await predictions.set_partial_approval_pending(open_fixture_id, "u1", False) - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert updated is True assert prediction is not None @@ -227,7 +237,7 @@ async def test_clearing_pending_does_not_clear_late_status( @pytest.mark.asyncio async def test_setting_pending_does_not_force_late_status(self, prediction_db, open_fixture_id): - await prediction_db.save_prediction( + await prediction_db.predictions.save_prediction( open_fixture_id, "u1", "User", @@ -238,7 +248,7 @@ async def test_setting_pending_does_not_force_late_status(self, prediction_db, o predictions = PredictionRepository(prediction_db.db_path) updated = await predictions.set_partial_approval_pending(open_fixture_id, "u1", True) - prediction = await prediction_db.get_prediction(open_fixture_id, "u1", "111111") + prediction = await prediction_db.predictions.get_prediction(open_fixture_id, "u1", "111111") assert updated is True assert prediction is not None diff --git a/tests/database/test_schema_validation.py b/tests/database/test_schema_validation.py index fb7de5a..70f6da4 100644 --- a/tests/database/test_schema_validation.py +++ b/tests/database/test_schema_validation.py @@ -11,8 +11,8 @@ class TestSchemaValidation: async def test_initialize_is_safe_for_current_schema_existing_data(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_prediction( + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -20,15 +20,15 @@ async def test_initialize_is_safe_for_current_schema_existing_data(self, temp_db public_message_id="message-1", public_message_kind="thread_prediction", ) - await db.save_results(fixture_id, ["2-1"]) + await db.results.save_results(fixture_id, ["2-1"]) restarted_db = Database(temp_db_path) await restarted_db.initialize() - await restarted_db.save_results(fixture_id, ["3-1"]) + await restarted_db.results.save_results(fixture_id, ["3-1"]) - fixture = await restarted_db.get_fixture_by_id(fixture_id, "111111") - prediction = await restarted_db.get_prediction(fixture_id, "user-1", "111111") - results = await restarted_db.get_results(fixture_id) + fixture = await restarted_db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await restarted_db.predictions.get_prediction(fixture_id, "user-1", "111111") + results = await restarted_db.results.get_results(fixture_id) assert fixture is not None assert fixture["guild_id"] == "111111" @@ -144,16 +144,16 @@ async def test_initialize_creates_missing_result_unique_index_for_current_schema ): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["1-0"]) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["1-0"]) async with aiosqlite.connect(temp_db_path) as conn: await conn.execute("DROP INDEX idx_results_fixture_id_unique") await conn.commit() await db.initialize() - await db.save_results(fixture_id, ["2-0"]) + await db.results.save_results(fixture_id, ["2-0"]) - assert await db.get_results(fixture_id) == ["2-0"] + assert await db.results.get_results(fixture_id) == ["2-0"] async with ( aiosqlite.connect(temp_db_path) as conn, conn.execute( @@ -350,7 +350,7 @@ async def test_initialize_rejects_existing_schema_with_missing_table(self, temp_ async def test_initialize_rejects_blank_fixture_guild_ownership(self, temp_db_path, guild_id): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) async with aiosqlite.connect(temp_db_path) as conn: await conn.execute( "UPDATE fixtures SET guild_id = ? WHERE id = ?", (guild_id, fixture_id) @@ -402,7 +402,7 @@ async def test_initialize_rejects_null_fixture_guild_ownership(self, temp_db_pat async def test_initialize_rejects_fixture_without_valid_season(self, temp_db_path, sql): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) async with aiosqlite.connect(temp_db_path) as conn: await conn.execute(sql, (fixture_id,)) await conn.commit() @@ -414,9 +414,11 @@ async def test_initialize_rejects_fixture_without_valid_season(self, temp_db_pat async def test_initialize_rejects_fixture_with_other_guild_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - other_fixture_id = await db.create_fixture("222222", 1, ["C - D"], datetime.now(UTC)) - other_fixture = await db.get_fixture_by_id(other_fixture_id, "222222") + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + other_fixture_id = await db.fixtures.create_fixture( + "222222", 1, ["C - D"], datetime.now(UTC) + ) + other_fixture = await db.fixtures.get_fixture_by_id(other_fixture_id, "222222") async with aiosqlite.connect(temp_db_path) as conn: await conn.execute( "UPDATE fixtures SET season_id = ? WHERE id = ?", diff --git a/tests/database/test_scores.py b/tests/database/test_scores.py index e678d40..7d105fe 100644 --- a/tests/database/test_scores.py +++ b/tests/database/test_scores.py @@ -13,36 +13,36 @@ class TestScores: async def test_result_correction_recalculates_with_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules( + await db.seasons.update_active_scoring_rules( "111111", {"exact_score_points": 5, "correct_outcome_points": 2} ) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.recalculate_fixture_scores(fixture_id) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.scores.recalculate_fixture_scores(fixture_id) - await db.save_results_with_recalc(fixture_id, ["2-0"]) + await db.results.save_results_with_recalc(fixture_id, ["2-0"]) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert scores[0]["points"] == 2 @pytest.mark.asyncio async def test_prediction_replacement_recalculates_with_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules( + await db.seasons.update_active_scoring_rules( "111111", {"exact_score_points": 5, "wrong_outcome_points": 1} ) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.recalculate_fixture_scores(fixture_id) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.scores.recalculate_fixture_scores(fixture_id) - updated = await db.admin_update_prediction_with_recalc( + updated = await db.predictions.admin_update_prediction_with_recalc( fixture_id, "user-1", ["1-2"], "admin-1" ) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert updated is True assert scores[0]["points"] == 1 @@ -50,17 +50,17 @@ async def test_prediction_replacement_recalculates_with_active_season_rules(self async def test_waiver_toggle_recalculates_with_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules( + await db.seasons.update_active_scoring_rules( "111111", {"exact_score_points": 5, "late_prediction_points": 1} ) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], True) - await db.recalculate_fixture_scores(fixture_id) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], True) + await db.scores.recalculate_fixture_scores(fixture_id) - waived = await db.toggle_late_penalty_waiver_with_recalc(fixture_id, "user-1") + waived = await db.predictions.toggle_late_penalty_waiver_with_recalc(fixture_id, "user-1") - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert waived is True assert scores[0]["points"] == 5 @@ -68,12 +68,12 @@ async def test_waiver_toggle_recalculates_with_active_season_rules(self, temp_db async def test_partial_approval_recalculates_with_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.recalculate_fixture_scores(fixture_id) - await db.save_prediction( + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.scores.recalculate_fixture_scores(fixture_id) + await db.predictions.save_prediction( fixture_id, "partial", "Partial User", @@ -83,9 +83,11 @@ async def test_partial_approval_recalculates_with_active_season_rules(self, temp pending_partial_approval=True, ) - approved = await db.approve_partial_prediction(fixture_id, "partial", "admin-1") + approved = await db.predictions.approve_partial_prediction_with_recalc( + fixture_id, "partial", "admin-1" + ) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert approved is True assert [(score["user_id"], score["points"]) for score in scores] == [ ("partial", 5), @@ -96,11 +98,11 @@ async def test_partial_approval_recalculates_with_active_season_rules(self, temp async def test_partial_rejection_recalculates_with_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.save_prediction( + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.predictions.save_prediction( fixture_id, "partial", "Partial User", @@ -109,11 +111,11 @@ async def test_partial_rejection_recalculates_with_active_season_rules(self, tem predicted_game_indexes=[0], pending_partial_approval=True, ) - await db.recalculate_fixture_scores(fixture_id) + await db.scores.recalculate_fixture_scores(fixture_id) - rejected = await db.reject_partial_prediction(fixture_id, "partial") + rejected = await db.predictions.reject_partial_prediction_with_recalc(fixture_id, "partial") - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert rejected is True assert [(score["user_id"], score["points"]) for score in scores] == [("user-1", 5)] @@ -121,7 +123,7 @@ async def test_partial_rejection_recalculates_with_active_season_rules(self, tem async def test_recalculate_fixture_scores_uses_active_season_rules(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules( + await db.seasons.update_active_scoring_rules( "111111", { "exact_score_points": 5, @@ -130,11 +132,11 @@ async def test_recalculate_fixture_scores_uses_active_season_rules(self, temp_db "late_prediction_points": 0, }, ) - fixture_id = await db.create_fixture( + fixture_id = await db.fixtures.create_fixture( "111111", 1, ["A - B", "C - D", "E - F"], datetime.now(UTC) ) - await db.save_results(fixture_id, ["2-1", "1-1", "2-0"]) - await db.save_prediction( + await db.results.save_results(fixture_id, ["2-1", "1-1", "2-0"]) + await db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -142,9 +144,9 @@ async def test_recalculate_fixture_scores_uses_active_season_rules(self, temp_db False, ) - await db.recalculate_fixture_scores(fixture_id) + await db.scores.recalculate_fixture_scores(fixture_id) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert scores[0]["points"] == 8 assert scores[0]["exact_scores"] == 1 assert scores[0]["correct_results"] == 1 @@ -153,16 +155,16 @@ async def test_recalculate_fixture_scores_uses_active_season_rules(self, temp_db async def test_late_prediction_uses_active_season_penalty_rule(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules("111111", {"late_prediction_points": 1}) - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "late", "Late User", ["2-1"], True) - await db.save_prediction(fixture_id, "waived", "Waived User", ["2-1"], True) - await db.set_late_penalty_waiver(fixture_id, "waived", True) + await db.seasons.update_active_scoring_rules("111111", {"late_prediction_points": 1}) + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "late", "Late User", ["2-1"], True) + await db.predictions.save_prediction(fixture_id, "waived", "Waived User", ["2-1"], True) + await db.predictions.set_late_penalty_waiver(fixture_id, "waived", True) - await db.recalculate_fixture_scores(fixture_id) + await db.scores.recalculate_fixture_scores(fixture_id) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert [(score["user_id"], score["points"]) for score in scores] == [ ("waived", 3), ("late", 1), @@ -172,16 +174,20 @@ async def test_late_prediction_uses_active_season_penalty_rule(self, temp_db_pat async def test_scoring_rules_are_guild_isolated(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) - guild_one_fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - guild_two_fixture_id = await db.create_fixture("222222", 1, ["A - B"], datetime.now(UTC)) + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) + guild_one_fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["A - B"], datetime.now(UTC) + ) + guild_two_fixture_id = await db.fixtures.create_fixture( + "222222", 1, ["A - B"], datetime.now(UTC) + ) for fixture_id in (guild_one_fixture_id, guild_two_fixture_id): - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.recalculate_fixture_scores(fixture_id) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.scores.recalculate_fixture_scores(fixture_id) - guild_one_scores = await db.get_scores_for_fixture(guild_one_fixture_id) - guild_two_scores = await db.get_scores_for_fixture(guild_two_fixture_id) + guild_one_scores = await db.scores.get_scores_for_fixture(guild_one_fixture_id) + guild_two_scores = await db.scores.get_scores_for_fixture(guild_two_fixture_id) assert guild_one_scores[0]["points"] == 5 assert guild_two_scores[0]["points"] == 3 @@ -189,18 +195,18 @@ async def test_scoring_rules_are_guild_isolated(self, temp_db_path): async def test_scoring_rule_changes_are_blocked_after_scores_exist(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - assert await db.active_season_has_scores("111111") is False - fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_results(fixture_id, ["2-1"]) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) - await db.recalculate_fixture_scores(fixture_id) + assert await db.seasons.active_season_has_scores("111111") is False + fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.results.save_results(fixture_id, ["2-1"]) + await db.predictions.save_prediction(fixture_id, "user-1", "User One", ["2-1"], False) + await db.scores.recalculate_fixture_scores(fixture_id) - assert await db.active_season_has_scores("111111") is True + assert await db.seasons.active_season_has_scores("111111") is True with pytest.raises(ValueError, match="Cannot change scoring rules"): - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) - assert await db.get_active_scoring_rules("111111") == { + assert await db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 3, "correct_outcome_points": 1, "wrong_outcome_points": 0, @@ -211,8 +217,8 @@ async def test_scoring_rule_changes_are_blocked_after_scores_exist(self, temp_db async def test_scoring_rule_change_block_is_guild_and_active_season_scoped(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) - await db.save_scores( + old_fixture_id = await db.fixtures.create_fixture("111111", 1, ["A - B"], datetime.now(UTC)) + await db.scores.save_scores( old_fixture_id, [ { @@ -224,12 +230,14 @@ async def test_scoring_rule_change_block_is_guild_and_active_season_scoped(self, } ], ) - await db.start_new_season("111111", "Next Season") + await db.seasons.start_new_season("111111", "Next Season") - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) - other_guild_fixture_id = await db.create_fixture("222222", 1, ["C - D"], datetime.now(UTC)) - await db.save_scores( + other_guild_fixture_id = await db.fixtures.create_fixture( + "222222", 1, ["C - D"], datetime.now(UTC) + ) + await db.scores.save_scores( other_guild_fixture_id, [ { @@ -242,10 +250,12 @@ async def test_scoring_rule_change_block_is_guild_and_active_season_scoped(self, ], ) - await db.update_active_scoring_rules("111111", {"correct_outcome_points": 2}) + await db.seasons.update_active_scoring_rules("111111", {"correct_outcome_points": 2}) - active_fixture_id = await db.create_fixture("111111", 1, ["E - F"], datetime.now(UTC)) - await db.save_scores( + active_fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["E - F"], datetime.now(UTC) + ) + await db.scores.save_scores( active_fixture_id, [ { @@ -259,9 +269,9 @@ async def test_scoring_rule_change_block_is_guild_and_active_season_scoped(self, ) with pytest.raises(ValueError, match="Cannot change scoring rules"): - await db.update_active_scoring_rules("111111", {"wrong_outcome_points": 1}) + await db.seasons.update_active_scoring_rules("111111", {"wrong_outcome_points": 1}) - assert await db.get_active_scoring_rules("111111") == { + assert await db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 5, "correct_outcome_points": 2, "wrong_outcome_points": 0, @@ -274,8 +284,10 @@ async def test_save_scores_does_not_mutate_when_write_lock_is_held( ): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) - await db.save_scores( + fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["Team A - Team B"], datetime.now(UTC) + ) + await db.scores.save_scores( fixture_id, [ { @@ -299,7 +311,7 @@ def connect_with_short_timeout(*args, **kwargs): await locked_conn.execute("BEGIN IMMEDIATE") with pytest.raises(aiosqlite.OperationalError, match="locked"): - await db.save_scores( + await db.scores.save_scores( fixture_id, [ { @@ -314,15 +326,17 @@ def connect_with_short_timeout(*args, **kwargs): await locked_conn.rollback() - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert [score["user_id"] for score in scores] == ["user-1"] @pytest.mark.asyncio async def test_save_scores_rolls_back_after_partial_write_failure(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) - await db.save_scores( + fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["Team A - Team B"], datetime.now(UTC) + ) + await db.scores.save_scores( fixture_id, [ { @@ -349,7 +363,7 @@ async def test_save_scores_rolls_back_after_partial_write_failure(self, temp_db_ await conn.commit() with pytest.raises(aiosqlite.IntegrityError, match="forced score insert failure"): - await db.save_scores( + await db.scores.save_scores( fixture_id, [ { @@ -369,8 +383,8 @@ async def test_save_scores_rolls_back_after_partial_write_failure(self, temp_db_ ], ) - scores = await db.get_scores_for_fixture(fixture_id) - fixture = await db.get_fixture_by_id(fixture_id, "111111") + scores = await db.scores.get_scores_for_fixture(fixture_id) + fixture = await db.fixtures.get_fixture_by_id(fixture_id, "111111") assert scores == [ { "user_id": "user-1", @@ -388,15 +402,17 @@ async def test_recalculate_fixture_scores_rolls_back_after_partial_write_failure ): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture( + fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B", "Team C - Team D"], datetime.now(UTC), ) - await db.save_prediction(fixture_id, "user-1", "User One", ["2-1", "1-1"], False) - await db.save_results(fixture_id, ["2-1", "1-1"]) - await db.save_scores( + await db.predictions.save_prediction( + fixture_id, "user-1", "User One", ["2-1", "1-1"], False + ) + await db.results.save_results(fixture_id, ["2-1", "1-1"]) + await db.scores.save_scores( fixture_id, [ { @@ -423,18 +439,20 @@ async def test_recalculate_fixture_scores_rolls_back_after_partial_write_failure await conn.commit() with pytest.raises(aiosqlite.IntegrityError, match="forced score insert failure"): - await db.recalculate_fixture_scores(fixture_id) + await db.scores.recalculate_fixture_scores(fixture_id) - scores = await db.get_scores_for_fixture(fixture_id) + scores = await db.scores.get_scores_for_fixture(fixture_id) assert [(score["user_id"], score["points"]) for score in scores] == [("original", 1)] @pytest.mark.asyncio async def test_standings_order_by_points_tiebreakers_and_name(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) + fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["Team A - Team B"], datetime.now(UTC) + ) - await db.save_scores( + await db.scores.save_scores( fixture_id, [ { @@ -475,7 +493,7 @@ async def test_standings_order_by_points_tiebreakers_and_name(self, temp_db_path ], ) - standings = await db.get_standings("111111") + standings = await db.scores.get_standings("111111") assert [row["user_id"] for row in standings] == [ "total", @@ -489,10 +507,10 @@ async def test_standings_order_by_points_tiebreakers_and_name(self, temp_db_path async def test_standings_are_active_season_scoped(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -505,10 +523,10 @@ async def test_standings_are_active_season_scoped(self, temp_db_path): ], ) await start_new_active_season(temp_db_path, "111111") - active_fixture_id = await db.create_fixture( + active_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["New Team A - New Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( active_fixture_id, [ { @@ -521,7 +539,7 @@ async def test_standings_are_active_season_scoped(self, temp_db_path): ], ) - standings = await db.get_standings("111111") + standings = await db.scores.get_standings("111111") assert [row["user_id"] for row in standings] == ["shared-user"] assert standings[0]["user_name"] == "Active Shared User" @@ -531,10 +549,10 @@ async def test_standings_are_active_season_scoped(self, temp_db_path): async def test_get_standings_for_season_returns_archived_season_scores(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -546,12 +564,12 @@ async def test_get_standings_for_season_returns_archived_season_scores(self, tem } ], ) - old_season = await db.get_active_season("111111") + old_season = await db.seasons.get_active_season("111111") await start_new_active_season(temp_db_path, "111111") - active_fixture_id = await db.create_fixture( + active_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["New Team A - New Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( active_fixture_id, [ { @@ -564,8 +582,8 @@ async def test_get_standings_for_season_returns_archived_season_scores(self, tem ], ) - standings = await db.get_standings_for_season("111111", old_season["id"]) - wrong_guild_standings = await db.get_standings_for_season("222222", old_season["id"]) + standings = await db.scores.get_standings_for_season("111111", old_season["id"]) + wrong_guild_standings = await db.scores.get_standings_for_season("222222", old_season["id"]) assert [row["user_id"] for row in standings] == ["old-user"] assert standings[0]["total_points"] == 30 @@ -576,10 +594,10 @@ async def test_last_fixture_scores_are_active_season_scoped(self, temp_db_path): db = Database(temp_db_path) await db.initialize() await start_new_active_season(temp_db_path, "111111") - active_fixture_id = await db.create_fixture( + active_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["New Team A - New Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( active_fixture_id, [ { @@ -592,10 +610,10 @@ async def test_last_fixture_scores_are_active_season_scoped(self, temp_db_path): ], ) await start_new_active_season(temp_db_path, "111111", "Archived Later Season") - later_archived_fixture_id = await db.create_fixture( + later_archived_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Archived Team A - Archived Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( later_archived_fixture_id, [ { @@ -618,7 +636,7 @@ async def test_last_fixture_scores_are_active_season_scoped(self, temp_db_path): ) await conn.commit() - last_fixture = await db.get_last_fixture_scores("111111") + last_fixture = await db.scores.get_last_fixture_scores("111111") assert last_fixture["fixture_id"] == active_fixture_id assert [score["user_id"] for score in last_fixture["scores"]] == ["active-user"] diff --git a/tests/database/test_seasons.py b/tests/database/test_seasons.py index 79ffbb1..d9c2ef1 100644 --- a/tests/database/test_seasons.py +++ b/tests/database/test_seasons.py @@ -12,19 +12,19 @@ class TestSeasons: async def test_create_fixture_uses_fresh_guild_active_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.upsert_guild_config("111111", "role-1", "channel-1") + await db.guild_config.upsert_guild_config("111111", "role-1", "channel-1") - first_fixture_id = await db.create_fixture( + first_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - second_fixture_id = await db.create_fixture( + second_fixture_id = await db.fixtures.create_fixture( "111111", 2, ["Team C - Team D"], datetime.now(UTC) ) - active_season = await db.get_active_season("111111") - first_fixture = await db.get_fixture_by_id(first_fixture_id, "111111") - second_fixture = await db.get_fixture_by_id(second_fixture_id, "111111") - config = await db.get_guild_config("111111") + active_season = await db.seasons.get_active_season("111111") + first_fixture = await db.fixtures.get_fixture_by_id(first_fixture_id, "111111") + second_fixture = await db.fixtures.get_fixture_by_id(second_fixture_id, "111111") + config = await db.guild_config.get_guild_config("111111") assert active_season is not None assert active_season["guild_id"] == "111111" @@ -39,17 +39,17 @@ async def test_active_seasons_are_guild_isolated(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - guild_one_fixture_id = await db.create_fixture( + guild_one_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - guild_two_fixture_id = await db.create_fixture( + guild_two_fixture_id = await db.fixtures.create_fixture( "222222", 1, ["Team C - Team D"], datetime.now(UTC) ) - guild_one_season = await db.get_active_season("111111") - guild_two_season = await db.get_active_season("222222") - guild_one_fixture = await db.get_fixture_by_id(guild_one_fixture_id, "111111") - guild_two_fixture = await db.get_fixture_by_id(guild_two_fixture_id, "222222") + guild_one_season = await db.seasons.get_active_season("111111") + guild_two_season = await db.seasons.get_active_season("222222") + guild_one_fixture = await db.fixtures.get_fixture_by_id(guild_one_fixture_id, "111111") + guild_two_fixture = await db.fixtures.get_fixture_by_id(guild_two_fixture_id, "222222") assert guild_one_season is not None assert guild_two_season is not None @@ -63,9 +63,11 @@ async def test_active_seasons_are_guild_isolated(self, temp_db_path): async def test_get_or_create_active_season_repairs_stale_config_pointer(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.upsert_guild_config("111111", "role-1", "channel-1") - fixture_id = await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) - fixture = await db.get_fixture_by_id(fixture_id, "111111") + await db.guild_config.upsert_guild_config("111111", "role-1", "channel-1") + fixture_id = await db.fixtures.create_fixture( + "111111", 1, ["Team A - Team B"], datetime.now(UTC) + ) + fixture = await db.fixtures.get_fixture_by_id(fixture_id, "111111") async with aiosqlite.connect(temp_db_path) as conn: cursor = await conn.execute( @@ -77,8 +79,8 @@ async def test_get_or_create_active_season_repairs_stale_config_pointer(self, te ) await conn.commit() - active_season = await db.get_or_create_active_season("111111") - config = await db.get_guild_config("111111") + active_season = await db.seasons.get_or_create_active_season("111111") + config = await db.guild_config.get_guild_config("111111") assert active_season["id"] == fixture["season_id"] assert config["active_season_id"] == active_season["id"] @@ -87,16 +89,16 @@ async def test_get_or_create_active_season_repairs_stale_config_pointer(self, te async def test_create_next_fixture_restarts_week_numbers_per_active_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - _old_fixture_id, old_week = await db.create_next_fixture( + _old_fixture_id, old_week = await db.fixtures.create_next_fixture( "111111", ["Team A - Team B"], datetime.now(UTC) ) await start_new_active_season(temp_db_path, "111111") - new_fixture_id, new_week = await db.create_next_fixture( + new_fixture_id, new_week = await db.fixtures.create_next_fixture( "111111", ["Team C - Team D"], datetime.now(UTC) ) - new_fixture = await db.get_fixture_by_id(new_fixture_id, "111111") - active_season = await db.get_active_season("111111") + new_fixture = await db.fixtures.get_fixture_by_id(new_fixture_id, "111111") + active_season = await db.seasons.get_active_season("111111") assert old_week == 1 assert new_week == 1 @@ -106,11 +108,11 @@ async def test_create_next_fixture_restarts_week_numbers_per_active_season(self, async def test_start_new_season_archives_previous_active_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.upsert_guild_config("111111", "role-1", "channel-1") - old_fixture_id = await db.create_fixture( + await db.guild_config.upsert_guild_config("111111", "role-1", "channel-1") + old_fixture_id = await db.fixtures.create_fixture( "111111", 7, ["Team A - Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -122,11 +124,11 @@ async def test_start_new_season_archives_previous_active_season(self, temp_db_pa } ], ) - old_season = await db.get_active_season("111111") + old_season = await db.seasons.get_active_season("111111") - new_season = await db.start_new_season("111111", "2026/27") - config = await db.get_guild_config("111111") - seasons = await db.get_seasons("111111") + new_season = await db.seasons.start_new_season("111111", "2026/27") + config = await db.guild_config.get_guild_config("111111") + seasons = await db.seasons.get_seasons("111111") assert old_season is not None assert new_season["name"] == "2026/27" @@ -142,22 +144,22 @@ async def test_start_new_season_archives_previous_active_season(self, temp_db_pa async def test_start_new_season_blocks_open_active_fixture(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) - old_season = await db.get_active_season("111111") + await db.fixtures.create_fixture("111111", 1, ["Team A - Team B"], datetime.now(UTC)) + old_season = await db.seasons.get_active_season("111111") with pytest.raises(ValueError, match="Close all open fixtures"): - await db.start_new_season("111111", "2026/27") + await db.seasons.start_new_season("111111", "2026/27") - assert await db.get_active_season("111111") == old_season + assert await db.seasons.get_active_season("111111") == old_season @pytest.mark.asyncio async def test_start_new_season_rejects_blank_name_without_mutating(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -169,22 +171,22 @@ async def test_start_new_season_rejects_blank_name_without_mutating(self, temp_d } ], ) - old_season = await db.get_active_season("111111") + old_season = await db.seasons.get_active_season("111111") with pytest.raises(ValueError, match="Season name is required"): - await db.start_new_season("111111", " ") + await db.seasons.start_new_season("111111", " ") - assert await db.get_active_season("111111") == old_season - assert await db.get_seasons("111111") == [old_season] + assert await db.seasons.get_active_season("111111") == old_season + assert await db.seasons.get_seasons("111111") == [old_season] @pytest.mark.asyncio async def test_start_new_season_rolls_back_when_new_season_insert_fails(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Team A - Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -196,7 +198,7 @@ async def test_start_new_season_rolls_back_when_new_season_insert_fails(self, te } ], ) - old_season = await db.get_active_season("111111") + old_season = await db.seasons.get_active_season("111111") async with aiosqlite.connect(temp_db_path) as conn: await conn.execute( """ @@ -211,19 +213,19 @@ async def test_start_new_season_rolls_back_when_new_season_insert_fails(self, te await conn.commit() with pytest.raises(aiosqlite.IntegrityError, match="broken season insert"): - await db.start_new_season("111111", "Broken Season") + await db.seasons.start_new_season("111111", "Broken Season") - assert await db.get_active_season("111111") == old_season - assert await db.get_seasons("111111") == [old_season] + assert await db.seasons.get_active_season("111111") == old_season + assert await db.seasons.get_seasons("111111") == [old_season] @pytest.mark.asyncio async def test_start_new_season_resets_next_fixture_week(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id, old_week = await db.create_next_fixture( + old_fixture_id, old_week = await db.fixtures.create_next_fixture( "111111", ["Team A - Team B"], datetime.now(UTC) ) - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -236,12 +238,12 @@ async def test_start_new_season_resets_next_fixture_week(self, temp_db_path): ], ) - await db.start_new_season("111111", "2026/27") - new_fixture_id, new_week = await db.create_next_fixture( + await db.seasons.start_new_season("111111", "2026/27") + new_fixture_id, new_week = await db.fixtures.create_next_fixture( "111111", ["Team C - Team D"], datetime.now(UTC) ) - new_fixture = await db.get_fixture_by_id(new_fixture_id, "111111") - active_season = await db.get_active_season("111111") + new_fixture = await db.fixtures.get_fixture_by_id(new_fixture_id, "111111") + active_season = await db.seasons.get_active_season("111111") assert old_week == 1 assert new_week == 1 @@ -257,12 +259,12 @@ async def test_start_new_season_uses_fresh_default_scoring_rules(self, temp_db_p "wrong_outcome_points": 1, "late_prediction_points": 1, } - await db.update_active_scoring_rules("111111", custom_rules) + await db.seasons.update_active_scoring_rules("111111", custom_rules) - await db.start_new_season("111111", "Next Season") + await db.seasons.start_new_season("111111", "Next Season") - seasons = await db.get_seasons("111111") - active_rules = await db.get_active_scoring_rules("111111") + seasons = await db.seasons.get_seasons("111111") + active_rules = await db.seasons.get_active_scoring_rules("111111") assert seasons[0]["scoring_rules"] == custom_rules assert active_rules == { "exact_score_points": 3, @@ -275,7 +277,7 @@ async def test_start_new_season_uses_fresh_default_scoring_rules(self, temp_db_p async def test_scoring_rule_updates_preserve_omitted_values(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules( + await db.seasons.update_active_scoring_rules( "111111", { "exact_score_points": 5, @@ -285,9 +287,9 @@ async def test_scoring_rule_updates_preserve_omitted_values(self, temp_db_path): }, ) - await db.update_active_scoring_rules("111111", {"late_prediction_points": 2}) + await db.seasons.update_active_scoring_rules("111111", {"late_prediction_points": 2}) - assert await db.get_active_scoring_rules("111111") == { + assert await db.seasons.get_active_scoring_rules("111111") == { "exact_score_points": 5, "correct_outcome_points": 2, "wrong_outcome_points": 1, @@ -308,37 +310,37 @@ async def test_invalid_scoring_rule_updates_do_not_mutate_existing_rules( ): db = Database(temp_db_path) await db.initialize() - await db.update_active_scoring_rules("111111", {"exact_score_points": 5}) - existing_rules = await db.get_active_scoring_rules("111111") + await db.seasons.update_active_scoring_rules("111111", {"exact_score_points": 5}) + existing_rules = await db.seasons.get_active_scoring_rules("111111") with pytest.raises(ValueError, match=message): - await db.update_active_scoring_rules("111111", rules) + await db.seasons.update_active_scoring_rules("111111", rules) - assert await db.get_active_scoring_rules("111111") == existing_rules + assert await db.seasons.get_active_scoring_rules("111111") == existing_rules @pytest.mark.asyncio async def test_fixture_queries_default_to_active_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.update_fixture_announcement(old_fixture_id, "old-message", "channel-1") + await db.fixtures.update_fixture_announcement(old_fixture_id, "old-message", "channel-1") await start_new_active_season(temp_db_path, "111111") - active_fixture_id = await db.create_fixture( + active_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["New Team A - New Team B"], datetime.now(UTC) ) - await db.update_fixture_announcement(active_fixture_id, "new-message", "channel-1") + await db.fixtures.update_fixture_announcement(active_fixture_id, "new-message", "channel-1") - current_fixture = await db.get_current_fixture("111111") - open_fixtures = await db.get_open_fixtures("111111") - recent_fixtures = await db.get_recent_fixtures("111111") - week_fixture = await db.get_open_fixture_by_week("111111", 1) - any_status_week_fixture = await db.get_fixture_by_week("111111", 1) - message_fixture = await db.get_fixture_by_message_id("new-message", "111111") - global_message_fixture = await db.get_fixture_by_message_id("new-message") + current_fixture = await db.fixtures.get_current_fixture("111111") + open_fixtures = await db.fixtures.get_open_fixtures("111111") + recent_fixtures = await db.fixtures.get_recent_fixtures("111111") + week_fixture = await db.fixtures.get_open_fixture_by_week("111111", 1) + any_status_week_fixture = await db.fixtures.get_fixture_by_week("111111", 1) + message_fixture = await db.fixtures.get_fixture_by_message_id("new-message", "111111") + global_message_fixture = await db.fixtures.get_fixture_by_message_id("new-message") - assert await db.get_fixture_by_id(old_fixture_id, "111111") is None + assert await db.fixtures.get_fixture_by_id(old_fixture_id, "111111") is None assert current_fixture["id"] == active_fixture_id assert [fixture["id"] for fixture in open_fixtures] == [active_fixture_id] assert [fixture["id"] for fixture in recent_fixtures] == [active_fixture_id] @@ -346,23 +348,25 @@ async def test_fixture_queries_default_to_active_season(self, temp_db_path): assert any_status_week_fixture["id"] == active_fixture_id assert message_fixture["id"] == active_fixture_id assert global_message_fixture["id"] == active_fixture_id - assert await db.get_fixture_by_message_id("old-message", "111111") is None - assert await db.get_fixture_by_message_id("old-message") is None + assert await db.fixtures.get_fixture_by_message_id("old-message", "111111") is None + assert await db.fixtures.get_fixture_by_message_id("old-message") is None @pytest.mark.asyncio async def test_all_open_fixtures_only_returns_active_season_fixtures(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - await db.create_fixture("111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC)) + await db.fixtures.create_fixture( + "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) + ) await start_new_active_season(temp_db_path, "111111") - active_fixture_id = await db.create_fixture( + active_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["New Team A - New Team B"], datetime.now(UTC) ) - other_guild_fixture_id = await db.create_fixture( + other_guild_fixture_id = await db.fixtures.create_fixture( "222222", 1, ["Other Team A - Other Team B"], datetime.now(UTC) ) - open_fixture_ids = [fixture["id"] for fixture in await db.get_all_open_fixtures()] + open_fixture_ids = [fixture["id"] for fixture in await db.fixtures.get_all_open_fixtures()] assert set(open_fixture_ids) == {active_fixture_id, other_guild_fixture_id} @@ -370,24 +374,26 @@ async def test_all_open_fixtures_only_returns_active_season_fixtures(self, temp_ async def test_archived_fixture_prediction_writes_are_rejected(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.save_prediction(old_fixture_id, "user-1", "User One", ["1-0"], False) + await db.predictions.save_prediction(old_fixture_id, "user-1", "User One", ["1-0"], False) await start_new_active_season(temp_db_path, "111111") - first_write = await db.try_save_prediction(old_fixture_id, "user-2", "User Two", ["2-0"]) - guarded_write = await db.save_prediction_guarded( + first_write = await db.predictions.try_save_prediction( + old_fixture_id, "user-2", "User Two", ["2-0"] + ) + guarded_write = await db.predictions.save_prediction_guarded( old_fixture_id, "user-1", "User One", ["9-9"] ) - admin_write = await db.admin_update_prediction_with_recalc( + admin_write = await db.predictions.admin_update_prediction_with_recalc( old_fixture_id, "user-1", ["8-8"], "admin-1" ) assert first_write == SaveResult.FIXTURE_CLOSED assert guarded_write == SaveResult.FIXTURE_CLOSED assert admin_write is False - assert await db.get_prediction(old_fixture_id, "user-1", "111111") is None + assert await db.predictions.get_prediction(old_fixture_id, "user-1", "111111") is None async with ( aiosqlite.connect(temp_db_path) as conn, conn.execute( @@ -402,10 +408,10 @@ async def test_archived_fixture_prediction_writes_are_rejected(self, temp_db_pat async def test_archived_pending_partials_are_hidden_and_not_mutated(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.save_prediction( + await db.predictions.save_prediction( old_fixture_id, "user-1", "User One", @@ -415,9 +421,13 @@ async def test_archived_pending_partials_are_hidden_and_not_mutated(self, temp_d ) await start_new_active_season(temp_db_path, "111111") - approved = await db.approve_partial_prediction(old_fixture_id, "user-1", "admin-1") - rejected = await db.reject_partial_prediction(old_fixture_id, "user-1") - pending = await db.get_pending_partial_predictions("111111") + approved = await db.predictions.approve_partial_prediction_with_recalc( + old_fixture_id, "user-1", "admin-1" + ) + rejected = await db.predictions.reject_partial_prediction_with_recalc( + old_fixture_id, "user-1" + ) + pending = await db.predictions.get_pending_partial_predictions("111111") assert approved is False assert rejected is False @@ -436,11 +446,11 @@ async def test_archived_pending_partials_are_hidden_and_not_mutated(self, temp_d async def test_archived_fixture_result_and_score_writes_are_rejected(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) - await db.save_results(old_fixture_id, ["0-0"]) - await db.save_scores( + await db.results.save_results(old_fixture_id, ["0-0"]) + await db.scores.save_scores( old_fixture_id, [ { @@ -455,13 +465,13 @@ async def test_archived_fixture_result_and_score_writes_are_rejected(self, temp_ await start_new_active_season(temp_db_path, "111111") with pytest.raises(ValueError): - await db.save_results(old_fixture_id, ["1-0"]) + await db.results.save_results(old_fixture_id, ["1-0"]) with pytest.raises(ValueError): - await db.save_results_with_recalc(old_fixture_id, ["1-0"]) + await db.results.save_results_with_recalc(old_fixture_id, ["1-0"]) with pytest.raises(ValueError): - await db.recalculate_fixture_scores(old_fixture_id) + await db.scores.recalculate_fixture_scores(old_fixture_id) with pytest.raises(ValueError): - await db.save_scores( + await db.scores.save_scores( old_fixture_id, [ { @@ -474,20 +484,20 @@ async def test_archived_fixture_result_and_score_writes_are_rejected(self, temp_ ], ) - assert await db.get_results(old_fixture_id) == ["0-0"] - scores = await db.get_scores_for_fixture(old_fixture_id) + assert await db.results.get_results(old_fixture_id) == ["0-0"] + scores = await db.scores.get_scores_for_fixture(old_fixture_id) assert [(score["user_id"], score["points"]) for score in scores] == [("old-user", 30)] @pytest.mark.asyncio async def test_archived_fixture_delete_requires_active_season(self, temp_db_path): db = Database(temp_db_path) await db.initialize() - old_fixture_id = await db.create_fixture( + old_fixture_id = await db.fixtures.create_fixture( "111111", 1, ["Old Team A - Old Team B"], datetime.now(UTC) ) await start_new_active_season(temp_db_path, "111111") - assert await db.delete_fixture(old_fixture_id, "111111") is False + assert await db.fixtures.delete_fixture(old_fixture_id, "111111") is False async with ( aiosqlite.connect(temp_db_path) as conn, conn.execute("SELECT 1 FROM fixtures WHERE id = ?", (old_fixture_id,)) as cursor, diff --git a/tests/test_admin_commands.py b/tests/test_admin_commands.py index 253e432..f39f326 100644 --- a/tests/test_admin_commands.py +++ b/tests/test_admin_commands.py @@ -54,7 +54,7 @@ async def test_accepts_typer_admin_role(self, mock_interaction_admin): @pytest.mark.asyncio async def test_configured_admin_role_grants_access(self, database, mock_interaction_admin): - await database.upsert_guild_config("111111", "987654", "123456") + await database.guild_config.upsert_guild_config("111111", "987654", "123456") member = mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id) member.roles = [MockRole("League Admin", role_id=987654)] @@ -64,7 +64,7 @@ async def test_configured_admin_role_grants_access(self, database, mock_interact async def test_configured_admin_role_rejects_name_only_admin( self, database, mock_interaction_admin ): - await database.upsert_guild_config("111111", "987654", "123456") + await database.guild_config.upsert_guild_config("111111", "987654", "123456") permission_error = await get_admin_permission_error(mock_interaction_admin, database) assert permission_error is not None @@ -76,7 +76,7 @@ async def test_configured_admin_check_uses_interaction_member_when_cache_misses( database, mock_interaction_admin, ): - await database.upsert_guild_config("111111", "987654", "123456") + await database.guild_config.upsert_guild_config("111111", "987654", "123456") mock_interaction_admin.guild._members.clear() mock_interaction_admin.user.roles = [MockRole("League Admin", role_id=987654)] @@ -105,7 +105,7 @@ def admin_cog(self, mock_bot, database): @pytest.mark.asyncio async def test_admin_panel_opens_unified_view(self, admin_cog, mock_interaction_admin): - await admin_cog.db.upsert_guild_config("111111", "987654", "123456") + await admin_cog.db.guild_config.upsert_guild_config("111111", "987654", "123456") member = mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id) member.roles = [MockRole("League Admin", role_id=987654)] @@ -230,7 +230,7 @@ async def test_inline_setup_selector_rechecks_setup_permission( view = GuildSetupPromptView(database, str(mock_interaction_admin.user.id)) assert await view.interaction_check(mock_interaction_admin) is False - assert await database.get_guild_config("111111") is None + assert await database.guild_config.get_guild_config("111111") is None @pytest.mark.asyncio async def test_inline_setup_prompt_saves_config( @@ -249,7 +249,7 @@ async def test_inline_setup_prompt_saves_config( await view.save_button.callback(mock_interaction_admin) - config = await database.get_guild_config("111111") + config = await database.guild_config.get_guild_config("111111") assert config["admin_role_id"] == "987654" assert config["league_channel_id"] == "765432" assert "this server's league" in mock_interaction_admin.response_sent[-1]["content"] @@ -272,7 +272,7 @@ async def test_inline_setup_prompt_requires_confirmation_for_everyone_role( await view.save_button.callback(mock_interaction_admin) assert isinstance(mock_interaction_admin.response_sent[-1]["view"], EveryoneRoleConfirmView) - assert await database.get_guild_config("111111") is None + assert await database.guild_config.get_guild_config("111111") is None confirm_view = mock_interaction_admin.response_sent[-1]["view"] confirm_button = next( @@ -282,7 +282,7 @@ async def test_inline_setup_prompt_requires_confirmation_for_everyone_role( ) await confirm_button.callback(mock_interaction_admin) - config = await database.get_guild_config("111111") + config = await database.guild_config.get_guild_config("111111") assert config["admin_role_id"] == str(mock_interaction_admin.guild.id) diff --git a/tests/test_admin_panel_command.py b/tests/test_admin_panel_command.py index 241249f..b473c11 100644 --- a/tests/test_admin_panel_command.py +++ b/tests/test_admin_panel_command.py @@ -69,7 +69,7 @@ async def test_create_fixture_modal_warns_when_other_fixtures_are_open( mock_interaction_admin, sample_games, ): - await admin_cog.db.create_fixture( + await admin_cog.db.fixtures.create_fixture( "111111", 5, sample_games, datetime.now(UTC) + timedelta(days=1) ) modal = CreateFixtureModal( @@ -213,7 +213,7 @@ async def test_create_fixture_confirm_creates_fixture_and_announcement( ) await confirm_button.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(1, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(1, "111111") assert fixture is not None assert fixture["message_id"] == "999999" assert fixture["channel_id"] == str(mock_interaction_admin.channel.id) @@ -250,7 +250,7 @@ async def test_create_fixture_confirm_warns_when_thread_creation_fails( ) await confirm_button.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(1, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(1, "111111") assert fixture is not None assert fixture["message_id"] == "999999" assert fixture["channel_id"] == str(mock_interaction_admin.channel.id) @@ -284,7 +284,7 @@ async def test_create_fixture_confirm_warns_when_announcement_fails( ) await confirm_button.callback(mock_interaction_admin) - assert await admin_cog.db.get_fixture_by_id(1, "111111") is not None + assert await admin_cog.db.fixtures.get_fixture_by_id(1, "111111") is not None assert ( "couldn't announce it in the channel" in mock_interaction_admin.followup_sent[-1]["content"] @@ -310,7 +310,7 @@ async def test_create_fixture_confirm_warns_when_week_changes_between_preview_an modal.deadline_input._value = "2026-04-20 18:00" await modal.on_submit(mock_interaction_admin) - await admin_cog.db.create_fixture( + await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -322,7 +322,7 @@ async def test_create_fixture_confirm_warns_when_week_changes_between_preview_an ) await confirm_button.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(2, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(2, "111111") assert fixture is not None assert "Week number changed" in mock_interaction_admin.response_sent[-1]["content"] @@ -333,7 +333,7 @@ async def test_create_fixture_confirm_posts_to_configured_league_channel( mock_interaction_admin, sample_games, ): - await admin_cog.db.upsert_guild_config( + await admin_cog.db.guild_config.upsert_guild_config( "111111", str( mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id).roles[0].id @@ -369,7 +369,7 @@ async def test_create_fixture_confirm_posts_to_configured_league_channel( configured_channel.send.assert_awaited_once() mock_interaction_admin.channel.send.assert_not_awaited() - fixture = await admin_cog.db.get_current_fixture("111111") + fixture = await admin_cog.db.fixtures.get_current_fixture("111111") assert fixture["channel_id"] == "654321" @pytest.mark.asyncio @@ -379,7 +379,7 @@ async def test_create_fixture_confirm_rejects_unavailable_configured_channel( mock_interaction_admin, sample_games, ): - await admin_cog.db.upsert_guild_config( + await admin_cog.db.guild_config.upsert_guild_config( "111111", str( mock_interaction_admin.guild.get_member(mock_interaction_admin.user.id).roles[0].id @@ -408,7 +408,7 @@ async def test_create_fixture_confirm_rejects_unavailable_configured_channel( await confirm_button.callback(mock_interaction_admin) assert "league channel" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_current_fixture("111111") is None + assert await admin_cog.db.fixtures.get_current_fixture("111111") is None @pytest.mark.asyncio async def test_create_fixture_confirm_cancel_leaves_database_unchanged( @@ -432,7 +432,7 @@ async def test_create_fixture_confirm_cancel_leaves_database_unchanged( await cancel_button.callback(mock_interaction_admin) assert "Fixture creation cancelled" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_current_fixture("111111") is None + assert await admin_cog.db.fixtures.get_current_fixture("111111") is None @pytest.mark.asyncio async def test_create_fixture_confirm_rechecks_admin_permission( @@ -461,7 +461,7 @@ async def test_create_fixture_confirm_rechecks_admin_permission( await confirm_button.callback(mock_interaction_admin) assert "no longer have permission" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_current_fixture("111111") is None + assert await admin_cog.db.fixtures.get_current_fixture("111111") is None @pytest.mark.asyncio async def test_panel_command_returns_view(self, admin_cog, mock_interaction_admin): diff --git a/tests/test_admin_panel_modals.py b/tests/test_admin_panel_modals.py index 11cbc25..5b5ee16 100644 --- a/tests/test_admin_panel_modals.py +++ b/tests/test_admin_panel_modals.py @@ -31,10 +31,10 @@ async def test_enter_results_modal_prefills_fixture_template( admin_cog, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 24, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -50,10 +50,10 @@ async def test_enter_results_modal_shows_parse_errors( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 25, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -70,10 +70,10 @@ async def test_enter_results_modal_rechecks_admin_permission( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 28, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -92,10 +92,10 @@ async def test_enter_results_modal_save_results_persists_fixture_results( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 26, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -112,7 +112,7 @@ async def test_enter_results_modal_save_results_persists_fixture_results( await confirm_button.callback(mock_interaction_admin) assert "Results Saved!" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_results(fixture_id) == ["2-1", "1-1", "0-2"] + assert await admin_cog.db.results.get_results(fixture_id) == ["2-1", "1-1", "0-2"] @pytest.mark.asyncio async def test_enter_results_modal_save_results_supports_cancelled_games( @@ -121,10 +121,10 @@ async def test_enter_results_modal_save_results_supports_cancelled_games( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 30, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -140,7 +140,7 @@ async def test_enter_results_modal_save_results_supports_cancelled_games( ) await confirm_button.callback(mock_interaction_admin) - assert await admin_cog.db.get_results(fixture_id) == ["x", "1-1", "0-2"] + assert await admin_cog.db.results.get_results(fixture_id) == ["x", "1-1", "0-2"] @pytest.mark.asyncio async def test_enter_results_confirm_shows_save_error_when_fixture_becomes_unavailable( @@ -149,17 +149,17 @@ async def test_enter_results_confirm_shows_save_error_when_fixture_becomes_unava mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 31, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) modal.results_input._value = "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" await modal.on_submit(mock_interaction_admin) - await admin_cog.db.save_scores( + await admin_cog.db.scores.save_scores( fixture_id, [ { @@ -189,10 +189,10 @@ async def test_enter_results_modal_cancel_leaves_results_unsaved( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 27, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -207,7 +207,7 @@ async def test_enter_results_modal_cancel_leaves_results_unsaved( await cancel_button.callback(mock_interaction_admin) assert "Results entry cancelled" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_results(fixture_id) is None + assert await admin_cog.db.results.get_results(fixture_id) is None @pytest.mark.asyncio async def test_enter_results_confirm_rechecks_admin_permission( @@ -216,10 +216,10 @@ async def test_enter_results_confirm_rechecks_admin_permission( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 29, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = EnterResultsModal(fixture, admin_cog.db) @@ -239,7 +239,7 @@ async def test_enter_results_confirm_rechecks_admin_permission( await confirm_button.callback(mock_interaction_admin) assert "no longer have permission" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_results(fixture_id) is None + assert await admin_cog.db.results.get_results(fixture_id) is None @pytest.mark.asyncio async def test_replace_prediction_modal_rechecks_admin_permission( @@ -248,10 +248,10 @@ async def test_replace_prediction_modal_rechecks_admin_permission( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -266,8 +266,8 @@ async def test_replace_prediction_modal_rechecks_admin_permission( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "user-1", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "user-1", "111111") assert fixture is not None assert prediction is not None @@ -289,10 +289,10 @@ async def test_prediction_panel_clears_actions_for_deleted_prediction( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 9, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -305,7 +305,7 @@ async def test_prediction_panel_clears_actions_for_deleted_prediction( await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - await admin_cog.db.delete_prediction(fixture_id, "user-1") + await admin_cog.db.predictions.delete_prediction(fixture_id, "user-1") view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) @@ -322,10 +322,10 @@ async def test_replace_prediction_modal_updates_inline_panel_content( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 15, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -340,8 +340,8 @@ async def test_replace_prediction_modal_updates_inline_panel_content( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "user-1", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "user-1", "111111") assert fixture is not None assert prediction is not None @@ -356,7 +356,9 @@ async def test_replace_prediction_modal_updates_inline_panel_content( "Replaced User One's prediction in week 15." in mock_interaction_admin.response_sent[-1]["content"] ) - updated_prediction = await admin_cog.db.get_prediction(fixture_id, "user-1", "111111") + updated_prediction = await admin_cog.db.predictions.get_prediction( + fixture_id, "user-1", "111111" + ) assert updated_prediction is not None assert updated_prediction["predictions"] == ["2-1", "1-1", "0-2"] @@ -367,10 +369,10 @@ async def test_replace_prediction_modal_dms_affected_user( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 36, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -389,8 +391,8 @@ async def test_replace_prediction_modal_dms_affected_user( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["111"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "111", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") assert fixture is not None assert prediction is not None @@ -412,10 +414,10 @@ async def test_replace_prediction_modal_ignores_dm_failures( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 37, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -435,8 +437,8 @@ async def test_replace_prediction_modal_ignores_dm_failures( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["111"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "111", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") assert fixture is not None assert prediction is not None @@ -458,10 +460,10 @@ async def test_replace_prediction_modal_fetches_uncached_user( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 41, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -481,8 +483,8 @@ async def test_replace_prediction_modal_fetches_uncached_user( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["111"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "111", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") assert fixture is not None assert prediction is not None @@ -502,10 +504,10 @@ async def test_replace_prediction_modal_recovers_when_prediction_disappears( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 21, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -520,11 +522,11 @@ async def test_replace_prediction_modal_recovers_when_prediction_disappears( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") - prediction = await admin_cog.db.get_prediction(fixture_id, "user-1", "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "user-1", "111111") assert fixture is not None assert prediction is not None - await admin_cog.db.delete_prediction(fixture_id, "user-1") + await admin_cog.db.predictions.delete_prediction(fixture_id, "user-1") modal = ReplacePredictionModal(view, fixture, prediction) modal.predictions_input._value = ( @@ -546,17 +548,17 @@ async def test_correct_results_modal_rechecks_admin_permission( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 2, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) @@ -575,22 +577,22 @@ async def test_correct_results_modal_handles_deleted_fixture( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 6, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) modal.results_input._value = "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) await modal.on_submit(mock_interaction_admin) @@ -603,12 +605,12 @@ async def test_correct_results_modal_rejects_cross_guild_fixture( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "guild-2", 7, sample_games, datetime.now(UTC) + timedelta(days=1) ) original_results = ["0-0", "1-1", "2-2"] - await admin_cog.db.save_results(fixture_id, original_results) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "guild-2") + await admin_cog.db.results.save_results(fixture_id, original_results) + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "guild-2") assert fixture is not None view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" @@ -621,7 +623,7 @@ async def test_correct_results_modal_rejects_cross_guild_fixture( await modal.on_submit(mock_interaction_admin) assert "Fixture not found" in mock_interaction_admin.response_sent[-1]["content"] - assert await admin_cog.db.get_results(fixture_id) == original_results + assert await admin_cog.db.results.get_results(fixture_id) == original_results @pytest.mark.asyncio async def test_correct_results_modal_prefills_stored_results( @@ -630,17 +632,17 @@ async def test_correct_results_modal_prefills_stored_results( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 7, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) @@ -656,17 +658,17 @@ async def test_correct_results_modal_updates_inline_panel_content( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 16, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) @@ -678,7 +680,7 @@ async def test_correct_results_modal_updates_inline_panel_content( "Saved corrected results for week 16." in mock_interaction_admin.response_sent[-1]["content"] ) - assert await admin_cog.db.get_results(fixture_id) == ["2-1", "1-1", "0-2"] + assert await admin_cog.db.results.get_results(fixture_id) == ["2-1", "1-1", "0-2"] @pytest.mark.asyncio async def test_correct_results_modal_dms_all_fixture_participants( @@ -687,14 +689,14 @@ async def test_correct_results_modal_dms_all_fixture_participants( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 38, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", ["1-0", "1-1", "0-0"], False ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "222", "User Two", ["0-0", "2-2", "1-1"], False ) user_one = MockUser("111", "User One") @@ -707,7 +709,7 @@ async def test_correct_results_modal_dms_all_fixture_participants( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) view.bot = admin_cog.bot - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) @@ -725,14 +727,14 @@ async def test_correct_results_modal_continues_after_one_dm_failure( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 42, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", ["1-0", "1-1", "0-0"], False ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "222", "User Two", ["0-0", "2-2", "1-1"], False ) failing_user = MagicMock() @@ -746,7 +748,7 @@ async def test_correct_results_modal_continues_after_one_dm_failure( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) view.bot = admin_cog.bot - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) @@ -763,14 +765,14 @@ async def test_correct_results_modal_continues_after_fetch_user_failure( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 43, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", ["1-0", "1-1", "0-0"], False ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "222", "User Two", ["0-0", "2-2", "1-1"], False ) user_two = MockUser("222", "User Two") @@ -783,7 +785,7 @@ async def test_correct_results_modal_continues_after_fetch_user_failure( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) view.bot = admin_cog.bot - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) diff --git a/tests/test_admin_panel_predictions.py b/tests/test_admin_panel_predictions.py index a2d76cd..a0168ef 100644 --- a/tests/test_admin_panel_predictions.py +++ b/tests/test_admin_panel_predictions.py @@ -60,7 +60,7 @@ async def test_prediction_panel_initializes_empty_user_select( mock_interaction_admin, sample_games, ): - await admin_cog.db.create_fixture( + await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -85,10 +85,10 @@ async def test_prediction_panel_buttons_enable_as_selections_are_made( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -134,7 +134,7 @@ async def test_prediction_panel_shows_no_predictions_inline_after_fixture_select mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 10, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -160,11 +160,11 @@ async def test_prediction_panel_keeps_view_button_for_overflowing_user_lists( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 17, sample_games, datetime.now(UTC) + timedelta(days=1) ) for index in range(30): - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, f"user-{index}", f"User {index:02d}", @@ -198,11 +198,11 @@ async def test_prediction_panel_view_predictions_recovers_when_fixture_is_delete mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 19, sample_games, datetime.now(UTC) + timedelta(days=1) ) for index in range(26): - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, f"user-{index}", f"User {index:02d}", @@ -216,7 +216,7 @@ async def test_prediction_panel_view_predictions_recovers_when_fixture_is_delete await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) view_button = _get_button(view, "View Predictions") await view_button.callback(mock_interaction_admin) @@ -231,11 +231,11 @@ async def test_prediction_panel_view_predictions_recovers_when_predictions_disap mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 22, sample_games, datetime.now(UTC) + timedelta(days=1) ) for index in range(26): - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, f"user-{index}", f"User {index:02d}", @@ -250,7 +250,7 @@ async def test_prediction_panel_view_predictions_recovers_when_predictions_disap view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) for index in range(26): - await admin_cog.db.delete_prediction(fixture_id, f"user-{index}") + await admin_cog.db.predictions.delete_prediction(fixture_id, f"user-{index}") view_button = _get_button(view, "View Predictions") await view_button.callback(mock_interaction_admin) @@ -272,11 +272,11 @@ async def test_prediction_panel_view_predictions_truncates_long_summary( f"Very Long Home Team {index:02d} - Very Long Away Team {index:02d}" for index in range(1, 21) ] - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 20, games, datetime.now(UTC) + timedelta(days=1) ) for index in range(26): - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, f"user-{index}", f"Very Long User Name {index:02d}", @@ -305,10 +305,10 @@ async def test_prediction_panel_replace_opens_modal( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -342,10 +342,10 @@ async def test_prediction_panel_toggle_waiver_dms_affected_user( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 34, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -379,10 +379,10 @@ async def test_prediction_panel_toggle_waiver_ignores_dm_failures( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 35, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -415,10 +415,10 @@ async def test_prediction_panel_toggle_waiver_fetches_uncached_user( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 40, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -451,10 +451,10 @@ async def test_prediction_panel_replace_recovers_when_prediction_is_deleted( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 13, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -470,7 +470,7 @@ async def test_prediction_panel_replace_recovers_when_prediction_is_deleted( await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) - await admin_cog.db.delete_prediction(fixture_id, "user-1") + await admin_cog.db.predictions.delete_prediction(fixture_id, "user-1") replace_button = _get_button(view, "Replace Prediction") await replace_button.callback(mock_interaction_admin) @@ -486,10 +486,10 @@ async def test_prediction_panel_recovers_when_fixture_disappears_before_user_sel mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 11, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -503,7 +503,7 @@ async def test_prediction_panel_recovers_when_fixture_disappears_before_user_sel await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) @@ -521,10 +521,10 @@ async def test_prediction_panel_toggle_waiver_updates_status( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 2, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -548,7 +548,7 @@ async def test_prediction_panel_toggle_waiver_updates_status( ) await toggle_button.callback(mock_interaction_admin) - prediction = await admin_cog.db.get_prediction(fixture_id, "user-1", "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "user-1", "111111") assert prediction is not None assert prediction["late_penalty_waived"] == 1 assert "waiver enabled" in mock_interaction_admin.response_sent[-1]["content"].lower() @@ -560,10 +560,10 @@ async def test_prediction_panel_toggle_waiver_recovers_when_prediction_is_delete mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 18, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -579,7 +579,7 @@ async def test_prediction_panel_toggle_waiver_recovers_when_prediction_is_delete await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["user-1"] await view.user_select.callback(mock_interaction_admin) - await admin_cog.db.delete_prediction(fixture_id, "user-1") + await admin_cog.db.predictions.delete_prediction(fixture_id, "user-1") toggle_button = _get_button(view, "Toggle Late Waiver") await toggle_button.callback(mock_interaction_admin) diff --git a/tests/test_admin_panel_results.py b/tests/test_admin_panel_results.py index b0cd078..d91f06d 100644 --- a/tests/test_admin_panel_results.py +++ b/tests/test_admin_panel_results.py @@ -29,10 +29,10 @@ async def test_results_panel_correct_opens_modal( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 3, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" @@ -56,10 +56,10 @@ async def test_results_panel_buttons_enable_after_fixture_selection( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 4, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" @@ -84,7 +84,7 @@ async def test_results_panel_requires_existing_results( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 5, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -113,10 +113,10 @@ async def test_results_panel_correct_recovers_when_fixture_is_deleted( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 14, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" @@ -124,7 +124,7 @@ async def test_results_panel_correct_recovers_when_fixture_is_deleted( await view.load_fixture_options() view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) correct_button = _get_button(view, "Correct Results") await correct_button.callback(mock_interaction_admin) @@ -140,14 +140,14 @@ async def test_fixture_select_removes_deleted_fixture_option( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 23, sample_games, datetime.now(UTC) + timedelta(days=1) ) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" ) await view.load_fixture_options() - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) view.fixture_select._values = [str(fixture_id)] await view.fixture_select.callback(mock_interaction_admin) @@ -164,10 +164,10 @@ async def test_results_panel_truncates_long_inline_preview( f"Very Long Home Team {index:02d} - Very Long Away Team {index:02d}" for index in range(1, 101) ] - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 12, games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["1-0"] * len(games)) + await admin_cog.db.results.save_results(fixture_id, ["1-0"] * len(games)) view = ResultsPanelView( admin_cog.db, admin_cog.service, str(mock_interaction_admin.user.id), "111111" @@ -187,17 +187,17 @@ async def test_unified_panel_correct_results_clears_stale_user_on_success( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 32, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", ["1-0", "1-1", "0-2"], False, ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) admin_cog.bot.get_user.return_value = None view = UnifiedAdminPanelView( @@ -214,7 +214,7 @@ async def test_unified_panel_correct_results_clears_stale_user_on_success( view.user_select._values = ["111"] await view.user_select.callback(mock_interaction_admin) - fixture = await admin_cog.db.get_fixture_by_id(fixture_id, "111111") + fixture = await admin_cog.db.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None modal = CorrectResultsModal(view, fixture, ["1-0", "1-1", "0-0"]) modal.results_input._value = "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" @@ -235,17 +235,17 @@ async def test_unified_panel_correct_results_clears_stale_user_on_deleted_fixtur mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 33, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", ["1-0", "1-1", "0-2"], False, ) - await admin_cog.db.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await admin_cog.db.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) view = UnifiedAdminPanelView( admin_cog.db, @@ -260,7 +260,7 @@ async def test_unified_panel_correct_results_clears_stale_user_on_deleted_fixtur await view.fixture_select.callback(mock_interaction_admin) view.user_select._values = ["111"] await view.user_select.callback(mock_interaction_admin) - await admin_cog.db.delete_fixture(fixture_id) + await admin_cog.db.fixtures.delete_fixture(fixture_id) correct_button = _get_button(view, "Correct Results") await correct_button.callback(mock_interaction_admin) @@ -277,10 +277,10 @@ async def test_unified_panel_shows_partial_approval_buttons_for_pending_predicti mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 50, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -315,10 +315,10 @@ async def test_unified_panel_approve_partial_prediction( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 51, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -347,7 +347,7 @@ async def test_unified_panel_approve_partial_prediction( approve_button = _get_button(view, "Approve Late") await approve_button.callback(mock_interaction_admin) - prediction = await admin_cog.db.get_prediction(fixture_id, "111", "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") assert prediction is not None assert prediction["pending_partial_approval"] is False assert prediction["is_late"] == 0 @@ -360,10 +360,10 @@ async def test_unified_panel_reject_partial_prediction( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 52, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -392,7 +392,7 @@ async def test_unified_panel_reject_partial_prediction( reject_button = _get_button(view, "Reject Late") await reject_button.callback(mock_interaction_admin) - assert await admin_cog.db.get_prediction(fixture_id, "111", "111111") is None + assert await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") is None assert "rejected" in target_user.dm_sent[-1].lower() @pytest.mark.asyncio @@ -408,10 +408,10 @@ async def test_unified_panel_partial_review_edits_public_bot_post( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 70, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.update_fixture_announcement( + await admin_cog.db.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456", @@ -423,7 +423,7 @@ async def test_unified_panel_partial_review_edits_public_bot_post( admin_cog.bot.get_channel.side_effect = lambda channel_id: ( thread if channel_id == 789012 else None ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -469,10 +469,10 @@ async def test_unified_panel_partial_review_updates_thread_reaction( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 71, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.update_fixture_announcement( + await admin_cog.db.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456", @@ -489,7 +489,7 @@ async def test_unified_panel_partial_review_updates_thread_reaction( admin_cog.bot.get_channel.side_effect = lambda channel_id: ( thread if channel_id == 789012 else None ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -530,15 +530,15 @@ async def test_unified_panel_approve_partial_prediction_ignores_bad_fixture_thre mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 74, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.update_fixture_announcement( + await admin_cog.db.fixtures.update_fixture_announcement( fixture_id, message_id="not-a-thread-id", channel_id="123456", ) - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -569,7 +569,7 @@ async def test_unified_panel_approve_partial_prediction_ignores_bad_fixture_thre approve_button = _get_button(view, "Approve Late") await approve_button.callback(mock_interaction_admin) - prediction = await admin_cog.db.get_prediction(fixture_id, "111", "111111") + prediction = await admin_cog.db.predictions.get_prediction(fixture_id, "111", "111111") assert prediction is not None assert prediction["pending_partial_approval"] is False assert "approved" in target_user.dm_sent[-1].lower() @@ -581,11 +581,11 @@ async def test_unified_panel_approve_partial_prediction_recalculates_scores( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 53, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "999", "Full User", @@ -593,7 +593,7 @@ async def test_unified_panel_approve_partial_prediction_recalculates_scores( False, ) await admin_cog.service.calculate_fixture_scores(fixture_id, "111111") - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -622,7 +622,7 @@ async def test_unified_panel_approve_partial_prediction_recalculates_scores( approve_button = _get_button(view, "Approve Late") await approve_button.callback(mock_interaction_admin) - standings = await admin_cog.db.get_standings("111111") + standings = await admin_cog.db.scores.get_standings("111111") assert {row["user_id"] for row in standings} == {"999", "111"} @pytest.mark.asyncio @@ -632,11 +632,11 @@ async def test_unified_panel_reject_partial_prediction_recalculates_scores( mock_interaction_admin, sample_games, ): - fixture_id = await admin_cog.db.create_fixture( + fixture_id = await admin_cog.db.fixtures.create_fixture( "111111", 54, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await admin_cog.db.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await admin_cog.db.save_prediction( + await admin_cog.db.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await admin_cog.db.predictions.save_prediction( fixture_id, "999", "Full User", @@ -644,7 +644,7 @@ async def test_unified_panel_reject_partial_prediction_recalculates_scores( False, ) await admin_cog.service.calculate_fixture_scores(fixture_id, "111111") - await admin_cog.db.save_prediction( + await admin_cog.db.predictions.save_prediction( fixture_id, "111", "User One", @@ -673,5 +673,5 @@ async def test_unified_panel_reject_partial_prediction_recalculates_scores( reject_button = _get_button(view, "Reject Late") await reject_button.callback(mock_interaction_admin) - standings = await admin_cog.db.get_standings("111111") + standings = await admin_cog.db.scores.get_standings("111111") assert {row["user_id"] for row in standings} == {"999"} diff --git a/tests/test_admin_service.py b/tests/test_admin_service.py index 4ebcf70..e27e0d8 100644 --- a/tests/test_admin_service.py +++ b/tests/test_admin_service.py @@ -26,21 +26,21 @@ async def test_toggle_late_penalty_waiver_recalculates_closed_fixture( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) - timedelta(hours=2) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "late-user", "Late User", ["2-1", "1-1", "0-2"], True, ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) await service.calculate_fixture_scores(fixture_id, "111111") - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert standings[0]["total_points"] == 0 fixture, prediction, recalculation = await service.toggle_late_penalty_waiver( @@ -53,7 +53,7 @@ async def test_toggle_late_penalty_waiver_recalculates_closed_fixture( assert prediction["late_penalty_waived"] == 1 assert recalculation is not None - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert standings[0]["total_points"] == 9 @pytest.mark.asyncio @@ -64,18 +64,18 @@ async def test_toggle_late_waiver_rolls_back_if_recalculation_fails( monkeypatch, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) - timedelta(hours=2) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "late-user", "Late User", ["2-1", "1-1", "0-2"], True, ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) await service.calculate_fixture_scores(fixture_id, "111111") async def raise_recalc_error(*_args, **_kwargs): @@ -88,7 +88,7 @@ async def raise_recalc_error(*_args, **_kwargs): with pytest.raises(RuntimeError, match="boom"): await service.toggle_late_penalty_waiver(fixture_id, "late-user", "111111") - prediction = await database.get_prediction(fixture_id, "late-user", "111111") + prediction = await database.predictions.get_prediction(fixture_id, "late-user", "111111") assert prediction is not None assert prediction["late_penalty_waived"] == 0 @@ -106,12 +106,12 @@ async def test_wrong_guild_service_actions_raise_fixture_not_found( self, database, sample_games ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "guild-2", 1, sample_games, datetime.now(UTC) - timedelta(hours=2) ) original_prediction = ["1-0", "1-1"] original_results = ["0-0", "1-1", "2-2"] - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -120,7 +120,7 @@ async def test_wrong_guild_service_actions_raise_fixture_not_found( predicted_game_indexes=[0, 1], pending_partial_approval=True, ) - await database.save_results(fixture_id, original_results) + await database.results.save_results(fixture_id, original_results) with pytest.raises(FixtureNotFoundError): await service.get_fixture_prediction_summary(fixture_id, "111111") @@ -147,19 +147,19 @@ async def test_wrong_guild_service_actions_raise_fixture_not_found( "111111", ) - prediction = await database.get_prediction(fixture_id, "user-1", "guild-2") + prediction = await database.predictions.get_prediction(fixture_id, "user-1", "guild-2") assert prediction is not None assert prediction["predictions"] == original_prediction assert prediction["pending_partial_approval"] is True - assert await database.get_results(fixture_id) == original_results - assert await database.get_scores_for_fixture(fixture_id) == [] + assert await database.results.get_results(fixture_id) == original_results + assert await database.scores.get_scores_for_fixture(fixture_id) == [] @pytest.mark.asyncio async def test_get_fixture_prediction_summary_raises_typed_no_predictions( self, database, sample_games ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -171,7 +171,7 @@ async def test_replace_prediction_raises_typed_prediction_not_found( self, database, sample_games ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -189,10 +189,10 @@ async def test_toggle_waiver_raises_typed_prediction_disappeared( self, database, sample_games, monkeypatch ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) - timedelta(hours=2) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "late-user", "Late User", @@ -200,7 +200,7 @@ async def test_toggle_waiver_raises_typed_prediction_disappeared( True, ) - original_get_prediction = database.get_prediction + original_get_prediction = database.predictions.get_prediction async def disappear_after_toggle(request_fixture_id: int, user_id: str, guild_id: str): prediction = await original_get_prediction(request_fixture_id, user_id, guild_id) @@ -208,7 +208,7 @@ async def disappear_after_toggle(request_fixture_id: int, user_id: str, guild_id return None return prediction - monkeypatch.setattr(database, "get_prediction", disappear_after_toggle) + monkeypatch.setattr(database.predictions, "get_prediction", disappear_after_toggle) with pytest.raises(PredictionDisappearedError, match="waiver update"): await service.toggle_late_penalty_waiver(fixture_id, "late-user", "111111") @@ -224,11 +224,11 @@ async def test_replace_prediction_preserves_original_timing_metadata( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "user-1", "User One", @@ -236,7 +236,7 @@ async def test_replace_prediction_preserves_original_timing_metadata( True, ) - original = await database.get_prediction(fixture_id, "user-1", "111111") + original = await database.predictions.get_prediction(fixture_id, "user-1", "111111") assert original is not None fixture, updated_prediction, recalculation = await service.replace_prediction( @@ -263,18 +263,18 @@ async def test_replace_prediction_rolls_back_if_recalculation_fails( monkeypatch, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "user-1", "User One", ["1-0", "1-1", "0-0"], False, ) - await database.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await database.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) await service.calculate_fixture_scores(fixture_id, "111111") async def raise_recalc_error(*_args, **_kwargs): @@ -293,7 +293,7 @@ async def raise_recalc_error(*_args, **_kwargs): "111111", ) - prediction = await database.get_prediction(fixture_id, "user-1", "111111") + prediction = await database.predictions.get_prediction(fixture_id, "user-1", "111111") assert prediction is not None assert prediction["predictions"] == ["1-0", "1-1", "0-0"] assert prediction["admin_edited_by"] is None @@ -305,21 +305,21 @@ async def test_replace_prediction_recalculates_scored_fixture( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "user-1", "User One", ["1-0", "1-1", "0-0"], False, ) - await database.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await database.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) await service.calculate_fixture_scores(fixture_id, "111111") - before = await database.get_standings("111111") + before = await database.scores.get_standings("111111") assert before[0]["total_points"] == 9 _fixture, updated_prediction, recalculation = await service.replace_prediction( @@ -333,7 +333,7 @@ async def test_replace_prediction_recalculates_scored_fixture( assert updated_prediction["admin_edited_by"] == "admin-1" assert recalculation is not None - after = await database.get_standings("111111") + after = await database.scores.get_standings("111111") assert after[0]["total_points"] == 2 @@ -347,41 +347,41 @@ async def test_correct_results_recalculates_fixture_scores_and_standings( sample_games, ): service = AdminService(database) - fixture1_id = await database.create_fixture( + fixture1_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - fixture2_id = await database.create_fixture( + fixture2_id = await database.fixtures.create_fixture( "111111", 2, sample_games, datetime.now(UTC) + timedelta(days=2) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture1_id, "user-1", "User One", ["1-0", "1-1", "0-2"], False, ) - await database.save_prediction( + await database.predictions.save_prediction( fixture1_id, "user-2", "User Two", ["2-1", "1-1", "0-2"], False, ) - await database.save_results(fixture1_id, ["1-0", "1-1", "0-2"]) + await database.results.save_results(fixture1_id, ["1-0", "1-1", "0-2"]) await service.calculate_fixture_scores(fixture1_id, "111111") - await database.save_prediction( + await database.predictions.save_prediction( fixture2_id, "user-1", "User One", ["2-0", "0-0", "1-1"], False, ) - await database.save_results(fixture2_id, ["2-0", "0-0", "1-1"]) + await database.results.save_results(fixture2_id, ["2-0", "0-0", "1-1"]) await service.calculate_fixture_scores(fixture2_id, "111111") - before = await database.get_standings("111111") + before = await database.scores.get_standings("111111") assert before[0]["user_id"] == "user-1" assert before[0]["total_points"] == 18 assert before[1]["total_points"] == 7 @@ -396,7 +396,7 @@ async def test_correct_results_recalculates_fixture_scores_and_standings( assert results == ["2-1", "1-1", "0-2"] assert recalculation is not None - after = await database.get_standings("111111") + after = await database.scores.get_standings("111111") assert after[0]["user_id"] == "user-1" assert after[0]["total_points"] == 16 assert after[1]["user_id"] == "user-2" @@ -420,17 +420,17 @@ async def test_correct_results_rolls_back_if_recalculation_fails( monkeypatch, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "user-1", "User One", ["1-0", "1-1", "0-0"], False, ) - await database.save_results(fixture_id, ["1-0", "1-1", "0-0"]) + await database.results.save_results(fixture_id, ["1-0", "1-1", "0-0"]) await service.calculate_fixture_scores(fixture_id, "111111") async def raise_recalc_error(*_args, **_kwargs): @@ -445,7 +445,7 @@ async def raise_recalc_error(*_args, **_kwargs): "111111", ) - assert await database.get_results(fixture_id) == ["1-0", "1-1", "0-0"] + assert await database.results.get_results(fixture_id) == ["1-0", "1-1", "0-0"] class TestPartialPredictionApproval: @@ -456,18 +456,18 @@ async def test_pending_partial_predictions_are_excluded_from_scoring( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 3, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "111", "Full User", ["2-1", "1-1", "0-2"], False, ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "222", "Pending User", @@ -488,22 +488,22 @@ async def test_calculated_score_payload_uses_fixture_guild_standings( sample_games, ): service = AdminService(database) - current_fixture_id = await database.create_fixture( + current_fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - other_fixture_id = await database.create_fixture( + other_fixture_id = await database.fixtures.create_fixture( "guild-2", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(current_fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(current_fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( current_fixture_id, "shared-user", "Guild One", ["2-1", "1-1", "0-2"], False, ) - await database.save_results(other_fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(other_fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( other_fixture_id, "shared-user", "Guild Two", @@ -527,11 +527,11 @@ async def test_approved_partial_prediction_scores_only_selected_rows( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 4, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "222", "Partial User", @@ -563,11 +563,11 @@ async def test_initial_calculation_and_recalculation_persist_same_scores( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 4, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "partial", "Partial User", @@ -575,8 +575,8 @@ async def test_initial_calculation_and_recalculation_persist_same_scores( True, predicted_game_indexes=[1, 2], ) - await database.set_late_penalty_waiver(fixture_id, "partial", True) - await database.save_prediction( + await database.predictions.set_late_penalty_waiver(fixture_id, "partial", True) + await database.predictions.save_prediction( fixture_id, "full", "Full User", @@ -585,10 +585,10 @@ async def test_initial_calculation_and_recalculation_persist_same_scores( ) await service.calculate_fixture_scores(fixture_id, "111111") - initial_scores = await database.get_scores_for_fixture(fixture_id) + initial_scores = await database.scores.get_scores_for_fixture(fixture_id) - assert await database.save_results_with_recalc(fixture_id, ["2-1", "1-1", "0-2"]) - recalculated_scores = await database.get_scores_for_fixture(fixture_id) + assert await database.results.save_results_with_recalc(fixture_id, ["2-1", "1-1", "0-2"]) + recalculated_scores = await database.scores.get_scores_for_fixture(fixture_id) score_fields = ("user_id", "points", "exact_scores", "correct_results") assert [tuple(score[field] for field in score_fields) for score in recalculated_scores] == [ @@ -606,11 +606,11 @@ async def test_approve_partial_prediction_recalculates_scored_fixture( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 5, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "111", "Full User", @@ -618,7 +618,7 @@ async def test_approve_partial_prediction_recalculates_scored_fixture( False, ) await service.calculate_fixture_scores(fixture_id, "111111") - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "222", "Pending User", @@ -637,7 +637,7 @@ async def test_approve_partial_prediction_recalculates_scored_fixture( assert approved_prediction["pending_partial_approval"] is False assert recalculation is not None - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert {row["user_id"] for row in standings} == {"111", "222"} @pytest.mark.asyncio @@ -648,11 +648,11 @@ async def test_approve_partial_prediction_rolls_back_if_recalculation_fails( monkeypatch, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 5, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "111", "Full User", @@ -660,7 +660,7 @@ async def test_approve_partial_prediction_rolls_back_if_recalculation_fails( False, ) await service.calculate_fixture_scores(fixture_id, "111111") - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "222", "Pending User", @@ -680,7 +680,7 @@ async def raise_recalc_error(*_args, **_kwargs): with pytest.raises(RuntimeError, match="boom"): await service.approve_partial_prediction(fixture_id, "222", "admin-1", "111111") - prediction = await database.get_prediction(fixture_id, "222", "111111") + prediction = await database.predictions.get_prediction(fixture_id, "222", "111111") assert prediction is not None assert prediction["pending_partial_approval"] is True assert prediction["is_late"] == 1 @@ -693,11 +693,11 @@ async def test_reject_partial_prediction_recalculates_scored_fixture( sample_games, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 6, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "111", "Full User", @@ -705,7 +705,7 @@ async def test_reject_partial_prediction_recalculates_scored_fixture( False, ) await service.calculate_fixture_scores(fixture_id, "111111") - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "222", "Pending User", @@ -723,7 +723,7 @@ async def test_reject_partial_prediction_recalculates_scored_fixture( assert prediction["pending_partial_approval"] is True assert recalculation is not None - assert await database.get_prediction(fixture_id, "222", "111111") is None + assert await database.predictions.get_prediction(fixture_id, "222", "111111") is None @pytest.mark.asyncio async def test_reject_partial_prediction_rolls_back_if_recalculation_fails( @@ -733,11 +733,11 @@ async def test_reject_partial_prediction_rolls_back_if_recalculation_fails( monkeypatch, ): service = AdminService(database) - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 6, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - await database.save_prediction( + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.predictions.save_prediction( fixture_id, "111", "Full User", @@ -745,7 +745,7 @@ async def test_reject_partial_prediction_rolls_back_if_recalculation_fails( False, ) await service.calculate_fixture_scores(fixture_id, "111111") - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, "222", "Pending User", @@ -765,7 +765,7 @@ async def raise_recalc_error(*_args, **_kwargs): with pytest.raises(RuntimeError, match="boom"): await service.reject_partial_prediction(fixture_id, "222", "111111") - prediction = await database.get_prediction(fixture_id, "222", "111111") + prediction = await database.predictions.get_prediction(fixture_id, "222", "111111") assert prediction is not None assert prediction["pending_partial_approval"] is True assert prediction["is_late"] == 1 diff --git a/tests/test_bot.py b/tests/test_bot.py index 4d3dead..1a5977b 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -260,7 +260,7 @@ def bot_instance(self): with patch("typer_bot.bot.commands.Bot.__init__", return_value=None): bot = TyperBot.__new__(TyperBot) bot.db = MagicMock() - bot.db.get_guild_config = AsyncMock(return_value=None) + bot.db.guild_config.get_guild_config = AsyncMock(return_value=None) yield bot @pytest.mark.asyncio @@ -275,7 +275,7 @@ async def test_on_guild_join_logs_invite_and_setup_state(self, bot_instance): with patch("typer_bot.bot.logger") as mock_logger: await bot_instance.on_guild_join(guild) - bot_instance.db.get_guild_config.assert_awaited_once_with("123456") + bot_instance.db.guild_config.get_guild_config.assert_awaited_once_with("123456") info_call = mock_logger.info.call_args assert info_call.args[0] == "Joined guild" assert info_call.kwargs["extra"] == { @@ -288,7 +288,7 @@ async def test_on_guild_join_logs_invite_and_setup_state(self, bot_instance): @pytest.mark.asyncio async def test_on_guild_join_logs_configured_state(self, bot_instance): - bot_instance.db.get_guild_config.return_value = {"guild_id": "123456"} + bot_instance.db.guild_config.get_guild_config.return_value = {"guild_id": "123456"} guild = MagicMock() guild.id = 123456 guild.name = "Configured Guild" @@ -303,7 +303,7 @@ async def test_on_guild_join_logs_configured_state(self, bot_instance): @pytest.mark.asyncio async def test_on_guild_join_logs_even_when_setup_lookup_fails(self, bot_instance): - bot_instance.db.get_guild_config.side_effect = RuntimeError("db unavailable") + bot_instance.db.guild_config.get_guild_config.side_effect = RuntimeError("db unavailable") guild = MagicMock() guild.id = 123456 guild.name = "New Guild" @@ -373,7 +373,7 @@ async def test_sync_fixture_thread_uses_stored_channel_id(self, bot_instance): message.thread = MagicMock(id=789012) channel = MagicMock() channel.fetch_message = AsyncMock(return_value=message) - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) bot_instance.get_channel.return_value = channel await bot_instance._sync_fixture_thread() @@ -406,7 +406,7 @@ async def test_sync_fixture_thread_scans_owning_guild_channels_only_without_chan guild.text_channels = [miss_channel, hit_channel] bot_instance.guilds = [other_guild, guild] bot_instance.get_guild.return_value = guild - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) await bot_instance._sync_fixture_thread() @@ -424,7 +424,7 @@ async def test_sync_fixture_thread_does_not_scan_when_stored_channel_missing( guild = MagicMock() guild.text_channels = [MagicMock()] bot_instance.guilds = [guild] - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) bot_instance.get_channel.return_value = None bot_instance.fetch_channel.side_effect = discord.NotFound(MagicMock(), "missing") @@ -442,7 +442,7 @@ async def test_sync_fixture_thread_does_not_scan_when_stored_channel_message_mis guild = MagicMock() guild.text_channels = [MagicMock()] bot_instance.guilds = [guild] - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) bot_instance.get_channel.return_value = channel await bot_instance._sync_fixture_thread() @@ -457,7 +457,7 @@ async def test_sync_fixture_thread_fetches_stored_channel_when_not_cached(self, message.thread = MagicMock(id=789012) channel = MagicMock() channel.fetch_message = AsyncMock(return_value=message) - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) bot_instance.get_channel.return_value = None bot_instance.fetch_channel.return_value = channel @@ -477,7 +477,7 @@ async def test_sync_fixture_thread_does_not_scan_with_invalid_stored_ids(self, b guild = MagicMock() guild.text_channels = [MagicMock()] bot_instance.guilds = [guild] - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) await bot_instance._sync_fixture_thread() @@ -511,7 +511,7 @@ async def test_reminder_24h_triggered_at_correct_time(self, mock_now, bot_instan "deadline": deadline, "week_number": 1, } - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) await bot_instance.reminder_task() @@ -531,7 +531,7 @@ async def test_reminder_1h_triggered_at_correct_time(self, mock_now, bot_instanc "deadline": deadline, "week_number": 1, } - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) await bot_instance.reminder_task() @@ -551,7 +551,7 @@ async def test_reminder_sent_at_exact_time(self, bot_instance): "deadline": deadline, "week_number": 1, } - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[fixture]) with freeze_time(current_time): await bot_instance.reminder_task() @@ -566,7 +566,7 @@ async def test_reminder_sent_at_exact_time(self, bot_instance): async def test_reminder_skips_if_no_fixture(self, mock_now, bot_instance): """Reminders are skipped when no fixture is active.""" mock_now.return_value = datetime.now(UTC) - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock(return_value=[]) await bot_instance.reminder_task() @@ -580,7 +580,9 @@ async def test_reminder_checks_all_open_fixtures(self, mock_now, bot_instance): mock_now.return_value = deadline - timedelta(hours=24) fixture_a = {"id": 1, "guild_id": "111111", "deadline": deadline, "week_number": 1} fixture_b = {"id": 2, "guild_id": "222222", "deadline": deadline, "week_number": 2} - bot_instance.db.get_all_open_fixtures = AsyncMock(return_value=[fixture_a, fixture_b]) + bot_instance.db.fixtures.get_all_open_fixtures = AsyncMock( + return_value=[fixture_a, fixture_b] + ) await bot_instance.reminder_task() @@ -607,7 +609,9 @@ async def test_send_reminder_to_configured_channel(self, bot_instance): mock_channel = MagicMock() mock_channel.send = AsyncMock() bot_instance.get_channel.return_value = mock_channel - bot_instance.db.get_guild_config = AsyncMock(return_value={"league_channel_id": "123456"}) + bot_instance.db.guild_config.get_guild_config = AsyncMock( + return_value={"league_channel_id": "123456"} + ) fixture = { "id": 1, @@ -634,7 +638,9 @@ async def test_send_reminder_routes_each_guild_to_its_configured_channel(self, b "222222": {"league_channel_id": "234567"}, } channels = {123456: channel_one, 234567: channel_two} - bot_instance.db.get_guild_config = AsyncMock(side_effect=lambda guild_id: configs[guild_id]) + bot_instance.db.guild_config.get_guild_config = AsyncMock( + side_effect=lambda guild_id: configs[guild_id] + ) bot_instance.get_channel.side_effect = lambda channel_id: channels[channel_id] deadline = datetime.now(UTC) + timedelta(days=1) @@ -653,7 +659,7 @@ async def test_send_reminder_routes_each_guild_to_its_configured_channel(self, b @pytest.mark.asyncio async def test_send_reminder_missing_guild_config(self, bot_instance): """Missing channel configuration skips delivery.""" - bot_instance.db.get_guild_config = AsyncMock(return_value=None) + bot_instance.db.guild_config.get_guild_config = AsyncMock(return_value=None) fixture = {"id": 1, "guild_id": "111111", "deadline": datetime.now(UTC), "week_number": 1} await bot_instance.send_reminder(fixture, "24 hours remaining") diff --git a/tests/test_integration.py b/tests/test_integration.py index c2e6e65..f033daf 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -13,23 +13,27 @@ async def test_full_workflow_create_predict_results_calculate(self, database): """End-to-end workflow produces correct standings.""" games = ["Team A - Team B", "Team C - Team D", "Team E - Team F"] deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, games, deadline) - await database.update_fixture_announcement( + fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + await database.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456" ) - fixture = await database.get_fixture_by_id(fixture_id, "111111") + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture["week_number"] == 1 assert len(fixture["games"]) == 3 - await database.save_prediction(fixture_id, "user1", "User1", ["2-1", "1-1", "0-2"], False) - await database.save_prediction(fixture_id, "user2", "User2", ["1-0", "2-2", "1-1"], False) + await database.predictions.save_prediction( + fixture_id, "user1", "User1", ["2-1", "1-1", "0-2"], False + ) + await database.predictions.save_prediction( + fixture_id, "user2", "User2", ["1-0", "2-2", "1-1"], False + ) - predictions = await database.get_all_predictions(fixture_id) + predictions = await database.predictions.get_all_predictions(fixture_id) assert len(predictions) == 2 - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) - results = await database.get_results(fixture_id) + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + results = await database.results.get_results(fixture_id) assert results == ["2-1", "1-1", "0-2"] from typer_bot.utils.scoring import calculate_points @@ -48,7 +52,7 @@ async def test_full_workflow_create_predict_results_calculate(self, database): ) scores.sort(key=lambda x: x["points"], reverse=True) - await database.save_scores(fixture_id, scores) + await database.scores.save_scores(fixture_id, scores) assert scores[0]["user_name"] == "User1" assert scores[0]["points"] == 9 @@ -59,14 +63,16 @@ async def test_multi_user_late_predictions(self, database): """Late predictions receive 0 points (100% penalty).""" games = ["Team A - Team B", "Team C - Team D", "Team E - Team F"] deadline = datetime.now(UTC) - timedelta(hours=1) - fixture_id = await database.create_fixture("111111", 1, games, deadline) + fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture_id, "user1", "User1", ["2-1", "1-1", "0-2"], True) + await database.predictions.save_prediction( + fixture_id, "user1", "User1", ["2-1", "1-1", "0-2"], True + ) - predictions = await database.get_all_predictions(fixture_id) + predictions = await database.predictions.get_all_predictions(fixture_id) assert predictions[0]["is_late"] == 1 # SQLite returns BOOLEAN columns as ints. - await database.save_results(fixture_id, ["2-1", "1-1", "0-2"]) + await database.results.save_results(fixture_id, ["2-1", "1-1", "0-2"]) from typer_bot.utils.scoring import calculate_points @@ -85,7 +91,7 @@ async def test_multi_user_late_predictions(self, database): } ) - await database.save_scores(fixture_id, scores) + await database.scores.save_scores(fixture_id, scores) assert scores[0]["points"] == 0 @@ -94,12 +100,16 @@ async def test_prediction_resubmission_replaces_existing(self, database): """Re-submitting a prediction replaces the existing one (upsert behavior).""" games = ["Team A - Team B", "Team C - Team D"] deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, games, deadline) + fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture_id, "user1", "User1", ["2-1", "1-0"], False) - await database.save_prediction(fixture_id, "user1", "User1", ["3-0", "2-1"], False) + await database.predictions.save_prediction( + fixture_id, "user1", "User1", ["2-1", "1-0"], False + ) + await database.predictions.save_prediction( + fixture_id, "user1", "User1", ["3-0", "2-1"], False + ) - predictions = await database.get_all_predictions(fixture_id) + predictions = await database.predictions.get_all_predictions(fixture_id) assert len(predictions) == 1 assert predictions[0]["predictions"] == ["3-0", "2-1"] @@ -112,25 +122,25 @@ async def test_delete_fixture_with_predictions(self, database): """Cascading deletion removes predictions and results.""" games = ["Team A - Team B"] deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, games, deadline) + fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture_id, "user1", "User1", "2-1", False) - await database.save_results(fixture_id, ["2-1"]) + await database.predictions.save_prediction(fixture_id, "user1", "User1", "2-1", False) + await database.results.save_results(fixture_id, ["2-1"]) - await database.delete_fixture(fixture_id) + await database.fixtures.delete_fixture(fixture_id) - assert await database.get_fixture_by_id(fixture_id, "111111") is None - assert len(await database.get_all_predictions(fixture_id)) == 0 + assert await database.fixtures.get_fixture_by_id(fixture_id, "111111") is None + assert len(await database.predictions.get_all_predictions(fixture_id)) == 0 @pytest.mark.asyncio async def test_standings_accumulate_across_fixtures(self, database): """Standings accumulate across multiple fixtures.""" games = ["Team A - Team B"] deadline = datetime.now(UTC) - timedelta(days=2) - fixture1_id = await database.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture1_id, "user1", "User1", "2-1", False) - await database.save_results(fixture1_id, ["2-1"]) - await database.save_scores( + fixture1_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + await database.predictions.save_prediction(fixture1_id, "user1", "User1", "2-1", False) + await database.results.save_results(fixture1_id, ["2-1"]) + await database.scores.save_scores( fixture1_id, [ { @@ -144,10 +154,10 @@ async def test_standings_accumulate_across_fixtures(self, database): ) deadline = datetime.now(UTC) - timedelta(days=1) - fixture2_id = await database.create_fixture("111111", 2, games, deadline) - await database.save_prediction(fixture2_id, "user1", "User1", "1-0", False) - await database.save_results(fixture2_id, ["1-0"]) - await database.save_scores( + fixture2_id = await database.fixtures.create_fixture("111111", 2, games, deadline) + await database.predictions.save_prediction(fixture2_id, "user1", "User1", "1-0", False) + await database.results.save_results(fixture2_id, ["1-0"]) + await database.scores.save_scores( fixture2_id, [ { @@ -160,7 +170,7 @@ async def test_standings_accumulate_across_fixtures(self, database): ], ) - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert len(standings) == 1 assert standings[0]["total_points"] == 6 @@ -168,10 +178,10 @@ async def test_standings_accumulate_across_fixtures(self, database): async def test_standings_are_guild_scoped_for_same_user(self, database): games = ["Team A - Team B"] deadline = datetime.now(UTC) - timedelta(days=1) - guild_one_fixture_id = await database.create_fixture("111111", 1, games, deadline) - guild_two_fixture_id = await database.create_fixture("guild-2", 1, games, deadline) + guild_one_fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + guild_two_fixture_id = await database.fixtures.create_fixture("guild-2", 1, games, deadline) - await database.save_scores( + await database.scores.save_scores( guild_one_fixture_id, [ { @@ -183,7 +193,7 @@ async def test_standings_are_guild_scoped_for_same_user(self, database): } ], ) - await database.save_scores( + await database.scores.save_scores( guild_two_fixture_id, [ { @@ -196,8 +206,8 @@ async def test_standings_are_guild_scoped_for_same_user(self, database): ], ) - guild_one_standings = await database.get_standings("111111") - guild_two_standings = await database.get_standings("guild-2") + guild_one_standings = await database.scores.get_standings("111111") + guild_two_standings = await database.scores.get_standings("guild-2") assert [row["user_id"] for row in guild_one_standings] == ["shared-user"] assert guild_one_standings[0]["user_name"] == "Guild One" @@ -212,10 +222,10 @@ async def test_standings_are_guild_scoped_for_same_user(self, database): async def test_last_fixture_scores_are_guild_scoped(self, database): games = ["Team A - Team B"] deadline = datetime.now(UTC) - timedelta(days=1) - guild_one_fixture_id = await database.create_fixture("111111", 1, games, deadline) - guild_two_fixture_id = await database.create_fixture("guild-2", 2, games, deadline) + guild_one_fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + guild_two_fixture_id = await database.fixtures.create_fixture("guild-2", 2, games, deadline) - await database.save_scores( + await database.scores.save_scores( guild_one_fixture_id, [ { @@ -227,7 +237,7 @@ async def test_last_fixture_scores_are_guild_scoped(self, database): } ], ) - await database.save_scores( + await database.scores.save_scores( guild_two_fixture_id, [ { @@ -240,8 +250,8 @@ async def test_last_fixture_scores_are_guild_scoped(self, database): ], ) - guild_one_last = await database.get_last_fixture_scores("111111") - guild_two_last = await database.get_last_fixture_scores("guild-2") + guild_one_last = await database.scores.get_last_fixture_scores("111111") + guild_two_last = await database.scores.get_last_fixture_scores("guild-2") assert guild_one_last is not None assert guild_one_last["fixture_id"] == guild_one_fixture_id @@ -255,10 +265,10 @@ async def test_max_week_number_increment(self, database): games = ["Team A - Team B"] deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 5, games, deadline) - await database.create_fixture("111111", 3, games, deadline) + await database.fixtures.create_fixture("111111", 5, games, deadline) + await database.fixtures.create_fixture("111111", 3, games, deadline) - max_week = await database.get_max_week_number("111111") + max_week = await database.fixtures.get_max_week_number("111111") assert max_week == 5 @pytest.mark.asyncio @@ -267,12 +277,14 @@ async def test_calculating_one_fixture_keeps_other_fixture_open(self, database): games = ["Team A - Team B", "Team C - Team D"] deadline = datetime.now(UTC) + timedelta(days=1) - fixture_one_id = await database.create_fixture("111111", 1, games, deadline) - fixture_two_id = await database.create_fixture("111111", 2, games, deadline) + fixture_one_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, games, deadline) - await database.save_prediction(fixture_one_id, "user1", "User1", ["2-1", "1-1"], False) - await database.save_results(fixture_one_id, ["2-1", "1-1"]) - await database.save_scores( + await database.predictions.save_prediction( + fixture_one_id, "user1", "User1", ["2-1", "1-1"], False + ) + await database.results.save_results(fixture_one_id, ["2-1", "1-1"]) + await database.scores.save_scores( fixture_one_id, [ { @@ -285,9 +297,9 @@ async def test_calculating_one_fixture_keeps_other_fixture_open(self, database): ], ) - fixture_one = await database.get_fixture_by_id(fixture_one_id, "111111") - fixture_two = await database.get_fixture_by_id(fixture_two_id, "111111") - open_fixtures = await database.get_open_fixtures("111111") + fixture_one = await database.fixtures.get_fixture_by_id(fixture_one_id, "111111") + fixture_two = await database.fixtures.get_fixture_by_id(fixture_two_id, "111111") + open_fixtures = await database.fixtures.get_open_fixtures("111111") assert fixture_one is not None assert fixture_one["status"] == "closed" @@ -304,12 +316,12 @@ async def test_prediction_uniqueness_per_user_fixture(self, database): """One prediction per user per fixture - re-submission replaces existing.""" games = ["Team A - Team B"] deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, games, deadline) + fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture_id, "user1", "User1", ["2-1"], False) - await database.save_prediction(fixture_id, "user1", "User1", ["1-0"], False) + await database.predictions.save_prediction(fixture_id, "user1", "User1", ["2-1"], False) + await database.predictions.save_prediction(fixture_id, "user1", "User1", ["1-0"], False) - predictions = await database.get_all_predictions(fixture_id) + predictions = await database.predictions.get_all_predictions(fixture_id) assert len(predictions) == 1 assert predictions[0]["predictions"] == ["1-0"] @@ -323,10 +335,10 @@ async def test_standings_aggregates_same_user_with_different_names(self, databas games = ["Team A - Team B"] deadline = datetime.now(UTC) - timedelta(days=2) - fixture1_id = await database.create_fixture("111111", 1, games, deadline) - await database.save_prediction(fixture1_id, "user1", "st4chu", ["2-1"], False) - await database.save_results(fixture1_id, ["2-1"]) - await database.save_scores( + fixture1_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + await database.predictions.save_prediction(fixture1_id, "user1", "st4chu", ["2-1"], False) + await database.results.save_results(fixture1_id, ["2-1"]) + await database.scores.save_scores( fixture1_id, [ { @@ -340,10 +352,10 @@ async def test_standings_aggregates_same_user_with_different_names(self, databas ) deadline = datetime.now(UTC) - timedelta(days=1) - fixture2_id = await database.create_fixture("111111", 2, games, deadline) - await database.save_prediction(fixture2_id, "user1", "Stachu", ["1-0"], False) - await database.save_results(fixture2_id, ["1-0"]) - await database.save_scores( + fixture2_id = await database.fixtures.create_fixture("111111", 2, games, deadline) + await database.predictions.save_prediction(fixture2_id, "user1", "Stachu", ["1-0"], False) + await database.results.save_results(fixture2_id, ["1-0"]) + await database.scores.save_scores( fixture2_id, [ { @@ -356,7 +368,7 @@ async def test_standings_aggregates_same_user_with_different_names(self, databas ], ) - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert len(standings) == 1 assert standings[0]["total_points"] == 6 assert standings[0]["weeks_played"] == 2 @@ -367,11 +379,11 @@ async def test_standings_shows_latest_name_when_changed_multiple_times(self, dat """Should show the most recent username from latest fixture.""" games = ["Team A - Team B"] - fixture1_id = await database.create_fixture( + fixture1_id = await database.fixtures.create_fixture( "111111", 1, games, datetime.now(UTC) - timedelta(days=3) ) - await database.save_results(fixture1_id, ["2-1"]) - await database.save_scores( + await database.results.save_results(fixture1_id, ["2-1"]) + await database.scores.save_scores( fixture1_id, [ { @@ -384,11 +396,11 @@ async def test_standings_shows_latest_name_when_changed_multiple_times(self, dat ], ) - fixture2_id = await database.create_fixture( + fixture2_id = await database.fixtures.create_fixture( "111111", 2, games, datetime.now(UTC) - timedelta(days=2) ) - await database.save_results(fixture2_id, ["2-1"]) - await database.save_scores( + await database.results.save_results(fixture2_id, ["2-1"]) + await database.scores.save_scores( fixture2_id, [ { @@ -401,11 +413,11 @@ async def test_standings_shows_latest_name_when_changed_multiple_times(self, dat ], ) - fixture3_id = await database.create_fixture( + fixture3_id = await database.fixtures.create_fixture( "111111", 3, games, datetime.now(UTC) - timedelta(days=1) ) - await database.save_results(fixture3_id, ["2-1"]) - await database.save_scores( + await database.results.save_results(fixture3_id, ["2-1"]) + await database.scores.save_scores( fixture3_id, [ { @@ -418,7 +430,7 @@ async def test_standings_shows_latest_name_when_changed_multiple_times(self, dat ], ) - standings = await database.get_standings("111111") + standings = await database.scores.get_standings("111111") assert len(standings) == 1 assert standings[0]["user_name"] == "NewName" assert standings[0]["total_points"] == 3 diff --git a/tests/test_seed_test_data.py b/tests/test_seed_test_data.py index f985ce5..7ef2800 100644 --- a/tests/test_seed_test_data.py +++ b/tests/test_seed_test_data.py @@ -37,11 +37,11 @@ async def test_seed_mixed_data_creates_expected_fixture_states(temp_db_path, tmp from typer_bot.database import Database db = Database(temp_db_path) - open_fixtures = await db.get_open_fixtures(DEFAULT_MANUAL_GUILD_ID) - standings = await db.get_standings(DEFAULT_MANUAL_GUILD_ID) - week_one = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 1) - week_two = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) - week_three = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 3) + open_fixtures = await db.fixtures.get_open_fixtures(DEFAULT_MANUAL_GUILD_ID) + standings = await db.scores.get_standings(DEFAULT_MANUAL_GUILD_ID) + week_one = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 1) + week_two = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) + week_three = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 3) assert [fixture["week_number"] for fixture in open_fixtures] == [2, 3] assert week_one is not None @@ -56,11 +56,11 @@ async def test_seed_mixed_data_creates_expected_fixture_states(temp_db_path, tmp assert standings[1]["user_name"] == "Seed Beta" assert standings[1]["total_points"] == 5 - week_one_results = await db.get_results(week_one["id"]) - week_one_scores = await db.get_scores_for_fixture(week_one["id"]) + week_one_results = await db.results.get_results(week_one["id"]) + week_one_scores = await db.scores.get_scores_for_fixture(week_one["id"]) - open_predictions = await db.get_all_predictions(week_two["id"]) - late_predictions = await db.get_all_predictions(week_three["id"]) + open_predictions = await db.predictions.get_all_predictions(week_two["id"]) + late_predictions = await db.predictions.get_all_predictions(week_three["id"]) assert week_one_results == ["2-1", "1-1", "0-2"] assert [score["user_name"] for score in week_one_scores] == ["Seed Alpha", "Seed Beta"] @@ -80,18 +80,18 @@ async def test_seed_mixed_data_includes_real_tester_when_user_id_provided(temp_d from typer_bot.database import Database db = Database(temp_db_path) - week_one = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 1) - week_two = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) + week_one = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 1) + week_two = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) assert week_one is not None assert week_two is not None - tester_scored_prediction = await db.get_prediction( + tester_scored_prediction = await db.predictions.get_prediction( week_one["id"], tester_user_id, DEFAULT_MANUAL_GUILD_ID ) - tester_prediction = await db.get_prediction( + tester_prediction = await db.predictions.get_prediction( week_two["id"], tester_user_id, DEFAULT_MANUAL_GUILD_ID ) - synthetic_prediction = await db.get_prediction( + synthetic_prediction = await db.predictions.get_prediction( week_two["id"], "seed-user-1", DEFAULT_MANUAL_GUILD_ID ) @@ -110,14 +110,14 @@ async def test_seed_mixed_data_resets_existing_database(temp_db_path, tmp_path): from typer_bot.database import Database db = Database(temp_db_path) - week_two = await db.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) + week_two = await db.fixtures.get_fixture_by_week(DEFAULT_MANUAL_GUILD_ID, 2) assert week_two is not None - await db.delete_fixture(week_two["id"]) + await db.fixtures.delete_fixture(week_two["id"]) await seed_mixed_test_data(temp_db_path, str(backup_dir), None, force_reset=True) refreshed_db = Database(temp_db_path) - open_fixtures = await refreshed_db.get_open_fixtures(DEFAULT_MANUAL_GUILD_ID) + open_fixtures = await refreshed_db.fixtures.get_open_fixtures(DEFAULT_MANUAL_GUILD_ID) assert [fixture["week_number"] for fixture in open_fixtures] == [2, 3] @@ -201,13 +201,15 @@ async def test_auto_seed_populates_empty_non_production_database(temp_db_path): ) seeded_db = Database(temp_db_path) - open_fixtures = await seeded_db.get_open_fixtures("111111") - week_two = await seeded_db.get_fixture_by_week("111111", 2) + open_fixtures = await seeded_db.fixtures.get_open_fixtures("111111") + week_two = await seeded_db.fixtures.get_fixture_by_week("111111", 2) assert seeded is True assert [fixture["week_number"] for fixture in open_fixtures] == [2, 3] assert week_two is not None - assert await seeded_db.get_prediction(week_two["id"], "tester-1", "111111") is not None + assert ( + await seeded_db.predictions.get_prediction(week_two["id"], "tester-1", "111111") is not None + ) @pytest.mark.asyncio @@ -216,7 +218,7 @@ async def test_auto_seed_does_not_reset_non_empty_database(temp_db_path): db = Database(temp_db_path) await db.initialize() - fixture_id = await db.create_fixture("111111", 99, ["Existing A - Existing B"], now()) + fixture_id = await db.fixtures.create_fixture("111111", 99, ["Existing A - Existing B"], now()) seeded = await maybe_auto_seed_test_data( temp_db_path, @@ -226,8 +228,8 @@ async def test_auto_seed_does_not_reset_non_empty_database(temp_db_path): guild_id="111111", ) - existing_fixture = await db.get_fixture_by_id(fixture_id, "111111") - open_fixtures = await db.get_open_fixtures("111111") + existing_fixture = await db.fixtures.get_fixture_by_id(fixture_id, "111111") + open_fixtures = await db.fixtures.get_open_fixtures("111111") assert seeded is False assert existing_fixture is not None diff --git a/tests/test_thread_prediction_handler.py b/tests/test_thread_prediction_handler.py index 90269a8..9575279 100644 --- a/tests/test_thread_prediction_handler.py +++ b/tests/test_thread_prediction_handler.py @@ -102,7 +102,7 @@ async def test_saves_valid_predictions(self, handler, fixture_with_thread, mock_ assert result is True assert "✅" in mock_message.reactions_added - predictions = await handler.db.get_all_predictions(fixture_with_thread["id"]) + predictions = await handler.db.predictions.get_all_predictions(fixture_with_thread["id"]) assert len(predictions) == 1 assert predictions[0]["user_id"] == "123456" assert not predictions[0]["is_late"] @@ -110,10 +110,10 @@ async def test_saves_valid_predictions(self, handler, fixture_with_thread, mock_ @pytest.mark.asyncio async def test_marks_late_predictions(self, handler, database, mock_message, sample_games): """Should mark predictions as late when past deadline.""" - await database.update_active_scoring_rules("111111", {"late_prediction_points": 1}) + await database.seasons.update_active_scoring_rules("111111", {"late_prediction_points": 1}) deadline = datetime.now(UTC) - timedelta(hours=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) - await database.update_fixture_announcement( + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456" ) @@ -125,7 +125,7 @@ async def test_marks_late_predictions(self, handler, database, mock_message, sam assert result is True assert "✅" in mock_message.reactions_added - predictions = await handler.db.get_all_predictions(fixture_id) + predictions = await handler.db.predictions.get_all_predictions(fixture_id) assert len(predictions) == 1 assert predictions[0]["is_late"] assert "Late prediction" in mock_message.author.dm_sent[0] @@ -143,7 +143,7 @@ async def test_accepts_pre_deadline_partial_thread_prediction( result = await handler.on_message(mock_message) assert result is True - predictions = await handler.db.get_all_predictions( + predictions = await handler.db.predictions.get_all_predictions( fixture_with_thread["id"], include_pending=True ) assert predictions[0]["predicted_game_indexes"] == [1, 2] @@ -156,11 +156,11 @@ async def test_marks_late_partial_thread_prediction_pending( self, handler, database, mock_message, sample_games ): deadline = datetime.now(UTC) - timedelta(hours=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) - await database.update_fixture_announcement( + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456" ) - await database.upsert_guild_config("111111", "4242", "123456") + await database.guild_config.upsert_guild_config("111111", "4242", "123456") mock_message.content = "Team C - Team D 1-1\nTeam E - Team F 0-2" mock_message.channel.id = 789012 @@ -168,7 +168,9 @@ async def test_marks_late_partial_thread_prediction_pending( assert result is True assert "⏳" in mock_message.reactions_added - predictions = await handler.db.get_all_predictions(fixture_id, include_pending=True) + predictions = await handler.db.predictions.get_all_predictions( + fixture_id, include_pending=True + ) assert predictions[0]["pending_partial_approval"] is True assert predictions[0]["predicted_game_indexes"] == [1, 2] assert predictions[0]["public_message_id"] == str(mock_message.id) @@ -203,7 +205,7 @@ async def test_rejects_thread_resubmission_without_overwriting_prediction( result = await handler.on_message(second_message) assert result is True - predictions = await handler.db.get_all_predictions(fixture_with_thread["id"]) + predictions = await handler.db.predictions.get_all_predictions(fixture_with_thread["id"]) assert predictions[0]["predictions"] == ["2-1", "1-1", "0-2"] assert "Use `/predict`" in second_message.author.dm_sent[-1] @@ -227,7 +229,7 @@ async def test_rate_limits_rapid_reposts(self, handler, fixture_with_thread, moc assert first is True assert second is True - predictions = await handler.db.get_all_predictions(fixture_with_thread["id"]) + predictions = await handler.db.predictions.get_all_predictions(fixture_with_thread["id"]) assert len(predictions) == 1 assert predictions[0]["predictions"] == ["2-1", "1-1", "0-2"] assert second_message.reactions_added == [] @@ -238,8 +240,8 @@ async def test_thread_prediction_cooldown_is_scoped_by_guild( self, handler, database, mock_message, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("222222", 1, sample_games, deadline) - await database.update_fixture_announcement( + fixture_id = await database.fixtures.create_fixture("222222", 1, sample_games, deadline) + await database.fixtures.update_fixture_announcement( fixture_id, message_id="888888", channel_id="456789" ) handler.record_thread_prediction_attempt("111111", "123456", datetime.now(UTC)) @@ -376,7 +378,7 @@ async def raise_error(*_args, **_kwargs): mock_message.channel.id = 789012 mock_message.content = "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" - monkeypatch.setattr(handler.db, "try_save_prediction", raise_error) + monkeypatch.setattr(handler.db.predictions, "try_save_prediction", raise_error) result = await handler.on_message(mock_message) assert result is True @@ -438,7 +440,7 @@ async def raise_forbidden(*_args, **_kwargs): result = await handler.on_message(mock_message) assert result is True - predictions = await handler.db.get_all_predictions(1) + predictions = await handler.db.predictions.get_all_predictions(1) assert len(predictions) == 1 assert len(mock_message.author.dm_sent) == 1 assert "Prediction saved" in mock_message.author.dm_sent[0] @@ -461,7 +463,7 @@ async def raise_forbidden(*_args, **_kwargs): result = await handler.on_message(mock_message) assert result is True - predictions = await handler.db.get_all_predictions(1) + predictions = await handler.db.predictions.get_all_predictions(1) assert len(predictions) == 1 @@ -476,8 +478,8 @@ async def test_multiple_users_same_thread( from tests.conftest import MockMessage, MockUser deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) - await database.update_fixture_announcement( + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.update_fixture_announcement( fixture_id, message_id="789012", channel_id="123456" ) @@ -504,7 +506,7 @@ async def test_multiple_users_same_thread( result = await handler.on_message(message2) assert result is True - predictions = await database.get_all_predictions(fixture_id) + predictions = await database.predictions.get_all_predictions(fixture_id) assert len(predictions) == 2 user_ids = {p["user_id"] for p in predictions} assert user_ids == {"111", "222"} @@ -522,7 +524,7 @@ async def test_fixture_closed_mid_submission(self, handler, mock_message, monkey async def return_closed(*_args, **_kwargs): return SaveResult.FIXTURE_CLOSED - monkeypatch.setattr(handler.db, "try_save_prediction", return_closed) + monkeypatch.setattr(handler.db.predictions, "try_save_prediction", return_closed) result = await handler.on_message(mock_message) @@ -539,7 +541,7 @@ async def test_duplicate_blocked_atomically(self, handler, mock_message, monkeyp async def return_duplicate(*_args, **_kwargs): return SaveResult.DUPLICATE - monkeypatch.setattr(handler.db, "try_save_prediction", return_duplicate) + monkeypatch.setattr(handler.db.predictions, "try_save_prediction", return_duplicate) result = await handler.on_message(mock_message) diff --git a/tests/user_commands/test_fixtures_command.py b/tests/user_commands/test_fixtures_command.py index 370438a..17fba89 100644 --- a/tests/user_commands/test_fixtures_command.py +++ b/tests/user_commands/test_fixtures_command.py @@ -14,7 +14,7 @@ async def test_no_open_fixture_shows_error(self, user_commands, mock_interaction async def test_single_open_fixture_lists_games_and_deadline( self, user_commands, mock_interaction, database, sample_games ): - await database.create_fixture( + await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -31,8 +31,8 @@ async def test_multiple_open_fixtures_list_each_week( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await user_commands.fixtures.callback(user_commands, mock_interaction) @@ -46,8 +46,8 @@ async def test_fixtures_only_shows_current_guild( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("guild-2", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("guild-2", 2, sample_games, deadline) await user_commands.fixtures.callback(user_commands, mock_interaction) diff --git a/tests/user_commands/test_help_command.py b/tests/user_commands/test_help_command.py index 2742f18..112f1e9 100644 --- a/tests/user_commands/test_help_command.py +++ b/tests/user_commands/test_help_command.py @@ -9,7 +9,7 @@ async def test_help_uses_active_season_scoring_rules( mock_interaction, database, ): - await database.update_active_scoring_rules( + await database.seasons.update_active_scoring_rules( "111111", { "exact_score_points": 5, diff --git a/tests/user_commands/test_my_predictions_command.py b/tests/user_commands/test_my_predictions_command.py index d121f37..cbb37ff 100644 --- a/tests/user_commands/test_my_predictions_command.py +++ b/tests/user_commands/test_my_predictions_command.py @@ -15,8 +15,8 @@ async def test_only_uses_current_guild_fixtures( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("guild-2", 1, sample_games, deadline) - await database.save_prediction( + fixture_id = await database.fixtures.create_fixture("guild-2", 1, sample_games, deadline) + await database.predictions.save_prediction( fixture_id, str(mock_interaction.user.id), mock_interaction.user.name, @@ -32,7 +32,7 @@ async def test_only_uses_current_guild_fixtures( async def test_single_fixture_without_prediction_shows_prompt( self, user_commands, mock_interaction, database, sample_games ): - await database.create_fixture( + await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) @@ -46,10 +46,10 @@ async def test_single_fixture_without_prediction_shows_prompt( async def test_single_fixture_prediction_shows_saved_scores( self, user_commands, mock_interaction, database, sample_games ): - fixture_id = await database.create_fixture( + fixture_id = await database.fixtures.create_fixture( "111111", 1, sample_games, datetime.now(UTC) + timedelta(days=1) ) - await database.save_prediction( + await database.predictions.save_prediction( fixture_id, str(mock_interaction.user.id), mock_interaction.user.name, @@ -70,9 +70,9 @@ async def test_multiple_open_fixtures_show_mixed_prediction_state( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_week_1 = await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) - await database.save_prediction( + fixture_week_1 = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) + await database.predictions.save_prediction( fixture_week_1, str(mock_interaction.user.id), mock_interaction.user.name, diff --git a/tests/user_commands/test_predict_command.py b/tests/user_commands/test_predict_command.py index cdacc17..bbaf891 100644 --- a/tests/user_commands/test_predict_command.py +++ b/tests/user_commands/test_predict_command.py @@ -18,7 +18,7 @@ async def _attach_prediction_threads(user_commands, database, fixture_ids, mock_ threads = {} for index, fixture_id in enumerate(fixture_ids, start=1): message_id = str(700000 + index) - await database.update_fixture_announcement( + await database.fixtures.update_fixture_announcement( fixture_id, message_id=message_id, channel_id="123456", @@ -51,8 +51,8 @@ async def test_multiple_open_fixtures_show_picker( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) @@ -63,7 +63,7 @@ async def test_predict_only_uses_current_guild_fixtures( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("guild-2", 1, sample_games, deadline) + await database.fixtures.create_fixture("guild-2", 1, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) @@ -74,8 +74,8 @@ async def test_fixture_picker_rejects_cross_guild_fixture( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("guild-2", 1, sample_games, deadline) - fixture = await database.get_fixture_by_id(fixture_id, "guild-2") + fixture_id = await database.fixtures.create_fixture("guild-2", 1, sample_games, deadline) + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "guild-2") view = FixtureSelectView( database, @@ -96,8 +96,8 @@ async def test_continue_predict_rejects_cross_guild_fixture( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_id = await database.create_fixture("guild-2", 1, sample_games, deadline) - fixture = await database.get_fixture_by_id(fixture_id, "guild-2") + fixture_id = await database.fixtures.create_fixture("guild-2", 1, sample_games, deadline) + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "guild-2") view = ContinuePredictView( database, @@ -118,8 +118,8 @@ async def test_multiple_open_fixture_picker_opens_modal_for_selection( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) @@ -136,7 +136,7 @@ async def test_multiple_open_fixture_picker_paginates_past_25( ): deadline = datetime.now(UTC) + timedelta(days=1) for week in range(1, 27): - await database.create_fixture("111111", week, sample_games, deadline) + await database.fixtures.create_fixture("111111", week, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) @@ -157,7 +157,7 @@ async def test_multiple_open_fixture_picker_paginates_past_25( async def test_predict_modal_prefills_existing_prediction( self, user_commands, mock_interaction, database ): - await database.save_prediction( + await database.predictions.save_prediction( 1, str(mock_interaction.user.id), mock_interaction.user.name, @@ -199,7 +199,7 @@ async def test_predict_modal_saves_prediction_and_offers_continue( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_two_id = await database.create_fixture("111111", 2, sample_games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [1, fixture_two_id], mock_interaction.guild ) @@ -217,7 +217,10 @@ async def test_predict_modal_saves_prediction_and_offers_continue( ) await modal.on_submit(mock_interaction) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is not None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is not None + ) assert isinstance(mock_interaction.response_sent[-1]["view"], ContinuePredictView) @pytest.mark.asyncio @@ -234,7 +237,10 @@ async def test_predict_modal_terminal_success_without_other_open_fixtures( ) await modal.on_submit(mock_interaction) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is not None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is not None + ) assert "You're done for now." in mock_interaction.response_sent[-1]["content"] assert "view" not in mock_interaction.response_sent[-1] @@ -244,7 +250,7 @@ async def test_predict_modal_overwrites_existing_prediction( self, user_commands, mock_interaction, database ): await _attach_prediction_threads(user_commands, database, [1], mock_interaction.guild) - await database.save_prediction( + await database.predictions.save_prediction( 1, str(mock_interaction.user.id), mock_interaction.user.name, @@ -260,7 +266,9 @@ async def test_predict_modal_overwrites_existing_prediction( ) await modal.on_submit(mock_interaction) - prediction = await database.get_prediction(1, str(mock_interaction.user.id), "111111") + prediction = await database.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) assert prediction is not None assert prediction["predictions"] == ["3-0", "0-0", "1-1"] @@ -269,13 +277,13 @@ async def test_predict_modal_overwrites_existing_prediction( async def test_predict_modal_marks_late_prediction( self, user_commands, mock_interaction, database ): - await database.update_active_scoring_rules("111111", {"late_prediction_points": 1}) + await database.seasons.update_active_scoring_rules("111111", {"late_prediction_points": 1}) await _attach_prediction_threads(user_commands, database, [1], mock_interaction.guild) - fixture = await database.get_fixture_by_id(1, "111111") + fixture = await database.fixtures.get_fixture_by_id(1, "111111") assert fixture is not None fixture["deadline"] = datetime.now(UTC) - timedelta(minutes=1) - user_commands.db.get_open_fixtures = AsyncMock(return_value=[fixture]) - user_commands.db.get_fixture_by_id = AsyncMock( + user_commands.db.fixtures.get_open_fixtures = AsyncMock(return_value=[fixture]) + user_commands.db.fixtures.get_fixture_by_id = AsyncMock( side_effect=lambda fixture_id, guild_id: ( fixture if (fixture_id, guild_id) == (1, "111111") else None ) @@ -289,7 +297,9 @@ async def test_predict_modal_marks_late_prediction( ) await modal.on_submit(mock_interaction) - prediction = await database.get_prediction(1, str(mock_interaction.user.id), "111111") + prediction = await database.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) assert prediction is not None assert prediction["is_late"] == 1 content = mock_interaction.response_sent[-1]["content"] @@ -309,7 +319,9 @@ async def test_predict_modal_accepts_pre_deadline_partial_prediction( modal.predictions_input._value = "Team C - Team D 1-1\nTeam E - Team F 0-2" await modal.on_submit(mock_interaction) - prediction = await database.get_prediction(1, str(mock_interaction.user.id), "111111") + prediction = await database.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) assert prediction is not None assert prediction["predictions"] == ["1-1", "0-2"] assert prediction["predicted_game_indexes"] == [1, 2] @@ -324,12 +336,12 @@ async def test_predict_modal_marks_late_partial_as_pending( ): await _attach_prediction_threads(user_commands, database, [1], mock_interaction.guild) admin_role = MockRole("League Admin", role_id=4242) - await database.upsert_guild_config("111111", str(admin_role.id), "123456") - fixture = await database.get_fixture_by_id(1, "111111") + await database.guild_config.upsert_guild_config("111111", str(admin_role.id), "123456") + fixture = await database.fixtures.get_fixture_by_id(1, "111111") assert fixture is not None fixture["deadline"] = datetime.now(UTC) - timedelta(minutes=1) - user_commands.db.get_open_fixtures = AsyncMock(return_value=[fixture]) - user_commands.db.get_fixture_by_id = AsyncMock( + user_commands.db.fixtures.get_open_fixtures = AsyncMock(return_value=[fixture]) + user_commands.db.fixtures.get_fixture_by_id = AsyncMock( side_effect=lambda fixture_id, guild_id: ( fixture if (fixture_id, guild_id) == (1, "111111") else None ) @@ -340,7 +352,9 @@ async def test_predict_modal_marks_late_partial_as_pending( modal.predictions_input._value = "Team C - Team D 1-1\nTeam E - Team F 0-2" await modal.on_submit(mock_interaction) - prediction = await database.get_prediction(1, str(mock_interaction.user.id), "111111") + prediction = await database.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) assert prediction is not None assert prediction["pending_partial_approval"] is True assert prediction["predicted_game_indexes"] == [1, 2] @@ -366,16 +380,16 @@ async def test_predict_modal_without_setup_does_not_ping_legacy_admin_role( mock_bot.db = database user_commands = UserCommands(mock_bot) deadline = datetime.now(UTC) - timedelta(minutes=1) - fixture_id = await database.create_fixture("111111", 1, sample_games, deadline) + fixture_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [fixture_id], mock_interaction.guild ) mock_interaction.guild.roles = [MockRole("typer-admin", role_id=4242)] - fixture = await database.get_fixture_by_id(fixture_id, "111111") + fixture = await database.fixtures.get_fixture_by_id(fixture_id, "111111") assert fixture is not None - user_commands.db.get_open_fixtures = AsyncMock(return_value=[fixture]) - user_commands.db.get_fixture_by_id = AsyncMock( + user_commands.db.fixtures.get_open_fixtures = AsyncMock(return_value=[fixture]) + user_commands.db.fixtures.get_fixture_by_id = AsyncMock( side_effect=lambda request_fixture_id, guild_id: ( fixture if (request_fixture_id, guild_id) == (fixture_id, "111111") else None ) @@ -396,11 +410,11 @@ async def test_predict_modal_replaces_previous_pending_bot_post( self, user_commands, mock_interaction, database ): await _attach_prediction_threads(user_commands, database, [1], mock_interaction.guild) - fixture = await database.get_fixture_by_id(1, "111111") + fixture = await database.fixtures.get_fixture_by_id(1, "111111") assert fixture is not None fixture["deadline"] = datetime.now(UTC) - timedelta(minutes=1) - user_commands.db.get_open_fixtures = AsyncMock(return_value=[fixture]) - user_commands.db.get_fixture_by_id = AsyncMock( + user_commands.db.fixtures.get_open_fixtures = AsyncMock(return_value=[fixture]) + user_commands.db.fixtures.get_fixture_by_id = AsyncMock( side_effect=lambda fixture_id, guild_id: ( fixture if (fixture_id, guild_id) == (1, "111111") else None ) @@ -419,7 +433,9 @@ async def test_predict_modal_replaces_previous_pending_bot_post( second_modal.predictions_input._value = "Team A - Team B 2-0\nTeam C - Team D 1-1" await second_modal.on_submit(mock_interaction) - prediction = await database.get_prediction(1, str(mock_interaction.user.id), "111111") + prediction = await database.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) assert prediction is not None assert prediction["public_message_id"] == "2" first_public_message.delete.assert_awaited_once() @@ -429,7 +445,7 @@ async def test_predict_modal_replaces_previous_pending_bot_post( async def test_my_predictions_shows_sparse_pending_prediction( self, user_commands, mock_interaction, database ): - await database.save_prediction( + await database.predictions.save_prediction( 1, str(mock_interaction.user.id), mock_interaction.user.name, @@ -461,7 +477,7 @@ async def test_predict_modal_reports_closed_fixture_during_submit( "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" ) monkeypatch.setattr( - user_commands.db, + user_commands.db.predictions, "save_prediction_guarded", AsyncMock(return_value=SaveResult.FIXTURE_CLOSED), ) @@ -491,7 +507,7 @@ async def test_predict_modal_reports_database_error( async def _raise(*_args, **_kwargs): raise RuntimeError("db failed") - monkeypatch.setattr(user_commands.db, "save_prediction_guarded", _raise) + monkeypatch.setattr(user_commands.db.predictions, "save_prediction_guarded", _raise) await modal.on_submit(mock_interaction) assert ( @@ -519,7 +535,9 @@ async def test_predict_modal_reports_missing_prediction_thread( in mock_interaction.response_sent[-1]["content"] ) assert ( - await user_commands.db.get_prediction(1, str(mock_interaction.user.id), "111111") + await user_commands.db.predictions.get_prediction( + 1, str(mock_interaction.user.id), "111111" + ) is None ) @@ -529,7 +547,9 @@ async def test_predict_modal_uses_fetch_channel_fallback( self, user_commands, mock_interaction, database ): thread = MockThread(thread_id="700001", name="week-1", guild=mock_interaction.guild) - await database.update_fixture_announcement(1, message_id="700001", channel_id="123456") + await database.fixtures.update_fixture_announcement( + 1, message_id="700001", channel_id="123456" + ) user_commands.bot.get_channel.return_value = None user_commands.bot.fetch_channel = AsyncMock(return_value=thread) @@ -541,7 +561,10 @@ async def test_predict_modal_uses_fetch_channel_fallback( ) await modal.on_submit(mock_interaction) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is not None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is not None + ) user_commands.bot.fetch_channel.assert_awaited_once_with(700001) @pytest.mark.asyncio @@ -570,7 +593,10 @@ async def raise_http_exception(*_args, **_kwargs): "does not have a usable prediction thread" in mock_interaction.response_sent[-1]["content"] ) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is None + ) @pytest.mark.asyncio @pytest.mark.usefixtures("fixture_with_dm") @@ -587,7 +613,7 @@ async def test_predict_modal_deletes_public_post_if_fixture_closes_during_save( "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" ) monkeypatch.setattr( - user_commands.db, + user_commands.db.predictions, "save_prediction_guarded", AsyncMock(return_value=SaveResult.FIXTURE_CLOSED), ) @@ -617,7 +643,10 @@ async def test_predict_modal_rejects_empty_partial_parse_result( "Please enter at least one prediction before submitting." in mock_interaction.response_sent[-1]["content"] ) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is None + ) @pytest.mark.asyncio @pytest.mark.usefixtures("fixture_with_dm") @@ -625,7 +654,7 @@ async def test_continue_predict_button_opens_next_modal( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_two_id = await database.create_fixture("111111", 2, sample_games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [1, fixture_two_id], mock_interaction.guild ) @@ -653,8 +682,8 @@ async def test_multi_fixture_flow_ends_without_continue_view_after_last_save( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_one_id = await database.create_fixture("111111", 1, sample_games, deadline) - fixture_two_id = await database.create_fixture("111111", 2, sample_games, deadline) + fixture_one_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [fixture_one_id, fixture_two_id], mock_interaction.guild ) @@ -681,8 +710,14 @@ async def test_multi_fixture_flow_ends_without_continue_view_after_last_save( ) await second_modal.on_submit(mock_interaction) - assert await database.get_prediction(1, str(mock_interaction.user.id), "111111") is not None - assert await database.get_prediction(2, str(mock_interaction.user.id), "111111") is not None + assert ( + await database.predictions.get_prediction(1, str(mock_interaction.user.id), "111111") + is not None + ) + assert ( + await database.predictions.get_prediction(2, str(mock_interaction.user.id), "111111") + is not None + ) assert "You're done for now." in mock_interaction.response_sent[-1]["content"] assert "view" not in mock_interaction.response_sent[-1] @@ -694,7 +729,7 @@ async def test_continue_predict_view_paginates_past_25( fixture_ids = [] for week in range(1, 28): fixture_ids.append( - await database.create_fixture("111111", week, sample_games, deadline) + await database.fixtures.create_fixture("111111", week, sample_games, deadline) ) await _attach_prediction_threads( user_commands, database, fixture_ids, mock_interaction.guild @@ -733,8 +768,8 @@ async def test_fixture_picker_rejects_wrong_user( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) @@ -757,12 +792,12 @@ async def test_fixture_picker_reports_closed_fixture( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - await database.create_fixture("111111", 1, sample_games, deadline) - await database.create_fixture("111111", 2, sample_games, deadline) + await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await user_commands.predict.callback(user_commands, mock_interaction) - await database.save_scores( + await database.scores.save_scores( 1, [ { @@ -787,8 +822,8 @@ async def test_continue_predict_button_rejects_wrong_user( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_one_id = await database.create_fixture("111111", 1, sample_games, deadline) - fixture_two_id = await database.create_fixture("111111", 2, sample_games, deadline) + fixture_one_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [fixture_one_id, fixture_two_id], mock_interaction.guild ) @@ -823,8 +858,8 @@ async def test_continue_predict_button_reports_closed_fixture( self, user_commands, mock_interaction, database, sample_games ): deadline = datetime.now(UTC) + timedelta(days=1) - fixture_one_id = await database.create_fixture("111111", 1, sample_games, deadline) - fixture_two_id = await database.create_fixture("111111", 2, sample_games, deadline) + fixture_one_id = await database.fixtures.create_fixture("111111", 1, sample_games, deadline) + fixture_two_id = await database.fixtures.create_fixture("111111", 2, sample_games, deadline) await _attach_prediction_threads( user_commands, database, [fixture_one_id, fixture_two_id], mock_interaction.guild ) @@ -840,7 +875,7 @@ async def test_continue_predict_button_reports_closed_fixture( "Team A - Team B 2-1\nTeam C - Team D 1-1\nTeam E - Team F 0-2" ) await modal.on_submit(mock_interaction) - await database.save_scores( + await database.scores.save_scores( fixture_two_id, [ { diff --git a/tests/user_commands/test_standings_command.py b/tests/user_commands/test_standings_command.py index 77477d1..bb07ceb 100644 --- a/tests/user_commands/test_standings_command.py +++ b/tests/user_commands/test_standings_command.py @@ -37,8 +37,8 @@ async def test_standings_sends_formatted_leaderboard(self, user_commands, mock_i } ], } - user_commands.db.get_standings = AsyncMock(return_value=standings) - user_commands.db.get_last_fixture_scores = AsyncMock(return_value=last_fixture) + user_commands.db.scores.get_standings = AsyncMock(return_value=standings) + user_commands.db.scores.get_last_fixture_scores = AsyncMock(return_value=last_fixture) await user_commands.standings.callback(user_commands, mock_interaction) @@ -52,9 +52,9 @@ async def test_standings_only_shows_current_guild_scores( ): games = ["Team A - Team B"] deadline = datetime.now(UTC) - timedelta(days=1) - current_fixture_id = await database.create_fixture("111111", 1, games, deadline) - other_fixture_id = await database.create_fixture("guild-2", 2, games, deadline) - await database.save_scores( + current_fixture_id = await database.fixtures.create_fixture("111111", 1, games, deadline) + other_fixture_id = await database.fixtures.create_fixture("guild-2", 2, games, deadline) + await database.scores.save_scores( current_fixture_id, [ { @@ -66,7 +66,7 @@ async def test_standings_only_shows_current_guild_scores( } ], ) - await database.save_scores( + await database.scores.save_scores( other_fixture_id, [ { diff --git a/typer_bot/bot.py b/typer_bot/bot.py index b36689b..bde0629 100644 --- a/typer_bot/bot.py +++ b/typer_bot/bot.py @@ -126,7 +126,7 @@ async def on_guild_join(self, guild: discord.Guild): """Log guild metadata and setup state when invited.""" setup_configured = None try: - config = await self.db.get_guild_config(str(guild.id)) + config = await self.db.guild_config.get_guild_config(str(guild.id)) setup_configured = config is not None except Exception: logger.exception( @@ -189,7 +189,7 @@ async def close(self): async def reminder_task(self): """Send reminders 24h and 1h before each open fixture deadline.""" current_time = now() - open_fixtures = await self.db.get_all_open_fixtures() + open_fixtures = await self.db.fixtures.get_all_open_fixtures() if not open_fixtures: return @@ -235,7 +235,7 @@ async def _before_cleanup_sessions(self): async def send_reminder(self, fixture: dict, time_description: str): """Send prediction reminder to configured channel.""" - config = await self.db.get_guild_config(fixture["guild_id"]) + config = await self.db.guild_config.get_guild_config(fixture["guild_id"]) if config is None: logger.warning( "Guild %s is missing TyperBot setup, skipping reminder for fixture %s", @@ -278,7 +278,7 @@ async def _sync_fixture_thread(self): threads inherit their parent message's snowflake ID. """ try: - open_fixtures = await self.db.get_all_open_fixtures() + open_fixtures = await self.db.fixtures.get_all_open_fixtures() if not open_fixtures: logger.debug("No open fixture found, skipping announcement verification") return diff --git a/typer_bot/commands/admin_commands.py b/typer_bot/commands/admin_commands.py index c2cf489..84541ad 100644 --- a/typer_bot/commands/admin_commands.py +++ b/typer_bot/commands/admin_commands.py @@ -38,7 +38,7 @@ async def _save_guild_config( admin_role: discord.Role, league_channel: discord.TextChannel, ) -> None: - await db.upsert_guild_config( + await db.guild_config.upsert_guild_config( str(interaction.guild_id), str(admin_role.id), str(league_channel.id), @@ -262,7 +262,7 @@ async def predicate(interaction: discord.Interaction) -> bool: return False if ( interaction.guild_id is not None - and await db.get_guild_config(str(interaction.guild_id)) is None + and await db.guild_config.get_guild_config(str(interaction.guild_id)) is None ): await send_setup_prompt_if_allowed(interaction, db) return False @@ -336,7 +336,7 @@ def cleanup_expired_state(self) -> int: async def panel(self, interaction: discord.Interaction): permission_error = await get_admin_permission_error(interaction, self.db) if permission_error is not None: - if await self.db.get_guild_config(str(interaction.guild_id)) is None: + if await self.db.guild_config.get_guild_config(str(interaction.guild_id)) is None: await send_setup_prompt_if_allowed(interaction, self.db) else: await interaction.response.send_message(permission_error, ephemeral=True) diff --git a/typer_bot/commands/admin_panel/base.py b/typer_bot/commands/admin_panel/base.py index d8342a7..4ca5683 100644 --- a/typer_bot/commands/admin_panel/base.py +++ b/typer_bot/commands/admin_panel/base.py @@ -374,7 +374,9 @@ async def callback(self, interaction: discord.Interaction): self.parent_view.selection.detail_lines = [] self.parent_view.selection.has_results = False - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None: self.parent_view.selection.fixture_id = None self.parent_view.selection.fixture_label = "" diff --git a/typer_bot/commands/admin_panel/fixture_modals.py b/typer_bot/commands/admin_panel/fixture_modals.py index b006a76..73a9ddd 100644 --- a/typer_bot/commands/admin_panel/fixture_modals.py +++ b/typer_bot/commands/admin_panel/fixture_modals.py @@ -87,7 +87,7 @@ def __init__( self.bot = bot async def _get_league_channel(self, guild_id: str): - config = await self.db.get_guild_config(guild_id) + config = await self.db.guild_config.get_guild_config(guild_id) if config is None: return None @@ -128,7 +128,7 @@ async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Bu ) return - fixture_id, allocated_week = await self.db.create_next_fixture( + fixture_id, allocated_week = await self.db.fixtures.create_next_fixture( str(interaction.guild_id), self.games, self.deadline, @@ -154,7 +154,7 @@ async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Bu f"• Or use `/predict` to fill a modal and post publicly here" ) - await self.db.update_fixture_announcement( + await self.db.fixtures.update_fixture_announcement( fixture_id, message_id=str(announcement.id), channel_id=str(league_channel.id), @@ -242,9 +242,11 @@ async def on_submit(self, interaction: discord.Interaction): await interaction.response.send_message(str(exc), ephemeral=True) return - preview_week_number = await self.db.get_max_week_number(str(interaction.guild_id)) + 1 + preview_week_number = ( + await self.db.fixtures.get_max_week_number(str(interaction.guild_id)) + 1 + ) preview = _build_fixture_preview_text(preview_week_number, games, deadline) - open_fixtures = await self.db.get_open_fixtures(str(interaction.guild_id)) + open_fixtures = await self.db.fixtures.get_open_fixtures(str(interaction.guild_id)) if open_fixtures: open_weeks = ", ".join(str(fixture["week_number"]) for fixture in open_fixtures) preview += ( diff --git a/typer_bot/commands/admin_panel/fixtures.py b/typer_bot/commands/admin_panel/fixtures.py index 2bf7b8b..b6ff4c2 100644 --- a/typer_bot/commands/admin_panel/fixtures.py +++ b/typer_bot/commands/admin_panel/fixtures.py @@ -80,7 +80,7 @@ def _refresh_items(self) -> None: self.add_item(FixturesDeleteButton(self, disabled=self.selection.fixture_id is None)) async def load_fixture_options(self) -> None: - fixtures = await self.db.get_open_fixtures(self.guild_id) + fixtures = await self.db.fixtures.get_open_fixtures(self.guild_id) self.fixture_select.update_options(fixtures) def render_content(self) -> str: @@ -124,7 +124,9 @@ async def callback(self, interaction: discord.Interaction): await interaction.response.send_message("Select an open fixture first.", ephemeral=True) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None or fixture["status"] != "open": await interaction.response.send_message( "Only open fixtures can be deleted from the panel.", ephemeral=True @@ -182,7 +184,7 @@ async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Bu return try: - deleted = await self.db.delete_fixture(self.fixture_id, self.guild_id) + deleted = await self.db.fixtures.delete_fixture(self.fixture_id, self.guild_id) if not deleted: await interaction.response.edit_message( content="Fixture no longer exists or belongs to another server.", diff --git a/typer_bot/commands/admin_panel/partial_review.py b/typer_bot/commands/admin_panel/partial_review.py index 3ca9d2a..9600a4e 100644 --- a/typer_bot/commands/admin_panel/partial_review.py +++ b/typer_bot/commands/admin_panel/partial_review.py @@ -149,7 +149,7 @@ def __init__(self, parent_view: UnifiedAdminPanelView, row: int | None = None): super().__init__(label="Review Late", style=discord.ButtonStyle.primary, row=row) async def callback(self, interaction: discord.Interaction): - pending_predictions = await self.parent_view.db.get_pending_partial_predictions( + pending_predictions = await self.parent_view.db.predictions.get_pending_partial_predictions( self.parent_view.guild_id ) if not pending_predictions: @@ -165,7 +165,7 @@ async def callback(self, interaction: discord.Interaction): next_prediction = pending_predictions[(index + 1) % len(pending_predictions)] break - fixture = await self.parent_view.db.get_fixture_by_id( + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( next_prediction["fixture_id"], self.parent_view.guild_id ) if fixture is None: diff --git a/typer_bot/commands/admin_panel/prediction_actions.py b/typer_bot/commands/admin_panel/prediction_actions.py index cc03e99..2f2a60d 100644 --- a/typer_bot/commands/admin_panel/prediction_actions.py +++ b/typer_bot/commands/admin_panel/prediction_actions.py @@ -50,8 +50,10 @@ async def callback(self, interaction: discord.Interaction): ) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) - prediction = await self.parent_view.db.get_prediction( + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) + prediction = await self.parent_view.db.predictions.get_prediction( fixture_id, user_id, self.parent_view.guild_id ) if fixture is None: @@ -177,7 +179,9 @@ async def callback(self, interaction: discord.Interaction): ) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None: self.parent_view.selection.fixture_id = None self.parent_view.selection.fixture_label = "" @@ -194,7 +198,7 @@ async def callback(self, interaction: discord.Interaction): ) return - prediction = await self.parent_view.db.get_prediction( + prediction = await self.parent_view.db.predictions.get_prediction( fixture_id, user_id, self.parent_view.guild_id ) if prediction is None: diff --git a/typer_bot/commands/admin_panel/prediction_modals.py b/typer_bot/commands/admin_panel/prediction_modals.py index 9135137..6f07f09 100644 --- a/typer_bot/commands/admin_panel/prediction_modals.py +++ b/typer_bot/commands/admin_panel/prediction_modals.py @@ -227,7 +227,7 @@ async def on_submit(self, interaction: discord.Interaction): view=self.parent_view, ) - predictions = await self.parent_view.db.get_all_predictions(fixture["id"]) + predictions = await self.parent_view.db.predictions.get_all_predictions(fixture["id"]) dm_message_lines = [ f"Results were corrected for Week {fixture['week_number']}.", "", diff --git a/typer_bot/commands/admin_panel/predictions.py b/typer_bot/commands/admin_panel/predictions.py index 9af059c..83e972e 100644 --- a/typer_bot/commands/admin_panel/predictions.py +++ b/typer_bot/commands/admin_panel/predictions.py @@ -56,7 +56,7 @@ def _refresh_items(self) -> None: ) async def load_fixture_options(self) -> None: - fixtures = await self.db.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) + fixtures = await self.db.fixtures.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) self.fixture_select.update_options(fixtures) async def load_user_options(self) -> None: @@ -65,7 +65,7 @@ async def load_user_options(self) -> None: self.user_select.update_options([]) return - predictions = await self.db.get_all_predictions( + predictions = await self.db.predictions.get_all_predictions( self.selection.fixture_id, include_pending=True ) self.has_user_overflow = len(predictions) > MAX_SELECT_OPTIONS @@ -153,7 +153,9 @@ async def callback(self, interaction: discord.Interaction): await interaction.response.send_message("Select a fixture first.", ephemeral=True) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None: self.parent_view.selection.fixture_id = None self.parent_view.selection.fixture_label = "" @@ -170,7 +172,7 @@ async def callback(self, interaction: discord.Interaction): ) return - prediction = await self.parent_view.db.get_prediction( + prediction = await self.parent_view.db.predictions.get_prediction( fixture_id, selected_user_id, self.parent_view.guild_id ) if prediction is None: diff --git a/typer_bot/commands/admin_panel/result_modals.py b/typer_bot/commands/admin_panel/result_modals.py index 1adb3a5..7470df2 100644 --- a/typer_bot/commands/admin_panel/result_modals.py +++ b/typer_bot/commands/admin_panel/result_modals.py @@ -54,7 +54,7 @@ async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Bu return try: - await self.db.save_results(self.fixture["id"], self.results) + await self.db.results.save_results(self.fixture["id"], self.results) except ValueError as exc: await interaction.response.edit_message( content=f"**Cannot save results:** {exc}", view=None diff --git a/typer_bot/commands/admin_panel/results.py b/typer_bot/commands/admin_panel/results.py index 227313a..74e5d76 100644 --- a/typer_bot/commands/admin_panel/results.py +++ b/typer_bot/commands/admin_panel/results.py @@ -38,7 +38,7 @@ def _refresh_items(self) -> None: self.add_item(CorrectResultsButton(self, disabled=self.selection.fixture_id is None)) async def load_fixture_options(self) -> None: - fixtures = await self.db.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) + fixtures = await self.db.fixtures.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) self.fixture_select.update_options(fixtures) async def populate_fixture_details(self, fixture: dict | None) -> None: @@ -48,7 +48,7 @@ async def populate_fixture_details(self, fixture: dict | None) -> None: if fixture is None: return - results = await self.db.get_results(fixture["id"]) + results = await self.db.results.get_results(fixture["id"]) if not results: self.selection.status_message = "No results saved for that fixture yet." return @@ -93,7 +93,9 @@ async def callback(self, interaction: discord.Interaction): await interaction.response.send_message("Select a fixture first.", ephemeral=True) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None: self.parent_view.selection.fixture_id = None self.parent_view.selection.fixture_label = "" @@ -111,7 +113,7 @@ async def callback(self, interaction: discord.Interaction): view=self.parent_view, ) return - results = await self.parent_view.db.get_results(fixture_id) + results = await self.parent_view.db.results.get_results(fixture_id) if not results: await interaction.response.send_message( "No results are stored for that fixture yet. Use the Enter Results button in `/admin panel` first.", diff --git a/typer_bot/commands/admin_panel/unified.py b/typer_bot/commands/admin_panel/unified.py index 8a658b4..4046ccc 100644 --- a/typer_bot/commands/admin_panel/unified.py +++ b/typer_bot/commands/admin_panel/unified.py @@ -153,13 +153,15 @@ def _refresh_items(self) -> None: self.add_item(SetupBotButton(self, row=4)) async def load_fixture_options(self) -> None: - self.guild_config = await self.db.get_guild_config(self.guild_id) - self.active_season = await self.db.get_or_create_active_season(self.guild_id) - self.active_season_has_scores = await self.db.active_season_has_scores(self.guild_id) - fixtures = await self.db.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) + self.guild_config = await self.db.guild_config.get_guild_config(self.guild_id) + self.active_season = await self.db.seasons.get_or_create_active_season(self.guild_id) + self.active_season_has_scores = await self.db.seasons.active_season_has_scores( + self.guild_id + ) + fixtures = await self.db.fixtures.get_recent_fixtures(self.guild_id, MAX_SELECT_OPTIONS) self.fixture_select.update_options(fixtures) self.has_pending_partials = bool( - await self.db.get_pending_partial_predictions(self.guild_id) + await self.db.predictions.get_pending_partial_predictions(self.guild_id) ) self._refresh_items() @@ -169,7 +171,7 @@ async def load_user_options(self) -> None: self.user_select.update_options([]) return - predictions = await self.db.get_all_predictions( + predictions = await self.db.predictions.get_all_predictions( self.selection.fixture_id, include_pending=True ) self.has_user_overflow = len(predictions) > MAX_SELECT_OPTIONS @@ -179,10 +181,10 @@ async def set_selected_prediction(self) -> None: self.current_prediction = None if self.selection.fixture_id is None or self.selection.user_id is None: return - fixture = await self.db.get_fixture_by_id(self.selection.fixture_id, self.guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(self.selection.fixture_id, self.guild_id) if fixture is None: return - self.current_prediction = await self.db.get_prediction( + self.current_prediction = await self.db.predictions.get_prediction( self.selection.fixture_id, self.selection.user_id, self.guild_id ) @@ -192,7 +194,7 @@ async def populate_fixture_details(self, fixture: dict | None) -> None: if fixture is None: return - results = await self.db.get_results(fixture["id"]) + results = await self.db.results.get_results(fixture["id"]) if results: self.selection.has_results = True self.selection.detail_lines = _build_detail_lines(fixture["games"], results) diff --git a/typer_bot/commands/admin_panel/unified_actions.py b/typer_bot/commands/admin_panel/unified_actions.py index 2373ba5..1c2b2ef 100644 --- a/typer_bot/commands/admin_panel/unified_actions.py +++ b/typer_bot/commands/admin_panel/unified_actions.py @@ -101,7 +101,7 @@ async def on_submit(self, interaction: discord.Interaction): return try: - season = await self.parent_view.db.start_new_season( + season = await self.parent_view.db.seasons.start_new_season( str(interaction.guild_id), self.name_input.value, ) @@ -188,7 +188,7 @@ async def on_submit(self, interaction: discord.Interaction): ) return - active_season = await self.parent_view.db.get_or_create_active_season( + active_season = await self.parent_view.db.seasons.get_or_create_active_season( str(interaction.guild_id) ) if active_season["id"] != self.season_id: @@ -199,7 +199,7 @@ async def on_submit(self, interaction: discord.Interaction): return try: - rules = await self.parent_view.db.update_active_scoring_rules( + rules = await self.parent_view.db.seasons.update_active_scoring_rules( str(interaction.guild_id), { "exact_score_points": self.exact_input.value, @@ -227,11 +227,11 @@ def __init__(self, parent_view: UnifiedAdminPanelView, row: int | None = None): super().__init__(label="Scoring Rules", style=discord.ButtonStyle.secondary, row=row) async def callback(self, interaction: discord.Interaction): - self.parent_view.active_season = await self.parent_view.db.get_or_create_active_season( - self.parent_view.guild_id + self.parent_view.active_season = ( + await self.parent_view.db.seasons.get_or_create_active_season(self.parent_view.guild_id) ) self.parent_view.active_season_has_scores = ( - await self.parent_view.db.active_season_has_scores(self.parent_view.guild_id) + await self.parent_view.db.seasons.active_season_has_scores(self.parent_view.guild_id) ) if self.parent_view.active_season_has_scores: self.parent_view.selection.status_message = ( @@ -267,7 +267,9 @@ async def on_submit(self, interaction: discord.Interaction): ) return - open_fixtures = await self.parent_view.db.get_open_fixtures(self.parent_view.guild_id) + open_fixtures = await self.parent_view.db.fixtures.get_open_fixtures( + self.parent_view.guild_id + ) matching = [fixture for fixture in open_fixtures if fixture["week_number"] == week_number] if not matching: await interaction.response.send_message( @@ -326,13 +328,15 @@ async def callback(self, interaction: discord.Interaction): if fixture_id is None: await interaction.response.send_message("Select a fixture first.", ephemeral=True) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None or fixture["status"] != "open": await interaction.response.send_message( "That fixture is no longer open.", ephemeral=True ) return - existing_results = await self.parent_view.db.get_results(fixture_id) + existing_results = await self.parent_view.db.results.get_results(fixture_id) if existing_results: await interaction.response.send_message( "Results already entered for this fixture. Use Correct Results instead.", @@ -357,7 +361,9 @@ async def callback(self, interaction: discord.Interaction): if fixture_id is None: await interaction.response.send_message("Select a fixture first.", ephemeral=True) return - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is None or fixture["status"] != "open": await self._refresh_parent_panel(fixture_id) await interaction.response.send_message( @@ -410,7 +416,9 @@ async def _edit_parent_message(self, interaction: discord.Interaction) -> None: await edit_message(content=self.parent_view.render_content(), view=self.parent_view) async def _refresh_parent_panel(self, fixture_id: int) -> None: - fixture = await self.parent_view.db.get_fixture_by_id(fixture_id, self.parent_view.guild_id) + fixture = await self.parent_view.db.fixtures.get_fixture_by_id( + fixture_id, self.parent_view.guild_id + ) if fixture is not None: self.parent_view.selection.fixture_status = fixture["status"] self.parent_view.selection.fixture_label = ( @@ -473,15 +481,17 @@ def __init__(self, parent_view: UnifiedAdminPanelView, row: int | None = None): super().__init__(label="Re-post Results", style=discord.ButtonStyle.secondary, row=row) async def callback(self, interaction: discord.Interaction): - fixture_data = await self.parent_view.db.get_last_fixture_scores(self.parent_view.guild_id) - standings = await self.parent_view.db.get_standings(self.parent_view.guild_id) + fixture_data = await self.parent_view.db.scores.get_last_fixture_scores( + self.parent_view.guild_id + ) + standings = await self.parent_view.db.scores.get_standings(self.parent_view.guild_id) if not fixture_data: await interaction.response.send_message( "No completed fixtures found with scores!", ephemeral=True ) return - config = await self.parent_view.db.get_guild_config(self.parent_view.guild_id) + config = await self.parent_view.db.guild_config.get_guild_config(self.parent_view.guild_id) channel = None if config is not None and self.parent_view.bot is not None: try: diff --git a/typer_bot/commands/user_commands.py b/typer_bot/commands/user_commands.py index b48741d..9f89f0f 100644 --- a/typer_bot/commands/user_commands.py +++ b/typer_bot/commands/user_commands.py @@ -227,7 +227,7 @@ async def callback(self, interaction: discord.Interaction): ) return - latest_fixture = await view.db.get_fixture_by_id(self.fixture["id"], view.guild_id) + latest_fixture = await view.db.fixtures.get_fixture_by_id(self.fixture["id"], view.guild_id) if latest_fixture is None or latest_fixture["status"] != "open": await interaction.response.edit_message( content="That fixture is no longer open. Use `/predict` to refresh the list.", @@ -235,7 +235,7 @@ async def callback(self, interaction: discord.Interaction): ) return - existing_prediction = await view.db.get_prediction( + existing_prediction = await view.db.predictions.get_prediction( self.fixture["id"], view.owner_user_id, view.guild_id ) modal = PredictModal( @@ -301,7 +301,7 @@ async def callback(self, interaction: discord.Interaction): return fixture_id = int(self.values[0]) - fixture = await view.db.get_fixture_by_id(fixture_id, view.guild_id) + fixture = await view.db.fixtures.get_fixture_by_id(fixture_id, view.guild_id) if fixture is None or fixture["status"] != "open": await interaction.response.edit_message( content="That fixture is no longer open. Use `/predict` to refresh the list.", @@ -309,7 +309,7 @@ async def callback(self, interaction: discord.Interaction): ) return - existing_prediction = await view.db.get_prediction( + existing_prediction = await view.db.predictions.get_prediction( fixture_id, view.owner_user_id, view.guild_id ) modal = PredictModal( @@ -396,7 +396,9 @@ def __init__( self.add_item(self.predictions_input) async def on_submit(self, interaction: discord.Interaction): - fixture = await self.db.get_fixture_by_id(self.fixture["id"], self.fixture["guild_id"]) + fixture = await self.db.fixtures.get_fixture_by_id( + self.fixture["id"], self.fixture["guild_id"] + ) if fixture is None or fixture["status"] != "open": await interaction.response.send_message( "This fixture is no longer open. Use `/predict` to refresh the list.", @@ -427,7 +429,7 @@ async def on_submit(self, interaction: discord.Interaction): return submitted_at = now() - existing_prediction = await self.db.get_prediction( + existing_prediction = await self.db.predictions.get_prediction( fixture["id"], str(interaction.user.id), fixture["guild_id"] ) submission = build_prediction_submission( @@ -469,7 +471,7 @@ async def on_submit(self, interaction: discord.Interaction): public_message_kind="bot_post", ) try: - result = await self.db.save_prediction_guarded( + result = await self.db.predictions.save_prediction_guarded( fixture["id"], str(interaction.user.id), self.user_name, @@ -534,7 +536,7 @@ async def on_submit(self, interaction: discord.Interaction): completed_fixture_ids = set(self.completed_fixture_ids) completed_fixture_ids.add(fixture["id"]) - open_fixtures = await self.db.get_open_fixtures(fixture["guild_id"]) + open_fixtures = await self.db.fixtures.get_open_fixtures(fixture["guild_id"]) remaining_fixtures = _remaining_open_fixtures(open_fixtures, completed_fixture_ids) if remaining_fixtures: view = ContinuePredictView( @@ -615,7 +617,7 @@ async def predict(self, interaction: discord.Interaction): if guild_id is None: return - open_fixtures = await self.db.get_open_fixtures(guild_id) + open_fixtures = await self.db.fixtures.get_open_fixtures(guild_id) if not open_fixtures: await interaction.response.send_message( "❌ No active fixture found! Ask an admin to create one.", ephemeral=True @@ -625,7 +627,7 @@ async def predict(self, interaction: discord.Interaction): existing_prediction = None if len(open_fixtures) == 1: fixture = open_fixtures[0] - existing_prediction = await self.db.get_prediction( + existing_prediction = await self.db.predictions.get_prediction( fixture["id"], str(interaction.user.id), guild_id ) modal = PredictModal( @@ -653,7 +655,9 @@ async def help(self, interaction: discord.Interaction): is_admin_user = await is_configured_admin(interaction, self.db) scoring_rules = None if interaction.guild_id is not None: - scoring_rules = await self.db.get_active_scoring_rules(str(interaction.guild_id)) + scoring_rules = await self.db.seasons.get_active_scoring_rules( + str(interaction.guild_id) + ) scoring_help = _format_scoring_help(scoring_rules) user_help = f"""## 📖 User Commands @@ -726,7 +730,7 @@ async def fixtures(self, interaction: discord.Interaction): if guild_id is None: return - open_fixtures = await self.db.get_open_fixtures(guild_id) + open_fixtures = await self.db.fixtures.get_open_fixtures(guild_id) if not open_fixtures: await interaction.response.send_message("❌ No active fixture found!", ephemeral=True) @@ -764,8 +768,8 @@ async def standings(self, interaction: discord.Interaction): if guild_id is None: return - standings = await self.db.get_standings(guild_id) - last_fixture = await self.db.get_last_fixture_scores(guild_id) + standings = await self.db.scores.get_standings(guild_id) + last_fixture = await self.db.scores.get_last_fixture_scores(guild_id) message = format_standings(standings, last_fixture) @@ -780,7 +784,7 @@ async def my_predictions(self, interaction: discord.Interaction): if guild_id is None: return - open_fixtures = await self.db.get_open_fixtures(guild_id) + open_fixtures = await self.db.fixtures.get_open_fixtures(guild_id) if not open_fixtures: await interaction.response.send_message("❌ No active fixture found!", ephemeral=True) @@ -788,7 +792,7 @@ async def my_predictions(self, interaction: discord.Interaction): if len(open_fixtures) == 1: fixture = open_fixtures[0] - prediction = await self.db.get_prediction( + prediction = await self.db.predictions.get_prediction( fixture["id"], str(interaction.user.id), guild_id ) @@ -833,7 +837,7 @@ async def my_predictions(self, interaction: discord.Interaction): has_any_prediction = False for fixture in open_fixtures: - prediction = await self.db.get_prediction(fixture["id"], user_id, guild_id) + prediction = await self.db.predictions.get_prediction(fixture["id"], user_id, guild_id) deadline_str = format_for_discord(fixture["deadline"], "F") relative_str = format_for_discord(fixture["deadline"], "R") diff --git a/typer_bot/database/connection.py b/typer_bot/database/connection.py index c0e54ed..61a4a10 100644 --- a/typer_bot/database/connection.py +++ b/typer_bot/database/connection.py @@ -198,11 +198,15 @@ async def _validate_unique_results(db: aiosqlite.Connection) -> None: class Database: - """Composition root for SQLite setup and the bot's stable data facade. - - Callers use this facade instead of reaching into repositories directly. It - owns path setup, schema initialization, startup validation, and the focused - repository objects that perform the actual reads and writes. + """Composition root for SQLite setup and repository wiring. + + Attributes: + fixtures: Fixture reads/writes. + guild_config: Guild setup reads/writes. + predictions: Prediction reads/writes. + results: Result reads/writes. + scores: Score and standings reads/writes. + seasons: Season and scoring-rule reads/writes. """ def __init__(self, db_path: str | None = None) -> None: @@ -212,12 +216,12 @@ def __init__(self, db_path: str | None = None) -> None: if db_dir and not db_dir.exists(): db_dir.mkdir(parents=True, exist_ok=True) - self._fixtures = FixtureRepository(self.db_path) - self._guild_config = GuildConfigRepository(self.db_path) - self._predictions = PredictionRepository(self.db_path) - self._results = ResultsRepository(self.db_path) - self._scores = ScoreRepository(self.db_path) - self._seasons = SeasonRepository(self.db_path) + self.fixtures = FixtureRepository(self.db_path) + self.guild_config = GuildConfigRepository(self.db_path) + self.predictions = PredictionRepository(self.db_path) + self.results = ResultsRepository(self.db_path) + self.scores = ScoreRepository(self.db_path) + self.seasons = SeasonRepository(self.db_path) async def initialize(self) -> None: """Create tables, enable WAL mode, and validate existing schema invariants. @@ -358,223 +362,3 @@ async def initialize(self) -> None: ) await db.commit() - - async def upsert_guild_config(self, guild_id, admin_role_id, league_channel_id): - return await self._guild_config.upsert_guild_config( - guild_id, admin_role_id, league_channel_id - ) - - async def get_guild_config(self, guild_id): - return await self._guild_config.get_guild_config(guild_id) - - async def get_active_season(self, guild_id): - return await self._seasons.get_active_season(guild_id) - - async def get_or_create_active_season(self, guild_id): - return await self._seasons.get_or_create_active_season(guild_id) - - async def get_seasons(self, guild_id): - return await self._seasons.get_seasons(guild_id) - - async def get_active_scoring_rules(self, guild_id): - return await self._seasons.get_active_scoring_rules(guild_id) - - async def active_season_has_scores(self, guild_id): - return await self._seasons.active_season_has_scores(guild_id) - - async def update_active_scoring_rules(self, guild_id, rules): - return await self._seasons.update_active_scoring_rules(guild_id, rules) - - async def start_new_season(self, guild_id, name): - return await self._seasons.start_new_season(guild_id, name) - - async def create_fixture(self, guild_id, week_number, games, deadline): - return await self._fixtures.create_fixture(guild_id, week_number, games, deadline) - - async def create_next_fixture(self, guild_id, games, deadline): - return await self._fixtures.create_next_fixture(guild_id, games, deadline) - - async def get_current_fixture(self, guild_id): - return await self._fixtures.get_current_fixture(guild_id) - - async def get_open_fixtures(self, guild_id): - return await self._fixtures.get_open_fixtures(guild_id) - - async def get_all_open_fixtures(self): - return await self._fixtures.get_all_open_fixtures() - - async def get_open_fixture_by_week(self, guild_id, week_number): - return await self._fixtures.get_open_fixture_by_week(guild_id, week_number) - - async def get_fixture_by_id(self, fixture_id, guild_id): - return await self._fixtures.get_fixture_by_id(fixture_id, guild_id) - - async def _get_fixture_by_id_unchecked(self, fixture_id): - return await self._fixtures._get_fixture_by_id_unchecked(fixture_id) - - async def get_fixture_by_week(self, guild_id, week_number): - return await self._fixtures.get_fixture_by_week(guild_id, week_number) - - async def get_recent_fixtures(self, guild_id, limit=25): - return await self._fixtures.get_recent_fixtures(guild_id, limit) - - async def get_fixture_by_message_id(self, message_id, guild_id=None): - return await self._fixtures.get_fixture_by_message_id(message_id, guild_id) - - async def get_max_week_number(self, guild_id): - return await self._fixtures.get_max_week_number(guild_id) - - async def delete_fixture(self, fixture_id, guild_id=None): - return await self._fixtures.delete_fixture(fixture_id, guild_id) - - async def update_fixture_announcement(self, fixture_id, message_id, channel_id): - return await self._fixtures.update_fixture_announcement(fixture_id, message_id, channel_id) - - async def save_prediction( - self, - fixture_id, - user_id, - user_name, - predictions, - is_late=False, - *, - predicted_game_indexes=None, - pending_partial_approval=False, - public_message_id=None, - public_message_kind=None, - ): - return await self._predictions.save_prediction( - fixture_id, - user_id, - user_name, - predictions, - is_late, - predicted_game_indexes=predicted_game_indexes, - pending_partial_approval=pending_partial_approval, - public_message_id=public_message_id, - public_message_kind=public_message_kind, - ) - - async def try_save_prediction( - self, - fixture_id, - user_id, - user_name, - predictions, - is_late=False, - *, - predicted_game_indexes=None, - pending_partial_approval=False, - public_message_id=None, - public_message_kind=None, - ): - """Insert once for thread submissions with atomic duplicate and open checks.""" - return await self._predictions.try_save_prediction( - fixture_id, - user_id, - user_name, - predictions, - is_late, - predicted_game_indexes=predicted_game_indexes, - pending_partial_approval=pending_partial_approval, - public_message_id=public_message_id, - public_message_kind=public_message_kind, - ) - - async def save_prediction_guarded( - self, - fixture_id, - user_id, - user_name, - predictions, - is_late=False, - *, - predicted_game_indexes=None, - pending_partial_approval=False, - public_message_id=None, - public_message_kind=None, - ): - """Upsert a prediction only while the fixture is still open.""" - return await self._predictions.save_prediction_guarded( - fixture_id, - user_id, - user_name, - predictions, - is_late, - predicted_game_indexes=predicted_game_indexes, - pending_partial_approval=pending_partial_approval, - public_message_id=public_message_id, - public_message_kind=public_message_kind, - ) - - async def get_prediction(self, fixture_id, user_id, guild_id): - return await self._predictions.get_prediction(fixture_id, user_id, guild_id) - - async def admin_update_prediction(self, fixture_id, user_id, predictions, admin_user_id): - return await self._predictions.admin_update_prediction( - fixture_id, user_id, predictions, admin_user_id - ) - - async def admin_update_prediction_with_recalc( - self, fixture_id, user_id, predictions, admin_user_id - ): - return await self._predictions.admin_update_prediction_with_recalc( - fixture_id, user_id, predictions, admin_user_id - ) - - async def set_late_penalty_waiver(self, fixture_id, user_id, waived): - return await self._predictions.set_late_penalty_waiver(fixture_id, user_id, waived) - - async def toggle_late_penalty_waiver_with_recalc(self, fixture_id, user_id): - """Toggle late-waiver state and recalculate scores when they already exist.""" - return await self._predictions.toggle_late_penalty_waiver_with_recalc(fixture_id, user_id) - - async def delete_prediction(self, fixture_id, user_id): - return await self._predictions.delete_prediction(fixture_id, user_id) - - async def get_all_predictions(self, fixture_id, include_pending=False): - return await self._predictions.get_all_predictions( - fixture_id, include_pending=include_pending - ) - - async def get_pending_partial_predictions(self, guild_id): - return await self._predictions.get_pending_partial_predictions(guild_id) - - async def approve_partial_prediction(self, fixture_id, user_id, admin_user_id): - return await self._predictions.approve_partial_prediction_with_recalc( - fixture_id, user_id, admin_user_id - ) - - async def reject_partial_prediction(self, fixture_id, user_id): - return await self._predictions.reject_partial_prediction_with_recalc(fixture_id, user_id) - - async def save_results(self, fixture_id, results): - return await self._results.save_results(fixture_id, results) - - async def save_results_with_recalc(self, fixture_id, results): - """Replace stored results and recalculate scores for already-scored fixtures.""" - return await self._results.save_results_with_recalc(fixture_id, results) - - async def get_results(self, fixture_id): - return await self._results.get_results(fixture_id) - - async def fixture_has_scores(self, fixture_id): - return await self._scores.fixture_has_scores(fixture_id) - - async def get_scores_for_fixture(self, fixture_id): - return await self._scores.get_scores_for_fixture(fixture_id) - - async def save_scores(self, fixture_id, scores): - return await self._scores.save_scores(fixture_id, scores) - - async def recalculate_fixture_scores(self, fixture_id): - return await self._scores.recalculate_fixture_scores(fixture_id) - - async def get_standings(self, guild_id): - return await self._scores.get_standings(guild_id) - - async def get_standings_for_season(self, guild_id, season_id): - return await self._scores.get_standings_for_season(guild_id, season_id) - - async def get_last_fixture_scores(self, guild_id): - return await self._scores.get_last_fixture_scores(guild_id) diff --git a/typer_bot/database/fixtures.py b/typer_bot/database/fixtures.py index 850ecc4..0463650 100644 --- a/typer_bot/database/fixtures.py +++ b/typer_bot/database/fixtures.py @@ -87,9 +87,9 @@ async def create_fixture( duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.create_fixture completed", + "db.fixtures.create_fixture completed", extra={ - "operation": "db.create_fixture", + "operation": "db.fixtures.create_fixture", "week_number": week_number, "fixture_id": cursor.lastrowid, "duration_ms": round(duration_ms, 2), @@ -138,9 +138,9 @@ async def create_next_fixture( duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.create_next_fixture completed", + "db.fixtures.create_next_fixture completed", extra={ - "operation": "db.create_next_fixture", + "operation": "db.fixtures.create_next_fixture", "week_number": next_week, "fixture_id": insert_cursor.lastrowid, "duration_ms": round(duration_ms, 2), diff --git a/typer_bot/database/predictions.py b/typer_bot/database/predictions.py index 9af9c13..eecf2c4 100644 --- a/typer_bot/database/predictions.py +++ b/typer_bot/database/predictions.py @@ -169,9 +169,9 @@ async def save_prediction( duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.save_prediction completed", + "db.predictions.save_prediction completed", extra={ - "operation": "db.save_prediction", + "operation": "db.predictions.save_prediction", "fixture_id": fixture_id, "user_id": user_id, "duration_ms": round(duration_ms, 2), @@ -256,9 +256,9 @@ async def try_save_prediction( duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.try_save_prediction completed", + "db.predictions.try_save_prediction completed", extra={ - "operation": "db.try_save_prediction", + "operation": "db.predictions.try_save_prediction", "fixture_id": fixture_id, "user_id": user_id, "duration_ms": round(duration_ms, 2), @@ -336,9 +336,9 @@ async def save_prediction_guarded( duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.save_prediction_guarded completed", + "db.predictions.save_prediction_guarded completed", extra={ - "operation": "db.save_prediction_guarded", + "operation": "db.predictions.save_prediction_guarded", "fixture_id": fixture_id, "user_id": user_id, "duration_ms": round(duration_ms, 2), diff --git a/typer_bot/database/results.py b/typer_bot/database/results.py index 7ce66ee..5f31752 100644 --- a/typer_bot/database/results.py +++ b/typer_bot/database/results.py @@ -57,9 +57,9 @@ async def save_results(self, fixture_id: int, results: list[str]) -> None: duration_ms = (time.perf_counter() - start_time) * 1000 logger.debug( - "db.save_results completed", + "db.results.save_results completed", extra={ - "operation": "db.save_results", + "operation": "db.results.save_results", "fixture_id": fixture_id, "duration_ms": round(duration_ms, 2), "rows_affected": cursor.rowcount, diff --git a/typer_bot/database/scores.py b/typer_bot/database/scores.py index f90fff8..ab433fb 100644 --- a/typer_bot/database/scores.py +++ b/typer_bot/database/scores.py @@ -165,7 +165,7 @@ async def save_scores(self, fixture_id: int, scores: list[dict]) -> None: import time start_time = time.perf_counter() - operation = "db.save_scores.transaction" + operation = "db.scores.save_scores.transaction" logger.debug( "Transaction started", diff --git a/typer_bot/dev/seed_test_data.py b/typer_bot/dev/seed_test_data.py index 4e47959..4530243 100644 --- a/typer_bot/dev/seed_test_data.py +++ b/typer_bot/dev/seed_test_data.py @@ -113,7 +113,7 @@ async def _seed_mixed_rows(db: Database, tester_user_id: str | None, guild_id: s current_time = now() users = _build_seed_users() - scored_fixture_id = await db.create_fixture( + scored_fixture_id = await db.fixtures.create_fixture( guild_id, 1, SAMPLE_GAMES, @@ -127,7 +127,7 @@ async def _seed_mixed_rows(db: Database, tester_user_id: str | None, guild_id: s scored_scores: list[dict[str, int | str]] = [] for user in users: predictions = scored_predictions[user["user_id"]] - await db.save_prediction( + await db.predictions.save_prediction( scored_fixture_id, user["user_id"], user["user_name"], @@ -153,10 +153,10 @@ async def _seed_mixed_rows(db: Database, tester_user_id: str | None, guild_id: s str(score["user_name"]).lower(), ) ) - await db.save_results(scored_fixture_id, scored_results) - await db.save_scores(scored_fixture_id, scored_scores) + await db.results.save_results(scored_fixture_id, scored_results) + await db.scores.save_scores(scored_fixture_id, scored_scores) - open_fixture_id = await db.create_fixture( + open_fixture_id = await db.fixtures.create_fixture( guild_id, 2, SAMPLE_GAMES, @@ -174,7 +174,7 @@ async def _seed_mixed_rows(db: Database, tester_user_id: str | None, guild_id: s open_predictions.append((open_fixture_users[2], ["0-1", "1-1", "0-2"], False)) for user, predictions, is_late in open_predictions: - await db.save_prediction( + await db.predictions.save_prediction( open_fixture_id, user["user_id"], user["user_name"], @@ -182,13 +182,13 @@ async def _seed_mixed_rows(db: Database, tester_user_id: str | None, guild_id: s is_late, ) - late_fixture_id = await db.create_fixture( + late_fixture_id = await db.fixtures.create_fixture( guild_id, 3, SAMPLE_GAMES, current_time - timedelta(hours=3), ) - await db.save_prediction( + await db.predictions.save_prediction( late_fixture_id, users[1]["user_id"], users[1]["user_name"], diff --git a/typer_bot/handlers/thread_prediction_handler.py b/typer_bot/handlers/thread_prediction_handler.py index af67560..3bb3dad 100644 --- a/typer_bot/handlers/thread_prediction_handler.py +++ b/typer_bot/handlers/thread_prediction_handler.py @@ -88,7 +88,9 @@ async def on_message(self, message: discord.Message): return False message_id = str(message.channel.id) - fixture = await self.db.get_fixture_by_message_id(message_id, str(message.guild.id)) + fixture = await self.db.fixtures.get_fixture_by_message_id( + message_id, str(message.guild.id) + ) if not fixture: return False @@ -167,7 +169,7 @@ async def on_message(self, message: discord.Message): ) try: - result = await self.db.try_save_prediction( + result = await self.db.predictions.try_save_prediction( fixture["id"], user_id, message.author.display_name, diff --git a/typer_bot/services/admin_service.py b/typer_bot/services/admin_service.py index fcfb06a..eb28240 100644 --- a/typer_bot/services/admin_service.py +++ b/typer_bot/services/admin_service.py @@ -33,21 +33,21 @@ def __init__(self, db: Database): self.db = db async def _build_score_result(self, fixture_id: int, guild_id: str) -> FixtureScoreResult: - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - results = await self.db.get_results(fixture_id) + results = await self.db.results.get_results(fixture_id) if not results: raise ValueError("No results entered for this fixture") - predictions = await self.db.get_all_predictions(fixture_id) + predictions = await self.db.predictions.get_all_predictions(fixture_id) if not predictions: raise ValueError("No predictions found for this fixture") - scores = await self.db.get_scores_for_fixture(fixture_id) - standings = await self.db.get_standings(guild_id) - last_fixture = await self.db.get_last_fixture_scores(guild_id) + scores = await self.db.scores.get_scores_for_fixture(fixture_id) + standings = await self.db.scores.get_standings(guild_id) + last_fixture = await self.db.scores.get_last_fixture_scores(guild_id) return FixtureScoreResult( fixture=fixture, results=results, @@ -59,19 +59,19 @@ async def _build_score_result(self, fixture_id: int, guild_id: str) -> FixtureSc async def calculate_fixture_scores(self, fixture_id: int, guild_id: str) -> FixtureScoreResult: """Recalculate one fixture and refresh standings.""" - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - results = await self.db.get_results(fixture_id) + results = await self.db.results.get_results(fixture_id) if not results: raise ValueError("No results entered for this fixture") - predictions = await self.db.get_all_predictions(fixture_id) + predictions = await self.db.predictions.get_all_predictions(fixture_id) if not predictions: raise ValueError("No predictions found for this fixture") - await self.db.recalculate_fixture_scores(fixture_id) + await self.db.scores.recalculate_fixture_scores(fixture_id) return await self._build_score_result(fixture_id, guild_id) @@ -79,7 +79,7 @@ async def maybe_recalculate_fixture( self, fixture_id: int, guild_id: str ) -> FixtureScoreResult | None: """Recalculate a fixture only when it has already been scored.""" - if not await self.db.fixture_has_scores(fixture_id): + if not await self.db.scores.fixture_has_scores(fixture_id): return None return await self.calculate_fixture_scores(fixture_id, guild_id) @@ -92,11 +92,13 @@ async def get_fixture_prediction_summary( FixtureNotFoundError: Fixture was deleted before the panel action ran. NoPredictionsSavedError: Fixture still exists but has no saved predictions. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - predictions = await self.db.get_all_predictions(fixture_id, include_pending=True) + predictions = await self.db.predictions.get_all_predictions( + fixture_id, include_pending=True + ) if not predictions: raise NoPredictionsSavedError @@ -116,24 +118,28 @@ async def approve_partial_prediction( FixtureNotFoundError: Fixture was deleted before the review action ran. ValueError: The selected prediction is not pending review or the write failed. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + prediction = await self.db.predictions.get_prediction(fixture_id, user_id, guild_id) if prediction is None or not prediction["pending_partial_approval"]: raise ValueError("No late prediction awaiting review for that user") - approved = await self.db.approve_partial_prediction(fixture_id, user_id, admin_user_id) + approved = await self.db.predictions.approve_partial_prediction_with_recalc( + fixture_id, user_id, admin_user_id + ) if not approved: raise ValueError("Partial approval failed") - refreshed_prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + refreshed_prediction = await self.db.predictions.get_prediction( + fixture_id, user_id, guild_id + ) if refreshed_prediction is None: raise ValueError("Prediction disappeared after approval") recalculation = None - if await self.db.fixture_has_scores(fixture_id): + if await self.db.scores.fixture_has_scores(fixture_id): recalculation = await self._build_score_result(fixture_id, guild_id) return fixture, refreshed_prediction, recalculation @@ -149,20 +155,22 @@ async def reject_partial_prediction( FixtureNotFoundError: Fixture was deleted before the review action ran. ValueError: The selected prediction is not pending review or the write failed. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + prediction = await self.db.predictions.get_prediction(fixture_id, user_id, guild_id) if prediction is None or not prediction["pending_partial_approval"]: raise ValueError("No late prediction awaiting review for that user") - rejected = await self.db.reject_partial_prediction(fixture_id, user_id) + rejected = await self.db.predictions.reject_partial_prediction_with_recalc( + fixture_id, user_id + ) if not rejected: raise ValueError("Partial rejection failed") recalculation = None - if await self.db.fixture_has_scores(fixture_id): + if await self.db.scores.fixture_has_scores(fixture_id): recalculation = await self._build_score_result(fixture_id, guild_id) return fixture, prediction, recalculation @@ -182,11 +190,13 @@ async def replace_prediction( PredictionDisappearedError: Update succeeded but the refreshed row vanished. ValueError: Validation or write failures that should surface directly to admins. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - existing_prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + existing_prediction = await self.db.predictions.get_prediction( + fixture_id, user_id, guild_id + ) if existing_prediction is None: raise PredictionNotFoundError @@ -194,7 +204,7 @@ async def replace_prediction( if errors: raise ValueError("\n".join(errors)) - updated = await self.db.admin_update_prediction_with_recalc( + updated = await self.db.predictions.admin_update_prediction_with_recalc( fixture_id, user_id, predictions, @@ -203,12 +213,14 @@ async def replace_prediction( if not updated: raise ValueError("Prediction update failed") - refreshed_prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + refreshed_prediction = await self.db.predictions.get_prediction( + fixture_id, user_id, guild_id + ) if refreshed_prediction is None: raise PredictionDisappearedError("update") recalculation = None - if await self.db.fixture_has_scores(fixture_id): + if await self.db.scores.fixture_has_scores(fixture_id): recalculation = await self._build_score_result(fixture_id, guild_id) return fixture, refreshed_prediction, recalculation @@ -226,26 +238,30 @@ async def toggle_late_penalty_waiver( PredictionDisappearedError: Waiver update succeeded but the refreshed row vanished. ValueError: On-time or write-failure cases that should surface directly to admins. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError - prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + prediction = await self.db.predictions.get_prediction(fixture_id, user_id, guild_id) if prediction is None: raise PredictionNotFoundError if not prediction["is_late"]: raise ValueError("That prediction was submitted on time") - waived = await self.db.toggle_late_penalty_waiver_with_recalc(fixture_id, user_id) + waived = await self.db.predictions.toggle_late_penalty_waiver_with_recalc( + fixture_id, user_id + ) if waived is None: raise ValueError("Late waiver update failed") - refreshed_prediction = await self.db.get_prediction(fixture_id, user_id, guild_id) + refreshed_prediction = await self.db.predictions.get_prediction( + fixture_id, user_id, guild_id + ) if refreshed_prediction is None: raise PredictionDisappearedError("waiver update") recalculation = None - if await self.db.fixture_has_scores(fixture_id): + if await self.db.scores.fixture_has_scores(fixture_id): recalculation = await self._build_score_result(fixture_id, guild_id) return fixture, refreshed_prediction, recalculation @@ -261,7 +277,7 @@ async def correct_results( FixtureNotFoundError: Fixture was deleted before the admin action ran. ValueError: Result parsing or write failures that should surface directly to admins. """ - fixture = await self.db.get_fixture_by_id(fixture_id, guild_id) + fixture = await self.db.fixtures.get_fixture_by_id(fixture_id, guild_id) if fixture is None: raise FixtureNotFoundError @@ -269,7 +285,7 @@ async def correct_results( if errors: raise ValueError("\n".join(errors)) - recalculated = await self.db.save_results_with_recalc(fixture_id, results) + recalculated = await self.db.results.save_results_with_recalc(fixture_id, results) recalculation = ( await self._build_score_result(fixture_id, guild_id) if recalculated else None ) diff --git a/typer_bot/services/calculation_posting.py b/typer_bot/services/calculation_posting.py index 9278a40..b4554d7 100644 --- a/typer_bot/services/calculation_posting.py +++ b/typer_bot/services/calculation_posting.py @@ -52,7 +52,7 @@ async def _post_calculation_to_channel( ) return - config = await db.get_guild_config(str(interaction.guild_id)) + config = await db.guild_config.get_guild_config(str(interaction.guild_id)) channel = None if config is not None: try: diff --git a/typer_bot/utils/logger.py b/typer_bot/utils/logger.py index 0abf624..ed60fdb 100644 --- a/typer_bot/utils/logger.py +++ b/typer_bot/utils/logger.py @@ -279,7 +279,7 @@ class LogTimer: Args: logger: Logger instance to use for output - operation: Human-readable operation name (e.g., "db.save_prediction") + operation: Human-readable operation name (e.g., "db.predictions.save_prediction") event_type: Optional semantic event type for filtering (e.g., "transaction.commit") level: Log level for successful completion (default: DEBUG) **extra_fields: Additional context fields to include in log entry @@ -288,18 +288,18 @@ class LogTimer: duration_ms: Duration of the operation in milliseconds (available after exit) Example: - with LogTimer(logger, "database.save_prediction"): - await db.save_prediction(...) + with LogTimer(logger, "db.predictions.save_prediction"): + await db.predictions.save_prediction(...) # With event_type and extra fields with LogTimer( logger, - "db.save_prediction", + "db.predictions.save_prediction", event_type="prediction.saved", user_id=user_id, fixture_id=fixture_id ): - await db.save_prediction(...) + await db.predictions.save_prediction(...) """ def __init__( diff --git a/typer_bot/utils/permissions.py b/typer_bot/utils/permissions.py index c806942..66e38bd 100644 --- a/typer_bot/utils/permissions.py +++ b/typer_bot/utils/permissions.py @@ -56,7 +56,7 @@ async def is_configured_admin(interaction: discord.Interaction, db: "Database") if not interaction.guild or interaction.guild_id is None: return False - config = await db.get_guild_config(str(interaction.guild_id)) + config = await db.guild_config.get_guild_config(str(interaction.guild_id)) if config is None: return False @@ -66,7 +66,7 @@ async def is_configured_admin(interaction: discord.Interaction, db: "Database") async def get_configured_admin_role_mention(guild_id: str, db: "Database") -> str | None: """Return the configured TyperBot admin role mention for one guild.""" - config = await db.get_guild_config(guild_id) + config = await db.guild_config.get_guild_config(guild_id) if config is None: return None return f"<@&{config['admin_role_id']}>" @@ -80,7 +80,7 @@ async def get_admin_permission_error( if not interaction.guild or interaction.guild_id is None: return "This command can only be used in a server." - config = await db.get_guild_config(str(interaction.guild_id)) + config = await db.guild_config.get_guild_config(str(interaction.guild_id)) if config is None: return SETUP_REQUIRED_MESSAGE