From bc0f339d322908f19abcb6077a6f823c8e36882f Mon Sep 17 00:00:00 2001 From: jsteckler1 Date: Mon, 24 Nov 2025 15:46:14 -0500 Subject: [PATCH 1/2] Initial --- README.md | 26 +++ pyproject.toml | 4 + webapp/__init__.py | 1 + webapp/config.py | 23 +++ webapp/main.py | 70 +++++++ webapp/services/__init__.py | 1 + webapp/services/sleeper_client.py | 296 ++++++++++++++++++++++++++++++ webapp/static/app.css | 238 ++++++++++++++++++++++++ webapp/templates/base.html | 26 +++ webapp/templates/dashboard.html | 151 +++++++++++++++ webapp/templates/draft.html | 41 +++++ webapp/templates/players.html | 58 ++++++ webapp/templates/rosters.html | 32 ++++ 13 files changed, 967 insertions(+) create mode 100644 webapp/__init__.py create mode 100644 webapp/config.py create mode 100644 webapp/main.py create mode 100644 webapp/services/__init__.py create mode 100644 webapp/services/sleeper_client.py create mode 100644 webapp/static/app.css create mode 100644 webapp/templates/base.html create mode 100644 webapp/templates/dashboard.html create mode 100644 webapp/templates/draft.html create mode 100644 webapp/templates/players.html create mode 100644 webapp/templates/rosters.html diff --git a/README.md b/README.md index b97d84b..2c9a1c5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Original Repository: https://github.com/SwapnikKatkoori/sleeper-api-wrapper 4. [Notes](#notes) 5. [Dependencies](#depends) 6. [License](#license) +7. [FastAPI Web Demo](#fastapi) # Project Roadmap @@ -50,3 +51,28 @@ This package is intended to be used by Python version 3.8 and higher. There migh # License This project is licensed under the terms of the MIT license. + + +# FastAPI Web Demo + +A simple FastAPI app lives under `webapp/` to showcase the wrapper in a browser for league `1257507151190958081`. It is intentionally lean but organized for extension. + +### Run locally +``` +pip install -e . +pip install "fastapi>=0.115" "uvicorn[standard]>=0.30" "jinja2>=3.1" "python-multipart>=0.0.9" +uvicorn webapp.main:app --reload +``` + +Override the league or sport via environment variables: +``` +export LEAGUE_ID=YOUR_LEAGUE_ID +export SPORT=nfl +export SCORE_TYPE=pts_ppr +``` + +Pages: +- `/` League dashboard (standings, managers, matchups, recent transactions). +- `/rosters` Per-manager rosters with starters/bench. +- `/draft` Draft results for the league's primary draft. +- `/players` Player stats for the season; search or use trending adds/drops. diff --git a/pyproject.toml b/pyproject.toml index 6aa3db0..0ec3bc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,7 @@ build-backend = "poetry.core.masonry.api" pytest = "*" pytest-mock = "*" tox = "^4.31.0" +fastapi = "^0.115.0" +uvicorn = {extras = ["standard"], version = "^0.30.0"} +jinja2 = "^3.1.0" +python-multipart = "^0.0.9" diff --git a/webapp/__init__.py b/webapp/__init__.py new file mode 100644 index 0000000..6446e14 --- /dev/null +++ b/webapp/__init__.py @@ -0,0 +1 @@ +# FastAPI proof-of-concept package for Sleeper league views. diff --git a/webapp/config.py b/webapp/config.py new file mode 100644 index 0000000..6950fbd --- /dev/null +++ b/webapp/config.py @@ -0,0 +1,23 @@ +from functools import lru_cache + +from pydantic import BaseSettings + + +class Settings(BaseSettings): + """Configuration for the FastAPI proof of concept.""" + + league_id: str = "1257507151190958081" + sport: str = "nfl" + score_type: str = "pts_ppr" + league_cache_ttl_seconds: int = 300 + players_cache_ttl_seconds: int = 21600 + stats_cache_ttl_seconds: int = 900 + + class Config: + env_file = ".env" + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + """Returns cached settings instance.""" + return Settings() diff --git a/webapp/main.py b/webapp/main.py new file mode 100644 index 0000000..3767066 --- /dev/null +++ b/webapp/main.py @@ -0,0 +1,70 @@ +from fastapi import Depends, FastAPI, Query, Request +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from webapp.config import Settings, get_settings +from webapp.services.sleeper_client import SleeperClient, get_client + +app = FastAPI(title="Sleeper League Dashboard", version="0.1.0") + +templates = Jinja2Templates(directory="webapp/templates") +app.mount("/static", StaticFiles(directory="webapp/static"), name="static") + + +def client_dependency(settings: Settings = Depends(get_settings)) -> SleeperClient: + return get_client(settings) + + +@app.get("/", response_class=HTMLResponse) +def league_dashboard( + request: Request, + week: int | None = Query(default=None, description="Override week to view"), + client: SleeperClient = Depends(client_dependency), +): + data = client.league_dashboard(week=week) + return templates.TemplateResponse( + "dashboard.html", {"request": request, "title": "League Dashboard", **data} + ) + + +@app.get("/rosters", response_class=HTMLResponse) +def rosters( + request: Request, + client: SleeperClient = Depends(client_dependency), +): + data = client.roster_view() + return templates.TemplateResponse( + "rosters.html", {"request": request, "title": "Rosters", **data} + ) + + +@app.get("/draft", response_class=HTMLResponse) +def draft( + request: Request, + client: SleeperClient = Depends(client_dependency), +): + data = client.draft_results() + return templates.TemplateResponse( + "draft.html", {"request": request, "title": "Draft Results", **data} + ) + + +@app.get("/players", response_class=HTMLResponse) +def players( + request: Request, + q: str | None = Query(default=None, description="Player name search"), + season: str | None = Query(default=None, description="Season year for stats"), + add_drop: str = Query(default="add", description="Trending add or drop feed"), + client: SleeperClient = Depends(client_dependency), +): + data = client.player_stats(query=q, season=season, add_drop=add_drop) + return templates.TemplateResponse( + "players.html", + {"request": request, "title": "Player Stats", "query": q, "add_drop": add_drop, **data}, + ) + + +@app.get("/health") +def health() -> dict: + return {"status": "ok"} diff --git a/webapp/services/__init__.py b/webapp/services/__init__.py new file mode 100644 index 0000000..1677a40 --- /dev/null +++ b/webapp/services/__init__.py @@ -0,0 +1 @@ +# Services for fetching and transforming Sleeper data for the web app. diff --git a/webapp/services/sleeper_client.py b/webapp/services/sleeper_client.py new file mode 100644 index 0000000..1323709 --- /dev/null +++ b/webapp/services/sleeper_client.py @@ -0,0 +1,296 @@ +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +from sleeper_wrapper import Drafts, League, Players, Stats, get_sport_state + +from webapp.config import Settings + + +@dataclass +class TTLCache: + """Lightweight TTL cache to avoid hammering the Sleeper API.""" + + ttl_seconds: int + value: Any = None + expires_at: float = 0 + + def get(self) -> Any: + if self.value is not None and time.time() < self.expires_at: + return self.value + return None + + def set(self, value: Any) -> None: + self.value = value + self.expires_at = time.time() + self.ttl_seconds + + +class SleeperClient: + """Fetches and shapes data for the FastAPI views.""" + + def __init__(self, settings: Settings): + self.settings = settings + self._league_cache = TTLCache(settings.league_cache_ttl_seconds) + self._players_cache = TTLCache(settings.players_cache_ttl_seconds) + self._stats_cache: Dict[str, TTLCache] = {} + + def _fetch_league_bundle(self) -> Dict[str, Any]: + """Returns cached league info, rosters, users, and sport state.""" + cached = self._league_cache.get() + if cached: + return cached + + league = League(self.settings.league_id) + info = league.get_league() + rosters = league.get_rosters() + users = league.get_users() + state = get_sport_state(self.settings.sport) + + bundle = { + "league": league, + "info": info, + "rosters": rosters, + "users": users, + "state": state, + } + self._league_cache.set(bundle) + return bundle + + def _user_lookup(self, users: List[Dict[str, Any]]) -> Dict[str, str]: + result = {} + for user in users: + team_name = user.get("metadata", {}).get("team_name") + name = team_name or user.get("display_name") or user.get("username") + if user.get("user_id"): + result[user["user_id"]] = name + return result + + def _roster_lookup(self, rosters: List[Dict[str, Any]]) -> Dict[int, str]: + return {roster["roster_id"]: roster.get("owner_id") for roster in rosters} + + def _players(self) -> Dict[str, Dict[str, Any]]: + """Returns cached player directory keyed by player_id.""" + cached = self._players_cache.get() + if cached: + return cached + players = Players().get_all_players(self.settings.sport) + if not isinstance(players, dict): + players = {} + self._players_cache.set(players) + return players + + def _season_stats(self, season: str) -> Dict[str, Any]: + """Returns cached season stats.""" + cache = self._stats_cache.get(season) + if cache and cache.get() is not None: + return cache.get() + + cache = TTLCache(self.settings.stats_cache_ttl_seconds) + self._stats_cache[season] = cache + + stats = Stats().get_all_stats("regular", season) + if not isinstance(stats, dict): + stats = {} + cache.set(stats) + return stats + + def league_dashboard(self, week: Optional[int] = None) -> Dict[str, Any]: + bundle = self._fetch_league_bundle() + league = bundle["league"] + info = bundle["info"] + rosters = bundle["rosters"] + users = bundle["users"] + state = bundle["state"] + + target_week = week or state.get("week") or info.get("week") or 1 + matchups = league.get_matchups(target_week) + if not isinstance(matchups, list): + matchups = [] + scoreboard = self._build_scoreboard(matchups, rosters, users, target_week) + + standings = league.get_standings(rosters=rosters, users=users) + transactions = league.get_transactions(target_week) + if not isinstance(transactions, list): + transactions = [] + + return { + "info": info, + "state": state, + "settings": info.get("settings", {}), + "standings": standings, + "managers": users, + "scoreboard": scoreboard, + "transactions": transactions or [], + "week": target_week, + } + + def _build_scoreboard( + self, + matchups: List[Dict[str, Any]], + rosters: List[Dict[str, Any]], + users: List[Dict[str, Any]], + week: int, + ) -> List[Dict[str, Any]]: + if not matchups: + return [] + user_lookup = self._user_lookup(users) + roster_lookup = self._roster_lookup(rosters) + + matchup_groups: Dict[int, List[Dict[str, Any]]] = {} + for matchup in matchups: + matchup_id = matchup.get("matchup_id", 0) + matchup_groups.setdefault(matchup_id, []).append(matchup) + + scoreboard = [] + for matchup_id, teams in sorted(matchup_groups.items()): + entries = [] + for team in teams: + roster_id = team.get("roster_id") + owner_id = roster_lookup.get(roster_id) + team_name = user_lookup.get(owner_id, f"Roster {roster_id}") + points_raw = team.get("points", 0) + projected_raw = team.get("projected_points") + points = round(points_raw, 2) if isinstance(points_raw, (int, float)) else 0 + projected = round(projected_raw, 2) if isinstance(projected_raw, (int, float)) else None + entries.append( + { + "team_name": team_name, + "points": points, + "projected": projected, + } + ) + scoreboard.append({"matchup_id": matchup_id, "week": week, "teams": entries}) + return scoreboard + + def roster_view(self) -> Dict[str, Any]: + bundle = self._fetch_league_bundle() + info = bundle["info"] + rosters = bundle["rosters"] + users = bundle["users"] + user_lookup = self._user_lookup(users) + players = self._players() + + def hydrate_player(player_id: str) -> Dict[str, Any]: + player = players.get(player_id, {}) + full_name = player.get("full_name") or f"{player.get('first_name', '')} {player.get('last_name', '')}".strip() + return { + "id": player_id, + "name": full_name or player_id, + "position": player.get("position"), + "team": player.get("team"), + } + + hydrated = [] + for roster in rosters: + owner_id = roster.get("owner_id") + starters_set = set(roster.get("starters", [])) + all_players = [hydrate_player(pid) for pid in roster.get("players", [])] + starters_full = [hydrate_player(pid) for pid in roster.get("starters", [])] + bench = [p for p in all_players if p["id"] not in starters_set] + hydrated.append( + { + "roster_id": roster.get("roster_id"), + "owner_id": owner_id, + "owner_name": user_lookup.get(owner_id, "Unassigned"), + "players": all_players, + "starters": starters_full, + "bench": bench, + } + ) + + return {"info": info, "rosters": hydrated, "users": users} + + def draft_results(self) -> Dict[str, Any]: + bundle = self._fetch_league_bundle() + info = bundle["info"] + league = bundle["league"] + users = bundle["users"] + user_lookup = self._user_lookup(users) + players = self._players() + + drafts = league.get_all_drafts() + if not drafts: + return {"draft": None, "picks": []} + active_draft = drafts[0] + draft = Drafts(active_draft["draft_id"]) + picks = draft.get_all_picks() or [] + + def player_name(player_id: str) -> str: + player = players.get(player_id, {}) + return player.get("full_name") or f"{player.get('first_name', '')} {player.get('last_name', '')}".strip() or player_id + + decorated = [] + for pick in sorted(picks, key=lambda p: (p.get("round", 0), p.get("pick_no", 0))): + player_id = pick.get("player_id") + decorated.append( + { + "round": pick.get("round"), + "pick_no": pick.get("pick_no"), + "roster_id": pick.get("roster_id"), + "picked_by": user_lookup.get(pick.get("picked_by")), + "player": player_name(player_id), + "player_id": player_id, + "metadata": pick.get("metadata", {}), + } + ) + + return {"info": info, "draft": active_draft, "picks": decorated} + + def player_stats( + self, query: Optional[str] = None, season: Optional[str] = None, add_drop: str = "add" + ) -> Dict[str, Any]: + bundle = self._fetch_league_bundle() + info = bundle["info"] + default_season = info.get("season") + season_to_use = str(season or default_season) + players = self._players() + stats = self._season_stats(season_to_use) if season_to_use else {} + + def enrich(player_id: str) -> Dict[str, Any]: + player = players.get(player_id, {}) + stat_line = stats.get(player_id, {}) + full_name = player.get("full_name") or f"{player.get('first_name', '')} {player.get('last_name', '')}".strip() + return { + "id": player_id, + "name": full_name or player_id, + "position": player.get("position"), + "team": player.get("team"), + "pts_ppr": stat_line.get("pts_ppr"), + "pts_std": stat_line.get("pts_std"), + "pts_half_ppr": stat_line.get("pts_half_ppr"), + "games": stat_line.get("gp"), + } + + results: List[Dict[str, Any]] = [] + source: str + if query: + query_lower = query.lower() + for player_id, player in players.items(): + name = player.get("full_name") or f"{player.get('first_name', '')} {player.get('last_name', '')}".strip() + if name and query_lower in name.lower(): + results.append(enrich(player_id)) + if len(results) >= 25: + break + source = "search" + else: + trending = Players().get_trending_players(self.settings.sport, add_drop=add_drop, hours=168, limit=25) + for entry in trending: + player_id = entry.get("player_id") + annotated = enrich(player_id) + annotated["count"] = entry.get("count") + annotated["direction"] = add_drop + results.append(annotated) + source = "trending" + + return {"info": info, "season": season_to_use, "results": results, "source": source} + + +_client_instance: Optional[SleeperClient] = None + + +def get_client(settings: Settings) -> SleeperClient: + """Factory for dependency injection.""" + global _client_instance + if _client_instance is None: + _client_instance = SleeperClient(settings) + return _client_instance diff --git a/webapp/static/app.css b/webapp/static/app.css new file mode 100644 index 0000000..53b7691 --- /dev/null +++ b/webapp/static/app.css @@ -0,0 +1,238 @@ +:root { + --bg: #0b1724; + --card: #132235; + --text: #f5f7fb; + --muted: #9eb0c7; + --accent: #4dc3ff; + --border: #20324a; + --shadow: 0 10px 30px rgba(0, 0, 0, 0.25); + --radius: 10px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Inter", system-ui, -apple-system, sans-serif; + background: radial-gradient(circle at 20% 20%, rgba(77, 195, 255, 0.08), transparent 25%), + radial-gradient(circle at 80% 0%, rgba(0, 255, 214, 0.08), transparent 20%), + var(--bg); + color: var(--text); + min-height: 100vh; +} + +.top-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background: rgba(12, 24, 36, 0.85); + position: sticky; + top: 0; + z-index: 10; + backdrop-filter: blur(10px); +} + +.brand__title { + font-weight: 700; + letter-spacing: 0.02em; +} + +.brand__meta { + font-size: 12px; + color: var(--muted); +} + +.nav a { + color: var(--text); + text-decoration: none; + margin-left: 16px; + font-weight: 600; +} + +.nav a:hover { + color: var(--accent); +} + +.container { + max-width: 1200px; + margin: 24px auto 60px; + padding: 0 16px; +} + +.hero { + margin-bottom: 20px; +} + +.hero h1 { + margin: 0 0 6px 0; +} + +.pill-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.pill { + background: rgba(77, 195, 255, 0.12); + border: 1px solid var(--border); + padding: 8px 12px; + border-radius: 20px; + color: var(--text); + font-size: 13px; +} + +.panel { + background: rgba(19, 34, 53, 0.85); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + margin-bottom: 24px; + box-shadow: var(--shadow); +} + +.panel__header { + display: flex; + justify-content: space-between; + align-items: flex-end; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 12px; +} + +.panel__header h2, +.panel__header h1 { + margin: 0; +} + +.inline-form { + display: flex; + gap: 8px; + align-items: center; +} + +.inline-form input, +.inline-form select { + padding: 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: #0f1c2b; + color: var(--text); +} + +.inline-form button { + padding: 8px 12px; + border: none; + border-radius: 6px; + background: var(--accent); + color: #032039; + font-weight: 700; + cursor: pointer; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + padding: 10px 8px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +th { + color: var(--muted); + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.grid { + display: grid; + gap: 12px; +} + +.grid.two { + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.grid.three { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); +} + +.card { + background: #0f1c2b; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + box-shadow: var(--shadow); +} + +.card__title { + font-weight: 700; + margin-bottom: 8px; +} + +.card__body { + color: var(--muted); +} + +.muted { + color: var(--muted); + font-size: 13px; +} + +.list { + list-style: none; + padding-left: 0; + margin: 0; +} + +.list li { + padding: 6px 0; + border-bottom: 1px solid var(--border); +} + +.score-row { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.score { + background: rgba(77, 195, 255, 0.08); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px; + flex: 1; +} + +.score__name { + font-weight: 700; +} + +.score__points { + font-size: 24px; + font-weight: 800; +} + +.score__projected { + color: var(--muted); +} + +@media (max-width: 720px) { + .top-bar { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .score-row { + flex-direction: column; + } +} diff --git a/webapp/templates/base.html b/webapp/templates/base.html new file mode 100644 index 0000000..658ae0f --- /dev/null +++ b/webapp/templates/base.html @@ -0,0 +1,26 @@ + + + + + + {{ title or "Sleeper League" }} + + + +
+
+
Sleeper League
+
League ID: {{ info.league_id if info else "" }}
+
+ +
+
+ {% block content %}{% endblock %} +
+ + diff --git a/webapp/templates/dashboard.html b/webapp/templates/dashboard.html new file mode 100644 index 0000000..936db0b --- /dev/null +++ b/webapp/templates/dashboard.html @@ -0,0 +1,151 @@ +{% extends "base.html" %} +{% block content %} +
+

