Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 63 additions & 58 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,83 +1,88 @@
# Fantasy Football API Configuration
# Copy this file to .env and add your actual API keys
# FFPy environment template
#
# Happy path
# ----------
# cp .env.example .env # first-time setup (make bootstrap does this too)
# Edit .env — fill keys for the workflows you use (see REQUIRED notes below)
# make supabase.check # optional: verify Supabase URL/key before Fly deploy
# make fly.secrets # production: push Supabase + app URL vars to Fly
#
# Where secrets live (do not mix these up)
# ----------------------------------------
# .env local dev; also the source for `make fly.secrets`
# Fly app secrets production runtime (set via make fly.secrets, not in git)
# GitHub secret FLY_API_TOKEN only — for CI auto-deploy on push to main
# Create with: make fly.token → add to GitHub → Environments → production
#
# Required keys by workflow
# -------------------------
# Streamlit only (make run) nothing required — defaults are fine
# Web apps with auth (League / Pick'em) SUPABASE_URL + SUPABASE_PUBLISHABLE_KEY
# CFB fantasy pipeline CFBD_API_KEY
# Fly production deploy SUPABASE_URL + SUPABASE_PUBLISHABLE_KEY in .env,
# then make fly.secrets; FLY_API_TOKEN in GitHub only

# =============================================================================
# Core app
# =============================================================================

# API Provider Selection
# Options: "espn" (free, no key needed), "sportsdata" (paid, more features)
API_PROVIDER=espn

# SportsDataIO API Key (Optional - Free tier: 1000 calls/month)
# Get your key at: https://sportsdata.io/
# Choose NFL → Projections endpoint
SPORTSDATA_API_KEY=your_sportsdata_api_key_here

# RapidAPI Key (Optional alternative)
# Get your key at: https://rapidapi.com/
RAPIDAPI_KEY=your_rapidapi_key_here

# NFL Season Configuration
NFL_SEASON=2024 # Use 2024 for historical projection model demo

# Cache Settings (in seconds, default: 1 hour)
NFL_SEASON=2024
CACHE_TTL=3600

# Database Configuration
DATABASE_PATH=~/.ffpy/ffpy.db # Default: user home directory
# Or use custom path:
# DATABASE_PATH=/custom/path/to/ffpy.db
DATABASE_TYPE=sqlite # Currently only sqlite supported

# ESPN League Integration (Optional - for accessing your ESPN league)
# Get these values from your browser after logging into ESPN Fantasy
# See docs/ESPN_API_INTEGRATION_GUIDE.md for detailed instructions

# Your ESPN League ID (from URL: fantasy.espn.com/football/league?leagueId=XXXXXX)
ESPN_LEAGUE_ID=

# For PRIVATE leagues only (not needed for public leagues):
# SWID Cookie (looks like: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX})
ESPN_SWID=

# ESPN_S2 Cookie (long string, ~200 characters)
ESPN_S2=
DATABASE_PATH=~/.ffpy/ffpy.db
DATABASE_TYPE=sqlite

# -------------------------------------------------------------------
# Web Demo Auth / Anti-Abuse (planned Supabase deployment scaffold)
# These are not used by the current local app yet.
# -------------------------------------------------------------------
# =============================================================================
# Web apps — League Manager & Pick'em (REQUIRED when WEB_AUTH_ENABLED=true)
# =============================================================================

# Public app URL (used by Supabase email confirmation redirects)
# Production: https://ffpy-pickem.fly.dev Local: http://localhost:8080
PUBLIC_APP_URL=http://localhost:8080
WEB_AUTH_ENABLED=false
PUBLIC_APP_URL=http://localhost:8080

# Supabase project settings
# Supabase → Project Settings → API
SUPABASE_URL=
# Preferred for new Supabase projects. Settings -> API Keys -> Publishable key.
# Preferred. Settings API Keys Publishable key.
SUPABASE_PUBLISHABLE_KEY=
# Legacy fallback for older projects.
# Legacy fallback if your project has no publishable key yet.
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Legacy/local HS256 token support only. Leave blank to verify Supabase JWT signing keys through JWKS.

