← back to djacobs__Pinwheel

Function bodies 688 total

All specs Real LLM only Function bodies
_get_game_phase function · python · L208-L236 (29 LOC)
src/pinwheel/api/pages.py
async def _get_game_phase(repo: RepoDep, season_id: str, round_number: int) -> str | None:
    """Return the phase for a specific round's games ('semifinal', 'finals', or None).

    Reads the precise phase directly from the schedule entry when available.
    Falls back to inference (comparing team pairs against the initial playoff
    round) for legacy entries stored as ``"playoff"``.
    """
    schedule = await repo.get_schedule_for_round(season_id, round_number)
    if not schedule:
        return None
    entry_phase = schedule[0].phase
    if entry_phase in ("semifinal", "finals"):
        return entry_phase
    if entry_phase != "playoff":
        return None
    # Legacy fallback: infer from team pairs
    full_playoff = await repo.get_full_schedule(season_id, phase="playoff")
    if not full_playoff:
        return "semifinal"
    earliest_round = min(s.round_number for s in full_playoff)
    initial_pairs = [
        frozenset({s.home_team_id, s.away_team_id})
        for s i
_generate_series_description function · python · L239-L308 (70 LOC)
src/pinwheel/api/pages.py
async def _generate_series_description(
    phase: str,
    home_team_name: str,
    away_team_name: str,
    home_wins: int,
    away_wins: int,
    best_of: int,
    wins_needed: int,
) -> str | None:
    """Call Haiku to generate a natural-language series description.

    Returns None on any failure (caller falls back to template).
    """
    import os

    api_key = os.environ.get("ANTHROPIC_API_KEY", "")
    if not api_key:
        return None

    clinched = home_wins >= wins_needed or away_wins >= wins_needed
    if home_wins > away_wins:
        leader = home_team_name
        leader_wins, trailer_wins = home_wins, away_wins
    elif away_wins > home_wins:
        leader = away_team_name
        leader_wins, trailer_wins = away_wins, home_wins
    else:
        leader = ""
        leader_wins, trailer_wins = home_wins, away_wins

    phase_label = "Championship Finals" if phase == "finals" else "Semifinal Series"
    is_sweep = clinched and min(home_wins, away_wins) == 0

   
_build_series_description_fallback function · python · L311-L338 (28 LOC)
src/pinwheel/api/pages.py
def _build_series_description_fallback(
    phase: str,
    phase_label: str,
    home_team_name: str,
    away_team_name: str,
    home_wins: int,
    away_wins: int,
    wins_needed: int,
) -> str:
    """Template fallback for series descriptions (used when Haiku unavailable)."""
    if home_wins >= wins_needed:
        outcome = "championship" if phase == "finals" else "series"
        return f"{phase_label} · {home_team_name} win {outcome} {home_wins}-{away_wins}"
    if away_wins >= wins_needed:
        outcome = "championship" if phase == "finals" else "series"
        return f"{phase_label} · {away_team_name} win {outcome} {away_wins}-{home_wins}"
    if home_wins == away_wins:
        record_text = f"Series tied {home_wins}-{away_wins}"
    elif home_wins > away_wins:
        record_text = f"{home_team_name} lead {home_wins}-{away_wins}"
    else:
        record_text = f"{away_team_name} lead {away_wins}-{home_wins}"
    clinch_text = (
        f"First to {wins_needed} wins is 
build_series_context function · python · L341-L397 (57 LOC)
src/pinwheel/api/pages.py
async def build_series_context(
    phase: str,
    home_team_name: str,
    away_team_name: str,
    home_wins: int,
    away_wins: int,
    best_of: int,
) -> dict:
    """Build a series context dict for display in the arena template.

    Calls Haiku to generate a natural-language description; falls back to
    a rigid template when the API is unavailable.

    Args:
        phase: 'semifinal' or 'finals'.
        home_team_name: Display name for home team.
        away_team_name: Display name for away team.
        home_wins: Number of series wins for the home team.
        away_wins: Number of series wins for the away team.
        best_of: Best-of-N for this series round.

    Returns:
        Dict with keys: phase, phase_label, home_wins, away_wins, best_of,
        wins_needed, description.
    """
    wins_needed = (best_of + 1) // 2
    phase_label = "CHAMPIONSHIP FINALS" if phase == "finals" else "SEMIFINAL SERIES"

    # Try Haiku for a natural description
    haiku_desc = 
_compute_series_context_for_game function · python · L400-L449 (50 LOC)
src/pinwheel/api/pages.py
async def _compute_series_context_for_game(
    repo: RepoDep,
    season_id: str,
    home_team_id: str,
    away_team_id: str,
    home_team_name: str,
    away_team_name: str,
    game_phase: str | None,
    ruleset: RuleSet | None = None,
    round_number: int | None = None,
) -> dict | None:
    """Compute series context for a specific playoff matchup.

    Returns None if the game is not a playoff game.

    Args:
        round_number: If set, only count series games played BEFORE this round,
            so the displayed headline reflects the pre-game stakes rather than
            the current series state.
    """
    if not game_phase:
        return None

    # Get ruleset for best-of values
    if ruleset is None:
        season = await repo.get_season(season_id)
        if season and season.current_ruleset:
            ruleset = RuleSet(**season.current_ruleset)
        else:
            ruleset = DEFAULT_RULESET

    best_of = (
        ruleset.playoff_finals_best_of if gam
_compute_streaks_from_games function · python · L452-L478 (27 LOC)
src/pinwheel/api/pages.py
def _compute_streaks_from_games(games: list[object]) -> dict[str, int]:
    """Compute current win/loss streaks per team from game result rows.

    Positive = win streak, negative = loss streak. Resets on reversal.
    """
    sorted_games = sorted(games, key=lambda g: (g.round_number, g.matchup_index))
    team_results: dict[str, list[bool]] = {}
    for g in sorted_games:
        for tid in (g.home_team_id, g.away_team_id):
            if tid not in team_results:
                team_results[tid] = []
            team_results[tid].append(g.winner_team_id == tid)

    streaks: dict[str, int] = {}
    for tid, results in team_results.items():
        if not results:
            streaks[tid] = 0
            continue
        streak = 0
        last_result = results[-1]
        for r in reversed(results):
            if r == last_result:
                streak += 1
            else:
                break
        streaks[tid] = streak if last_result else -streak
    return streaks
what_changed_partial function · python · L1076-L1235 (160 LOC)
src/pinwheel/api/pages.py
async def what_changed_partial(request: Request, repo: RepoDep) -> HTMLResponse:
    """HTMX partial — returns the what-changed widget HTML fragment.

    Polled by the home page via hx-trigger="every 60s" to keep the
    widget up-to-date without a full page reload.
    """
    season_id, _ = await _get_active_season(repo)
    if not season_id:
        return HTMLResponse("")

    standings = await _get_standings(repo, season_id)

    # Find current round
    current_round = await repo.get_latest_round_number(season_id) or 0

    if current_round <= 0:
        return HTMLResponse("")

    season_phase = await _get_season_phase(repo, season_id)
    all_games = await repo.get_all_games(season_id)
    streaks: dict[str, int] = {}
    if all_games:
        streaks = _compute_streaks_from_games(all_games)

    # Previous round standings
    prev_standings: list[dict] = []
    prev_streaks: dict[str, int] = {}
    if current_round > 1:
        prev_results: list[dict] = []
        for rn in
About: code-quality intelligence by Repobility · https://repobility.com
play_page function · python · L1239-L1366 (128 LOC)
src/pinwheel/api/pages.py
async def play_page(request: Request, repo: RepoDep, current_user: OptionalUser) -> HTMLResponse:
    """How to Play — onboarding page for new players."""
    settings = request.app.state.settings
    season_id, season_name = await _get_active_season(repo)

    # Current league state for context
    current_round = 0
    total_teams = 0
    total_hoopers = 0
    total_games = 0
    season_status = ""
    season_phase_desc = ""
    team_names: list[str] = []

    if season_id:
        standings = await _get_standings(repo, season_id)
        total_teams = len(standings)
        total_games = sum(s["wins"] for s in standings)
        current_round = await repo.get_latest_round_number(season_id) or 0
        # Count agents + collect team names
        for s in standings:
            team = await repo.get_team(s["team_id"])
            if team:
                total_hoopers += len(team.hoopers)
                team_names.append(team.name)

        # Season phase context (season loaded belo
_compute_standings_callouts function · python · L1648-L1716 (69 LOC)
src/pinwheel/api/pages.py
def _compute_standings_callouts(
    standings: list[dict],
    streaks: dict[str, int],
    current_round: int,
    total_rounds: int,
) -> list[str]:
    """Compute 2-4 narrative callouts from standings data.

    Returns a list of short, punchy observations about the standings.
    """
    if not standings:
        return []

    callouts: list[str] = []

    # Tightest race — smallest gap in wins between adjacent teams
    if len(standings) >= 2:
        min_gap = float("inf")
        tight_pair = None
        for i in range(len(standings) - 1):
            gap = standings[i]["wins"] - standings[i + 1]["wins"]
            if gap < min_gap:
                min_gap = gap
                tight_pair = (standings[i], standings[i + 1], i + 1)

        if tight_pair and min_gap <= 1:
            team_a, team_b, seed = tight_pair
            if min_gap == 0:
                callouts.append(
                    f"{team_a['team_name']} and {team_b['team_name']} "
                    f"tied f
standings_page function · python · L1727-L1814 (88 LOC)
src/pinwheel/api/pages.py
async def standings_page(
    request: Request, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Standings page with narrative context."""
    season_id = await _get_active_season_id(repo)
    standings: list[dict] = []
    season_phase = ""
    streaks: dict[str, int] = {}
    callouts: list[str] = []
    sos: dict[str, dict[str, int]] = {}
    magic_numbers: dict[str, int | None] = {}
    trajectory: dict[str, int] = {}

    if season_id:
        standings = await _get_standings(repo, season_id)
        season_phase = await _get_season_phase(repo, season_id)
        all_games = await repo.get_all_games(season_id)
        if all_games:
            streaks = _compute_streaks_from_games(all_games)

        # Compute current round and total rounds
        current_round = await repo.get_latest_round_number(season_id) or 0

        # Get total scheduled rounds
        full_schedule = await repo.get_full_schedule(season_id)
        total_rounds = max((s.round_number for s
_compute_game_standings function · python · L1817-L1846 (30 LOC)
src/pinwheel/api/pages.py
def _compute_game_standings(
    all_games: list[object],
    up_to_round: int,
) -> list[dict]:
    """Compute standings from games played before a given round.

    Used to determine game significance (first-place showdown, clinch scenarios)
    based on where teams stood going into the game.

    Args:
        all_games: All game results in the season.
        up_to_round: Include games from rounds strictly before this one.

    Returns:
        Sorted standings list (same format as compute_standings).
    """
    prior_results: list[dict] = [
        {
            "home_team_id": g.home_team_id,
            "away_team_id": g.away_team_id,
            "home_score": g.home_score,
            "away_score": g.away_score,
            "winner_team_id": g.winner_team_id,
        }
        for g in all_games
        if g.round_number < up_to_round
    ]
    if not prior_results:
        return []
    return compute_standings(prior_results)
team_page function · python · L2122-L2296 (175 LOC)
src/pinwheel/api/pages.py
async def team_page(
    request: Request, team_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Team profile page."""
    team = await repo.get_team(team_id)
    if not team:
        raise HTTPException(404, "Team not found")

    # Use the team's own season for contextual data (standings, governors,
    # strategy, league averages).  This ensures team pages remain fully
    # populated even when a newer season is active — e.g. when a user
    # follows a link from an old game detail page.
    season_id = team.season_id
    team_standings = None
    standing_position = None
    league_name = None

    # League averages for spider chart shadow
    league_avg = {}
    if season_id:
        standings = await _get_standings(repo, season_id)
        for idx, s in enumerate(standings):
            if s["team_id"] == team_id:
                team_standings = s
                standing_position = idx + 1
                break

        season = await repo.get_season
hooper_page function · python · L2300-L2488 (189 LOC)
src/pinwheel/api/pages.py
async def hooper_page(
    request: Request, hooper_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Individual hooper profile page."""
    hooper = await repo.get_hooper(hooper_id)
    if not hooper:
        raise HTTPException(404, "Hooper not found")

    team = await repo.get_team(hooper.team_id)
    season_id = await _get_active_season_id(repo)

    # Spider chart data
    league_avg = {}
    if season_id:
        league_avg = await repo.get_league_attribute_averages(season_id)

    hooper_pts = spider_chart_data(hooper.attributes) if hooper.attributes else []
    avg_pts = spider_chart_data(league_avg) if league_avg else []

    # Game log — grouped by season so current season shows game-level detail
    # and past seasons collapse to aggregate rows.
    # carry_over_teams creates new hooper IDs per season; we link across seasons
    # by name (the only stable identifier) to build a full career view.
    from collections import defaultdict

    all_hoo
hooper_bio_edit_form function · python · L2492-L2512 (21 LOC)
src/pinwheel/api/pages.py
async def hooper_bio_edit_form(
    request: Request, hooper_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Return HTMX fragment with bio edit form. Governor-only."""
    hooper = await repo.get_hooper(hooper_id)
    if not hooper:
        raise HTTPException(404, "Hooper not found")

    season_id = await _get_active_season_id(repo)
    if not current_user or not season_id:
        raise HTTPException(403, "Not authorized")

    enrollment = await repo.get_player_enrollment(current_user.discord_id, season_id)
    if not enrollment or enrollment[0] != hooper.team_id:
        raise HTTPException(403, "Not authorized — must be team governor")

    return templates.TemplateResponse(
        request,
        "partials/hooper_bio_edit.html",
        {"backstory": hooper.backstory or "", "hooper_id": hooper_id},
    )
hooper_bio_view function · python · L2516-L2535 (20 LOC)
src/pinwheel/api/pages.py
async def hooper_bio_view(
    request: Request, hooper_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Return HTMX fragment with bio display. Used after cancel/save."""
    hooper = await repo.get_hooper(hooper_id)
    if not hooper:
        raise HTTPException(404, "Hooper not found")

    season_id = await _get_active_season_id(repo)
    can_edit = False
    if current_user and season_id:
        enrollment = await repo.get_player_enrollment(current_user.discord_id, season_id)
        if enrollment and enrollment[0] == hooper.team_id:
            can_edit = True

    return templates.TemplateResponse(
        request,
        "partials/hooper_bio_view.html",
        {"backstory": hooper.backstory, "can_edit": can_edit, "hooper_id": hooper_id},
    )
Want this analysis on your repo? https://repobility.com/scan/
update_hooper_bio function · python · L2539-L2565 (27 LOC)
src/pinwheel/api/pages.py
async def update_hooper_bio(
    request: Request, hooper_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Update hooper bio. Governor-only."""
    hooper = await repo.get_hooper(hooper_id)
    if not hooper:
        raise HTTPException(404, "Hooper not found")

    season_id = await _get_active_season_id(repo)
    if not current_user or not season_id:
        raise HTTPException(403, "Not authorized")

    enrollment = await repo.get_player_enrollment(current_user.discord_id, season_id)
    if not enrollment or enrollment[0] != hooper.team_id:
        raise HTTPException(403, "Not authorized — must be team governor")

    form = await request.form()
    backstory = str(form.get("backstory", "")).strip()
    await repo.update_hooper_backstory(hooper_id, backstory)
    await repo.session.commit()

    # Return the view fragment
    return templates.TemplateResponse(
        request,
        "partials/hooper_bio_view.html",
        {"backstory": backstory, "ca
governor_profile_page function · python · L2569-L2604 (36 LOC)
src/pinwheel/api/pages.py
async def governor_profile_page(
    request: Request, player_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Governor profile page -- governance record and activity history."""
    player = await repo.get_player(player_id)
    if not player:
        raise HTTPException(404, "Governor not found")

    season_id = await _get_active_season_id(repo)
    team = None
    activity: dict = {
        "proposals_submitted": 0,
        "proposals_passed": 0,
        "proposals_failed": 0,
        "votes_cast": 0,
        "proposal_list": [],
        "token_balance": None,
    }

    if player.team_id:
        team = await repo.get_team(player.team_id)

    if season_id:
        activity = await repo.get_governor_activity(player_id, season_id)

    return templates.TemplateResponse(
        request,
        "pages/governor.html",
        {
            "active_page": "governance",
            "player": player,
            "team": team,
            "activity": activity,
governance_page function · python · L2608-L2730 (123 LOC)
src/pinwheel/api/pages.py
async def governance_page(
    request: Request, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Governance audit trail — proposals, outcomes, vote totals.

    Publicly viewable. Proposing and voting require Discord auth
    (via bot slash commands).
    """
    season_id = await _get_active_season_id(repo)
    proposals = []
    rules_changed = []
    season_phase = ""

    if season_id:
        season_phase = await _get_season_phase(repo, season_id)

        # Gather all governance events we need
        submitted = await repo.get_events_by_type(
            season_id=season_id,
            event_types=["proposal.submitted"],
        )
        outcome_events = await repo.get_events_by_type(
            season_id=season_id,
            event_types=[
                "proposal.confirmed",
                "proposal.passed",
                "proposal.failed",
                "proposal.cancelled",
            ],
        )
        vote_events = await repo.get_events_by_
_compute_rule_impact function · python · L2812-L2845 (34 LOC)
src/pinwheel/api/pages.py
async def _compute_rule_impact(repo: RepoDep, season_id: str, round_enacted: int) -> str:
    """Compute a gameplay impact string for a rule change.

    Compares average total game score (home + away) before vs after
    the round the rule was enacted.  Returns a human-readable string
    like "Scoring +12% since change" or "Too early to measure".
    """
    min_games_after = 2

    # Before: rounds 1 through round_enacted - 1
    if round_enacted > 1:
        before_avg, before_count = await repo.get_avg_total_game_score_for_rounds(
            season_id,
            1,
            round_enacted - 1,
        )
    else:
        before_avg, before_count = 0.0, 0

    # After: round_enacted onward (large upper bound)
    after_avg, after_count = await repo.get_avg_total_game_score_for_rounds(
        season_id,
        round_enacted,
        9999,
    )

    if after_count < min_games_after:
        return "Too early to measure"
    if before_count == 0 or before_avg == 0:
        ret
rules_page function · python · L2849-L2981 (133 LOC)
src/pinwheel/api/pages.py
async def rules_page(request: Request, repo: RepoDep, current_user: OptionalUser) -> HTMLResponse:
    """Current rules page."""
    season_id = await _get_active_season_id(repo)
    ruleset = RuleSet()
    defaults = RuleSet()
    changes_from_default: dict = {}
    rule_history: list[dict[str, object]] = []
    rule_change_timeline: dict[str, list[dict[str, object]]] = {}

    if season_id:
        season = await repo.get_season(season_id)
        if season and season.current_ruleset:
            ruleset = RuleSet(**season.current_ruleset)

        for param in RuleSet.model_fields:
            current = getattr(ruleset, param)
            default = getattr(defaults, param)
            if current != default:
                changes_from_default[param] = {
                    "current": current,
                    "default": default,
                }

        rc_events = await repo.get_events_by_type(
            season_id=season_id,
            event_types=["rule.enacted"],
       
reports_page function · python · L2985-L3029 (45 LOC)
src/pinwheel/api/pages.py
async def reports_page(request: Request, repo: RepoDep, current_user: OptionalUser) -> HTMLResponse:
    """Reports archive page."""
    season_id = await _get_active_season_id(repo)
    reports = []
    season_phase = ""

    if season_id:
        season_phase = await _get_season_phase(repo, season_id)

        # Build a map of round_number -> phase for playoff-aware labelling
        round_phases: dict[int, str | None] = {}

        for rn in range(100, 0, -1):
            round_reports = await repo.get_reports_for_round(season_id, rn)
            for m in round_reports:
                if m.report_type != "private":
                    # Lazily compute phase for this round
                    if rn not in round_phases:
                        round_phases[rn] = await _get_game_phase(
                            repo,
                            season_id,
                            rn,
                        )
                    reports.append(
                        {
         
newspaper_page function · python · L3033-L3183 (151 LOC)
src/pinwheel/api/pages.py
async def newspaper_page(
    request: Request, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """The Pinwheel Post — newspaper-style round summary page."""
    from sqlalchemy import func, select

    from pinwheel.ai.insights import generate_newspaper_headlines_mock
    from pinwheel.db.models import BoxScoreRow, GameResultRow, HooperRow, TeamRow

    season_id, season_name = await _get_active_season(repo)
    headline = ""
    subhead = ""
    sim_report = ""
    gov_report = ""
    impact_report = ""
    highlight_reel = ""
    standings: list[dict] = []
    hot_players: list[dict] = []
    current_round = 0

    if season_id:
        # Find latest round
        current_round = await repo.get_latest_round_number(season_id) or 0

        if current_round > 0:
            # Detect playoff phase for this round (needed for headlines
            # and to override stale governance reports)
            round_phase = await _get_game_phase(repo, season_id, current_round)
 
playoffs_page function · python · L3187-L3203 (17 LOC)
src/pinwheel/api/pages.py
async def playoffs_page(
    request: Request, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Playoff bracket visualization page."""
    from pinwheel.api.games import _build_bracket_data

    bracket = await _build_bracket_data(repo)

    return templates.TemplateResponse(
        request,
        "pages/playoffs.html",
        {
            "active_page": "playoffs",
            "bracket": bracket,
            **_auth_context(request, current_user),
        },
    )
All rows scored by the Repobility analyzer (https://repobility.com)
season_archives_page function · python · L3207-L3236 (30 LOC)
src/pinwheel/api/pages.py
async def season_archives_page(
    request: Request, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """List all archived seasons."""
    archives = await repo.get_all_archives()
    archive_list = []
    for a in archives:
        archive_list.append(
            {
                "season_id": a.season_id,
                "season_name": a.season_name,
                "champion_team_name": a.champion_team_name,
                "total_games": a.total_games,
                "total_proposals": a.total_proposals,
                "total_rule_changes": a.total_rule_changes,
                "governor_count": a.governor_count,
                "created_at": a.created_at.isoformat() if a.created_at else "",
            }
        )

    return templates.TemplateResponse(
        request,
        "pages/season_archive.html",
        {
            "active_page": "archives",
            "archives": archive_list,
            "archive": None,
            **_auth_context(request, curr
season_archive_detail function · python · L3240-L3273 (34 LOC)
src/pinwheel/api/pages.py
async def season_archive_detail(
    request: Request, season_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """View a specific season's archive."""
    archive = await repo.get_season_archive(season_id)
    if not archive:
        raise HTTPException(404, "Archive not found")

    archive_data = {
        "season_id": archive.season_id,
        "season_name": archive.season_name,
        "champion_team_id": archive.champion_team_id,
        "champion_team_name": archive.champion_team_name,
        "final_standings": archive.final_standings or [],
        "final_ruleset": archive.final_ruleset or {},
        "rule_change_history": archive.rule_change_history or [],
        "total_games": archive.total_games,
        "total_proposals": archive.total_proposals,
        "total_rule_changes": archive.total_rule_changes,
        "governor_count": archive.governor_count,
        "reports": archive.reports or [],
        "created_at": archive.created_at.isoformat() 
history_page function · python · L3277-L3309 (33 LOC)
src/pinwheel/api/pages.py
async def history_page(request: Request, repo: RepoDep, current_user: OptionalUser) -> HTMLResponse:
    """Hall of History -- index of all past seasons with championship banners."""
    archives = await repo.get_all_archives()
    archive_list = []
    for a in archives:
        # Extract memorial data if available
        memorial = a.memorial or {}
        narrative_excerpt = str(memorial.get("season_narrative", ""))[:200]

        archive_list.append(
            {
                "season_id": a.season_id,
                "season_name": a.season_name,
                "champion_team_name": a.champion_team_name,
                "total_games": a.total_games,
                "total_proposals": a.total_proposals,
                "total_rule_changes": a.total_rule_changes,
                "governor_count": a.governor_count,
                "narrative_excerpt": narrative_excerpt,
                "has_memorial": bool(memorial.get("season_narrative")),
                "created_at": a.create
memorial_page function · python · L3313-L3361 (49 LOC)
src/pinwheel/api/pages.py
async def memorial_page(
    request: Request, season_id: str, repo: RepoDep, current_user: OptionalUser
) -> HTMLResponse:
    """Full memorial page for a completed season."""
    archive = await repo.get_season_archive(season_id)
    if not archive:
        raise HTTPException(404, "Season archive not found")

    memorial = archive.memorial or {}

    # Build structured memorial data for the template
    memorial_data = {
        "season_id": archive.season_id,
        "season_name": archive.season_name,
        "champion_team_id": archive.champion_team_id,
        "champion_team_name": archive.champion_team_name,
        "total_games": archive.total_games,
        "total_proposals": archive.total_proposals,
        "total_rule_changes": archive.total_rule_changes,
        "governor_count": archive.governor_count,
        "final_standings": archive.final_standings or [],
        "final_ruleset": archive.final_ruleset or {},
        "rule_change_history": archive.rule_change_history 
admin_landing_page function · python · L3365-L3385 (21 LOC)
src/pinwheel/api/pages.py
async def admin_landing_page(request: Request, current_user: OptionalUser) -> HTMLResponse:
    """Admin landing page — hub for admin tools.

    Redirects unauthenticated users to login. Returns 403 for non-admins.
    """
    settings = request.app.state.settings
    if current_user is None:
        oauth_enabled = bool(settings.discord_client_id and settings.discord_client_secret)
        if oauth_enabled:
            return RedirectResponse("/auth/login", status_code=302)
        raise HTTPException(403, "Not authorized")

    admin_id = settings.pinwheel_admin_discord_id
    if not admin_id or current_user.discord_id != admin_id:
        raise HTTPException(403, "Not authorized")

    return templates.TemplateResponse(
        request,
        "pages/admin.html",
        {"active_page": "admin", **_auth_context(request, current_user)},
    )
terms_page function · python · L3389-L3395 (7 LOC)
src/pinwheel/api/pages.py
async def terms_page(request: Request, current_user: OptionalUser) -> HTMLResponse:
    """Terms of Service."""
    return templates.TemplateResponse(
        request,
        "pages/terms.html",
        {"active_page": "terms", **_auth_context(request, current_user)},
    )
privacy_page function · python · L3399-L3405 (7 LOC)
src/pinwheel/api/pages.py
async def privacy_page(request: Request, current_user: OptionalUser) -> HTMLResponse:
    """Privacy Policy."""
    return templates.TemplateResponse(
        request,
        "pages/privacy.html",
        {"active_page": "privacy", **_auth_context(request, current_user)},
    )
get_round_reports function · python · L15-L36 (22 LOC)
src/pinwheel/api/reports.py
async def get_round_reports(
    season_id: str,
    round_number: int,
    repo: RepoDep,
    report_type: str | None = None,
) -> dict:
    """Get all public reports for a round. Private reports are excluded."""
    rows = await repo.get_reports_for_round(season_id, round_number, report_type)
    # Filter out private reports from public endpoint
    public = [r for r in rows if r.report_type != "private"]
    return {
        "data": [
            {
                "id": r.id,
                "report_type": r.report_type,
                "round_number": r.round_number,
                "content": r.content,
                "created_at": r.created_at.isoformat() if r.created_at else None,
            }
            for r in public
        ]
    }
If a scraper extracted this row, it came from Repobility (https://repobility.com)
get_private_reports function · python · L40-L84 (45 LOC)
src/pinwheel/api/reports.py
async def get_private_reports(
    request: Request,
    season_id: str,
    governor_id: str,
    repo: RepoDep,
    current_user: OptionalUser,
    round_number: int | None = None,
) -> dict:
    """Get private reports for a specific governor.

    Access control: requires an authenticated session whose player ID
    matches the requested governor_id.  In development mode, auth is
    bypassed so local testing works without Discord OAuth.
    """
    settings: Settings = request.app.state.settings
    is_dev = settings.pinwheel_env == "development"

    if not is_dev:
        if current_user is None:
            raise HTTPException(
                status_code=401,
                detail="Authentication required — please log in via Discord.",
            )

        player = await repo.get_player_by_discord_id(current_user.discord_id)
        if player is None or player.id != governor_id:
            raise HTTPException(
                status_code=403,
                detail="You can
get_latest_reports function · python · L88-L112 (25 LOC)
src/pinwheel/api/reports.py
async def get_latest_reports(season_id: str, repo: RepoDep) -> dict:
    """Get the most recent simulation and governance reports."""
    sim = await repo.get_latest_report(season_id, "simulation")
    gov = await repo.get_latest_report(season_id, "governance")

    result: dict = {}
    if sim:
        result["simulation"] = {
            "id": sim.id,
            "round_number": sim.round_number,
            "content": sim.content,
            "created_at": sim.created_at.isoformat() if sim.created_at else None,
        }
    if gov:
        result["governance"] = {
            "id": gov.id,
            "round_number": gov.round_number,
            "content": gov.content,
            "created_at": gov.created_at.isoformat() if gov.created_at else None,
        }

    if not result:
        raise HTTPException(status_code=404, detail="No reports found for this season")

    return {"data": result}
create_season_endpoint function · python · L26-L62 (37 LOC)
src/pinwheel/api/seasons.py
async def create_season_endpoint(
    body: CreateSeasonRequest,
    repo: RepoDep,
    _: Annotated[None, Depends(require_api_admin)],
) -> dict:
    """Admin endpoint to start a new season.

    Creates a new season with either default rules or carried-forward rules
    from a previous season. Teams, hoopers, and governor enrollments are
    carried over. Tokens are regenerated for all governors.
    """
    from pinwheel.core.season import start_new_season

    try:
        new_season = await start_new_season(
            repo=repo,
            league_id=body.league_id,
            season_name=body.name,
            carry_forward_rules=body.carry_forward_rules,
            previous_season_id=body.previous_season_id,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    teams = await repo.get_teams_for_season(new_season.id)

    return {
        "data": {
            "id": new_season.id,
            "league_id": new_season
get_standings function · python · L14-L46 (33 LOC)
src/pinwheel/api/standings.py
async def get_standings(season_id: str, repo: RepoDep) -> dict:
    """Get current standings for a season.

    Fetches all game results in a single query (replaces the old
    loop over rounds 1-50 that issued one query per round).
    Team names are resolved in a second bulk query instead of one
    query per standing entry.
    """
    games = await repo.get_all_games(season_id)
    all_results: list[dict] = [
        {
            "home_team_id": g.home_team_id,
            "away_team_id": g.away_team_id,
            "home_score": g.home_score,
            "away_score": g.away_score,
            "winner_team_id": g.winner_team_id,
        }
        for g in games
    ]

    standings = compute_standings(all_results)

    # Batch-fetch team names for all standing entries in one query
    team_ids = [s["team_id"] for s in standings]
    if team_ids:
        teams = await repo.get_teams_for_season(season_id)
        team_name_map = {t.id: t.name for t in teams}
        for s in standi
list_teams function · python · L13-L28 (16 LOC)
src/pinwheel/api/teams.py
async def list_teams(season_id: str, repo: RepoDep) -> dict:
    """List all teams for a season."""
    teams = await repo.get_teams_for_season(season_id)
    return {
        "data": [
            {
                "id": t.id,
                "name": t.name,
                "color": t.color,
                "motto": t.motto,
                "venue": t.venue,
                "hooper_count": len(t.hoopers),
            }
            for t in teams
        ],
    }
get_team function · python · L32-L55 (24 LOC)
src/pinwheel/api/teams.py
async def get_team(team_id: str, repo: RepoDep) -> dict:
    """Get a single team with its hoopers."""
    team = await repo.get_team(team_id)
    if not team:
        raise HTTPException(404, "Team not found")
    return {
        "data": {
            "id": team.id,
            "name": team.name,
            "color": team.color,
            "motto": team.motto,
            "venue": team.venue,
            "hoopers": [
                {
                    "id": h.id,
                    "name": h.name,
                    "archetype": h.archetype,
                    "attributes": h.attributes,
                    "is_active": h.is_active,
                }
                for h in team.hoopers
            ],
        },
    }
get_current_user function · python · L40-L60 (21 LOC)
src/pinwheel/auth/deps.py
async def get_current_user(request: Request) -> SessionUser | None:
    """Extract the current user from the signed session cookie.

    This is optional auth — returns None if the user is not logged in
    or if the cookie is invalid/expired.  Page handlers should work
    fine with ``current_user=None``.
    """
    raw = request.cookies.get(SESSION_COOKIE_NAME)
    if not raw:
        return None

    serializer = _get_serializer(request)
    try:
        data = serializer.loads(raw, max_age=SESSION_MAX_AGE)
        return SessionUser(**data)
    except BadSignature:
        logger.debug("Invalid or expired session cookie — ignoring")
        return None
    except (ValidationError, TypeError):
        logger.debug("Failed to deserialise session cookie", exc_info=True)
        return None
is_admin function · python · L72-L83 (12 LOC)
src/pinwheel/auth/deps.py
def is_admin(current_user: SessionUser | None, settings: Settings) -> bool:
    """Return True if *current_user* is the configured admin.

    Returns False when there is no user, no admin ID configured, or the
    IDs do not match.
    """
    if current_user is None:
        return False
    admin_id = settings.pinwheel_admin_discord_id
    if not admin_id:
        return False
    return current_user.discord_id == admin_id
About: code-quality intelligence by Repobility · https://repobility.com
check_admin_access function · python · L86-L121 (36 LOC)
src/pinwheel/auth/deps.py
def check_admin_access(
    current_user: SessionUser | None, request: Request
) -> RedirectResponse | HTMLResponse | None:
    """Gate admin routes.  Returns a denial response, or ``None`` if access is granted.

    Fail-closed: in production/staging, denies access when OAuth is
    misconfigured rather than falling through to unauthenticated access.
    In development mode, allows access without auth for local testing.

    Usage in a route handler::

        if (denied := check_admin_access(current_user, request)):
            return denied
    """
    settings: Settings = request.app.state.settings

    # Development mode: allow unauthenticated access for local testing.
    if settings.pinwheel_env == "development":
        return None

    # Production / staging: require authenticated admin.
    oauth_enabled = bool(settings.discord_client_id and settings.discord_client_secret)

    if not oauth_enabled:
        # Fail closed — OAuth not configured in non-dev = no admin access.
  
require_api_admin function · python · L124-L169 (46 LOC)
src/pinwheel/auth/deps.py
async def require_api_admin(
    request: Request,
    current_user: Annotated[SessionUser | None, Depends(get_current_user)],
) -> None:
    """Dependency that gates JSON API endpoints to admin users only.

    Raises HTTPException instead of returning HTML responses, making it
    suitable for use with ``Depends()`` on POST/PUT/DELETE API routes.

    Fail-closed: in production/staging, denies access when OAuth is
    misconfigured rather than falling through to unauthenticated access.
    In development mode, allows unauthenticated access for local testing.

    Usage in a route handler::

        @router.post("/api/something")
        async def my_endpoint(_: Annotated[None, Depends(require_api_admin)]) -> dict:
            ...
    """
    settings: Settings = request.app.state.settings

    # Development mode: allow unauthenticated access for local testing.
    if settings.pinwheel_env == "development":
        return

    # Production / staging: require authenticated admin.
    o
admin_auth_context function · python · L172-L194 (23 LOC)
src/pinwheel/auth/deps.py
def admin_auth_context(request: Request, current_user: SessionUser | None) -> dict:
    """Build auth-related template context for admin pages.

    Returns a dict suitable for splatting into a Jinja2 template context::

        return templates.TemplateResponse(
            request,
            "pages/my_admin_page.html",
            {
                "my_data": ...,
                **admin_auth_context(request, current_user),
            },
        )
    """
    settings: Settings = request.app.state.settings
    oauth_enabled = bool(settings.discord_client_id and settings.discord_client_secret)
    return {
        "current_user": current_user,
        "oauth_enabled": oauth_enabled,
        "pinwheel_env": settings.pinwheel_env,
        "app_version": APP_VERSION,
        "is_admin": is_admin(current_user, settings),
    }
login function · python · L56-L83 (28 LOC)
src/pinwheel/auth/oauth.py
async def login(request: Request) -> RedirectResponse:
    """Redirect to Discord OAuth2 consent page."""
    if not _oauth_enabled(request):
        return RedirectResponse(url="/", status_code=302)

    s = _settings(request)

    # Generate a CSRF-prevention state token and store it in a short-lived cookie.
    state = secrets.token_urlsafe(32)
    params = {
        "client_id": s.discord_client_id,
        "redirect_uri": s.discord_redirect_uri,
        "response_type": "code",
        "scope": DISCORD_SCOPES,
        "state": state,
    }
    redirect_url = f"{DISCORD_AUTHORIZE_URL}?{urlencode(params)}"
    response = RedirectResponse(url=redirect_url, status_code=302)
    is_prod = s.pinwheel_env == "production"
    response.set_cookie(
        "pinwheel_oauth_state",
        state,
        max_age=300,
        httponly=True,
        samesite="lax",
        secure=is_prod,
    )
    return response
callback function · python · L87-L167 (81 LOC)
src/pinwheel/auth/oauth.py
async def callback(
    request: Request,
    repo: RepoDep,
    code: str = "",
    state: str = "",
) -> RedirectResponse:
    """Handle the OAuth2 callback from Discord."""
    if not _oauth_enabled(request):
        return RedirectResponse(url="/", status_code=302)

    # Validate CSRF state
    expected_state = request.cookies.get("pinwheel_oauth_state", "")
    if not state or not expected_state or state != expected_state:
        logger.warning("OAuth state mismatch — possible CSRF")
        return RedirectResponse(url="/", status_code=302)

    s = _settings(request)

    # Exchange the code for an access token.
    try:
        token_data = await _exchange_code(
            code=code,
            client_id=s.discord_client_id,
            client_secret=s.discord_client_secret,
            redirect_uri=s.discord_redirect_uri,
        )
    except httpx.HTTPError:
        logger.exception("Discord token exchange error")
        return RedirectResponse(url="/", status_code=302)

_exchange_code function · python · L181-L203 (23 LOC)
src/pinwheel/auth/oauth.py
async def _exchange_code(
    *,
    code: str,
    client_id: str,
    client_secret: str,
    redirect_uri: str,
) -> dict[str, str]:
    """Exchange an authorization code for an access token."""
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            DISCORD_TOKEN_URL,
            data={
                "client_id": client_id,
                "client_secret": client_secret,
                "grant_type": "authorization_code",
                "code": code,
                "redirect_uri": redirect_uri,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )
        resp.raise_for_status()
        result: dict[str, str] = resp.json()
        return result
_fetch_user function · python · L206-L215 (10 LOC)
src/pinwheel/auth/oauth.py
async def _fetch_user(access_token: str) -> dict[str, str]:
    """Fetch the authenticated user's Discord profile."""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            DISCORD_USER_URL,
            headers={"Authorization": f"Bearer {access_token}"},
        )
        resp.raise_for_status()
        result: dict[str, str] = resp.json()
        return result
_find_project_root function · python · L12-L24 (13 LOC)
src/pinwheel/config.py
def _find_project_root() -> pathlib.Path:
    """Find the project root containing templates/ and static/ directories.

    In development: src/pinwheel/config.py → up 3 levels → project root.
    In Docker: installed package is in site-packages, but templates/ is at /app/.
    """
    source_root = pathlib.Path(__file__).resolve().parent.parent.parent
    if (source_root / "templates").exists():
        return source_root
    docker_root = pathlib.Path("/app")
    if (docker_root / "templates").exists():
        return docker_root
    return source_root
Want this analysis on your repo? https://repobility.com/scan/
_get_app_version function · python · L30-L49 (20 LOC)
src/pinwheel/config.py
def _get_app_version() -> str:
    """Return a short version string for cache busting (git hash or fallback)."""
    import subprocess

    try:
        return (
            subprocess.check_output(
                ["git", "rev-parse", "--short", "HEAD"],
                cwd=str(PROJECT_ROOT),
                stderr=subprocess.DEVNULL,
            )
            .decode()
            .strip()
        )
    except (subprocess.SubprocessError, OSError):
        # In Docker or without git, use a timestamp-based fallback
        import hashlib
        import time

        return hashlib.md5(str(int(time.time() / 3600)).encode()).hexdigest()[:7]
Settings._ensure_session_secret method · python · L124-L135 (12 LOC)
src/pinwheel/config.py
    def _ensure_session_secret(self) -> Settings:
        """Auto-generate session secret in dev; reject missing secret in production."""
        if not self.session_secret_key:
            if self.pinwheel_env == "production":
                msg = (
                    "SESSION_SECRET_KEY must be set in production. "
                    "Generate one with: python -c "
                    '"import secrets; print(secrets.token_urlsafe(32))"'
                )
                raise ValueError(msg)
            self.session_secret_key = secrets.token_urlsafe(32)
        return self
Settings.effective_game_cron method · python · L144-L157 (14 LOC)
src/pinwheel/config.py
    def effective_game_cron(self) -> str | None:
        """Return the cron expression that should drive game scheduling.

        Resolution order:
        1. If ``pinwheel_game_cron`` was explicitly changed from its default,
           honour the user override.
        2. Otherwise, derive from ``pinwheel_presentation_pace``.
        3. If pace is ``"manual"``, return ``None`` — the scheduler should not
           start an automatic job.
        """
        if self.pinwheel_game_cron != _DEFAULT_GAME_CRON:
            return self.pinwheel_game_cron

        return PACE_CRON_MAP.get(self.pinwheel_presentation_pace)
‹ prevpage 4 / 14next ›