{{ info.name or "League Dashboard" }}

+

Season {{ info.season }} · Week {{ week }} · Sport {{ info.sport }}

+
+
Status: {{ info.status or "active" }}
+
Scoring: {{ settings.scoring_settings or settings.get("scoring_settings", "standard") }}
+
Roster count: {{ info.total_rosters }}
+
+
+ +
+
+
+

Standings

+

Wins/Losses reflect Sleeper scoring (including custom settings).

+
+
+ + + + + + + + + + + {% for team, wins, losses, points in standings %} + + + + + + + {% endfor %} + +
TeamWLPoints
{{ team }}{{ wins }}{{ losses }}{{ points }}
+
+ +
+
+
+

League Settings

+

Pulled directly from Sleeper so custom scoring/positions are honored.

+
+
+
+
+

Structure

+
    +
  • Teams: {{ settings.teams or info.total_rosters }}
  • +
  • Playoff Teams: {{ settings.playoff_teams or "?" }}
  • +
  • Playoff Rounds: {{ settings.playoff_rounds or "?" }}
  • +
  • Draft Rounds: {{ settings.draft_rounds or "?" }}
  • +
  • Roster Positions: {{ info.roster_positions }}
  • +
+
+
+

Scoring

+
    +
  • PPR: {{ settings.scoring_settings.pts_per_reception if settings.scoring_settings else "0" }}
  • +
  • Half PPR: {{ settings.scoring_settings.rec_0_5 if settings.scoring_settings else "0" }}
  • +
  • Six-pt Pass TD: {{ settings.scoring_settings.pass_td if settings.scoring_settings else "?" }}
  • +
  • Custom scoring keys: {{ settings.scoring_settings.keys() if settings.scoring_settings else "standard" }}
  • +
