Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user