# Optional Supabase overrides (usually leave blank)
SUPABASE_JWT_SECRET=
# Optional override. Default is <SUPABASE_URL>/auth/v1/.well-known/jwks.json.
SUPABASE_JWKS_URL=
SUPABASE_JWT_AUDIENCE=authenticated
SUPABASE_FETCH_USER_ON_VERIFY=true
# Server-only admin key — never push to Fly via make fly.secrets; local tooling only.
SUPABASE_SERVICE_ROLE_KEY=

# =============================================================================
# Optional API keys
# =============================================================================

# SportsDataIO — https://sportsdata.io/
SPORTSDATA_API_KEY=

# RapidAPI — https://rapidapi.com/
RAPIDAPI_KEY=

# CollegeFootballData — https://collegefootballdata.com/key (REQUIRED for CFB pipeline)
CFBD_API_KEY=

# ESPN private leagues only — see docs/integration/espn.md
ESPN_LEAGUE_ID=
ESPN_SWID=
ESPN_S2=

# =============================================================================
# Optional — anti-abuse scaffold (not required for local dev today)
# =============================================================================

# Cloudflare Turnstile
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=

# Upstash Redis for rate limiting
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Abuse monitoring and quotas
ABUSE_HASH_SALT=
SESSION_COOKIE_SECURE=false
MAX_BACKTESTS_PER_HOUR=10
MAX_COMPARES_PER_HOUR=3
MAX_DAILY_COST_UNITS=100

# CollegeFootballData API (CFB fantasy stats — https://collegefootballdata.com)
CFBD_API_KEY=your_cfbd_api_key_here
18 changes: 16 additions & 2 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@ Three commands get you running with database-backed app data. Works on Linux, ma
make bootstrap
```

This installs `uv`, syncs Python dependencies, seeds `.env` from the template, and creates the SQLite database. Safe to re-run any time.
This installs `uv`, syncs Python dependencies, seeds `.env` from `.env.example`, and creates the SQLite database. Safe to re-run any time.

To create `.env` manually instead: `cp .env.example .env`. Open `.env.example` for **required keys by workflow** (Streamlit vs web auth vs CFB vs Fly deploy).

### Web app auth (League Manager / Pick'em)

```bash
cp .env.example .env # skip if make bootstrap already created .env
# Edit .env: SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, WEB_AUTH_ENABLED=true
make supabase.check # verify keys before deploying
make fly.secrets # push Supabase vars to Fly (production)
make fly.deploy # or merge to main for CI auto-deploy
```

`FLY_API_TOKEN` goes in GitHub (not `.env`) — see [docs/deployment/fly.md](docs/deployment/fly.md).

## Generate app data

Expand Down Expand Up @@ -137,4 +151,4 @@ Run all commands from **WSL** (Ubuntu recommended). `make` and `bash` need to be
- **`command not found: uv` right after bootstrap** — open a new shell, or `source ~/.local/bin/env`, then re-run `make bootstrap`.
- **Port 8501 already in use** — `make run PORT=8502`.
- **Browser doesn't open** — navigate to `http://localhost:8501` manually.
- **Need API keys** — edit `.env` (see `.env.example` for the list).
- **Need API keys** — `cp .env.example .env` if missing, then edit `.env` (required keys are documented at the top of `.env.example`).
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,24 @@ All database targets wrap the `ffpy-db` CLI — `uv run ffpy-db --help` for the

## Configuration

Copy and edit `.env`:
First-time setup copies the template automatically (`make bootstrap`). To create or reset it manually:

```bash
cp .env.example .env
```

Key settings: `API_PROVIDER` (espn/sportsdata), `NFL_SEASON`, `DATABASE_PATH`.
Then edit `.env` for the workflows you use. The template lists **required keys by workflow** at the top.

