feat(mcp): Live Tennis API provider — live scores, fixtures, players (#86) - #94
feat(mcp): Live Tennis API provider — live scores, fixtures, players (#86)#94bensynapse wants to merge 2 commits into
Conversation
Adds a native tennis data provider for the Sports Data MCP server, resolving issue WFord26#86. Follows the espn_api_handler.py pattern, shares the existing ResponseCache, and is gated on TENNIS_API_KEY the way ODDS_API_KEY and DASHBOARD_API_KEY already are — no key set, no handler built, no tools registered. v1 scope is the surface a free key can actually exercise: live scores, fixtures, and player lookups. Rankings (Pro) and H2H (Basic) are intentionally left out. Match prices are unaffected and stay with get_odds' tennis_atp_* passthrough. - sports_api/tennis_api_handler.py: handler + a per-minute token-bucket limiter and honest daily-quota tracking (from a 429 or /usage), deliberately NOT routed through key_manager.py (wrong quota shape) or the team formatters. - sports_api/tools/tennis_tools.py: register_tennis_tools(mcp, handler), prefixed tool names, registers nothing when the handler is None. Derives break-point state from the documented rule. - Composition root wires the handler (gated on TENNIS_API_KEY) with a CACHE_TTL_TENNIS_LIVE override (default 30) and passes it to diagnostics. - get_api_status folds in the tennis daily-quota from tracked state (no upstream call, so it stays quota-free). - .env.example documents the tier contract and the TTL survival knob with the arithmetic, next to TENNIS_API_KEY. - Tests: test_tennis_api_handler.py (aioresponses, real API shapes) covering the rate limiter and the daily-quota-exhausted branch; a no-key registration case in test_tools_registration.py. Vendor-authored (Live Tennis API) — disclosed in the module docstring and PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@bensynapse Thanks for the detailed scoping write-up — the tier arithmetic and the "don't ship tools that 402 on the key our docs tell people to get" call are both right, and the handler tracks the existing pattern closely. Two things I'd like fixed before this goes in. Both are in the quota state machine — the one part with no upstream to correct it, so a wrong guess there sticks. 1. Every 429 is treated as daily exhaustion — The comment there reasons that the token bucket already handles minute bursts, so a 429 that slips through must be the daily limit. The bucket can't carry that weight: it's process-local and starts full. Restart the server mid-minute after 30 requests have already gone out and the fresh bucket hands out 30 more — the API returns a minute-level 429, and Could you classify the 429 before parking? The response body, the rate-limit headers, and 2. It routes through Combined with (1), a park set in error has no way out short of restarting the process or waiting for UTC midnight. I'd like Related: On CI: the Backend/Frontend failures aren't yours. Both suites pass — the jobs die at the "Comment test results on PR" step, which 403s because fork PRs get a read-only I have some smaller notes beyond these two, but nothing else blocking — happy to send them if useful. |
… recovery path Addresses the two quota-state-machine issues raised in review. 1. A 429 no longer blanket-parks the day. _classify_rate_limit() reads the rate-limit scope/window headers, then per-window remaining, then the body wording, then Retry-After magnitude; only a genuine daily 429 calls _mark_daily_exhausted(). A minute-scale 429 drains the local bucket (TokenBucket.penalize) and returns rate_limited + retry_after_seconds, leaving the daily quota intact. Default when nothing says "daily" is minute-scale. 2. /usage is a recovery call again. get_usage() passes bypass_daily_gate=True so the free read reaches the API even while parked; _record_usage now clears the park on a positive remaining (was set-only); and /usage gets its own short TTL (CACHE_TTL_TENNIS_USAGE, default 15s) rather than sharing the live TTL. +23 unit tests. Full suite 197 passed. Signed-off-by: Ben Abulafia <ben@synapsereality.io>
|
Both quota-state-machine issues are addressed on the branch. 1 — a 429 no longer blanket-parks the day. The 2 — Tests: +23 unit tests (classifier per-signal + minute default, And thanks for the CI note — agreed, the "Comment test results" 403 is the fork-token scope, not a test failure. Ignoring it. Happy to take the smaller notes too if you want to send them. |
Closes #86.
Disclosure
Vendor-authored: I run the Live Tennis API, so this provider is a vendor contribution — judge it on the merits. It follows your
espn_api_handler.pypattern, shares yourResponseCache, and touches exactly one file outside its own module (diagnostics_tools.py, for the quota line you asked for).Scope (as agreed in the thread)
v1 = live + fixtures + players only — the surface a free key can genuinely exercise. Rankings (Pro) and H2H (Basic) are left out rather than shipped as tools that 402 on the key your setup docs tell people to get. Match prices are unaffected and stay with
get_oddsand itstennis_atp_*passthrough.The tier contract is in the module docstring and
.env.examplewith the arithmetic, not just the adjectives: free tier for fixtures, player lookups and low-cadence score checks; paid tier for live match-following. At the default 30s live TTL, following one three-hour match is ~360 requests against a free key's 100/day — it exhausts the day inside one match.CACHE_TTL_TENNIS_LIVEis the survival knob (a 900s TTL keeps a whole casual Slam day inside the free allowance).The 7 points
sports_api/tennis_api_handler.py— handler + a per-minute token bucket. It does not route throughkey_manager.py: that models a monthly sticky-until-exhausted quota with no per-minute concept, and its marker matching would classify a 429 burst as a drained key and park it. Instead the bucket gates outgoing requests locally, and the daily quota is tracked honestly (a 429 parks the key until the next UTC midnight;/usagerefreshes the numbers). No team formatters reused — tennis output is player-shaped and set-by-set.sports_api/tools/tennis_tools.pyexportingregister_tennis_tools(mcp, handler), re-exported fromtools/__init__.py. Tool names prefixed:get_tennis_scoreboard,get_tennis_match_score,get_tennis_fixtures,get_tennis_player.TENNIS_API_KEY. No key → no handler → nothing registered (register_tennis_toolsreturns early when the handler is None). Verified by a test.ResponseCachevia constructor,"tennis"namespace inmake_key,_is_cacheablecopied from ESPN (failures never cache), API key kept out of the cache key. The composition root mergesCACHE_TTL_TENNIS_LIVE(default 30) /_FIXTURES/_PLAYERover the handler defaults..env.exampledocuments the TTL knob and tier arithmetic right next toTENNIS_API_KEY.get_api_statusindiagnostics_tools.py— from the handler's tracked state, so it stays quota-free (no/usagecall inside the diagnostics tool). It's the only file outside my own that I touched.tests/test_tennis_api_handler.pywithaioresponses, mirroringtest_espn_api_handler.py, plus a no-key case intest_tools_registration.py. The rate limiter and the daily-quota-exhausted branch are covered specifically. Response samples are real Live Tennis API shapes (the{"data": ...}envelope,players.p1/p2, thesets/games/points/serverscore object), not invented.Verification
Full suite: 174 passed (was 143 before). Coverage on
sports_api/: handler 95%, TOTAL 65% (target 50%). Run withcd mcp && pip install -r requirements-dev.txt && pytest tests/.One note: I matched the existing hand-formatted style of
odds_api_handler.py/espn_tools.py(which aren't Black-clean in the tree today) rather than Black-format only the new files and diverge from their neighbors — happy to run Black across the module if you'd prefer.If a vendor-contributed provider still isn't a direction you want, close it — no hard feelings, and thanks for the detailed scoping in the thread.