feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)
Feature 1.2: Python FastAPI microservice wrapping nba_api - GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search - Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat - 27 Python tests passing Feature 1.4: Complete Supabase database schema - 6 tables: users, picks, scan_sessions, bets, outcomes, performance - RLS enabled on all tables with auth.uid() policies - 3 triggers: auto-create user, updated_at, scan count reset - 37 schema validation tests passing - Migration SQL ready, pending manual apply (WSL2 DNS blocker) Total: 92 tests (65 Node.js + 27 Python), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
import redis as redis_lib
|
||||
from app.config import REDIS_URL
|
||||
|
||||
_client = None
|
||||
|
||||
|
||||
def get_redis():
|
||||
global _client
|
||||
if _client is None:
|
||||
_client = redis_lib.from_url(REDIS_URL, decode_responses=True)
|
||||
return _client
|
||||
|
||||
|
||||
def cache_get(key):
|
||||
r = get_redis()
|
||||
data = r.get(key)
|
||||
if data is not None:
|
||||
return json.loads(data)
|
||||
return None
|
||||
|
||||
|
||||
def cache_set(key, value, ttl):
|
||||
r = get_redis()
|
||||
r.set(key, json.dumps(value), ex=ttl)
|
||||
|
||||
|
||||
def cache_health():
|
||||
try:
|
||||
r = get_redis()
|
||||
r.ping()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,50 @@
|
||||
from nba_api.stats.static import players
|
||||
from app.utils.cache import cache_get, cache_set
|
||||
from app.config import PLAYER_SEARCH_TTL
|
||||
|
||||
|
||||
def normalize_name(name):
|
||||
return name.strip().lower()
|
||||
|
||||
|
||||
def search_players(name):
|
||||
cache_key = f"nba:player:{normalize_name(name)}"
|
||||
cached = cache_get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
all_players = players.get_players()
|
||||
search_lower = normalize_name(name)
|
||||
|
||||
matches = []
|
||||
for p in all_players:
|
||||
full_name = p["full_name"].lower()
|
||||
if search_lower in full_name:
|
||||
matches.append({
|
||||
"player_id": p["id"],
|
||||
"full_name": p["full_name"],
|
||||
"is_active": p["is_active"],
|
||||
})
|
||||
|
||||
cache_set(cache_key, matches, PLAYER_SEARCH_TTL)
|
||||
return matches
|
||||
|
||||
|
||||
def resolve_player(name):
|
||||
"""Resolve a player name to a single active player. Returns (player_id, full_name) or raises."""
|
||||
matches = search_players(name)
|
||||
active = [m for m in matches if m["is_active"]]
|
||||
|
||||
if not active:
|
||||
if matches:
|
||||
# Return first inactive match as fallback
|
||||
return matches[0]["player_id"], matches[0]["full_name"]
|
||||
return None, None
|
||||
|
||||
# Prefer exact match
|
||||
search_lower = normalize_name(name)
|
||||
for m in active:
|
||||
if m["full_name"].lower() == search_lower:
|
||||
return m["player_id"], m["full_name"]
|
||||
|
||||
return active[0]["player_id"], active[0]["full_name"]
|
||||
Reference in New Issue
Block a user