| Workflow | Required in `.env` |
|----------|-------------------|
| Streamlit only (`make run`) | Nothing — defaults work |
| League / Pick'em web apps with auth | `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY` |
| CFB fantasy pipeline | `CFBD_API_KEY` |
| Fly production deploy | `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, then `make fly.secrets` |

**Where secrets go:** `.env` for local dev (and as the source for `make fly.secrets`); Fly app secrets for production runtime; `FLY_API_TOKEN` in GitHub only (not `.env`) for CI auto-deploy. See [docs/deployment/fly.md](docs/deployment/fly.md).

Verify Supabase before deploying: `make supabase.check`

## Features

Expand Down
44 changes: 32 additions & 12 deletions docs/deployment/fly.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,41 @@ Do not create extra volumes expecting SQLite data to replicate automatically. Mu
- add a real SQLite replication layer such as LiteFS
- run separate regional apps with an explicit data sync/restore process

Set the runtime secrets after the app exists. First create a local `.env` file; it is ignored by git:
## Environment happy path

`make bootstrap` seeds `.env` from `.env.example` on first run. To do it manually:

```bash
cp .env.example .env
```

Populate these values in `.env` from your Supabase dashboard:
### 1. Fill required keys in `.env`

- `SUPABASE_URL`: Project Settings -> API -> Project URL. It looks like `https://<project-ref>.supabase.co`.
- `SUPABASE_PUBLISHABLE_KEY`: Project Settings -> API Keys -> Publishable key. This is preferred for new Supabase projects.
From your Supabase dashboard (Project Settings → API):

Legacy fallback:
| Variable | Where to find it | Required |
|----------|------------------|----------|
| `SUPABASE_URL` | Project URL — `https://<project-ref>.supabase.co` | Yes |
| `SUPABASE_PUBLISHABLE_KEY` | API Keys → Publishable key | Yes (preferred) |
| `SUPABASE_ANON_KEY` | Legacy API Keys → anon key | Only if no publishable key |
| `PUBLIC_APP_URL` | Set to `https://ffpy-pickem.fly.dev` for production secrets | Yes for prod |

- `SUPABASE_ANON_KEY`: Project Settings -> API Keys -> Legacy API Keys -> anon key. Use this only if your project has not moved to publishable keys yet.
Optional in `.env` (usually leave blank):

Optional:
- `SUPABASE_JWKS_URL` — defaults to `<SUPABASE_URL>/auth/v1/.well-known/jwks.json`
- `SUPABASE_JWT_SECRET` — local HS256 dev tokens only; production uses JWKS

- `SUPABASE_JWKS_URL`: override for the signing-key discovery endpoint. Usually leave blank; the app defaults to `<SUPABASE_URL>/auth/v1/.well-known/jwks.json`.
- `SUPABASE_JWT_SECRET`: legacy/local HS256 support only. Set this if your Supabase project still uses legacy JWT-secret verification or if you want to mint local dev tokens. If you leave it empty, the backend verifies Supabase access tokens with the project's JWT signing keys through JWKS.
Do **not** put `FLY_API_TOKEN` in `.env` — that token is for GitHub Actions only (see [Deploy via CI](#deploy-via-ci) below).

Then push the required runtime secrets to Fly:
### 2. Verify locally

```bash
make supabase.check
```

### 3. Push runtime secrets to Fly

After the Fly app exists:

```bash
make fly.secrets
Expand Down Expand Up @@ -105,13 +119,19 @@ make fly.logs

The GitHub Actions workflow in [.github/workflows/ci-cd.yml](../../.github/workflows/ci-cd.yml) runs lint + tests + Docker build on every PR and push to `main`, then deploys to Fly only on `main`.

Create a deploy token for GitHub Actions:
### Deploy via CI

Merges to `main` auto-deploy when CI passes. Create a deploy token once:

```bash
make fly.token
```

Add the token to the GitHub repository secret named `FLY_API_TOKEN`.
Add the printed token to GitHub → **Settings** → **Environments** → **production** → **Environment secrets** → `FLY_API_TOKEN`.

(Repo-level **Actions secrets** also works; the workflow uses the `production` environment.)

Manual deploy from your machine does not need this token — use `fly auth login` and `make fly.deploy`.

## Supabase auth redirects (production)

Expand Down
5 changes: 5 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
app = "ffpy-pickem"
primary_region = "iad"

[vm]
memory = "512mb"
cpu_kind = "shared"
cpus = 1

[build]
dockerfile = "Dockerfile"

Expand Down
5 changes: 4 additions & 1 deletion scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ uv run ffpy-db migrate

echo
info "Bootstrap complete."
echo " Start the app: make run"
echo " Start Streamlit: make run"
echo " Generate app data: make data"
echo " Configure secrets: edit .env (seeded from .env.example — see REQUIRED notes in that file)"
echo " Verify Supabase: make supabase.check"
echo " Deploy to Fly: docs/deployment/fly.md"
echo " Lint before pushing: make precommit (also runs on git commit)"
echo " See all make targets: make help"
8 changes: 5 additions & 3 deletions src/ffpy/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(self, db_path: Optional[str] = None):
# Connect to database (check_same_thread=False for FastAPI/uvicorn thread safety)
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
self.conn.row_factory = sqlite3.Row # Access columns by name
self.conn.execute("PRAGMA foreign_keys = ON") # enable ON DELETE CASCADE

# Initialize schema
self.init_database()
Expand Down Expand Up @@ -3775,9 +3776,10 @@ def get_matchups_for_league(self, league_id: str, week: int) -> list[dict]:
return [dict(row) for row in cursor.fetchall()]

def delete_user_league(self, league_id: str, user_id: str) -> None:
"""Delete a league and its teams/matchups (CASCADE)."""
self.conn.execute(
"""Delete a league owned by ``user_id``; teams/matchups cascade."""
cursor = self.conn.execute(
"DELETE FROM user_leagues WHERE league_id = ? AND user_id = ?",
(league_id, user_id),
)
self.conn.commit()
if cursor.rowcount:
self.conn.commit()
29 changes: 26 additions & 3 deletions src/ffpy/draft_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

import numpy as np
Expand Down Expand Up @@ -86,19 +87,41 @@ def _map_position(pos: Optional[str]) -> str:
return pos


def _sleeper_players_cache_path() -> Path:
from ffpy.config import Config

cache_dir = Path(Config.DATABASE_PATH).expanduser().parent / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / "sleeper_players.json"


def load_sleeper_players(force: bool = False) -> Dict[str, Any]:
"""Return the Sleeper player map, cached at module level.
"""Return the Sleeper player map, cached in memory and on disk.

The Sleeper ``/players/nfl`` payload is large (~5 MB); caching avoids
refetching on every request.
The Sleeper ``/players/nfl`` payload is large (~15 MB). League import and
draft-help call this to resolve player names; the disk cache (6-hour TTL)
limits refetches. Hosts below ~512 MB RAM may still struggle on a cold load.
"""
now = time.time()
cached = _SLEEPER_PLAYERS_CACHE.get("data")
if not force and cached is not None and (now - _SLEEPER_PLAYERS_CACHE["fetched_at"]) < _SLEEPER_CACHE_TTL:
return cached

cache_path = _sleeper_players_cache_path()
if not force and cache_path.exists():
age = now - cache_path.stat().st_mtime
if age < _SLEEPER_CACHE_TTL:
with cache_path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
_SLEEPER_PLAYERS_CACHE["data"] = data
_SLEEPER_PLAYERS_CACHE["fetched_at"] = now
return data

from ffpy.integrations.sleeper import SleeperIntegration

data = SleeperIntegration.get_players()
with cache_path.open("w", encoding="utf-8") as handle:
json.dump(data, handle)
_SLEEPER_PLAYERS_CACHE["data"] = data
_SLEEPER_PLAYERS_CACHE["fetched_at"] = now
return data
Expand Down
Loading