Skip to content

Ask pypistats politely: a User-Agent that identifies us, and backoff - #140

Merged
arpanghoshal merged 1 commit into
mainfrom
fix/downloads-429
Sep 11, 2026
Merged

arpanghoshal merged 1 commit into
mainfrom
fix/downloads-429

Conversation

@arpanghoshal

@arpanghoshal arpanghoshal commented Sep 11, 2026

Copy link
Copy Markdown
Member

The 429

Publishing the count rather than letting shields render it live fixed one end of the problem: shields asks pypistats on behalf of every project it serves. It did not fix the other. pypistats rate-limits per IP, and a GitHub-hosted runner's IP is shared with everyone else's Actions. The first real run got 429 in under half a second, having asked for nothing:

curl: (22) The requested URL returned error: 429

The change

A User-Agent that identifies the caller and points at this repository, plus five attempts spread over roughly four minutes (0, 15, 45, 90, 120s). A daily job can afford to wait; a badge that publishes a wrong number cannot. Anything still not 200 leaves the badge on its last value, which is the designed failure and already how the missing-token branch behaves.

Also replaces curl -f with an explicit status read here, matching the traffic step: -f exits 22 and prints the code alone, and the code is the whole diagnosis.

Evidence

Run locally against a limit I had already exhausted: attempt 1 returned 429, attempt 2 at +15s returned 200, and the step set read=true with 3,450/month.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved download-count retrieval reliability by retrying temporary API failures.
    • Preserved the existing badge value when download statistics remain unavailable after retries.
    • Added clearer handling for failed requests to avoid displaying error responses as download counts.

Publishing the count instead of letting shields render it live fixed shields
asking pypistats for every project it serves. It did not fix the other end:
pypistats rate-limits per IP, a GitHub-hosted runner's IP is shared with
everyone else's Actions, and the first real run got 429 in under half a second
having asked for nothing.

So the caller is now a polite one. A User-Agent that says who it is and where
to complain, and five attempts spread over about four minutes; a daily job can
afford to wait, and a badge that publishes a wrong number cannot. Anything
still not 200 at the end leaves the badge on its last value, which is the
failure this was designed around.

Demonstrated rather than assumed: run locally against a deliberately exhausted
limit, the first attempt returned 429, the second at +15s returned 200, and
the step set read=true with 3,450/month.
@arpanghoshal
arpanghoshal enabled auto-merge (squash) September 11, 2026 16:11
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 092c7345-d308-458c-81f0-38cb194aee81

📥 Commits

Reviewing files that changed from the base of the PR and between 4a93ad5 and 35bf97b.

📒 Files selected for processing (1)
  • .github/workflows/traffic.yml

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The downloads workflow now retries pypistats requests with increasing delays, identifies requests with a User-Agent, applies a timeout, and preserves the badge value after repeated failures.

Changes

Traffic badge retrieval

Layer / File(s) Summary
Retry and fallback handling
.github/workflows/traffic.yml
The downloads job makes up to five pypistats requests with increasing delays and a 30-second timeout. It maps curl failures to status 000, reports non-200 responses, and retains the last badge value after all attempts fail.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 35bf9

The workflow now handles transient rate limits while preserving the public badge when retrieval ultimately fails. No actionable merge risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding an identifying User-Agent and retry backoff for pypistats requests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/downloads-429

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arpanghoshal
arpanghoshal merged commit 41a79b1 into main Sep 11, 2026
11 of 12 checks passed
@arpanghoshal
arpanghoshal deleted the fix/downloads-429 branch September 11, 2026 16:21
arpanghoshal added a commit that referenced this pull request Sep 11, 2026
…verflowError (#142)

Found by `fuzz` on #140, which is how a non-required check earns its place: the
failure had nothing to do with that PR and everything to do with main.

`Policy.from_yaml` promises PolicyError for anything malformed, without
qualification, and strict_load is the one loader every CTRLRun document goes
through. PyYAML converts a scalar before it has decided the document is well
formed, so three conversions came back as the interpreter's own exception and
went past `except yaml.YAMLError`: an over-long \U escape overflows converting
the codepoint to a C int; "\U00110000" is above the Unicode maximum and chr()
says ValueError; and `x: 2026-99-99` is a ValueError from datetime, which is
the one that matters because a mistyped month is a thing a person writes.

Nothing unsafe was admitted -- the document is refused either way -- so what
leaked is the exception type, and the type is the contract: a caller catching
PolicyError around a policy load caught none of these.

ValueError and OverflowError are refusals now. Deliberately not
`except Exception`: strict_load calls one thing, so a MemoryError there is not
the document's fault. RecursionError stays uncaught for the same reason
fuzz/properties.py excludes it.

Three regression tests, the first seeded from the fuzzer's own crash unit
decoded as the harness decodes it. Docs tail in CTRLRun/ctrlrun-docs@8f84def.

Merged under the authority granted 2026-09-11. CodeRabbit was rate limited and
reviewed nothing, which its passing status does not say; what stands behind
this instead is scripts/check.sh green locally (2,948 passed, mypy --strict
clean), fuzz passing in CI, and the crash unit replayed through check_policy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant