Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+97
View File
@@ -0,0 +1,97 @@
/**
* College Football Data (CFBD) — advanced college analytics.
*
* 100% free API key. Covers historical betting lines, player usage, team
* talent composites, advanced efficiency (PPA), recruiting. nba_api
* doesn't cover college; CFBD fills the gap for NCAAB and NCAAFB props.
*
* Note: CFBD's primary product is college football. College *basketball*
* coverage is via the /cbb endpoints. We expose both via the sport-aware
* functions below.
*/
const axios = require('axios');
const { createLimiter, createCircuitBreaker } = require('../../utils/rateLimiter');
const { cacheGet, cacheSet } = require('../../utils/redis');
const SOURCE = 'cfbd';
const HTTP_TIMEOUT_MS = 10_000;
const CACHE_TTL_SECONDS = 6 * 60 * 60; // 6h — most CFBD data is daily-fresh
const BASE_URL = process.env.CFBD_BASE_URL || 'https://api.collegefootballdata.com';
// Generous free tier — 10 req/min keeps us well under documented limits.
const limiter = createLimiter({ tokensPerInterval: 10, interval: 60_000 });
const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });
function configured() {
return !!process.env.CFBD_KEY;
}
async function fetchWithGuards(url, params, cacheKey) {
if (!configured()) return null;
const cached = await cacheGet(cacheKey);
if (cached) return cached;
await limiter.waitForToken();
try {
const data = await breaker.call(async () => {
const res = await axios.get(url, {
params,
headers: { Authorization: `Bearer ${process.env.CFBD_KEY}` },
timeout: HTTP_TIMEOUT_MS,
validateStatus: (s) => (s >= 200 && s < 300) || s === 429,
});
if (res.status === 429) {
const err = new Error('cfbd rate limited');
err.code = 'CFBD_429';
throw err;
}
return res.data;
});
await cacheSet(cacheKey, data, CACHE_TTL_SECONDS);
return data;
} catch (err) {
if (err?.code === 'CIRCUIT_OPEN') return null;
console.warn(`[${SOURCE}] fetch failed for ${cacheKey}:`, err?.message);
return null;
}
}
async function getTeamStats(team, year) {
const cacheKey = `cfbd:teamstats:${team}:${year}`;
const data = await fetchWithGuards(`${BASE_URL}/stats/season`, { year, team }, cacheKey);
return Array.isArray(data) ? data : [];
}
async function getPlayerUsage(player, team, year) {
const cacheKey = `cfbd:usage:${player}:${team}:${year}`;
const data = await fetchWithGuards(
`${BASE_URL}/player/usage`,
{ year, team, player },
cacheKey
);
return Array.isArray(data) ? data : [];
}
async function getTalentComposite(team, year) {
const cacheKey = `cfbd:talent:${team}:${year}`;
const data = await fetchWithGuards(`${BASE_URL}/talent`, { year }, cacheKey);
if (!Array.isArray(data)) return null;
return data.find((row) => (row.school || row.team) === team) || null;
}
async function getHistoricalLines(team, year) {
const cacheKey = `cfbd:lines:${team}:${year}`;
const data = await fetchWithGuards(`${BASE_URL}/lines`, { year, team }, cacheKey);
return Array.isArray(data) ? data : [];
}
module.exports = {
configured,
getTeamStats,
getPlayerUsage,
getTalentComposite,
getHistoricalLines,
__internals: { limiter, breaker, BASE_URL },
};