/** * Game-log service — fetches recent player game logs. * * Primary path: the Python FastAPI service at PYTHON_SERVICE_URL (default * http://localhost:8000). Its /stats/last-n and /wnba/stats/last-n * endpoints return per-game stat rows. * * Secondary path: not implemented in this session. If the Python service * is unreachable, we return null and let the feature cache omit the * features that depend on game logs. A flaky stats backend should NOT * generate fake feature values. */ const axios = require('axios'); const { cacheGet, cacheSet } = require('../../utils/redis'); const PYTHON_BASE = process.env.PYTHON_SERVICE_URL || 'http://localhost:8000'; const CACHE_TTL_SECONDS = 4 * 60 * 60; // 4h — game logs change once per night const HTTP_TIMEOUT_MS = 15_000; function pythonPath(sport) { switch (sport) { case 'nba': return '/stats/last-n'; case 'wnba': return '/wnba/stats/last-n'; default: return null; } } async function getGameLogs(playerName, sport, count = 20) { const path = pythonPath(sport); if (!path) return null; const cacheKey = `gamelogs:${sport}:${playerName}:${count}`; const cached = await cacheGet(cacheKey); if (cached) return cached; try { const res = await axios.get(`${PYTHON_BASE}${path}`, { params: { player: playerName, n: count }, timeout: HTTP_TIMEOUT_MS, }); const games = res.data?.games || res.data?.results || []; if (!Array.isArray(games) || games.length === 0) return null; await cacheSet(cacheKey, games, CACHE_TTL_SECONDS); return games; } catch (err) { // Python service down or returning 404 — return null, caller omits. if (err?.response?.status !== 404) { console.warn(`[gameLog] fetch failed for ${playerName}:`, err?.message); } return null; } } // Career playoff games — approximated from the season-avg endpoint's career // summary, if present. If the Python service doesn't surface this, return // null and let the caller skip the feature. async function getCareerPlayoffGames(playerName, sport) { if (sport !== 'nba' && sport !== 'wnba') return null; try { const res = await axios.get(`${PYTHON_BASE}/stats/season-avg`, { params: { player: playerName, season: 'career' }, timeout: HTTP_TIMEOUT_MS, }); const games = res.data?.career_playoff_games; return Number.isFinite(Number(games)) ? Number(games) : null; } catch { return null; } } // with/without analysis — compare a player's stats when a specific teammate // is in vs out. Requires the Python service to expose this; if not, the // feature falls back to a league-average bump (caller's choice). async function getWithWithoutStats(playerName, sport, statType, teammateName) { if (sport !== 'nba' && sport !== 'wnba') return null; try { const res = await axios.get(`${PYTHON_BASE}/stats/with-without`, { params: { player: playerName, stat_type: statType, teammate: teammateName }, timeout: HTTP_TIMEOUT_MS, }); return res.data || null; } catch { return null; } } module.exports = { getGameLogs, getCareerPlayoffGames, getWithWithoutStats };