+
+
+
+ +
+
+
+

Matchup Scoreboard

+

Week {{ week }}

+
+
+ + + +
+
+
+ {% if scoreboard %} + {% for matchup in scoreboard %} +
+
Matchup {{ matchup.matchup_id }}
+
+ {% for team in matchup.teams %} +
+
{{ team.team_name }}
+
{{ '%.2f' % (team.points or 0) }}
+ {% if team.projected is not none %} +
Proj {{ '%.1f' % team.projected }}
+ {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} + {% else %} +

No matchups available for this week.

+ {% endif %} +
+
+ +
+
+
+

Managers

+

Team names pulled from league metadata.

+
+
+
+ {% for manager in managers %} +
+
{{ manager.metadata.team_name or manager.display_name or manager.username }}
+
+
User: {{ manager.username }}
+
ID: {{ manager.user_id }}
+
+
+ {% endfor %} +
+
+ +
+
+
+

Recent Transactions (Week {{ week }})

+

Waivers, trades, free agency activity.

+
+
+ {% if transactions %} + + {% else %} +

No transactions recorded for this week.

+ {% endif %} +
+{% endblock %} diff --git a/webapp/templates/draft.html b/webapp/templates/draft.html new file mode 100644 index 0000000..a34d062 --- /dev/null +++ b/webapp/templates/draft.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+

Draft Results

+ {% if draft %} +

Draft ID {{ draft.draft_id }} · Type {{ draft.type }} · Rounds {{ draft.settings.rounds }}

+ {% else %} +

No drafts found for this league.

+ {% endif %} +
+
+ {% if picks %} + + + + + + + + + + + + {% for pick in picks %} + + + + + + + + {% endfor %} + +
RoundPickPlayerPicked ByRoster
{{ pick.round }}{{ pick.pick_no }}{{ pick.player }}{{ pick.picked_by or "Unknown" }}{{ pick.roster_id }}
+ {% else %} +

No picks to display.

+ {% endif %} +
+{% endblock %} diff --git a/webapp/templates/players.html b/webapp/templates/players.html new file mode 100644 index 0000000..5208412 --- /dev/null +++ b/webapp/templates/players.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+

Player Stats

+

Season {{ season }} · {{ "Search results" if query else "Trending "+add_drop }}

+
+
+ + + + +
+
+ {% if results %} + + + + + + + + + + + + + + + {% for player in results %} + + + + + + + + + + + {% endfor %} + +
NamePosTeamPPRStdHalf PPRGamesTrending
{{ player.name }}{{ player.position or "?" }}{{ player.team or "FA" }}{{ player.pts_ppr or "—" }}{{ player.pts_std or "—" }}{{ player.pts_half_ppr or "—" }}{{ player.games or "—" }} + {% if player.count %} + {{ player.count }} {{ player.direction }} + {% else %} + {% if source == "search" %}search{% else %}—{% endif %} + {% endif %} +
+ {% else %} +

