fix(odds): identify the odds requests to ESPN - #451
Merged
Conversation
The odds fetch used a bare requests.get, so it went out as python-requests/x.y -- the one agent ESPN is known to reject. Around 2026-08-04 it began 403ing browser strings and bare custom tokens alike; what it accepts is a token carrying a URL that says who is calling. Every other ESPN caller in the tree already sends that header (src/common/api_helper.py, src/base_classes/data_sources.py); this path was simply missed. It is the worst one to miss. Odds are fetched per live game from inside the live update loop, so its failures are the ones that cost the caller its whole update budget -- the same path the 5s timeout and the cooldown were added to protect. Sent via a session rather than per-call, which also reuses the connection across a slate. Deliberately no retry adapter, unlike api_helper: retries multiply request_timeout, which is 5s precisely to stay inside the 30s operation budget. The existing tests patched the module's requests.get, which this change bypasses -- test_base_odds_manager was consequently reaching the real ESPN and taking 404s. Both files now patch the session, and the new tests pin the agent against api_helper's live value so the two cannot drift apart the next time ESPN moves the goalposts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Contributor
📝 WalkthroughWalkthroughChangesESPN odds session migration
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/test_odds_request_budget.py`:
- Around line 80-88: Update test_the_header_reaches_the_request to mock
m.session.send instead of the session-level get path, then inspect the
PreparedRequest passed to send and assert its User-Agent exactly matches
m.session.headers["User-Agent"]. Preserve the existing single-request assertion
and get_odds invocation.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 570882b1-22f2-4674-b1a9-f4d01adb0851
📒 Files selected for processing (3)
src/base_odds_manager.pytest/test_base_odds_manager.pytest/test_odds_request_budget.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
src/base_odds_manager.pyfetched odds with a barerequests.get, so it identified itself aspython-requests/x.y— the one agent ESPN is known to reject. It now goes through a session that sends the same header every other ESPN caller in the tree already sends.Why this path in particular
Around 2026-08-04 ESPN began 403ing browser strings and bare custom tokens alike; what it accepts is a token carrying a URL that says who is calling.
src/common/api_helper.pyandsrc/base_classes/data_sources.pywere both updated then. The odds manager was missed.It is the worst one to miss. Odds are fetched per live game, from inside the live update loop, with
show_oddsdefaulting on — so this is the path whose failures cost the caller its entire update budget. It's the same path #449 added the 5s timeout and the 60s cooldown to protect.To be clear about scope: this is not what is failing on the dev rig today. Those failures are
NameResolutionError— DNS, which happens before a request is ever sent, so headers are irrelevant to them; there are zero real 403s in two days of logs. This closes a latent exposure on the path least able to absorb one.Notes
api_helper— retries multiplyrequest_timeout, which is 5s precisely to stay inside the 30s operation budget. A test pins this.HTTPError, aRequestException, so it opens the existing breaker rather than being paid once per game. Also pinned.Tests
The existing tests patched the module's
requests.get, which this change bypasses —test_base_odds_manager.pywas consequently reaching the real ESPN and taking 404s. Both files now patch the session.New coverage in
TestIdentifiesItselfToEspn: the agent names the project and links to it, it is byte-identical toAPIHelper's live value (compared against the object, not a copied literal, so the two can't drift), the header reaches the request, and no retrying adapter is mounted.46 passedacross both odds files;2751 passed, 60 skippedacross the suite. The one failure,test_install_lowmem.py::TestDiskBackedTmpdir, is pre-existing onmainand environment-dependent.🤖 Generated with Claude Code
https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Summary by CodeRabbit
Bug Fixes
Tests