Function bodies 688 total
_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 streakswhat_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 inAbout: 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 fstandings_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_seasonhooper_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_hoohooper_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, "cagovernor_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:
retrules_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, currseason_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.creatememorial_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 canget_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_seasonget_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 standilist_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 Noneis_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_idAbout: 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.
oadmin_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 responsecallback 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_rootWant 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 selfSettings.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)