No players to show for this selection.

+ {% endif %} +
+{% endblock %} diff --git a/webapp/templates/rosters.html b/webapp/templates/rosters.html new file mode 100644 index 0000000..cbb8e09 --- /dev/null +++ b/webapp/templates/rosters.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+

Rosters

+

Switch between managers to view starters and bench.

+
+
+
+ {% for roster in rosters %} +
+
{{ roster.owner_name }} Roster {{ roster.roster_id }}
+
+

Starters

+
    + {% for player in roster.starters %} +
  • {{ player.name }} ({{ player.position or "??" }} - {{ player.team or "FA" }})
  • + {% endfor %} +
+

Bench

+
    + {% for player in roster.bench %} +
  • {{ player.name }} ({{ player.position or "??" }} - {{ player.team or "FA" }})
  • + {% endfor %} +
+
+
+ {% endfor %} +
+
+{% endblock %} From 8e91a9de50d1932066dbd707ed6cb586567bae61 Mon Sep 17 00:00:00 2001 From: jsteckler1 Date: Mon, 24 Nov 2025 15:55:40 -0500 Subject: [PATCH 2/2] Update dependencies and refactor settings import - Added `pydantic-settings` dependency to `pyproject.toml`. - Changed import of `BaseSettings` from `pydantic` to `pydantic_settings` in `config.py`. --- pyproject.toml | 1 + webapp/config.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0ec3bc4..b04a091 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,3 +34,4 @@ fastapi = "^0.115.0" uvicorn = {extras = ["standard"], version = "^0.30.0"} jinja2 = "^3.1.0" python-multipart = "^0.0.9" +pydantic-settings = "^2.2.0" diff --git a/webapp/config.py b/webapp/config.py index 6950fbd..93c5269 100644 --- a/webapp/config.py +++ b/webapp/config.py @@ -1,6 +1,6 @@ from functools import lru_cache -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings):