sleeper_api 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/CLAUDE.md +35 -1
- data/README.md +108 -90
- data/lib/sleeper_api/client.rb +110 -0
- data/lib/sleeper_api/json_parser.rb +40 -0
- data/lib/sleeper_api/version.rb +1 -1
- data/lib/sleeper_api.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bd436fd0363716d6908e51d9137e89725230afb2c8a71479582a8894f12db57
|
|
4
|
+
data.tar.gz: b5933518c66d46edfc540ac261c0d473f62fc6c93f6cc03c9d7e426edebc9ed3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bdf8104296937896fdf11dcd71d12008e5801d9197552827293d734dd360032ac99dacaa5d7ff656e34ba2938f206bf8f864385d20b6f30230e12d819dc2039
|
|
7
|
+
data.tar.gz: 12eb88e92e6f27af1734b059b24313248ccf1888e6354437b391439920d0e3d2c5c00e263d7a957ad67cc791503978a92786b67b12eb6e871152a1b274e814c3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
+
## [1.3.1] - 2026-09-10
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Every request raised `ArgumentError: unknown keyword: quirks_mode` under
|
|
6
|
+
`json` 3.0.** HTTParty 0.24.2 parses with `JSON.parse(body, quirks_mode:
|
|
7
|
+
true, allow_nan: true)`; json 3.0 removed that keyword. Total breakage rather
|
|
8
|
+
than a degradation, and it arrives through a transitive bump rather than
|
|
9
|
+
anything a consumer chose — this gem has no committed lockfile, so CI went
|
|
10
|
+
red with no commit in between. httparty 0.24.2 is the newest release and has
|
|
11
|
+
no fix.
|
|
12
|
+
|
|
13
|
+
`SleeperApi::JsonParser` subclasses `HTTParty::Parser` and overrides its
|
|
14
|
+
`json` method to drop the flag; `Client` parses with it. **Deliberately not a
|
|
15
|
+
`json < 3` pin in the gemspec**, which would fix the same crash by forbidding
|
|
16
|
+
every app that uses this gem from upgrading json at all, for a flag none of
|
|
17
|
+
them asked for.
|
|
18
|
+
|
|
19
|
+
`quirks_mode: true` allowed a bare scalar at the top level, which is not
|
|
20
|
+
academic here — Sleeper answers an unknown username with a literal `null`.
|
|
21
|
+
Both majors parse that correctly without it: `JSON.parse("null")` is `nil`
|
|
22
|
+
under 2.21.2 and under 3.0.2. The suite passes under both.
|
|
23
|
+
|
|
24
|
+
### Documented
|
|
25
|
+
|
|
26
|
+
Three facts measured against the live API on 2026-09-10, while NFL week 1 was
|
|
27
|
+
half-played — one game finished, one being played, fourteen not yet started.
|
|
28
|
+
No behaviour changed; all three are things a caller could previously only find
|
|
29
|
+
out by being wrong first.
|
|
30
|
+
|
|
31
|
+
- **`#schedule`'s `status` values are named: `pre_game`, `in_game`, `complete`,
|
|
32
|
+
`canceled`.** `in_game` was read off a live game; the other three come from
|
|
33
|
+
the published 2025 and 2026 schedules. It is the only per-game signal that a
|
|
34
|
+
game has been played — this payload has a `date` and no kickoff time, so a
|
|
35
|
+
caller can know a game is under way but never how far into it. **A week is
|
|
36
|
+
not one event**: week 1 of 2026 held three of those states at once, so
|
|
37
|
+
reasoning about "has the week started" from `date` alone gets it wrong for
|
|
38
|
+
everyone whose game is on Sunday. Treat the four as open and match with a
|
|
39
|
+
fallback — a postponement would be a fifth and none has been seen.
|
|
40
|
+
|
|
41
|
+
- **`#projections` is a pre-game, whole-game projection that does not move
|
|
42
|
+
while the game is played.** Five players in one night's game held the same
|
|
43
|
+
`pts_ppr` to the decimal across six hours spanning its kickoff; a game that
|
|
44
|
+
had already finished still projected 19.69 for a player who scored 26.2, so
|
|
45
|
+
it does not settle onto the final either. This is the natural endpoint to
|
|
46
|
+
reach for when building anything live, and it cannot answer the question:
|
|
47
|
+
**there is no live projection here**, so nothing in this payload says whether
|
|
48
|
+
a player is on pace. A player on 8 of a projected 12 in the first quarter is
|
|
49
|
+
ahead of schedule, and this reports 12 all afternoon.
|
|
50
|
+
|
|
51
|
+
- **`#stats` answers a week still being played with a partial set, and says
|
|
52
|
+
nothing about being partial.** Mid-week-1 it returned 301 rows with 42
|
|
53
|
+
carrying a `pts_ppr`. That is distinct from the already-documented empty
|
|
54
|
+
week: a caller treating "the week's stats" as the whole week gets a
|
|
55
|
+
half-filled answer for as long as the week is in progress, which for a
|
|
56
|
+
regular-season week is most of five days. `#schedule`'s per-game `status` is
|
|
57
|
+
the only thing that tells a missing row from a scoreless one.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- `#schedule`'s spec fixture carries all four statuses rather than `pre_game`
|
|
62
|
+
alone, and an example pins that they pass through untranslated. The narrow
|
|
63
|
+
fixture was a shape live data produces only on a quiet Tuesday, and a fixture
|
|
64
|
+
narrower than the API is how a caller ends up written against a vocabulary of
|
|
65
|
+
one.
|
|
66
|
+
|
|
67
|
+
## [1.3.0] - 2026-08-27
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- `Client#stats(season, week:, season_type:, sport:)` and `Client#projections(...)` — per-player weekly statistics and projections, both undocumented, both under `/v1`. Found by probing on 2026-08-27; the consuming app's backlog had recorded "Sleeper has no projections endpoint and no player-stats endpoint" as settled fact, and several of its cards were blocked on that.
|
|
72
|
+
|
|
73
|
+
Each returns an object keyed by player id — plus `TEAM_XXX` keys for team-level rows — holding raw counting stats (`rec`, `rush_yd`, `off_snp`, `rec_rz_tgt`, …) alongside Sleeper's canned `pts_ppr` / `pts_half_ppr` / `pts_std`. 228 distinct fields were observed across one week of 2025. Omitting `week` requests season totals, which is a shorter path and a different resource rather than a default of week 1.
|
|
74
|
+
|
|
75
|
+
Three things a caller has to know, all confirmed live:
|
|
76
|
+
|
|
77
|
+
- **Nothing 404s.** An unplayed week (`regular/2026/1`), a week out of range (`regular/2025/99`) and an unrecognised season type (`banana`) all answer 200 with `{}`. An empty body is indistinguishable from a typo, so validate arguments rather than trusting emptiness.
|
|
78
|
+
- **A row count is not evidence of a projection.** `projections` for a season Sleeper has not projected still returns a full set of entries — 9,386 for 2030 — every one holding only `{"adp_dd_ppr" => 1000.0}` and no `pts_ppr`. Filter on the field you want.
|
|
79
|
+
- **`pre` and `post` restart week numbering at 1**, exactly as `#schedule` does, so rows from different season types must never be pooled.
|
|
80
|
+
|
|
81
|
+
`adp_dd_ppr` 1000.0 and `pos_rank_*` 999.0 are "unknown" sentinels rather than values.
|
|
82
|
+
|
|
83
|
+
Path segments are escaped, unlike the older `#get_user` — an unescaped segment can walk out of the endpoint entirely, which the consuming app had to work around at its own boundary.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- README.md contained 272 non-breaking spaces across 90 lines, 58 of them **inside ````ruby` code fences** — so the documented examples raised a syntax error when copy-pasted. `lib/` and `spec/` were unaffected; only the documentation was broken. Replaced with ordinary spaces.
|
|
88
|
+
|
|
1
89
|
## [1.2.0] - 2026-08-23
|
|
2
90
|
|
|
3
91
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -19,7 +19,7 @@ bundle exec rubocop -A # autocorrect
|
|
|
19
19
|
bin/console # IRB with the gem loaded
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
CI (`.github/workflows/ci.yml`) runs `bundle exec rake ci` on Ruby 3.2
|
|
22
|
+
CI (`.github/workflows/ci.yml`) runs `bundle exec rake ci` on Ruby 3.2 and 3.4. The gemspec claims `required_ruby_version >= 2.6.0` and RuboCop targets 2.6, but nothing tests below 3.2 — treat 2.6 compatibility as unverified.
|
|
23
23
|
|
|
24
24
|
## Architecture
|
|
25
25
|
|
|
@@ -44,6 +44,22 @@ Four layers, with a deliberate split between HTTP and modeling:
|
|
|
44
44
|
|
|
45
45
|
**Never fan out `get_players`.** It's a multi-megabyte payload of every NFL player. It's cached for 24h in an ivar on the client instance — which means the cache dies with the client. Consumers holding a short-lived client re-download it every time.
|
|
46
46
|
|
|
47
|
+
### Undocumented endpoints carry undocumented vocabularies
|
|
48
|
+
|
|
49
|
+
Sleeper publishes no field reference, so every enum this gem passes through was
|
|
50
|
+
found by looking. Four are known — `#schedule`'s game `status`, `Player`'s
|
|
51
|
+
`status` and `injury_status`, and a roster's `lineup_position` — and **none of
|
|
52
|
+
them is closed**. Match with a fallback rather than a whitelist, and never
|
|
53
|
+
translate one into a vocabulary of this gem's own: the caller is the only one
|
|
54
|
+
who knows what to do with a value nobody has seen.
|
|
55
|
+
|
|
56
|
+
**When a value is measured, name it in the docstring and put the date and the
|
|
57
|
+
circumstance next to it.** `in_game` shipped as an unnamed fallback in the
|
|
58
|
+
consuming app for a day before anyone saw a live game; the fallback was right,
|
|
59
|
+
and the thing that made it a fact rather than a guess was writing down that it
|
|
60
|
+
was read at 21:13 on 2026-09-10 during a specific game. A vocabulary with no
|
|
61
|
+
provenance is indistinguishable from one somebody assumed.
|
|
62
|
+
|
|
47
63
|
## Testing
|
|
48
64
|
|
|
49
65
|
RSpec + WebMock, with `WebMock.disable_net_connect!` — **specs must never hit the network**. Stub with `stub_request` (see `client_spec.rb`) or `instance_double(SleeperApi::Client)` for resource-object specs (see `league_spec.rb`).
|
|
@@ -54,6 +70,24 @@ RSpec + WebMock, with `WebMock.disable_net_connect!` — **specs must never hit
|
|
|
54
70
|
|
|
55
71
|
`lib/` is at 100% line coverage. Every bug found in this gem so far has been in a `League#format_*` method handling a field Sleeper omitted, exercised only by real data. When adding a formatter, write the nil-field case first.
|
|
56
72
|
|
|
73
|
+
### There is no committed Gemfile.lock, so CI resolves fresh every run
|
|
74
|
+
|
|
75
|
+
That is right for a library — a lockfile would hide exactly the incompatibility
|
|
76
|
+
a consumer is going to hit — but it has a consequence worth naming: **a green
|
|
77
|
+
run does not stay green.** A transitive release can turn CI red with no commit
|
|
78
|
+
in between, and the first PR opened afterwards looks like the culprit.
|
|
79
|
+
|
|
80
|
+
`json` 3.0 did this on 2026-09-11. It removed `quirks_mode`, which
|
|
81
|
+
`httparty` 0.24.2 passes on every JSON parse, so 11 examples began raising
|
|
82
|
+
`ArgumentError: unknown keyword: quirks_mode` — on a documentation-only PR that
|
|
83
|
+
touched no code. Check whether `main` is red before reading a failure as the
|
|
84
|
+
branch's fault; `main`'s last run can be weeks old.
|
|
85
|
+
|
|
86
|
+
`SleeperApi::JsonParser` is the fix, and it is worth reading before reaching for
|
|
87
|
+
a version pin: the gem overrides the one broken method rather than constraining
|
|
88
|
+
`json` in the gemspec, because a constraint there would forbid every consuming
|
|
89
|
+
app from upgrading `json` for a flag none of them asked for.
|
|
90
|
+
|
|
57
91
|
## Release
|
|
58
92
|
|
|
59
93
|
Version lives in `lib/sleeper_api/version.rb`. Update `CHANGELOG.md`, then `bundle exec rake release` from a clean `main`, which builds to `pkg/`, tags, pushes the tag, and uploads to rubygems.
|
data/README.md
CHANGED
|
@@ -2,71 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/sleeper_api)
|
|
4
4
|
|
|
5
|
-
A comprehensive Ruby gem
|
|
5
|
+
A comprehensive Ruby gem for interacting with [Sleeper's fantasy football API](https://docs.sleeper.com/). Built with performance, reliability, and developer experience in mind.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- Complete API Coverage
|
|
9
|
+
- Complete API Coverage - Users, leagues, drafts, players, matchups, transactions
|
|
10
10
|
|
|
11
|
-
- Performance Optimized
|
|
11
|
+
- Performance Optimized - Smart caching, connection pooling, rate limiting
|
|
12
12
|
|
|
13
|
-
- Robust Error Handling
|
|
13
|
+
- Robust Error Handling - Automatic retries, timeout management, detailed error messages
|
|
14
14
|
|
|
15
|
-
- Well Tested
|
|
15
|
+
- Well Tested - 90%+ test coverage with RSpec
|
|
16
16
|
|
|
17
|
-
- Highly Configurable
|
|
17
|
+
- Highly Configurable - Custom timeouts, retries, logging
|
|
18
18
|
|
|
19
|
-
- Production
|
|
19
|
+
- Production Ready - Type signatures, CI/CD, code quality tools
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
23
|
Add this line to your application's Gemfile:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
gem
|
|
26
|
+
gem 'sleeper_api'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
|
-
require
|
|
32
|
+
require 'sleeper_api'
|
|
33
33
|
|
|
34
|
-
#
|
|
35
|
-
client
|
|
36
|
-
league
|
|
34
|
+
# Basic usage with default configuration
|
|
35
|
+
client = SleeperApi.client
|
|
36
|
+
league = client.league("123456789012345678")
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
puts
|
|
40
|
-
puts
|
|
41
|
-
puts
|
|
38
|
+
# Access league information
|
|
39
|
+
puts league.name # "My Fantasy League"
|
|
40
|
+
puts league.total_rosters # 12
|
|
41
|
+
puts league.status # "in_season"
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
rosters
|
|
45
|
-
rosters.each
|
|
46
|
-
|
|
43
|
+
# Get rosters
|
|
44
|
+
rosters = league.rosters
|
|
45
|
+
rosters.each do |roster|
|
|
46
|
+
puts "#{roster[:owner_display_name]}: #{roster[:wins]}-#{roster[:losses]}"
|
|
47
47
|
end
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Configuration
|
|
51
51
|
|
|
52
|
-
Customize the gem's
|
|
52
|
+
Customize the gem's behavior:
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
|
-
SleeperApi.configure
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
SleeperApi.configure do |config|
|
|
56
|
+
config.timeout = 45 # Request timeout in seconds (10-60)
|
|
57
|
+
config.retries = 5 # Number of retries on failure (0-5)
|
|
58
|
+
config.logger = Logger.new(STDOUT) # Custom logger
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
client
|
|
61
|
+
# Configuration is applied to all subsequent client instances
|
|
62
|
+
client = SleeperApi.client
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## API Coverage
|
|
66
66
|
|
|
67
67
|
### Users
|
|
68
68
|
|
|
69
|
-
Get user information and
|
|
69
|
+
Get user information and their leagues/drafts:
|
|
70
70
|
|
|
71
71
|
```ruby
|
|
72
72
|
# Find a user by username
|
|
@@ -118,7 +118,7 @@ puts "Overall: #{summary[:total_wins]}-#{summary[:total_losses]}"
|
|
|
118
118
|
|
|
119
119
|
### Leagues
|
|
120
120
|
|
|
121
|
-
Access league
|
|
121
|
+
Access league data, rosters, matchups, and transactions:
|
|
122
122
|
|
|
123
123
|
```ruby
|
|
124
124
|
league = SleeperApi.client.league("123456")
|
|
@@ -180,7 +180,7 @@ matchup[:winner_owner] # => "Team Beta"
|
|
|
180
180
|
|
|
181
181
|
### Drafts
|
|
182
182
|
|
|
183
|
-
Access draft information, picks, and
|
|
183
|
+
Access draft information, picks, and traded picks:
|
|
184
184
|
|
|
185
185
|
```ruby
|
|
186
186
|
# Get a specific draft
|
|
@@ -237,19 +237,19 @@ team_3_rounds = draft.team_picks[3] # Team 3's picks by round
|
|
|
237
237
|
|
|
238
238
|
### Players
|
|
239
239
|
|
|
240
|
-
Access player data with automatic
|
|
240
|
+
Access player data with automatic caching:
|
|
241
241
|
|
|
242
242
|
```ruby
|
|
243
|
-
#
|
|
244
|
-
players
|
|
243
|
+
# Get all players (cached for 24 hours)
|
|
244
|
+
players = client.get_players
|
|
245
245
|
|
|
246
|
-
#
|
|
247
|
-
player
|
|
248
|
-
puts
|
|
246
|
+
# Find specific player
|
|
247
|
+
player = client.get_player_by_id("1234")
|
|
248
|
+
puts "#{player['first_name']} #{player['last_name']} - #{player['position']}"
|
|
249
249
|
|
|
250
|
-
#
|
|
251
|
-
trending_adds
|
|
252
|
-
trending_drops
|
|
250
|
+
# Get trending players
|
|
251
|
+
trending_adds = client.trending_players(type: "add", limit: 10)
|
|
252
|
+
trending_drops = client.trending_players(type: "drop", limit: 10)
|
|
253
253
|
```
|
|
254
254
|
|
|
255
255
|
### Player Helper
|
|
@@ -313,103 +313,121 @@ rosters.each do |roster|
|
|
|
313
313
|
end
|
|
314
314
|
```
|
|
315
315
|
|
|
316
|
-
### Additional
|
|
316
|
+
### Additional Endpoints
|
|
317
317
|
|
|
318
318
|
```ruby
|
|
319
|
-
#
|
|
320
|
-
state
|
|
321
|
-
puts
|
|
319
|
+
# Get NFL state
|
|
320
|
+
state = client.get_nfl_state
|
|
321
|
+
puts "Current week: #{state['week']}"
|
|
322
|
+
|
|
323
|
+
# Per-player weekly stats and projections (undocumented endpoints).
|
|
324
|
+
# Keyed by player id, plus TEAM_XXX keys for team-level rows.
|
|
325
|
+
week_stats = client.stats(2025, week: 1)
|
|
326
|
+
puts week_stats["4046"]["pts_ppr"] # => 21.4
|
|
327
|
+
puts week_stats["4046"]["off_snp"] # raw counting stats too
|
|
328
|
+
|
|
329
|
+
projected = client.projections(2026, week: 1)
|
|
330
|
+
|
|
331
|
+
# Omit the week for season totals — a different resource, not a default.
|
|
332
|
+
season_stats = client.stats(2025)
|
|
333
|
+
|
|
334
|
+
# Nothing here 404s: an unplayed week, a week out of range and an unknown
|
|
335
|
+
# season type all answer 200 with {}. And a projections call for a season
|
|
336
|
+
# Sleeper has not projected still returns thousands of entries carrying only
|
|
337
|
+
# an `adp_dd_ppr` sentinel — a row count is not evidence of a projection, so
|
|
338
|
+
# filter on the field you actually want.
|
|
339
|
+
real = projected.parsed_response.select { |_id, row| row.key?("pts_ppr") }
|
|
322
340
|
|
|
323
|
-
#
|
|
324
|
-
winners_bracket
|
|
325
|
-
losers_bracket
|
|
341
|
+
# Get playoff brackets
|
|
342
|
+
winners_bracket = client.get_league_playoff_bracket("league_id")
|
|
343
|
+
losers_bracket = client.get_league_toilet_bowl("league_id")
|
|
326
344
|
|
|
327
|
-
#
|
|
328
|
-
league_drafts
|
|
345
|
+
# Get league drafts
|
|
346
|
+
league_drafts = client.get_league_drafts("league_id")
|
|
329
347
|
|
|
330
|
-
#
|
|
331
|
-
traded_picks
|
|
348
|
+
# Get traded picks
|
|
349
|
+
traded_picks = client.get_league_traded_picks("league_id")
|
|
332
350
|
```
|
|
333
351
|
|
|
334
|
-
## Error
|
|
352
|
+
## Error Handling
|
|
335
353
|
|
|
336
354
|
The gem provides comprehensive error handling:
|
|
337
355
|
|
|
338
356
|
```ruby
|
|
339
357
|
begin
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
rescue
|
|
343
|
-
|
|
344
|
-
rescue
|
|
345
|
-
|
|
358
|
+
league = client.league("invalid_id")
|
|
359
|
+
# Process league data
|
|
360
|
+
rescue SleeperApi::Error => e
|
|
361
|
+
puts "API Error: #{e.message}"
|
|
362
|
+
rescue ArgumentError => e
|
|
363
|
+
puts "Invalid parameter: #{e.message}"
|
|
346
364
|
end
|
|
347
365
|
```
|
|
348
366
|
|
|
349
367
|
### Error Types
|
|
350
368
|
|
|
351
|
-
- SleeperApi::Error
|
|
369
|
+
- SleeperApi::Error - API-related errors (404, 500, timeouts, etc.)
|
|
352
370
|
|
|
353
|
-
- ArgumentError
|
|
371
|
+
- ArgumentError - Invalid parameters passed to methods
|
|
354
372
|
|
|
355
373
|
## Performance Considerations
|
|
356
374
|
|
|
357
375
|
### Caching
|
|
358
376
|
|
|
359
|
-
- Player data
|
|
377
|
+
- Player data is cached for 24 hours to reduce API calls
|
|
360
378
|
|
|
361
|
-
- League/User/Draft data
|
|
379
|
+
- League/User/Draft data is cached per instance
|
|
362
380
|
|
|
363
|
-
- Cache files are stored
|
|
381
|
+
- Cache files are stored in the current working directory
|
|
364
382
|
|
|
365
|
-
### Rate
|
|
383
|
+
### Rate Limiting
|
|
366
384
|
|
|
367
|
-
- Be mindful of Sleeper's rate limits: stay under
|
|
385
|
+
- Be mindful of Sleeper's rate limits: stay under 1000 API calls per minute
|
|
368
386
|
|
|
369
|
-
- The
|
|
387
|
+
- The gem automatically handles timeouts and retries
|
|
370
388
|
|
|
371
389
|
- Consider caching frequently accessed data in your application
|
|
372
390
|
|
|
373
391
|
### Memory Usage
|
|
374
392
|
|
|
375
|
-
- Large datasets
|
|
393
|
+
- Large datasets (like all players) are cached to disk
|
|
376
394
|
|
|
377
|
-
- League rosters and matchups
|
|
395
|
+
- League rosters and matchups are fetched lazily
|
|
378
396
|
|
|
379
|
-
- Use
|
|
397
|
+
- Use no_data: true when initializing leagues if you don't need immediate data
|
|
380
398
|
|
|
381
399
|
## Testing
|
|
382
400
|
|
|
383
401
|
The gem includes comprehensive tests:
|
|
384
402
|
|
|
385
403
|
```shellscript
|
|
386
|
-
#
|
|
387
|
-
bundle
|
|
388
|
-
#
|
|
389
|
-
bundle
|
|
390
|
-
#
|
|
391
|
-
bundle
|
|
404
|
+
# Run all tests
|
|
405
|
+
bundle exec rspec
|
|
406
|
+
# Run with coverage
|
|
407
|
+
bundle exec rspec --coverage
|
|
408
|
+
# Run specific test file
|
|
409
|
+
bundle exec rspec spec/sleeper_api/client_spec.rb
|
|
392
410
|
```
|
|
393
411
|
|
|
394
412
|
### Setup
|
|
395
413
|
|
|
396
414
|
```shellscript
|
|
397
|
-
git
|
|
398
|
-
cd
|
|
399
|
-
bundle
|
|
415
|
+
git clone https://github.com/eruity1/sleeper_api.git
|
|
416
|
+
cd sleeper_api
|
|
417
|
+
bundle install
|
|
400
418
|
```
|
|
401
419
|
|
|
402
420
|
### Code Quality
|
|
403
421
|
|
|
404
422
|
```shellscript
|
|
405
|
-
#
|
|
406
|
-
bundle
|
|
423
|
+
# Run all checks (tests + linting)
|
|
424
|
+
bundle exec rake ci
|
|
407
425
|
|
|
408
|
-
#
|
|
409
|
-
bundle
|
|
426
|
+
# Run RuboCop
|
|
427
|
+
bundle exec rubocop
|
|
410
428
|
|
|
411
|
-
#
|
|
412
|
-
bundle
|
|
429
|
+
# Auto-fix RuboCop issues
|
|
430
|
+
bundle exec rubocop -a
|
|
413
431
|
```
|
|
414
432
|
|
|
415
433
|
### Contributing
|
|
@@ -426,24 +444,24 @@ bundle exec rubocop -a
|
|
|
426
444
|
|
|
427
445
|
### Development Dependencies
|
|
428
446
|
|
|
429
|
-
- rspec
|
|
447
|
+
- rspec - Testing framework
|
|
430
448
|
|
|
431
|
-
- rubocop
|
|
449
|
+
- rubocop - Code style and quality
|
|
432
450
|
|
|
433
|
-
- simplecov
|
|
451
|
+
- simplecov - Test coverage
|
|
434
452
|
|
|
435
|
-
- webmock
|
|
453
|
+
- webmock - HTTP request mocking
|
|
436
454
|
|
|
437
455
|
## Requirements
|
|
438
456
|
|
|
439
457
|
- Ruby 2.6.0 or higher
|
|
440
458
|
|
|
441
|
-
- No external dependencies
|
|
459
|
+
- No external dependencies (HTTParty is bundled)
|
|
442
460
|
|
|
443
461
|
## License
|
|
444
462
|
|
|
445
|
-
The gem is available as open source
|
|
463
|
+
The gem is available as open source under the terms of the MIT License.
|
|
446
464
|
|
|
447
465
|
## Changelog
|
|
448
466
|
|
|
449
|
-
See
|
|
467
|
+
See CHANGELOG.md for version history and updates.
|
data/lib/sleeper_api/client.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "httparty"
|
|
2
2
|
require "json"
|
|
3
|
+
require "erb"
|
|
3
4
|
|
|
4
5
|
module SleeperApi
|
|
5
6
|
# HTTP client for Sleeper API requests.
|
|
@@ -21,6 +22,11 @@ module SleeperApi
|
|
|
21
22
|
# pass to make_request.
|
|
22
23
|
base_uri "https://api.sleeper.app"
|
|
23
24
|
|
|
25
|
+
# Not HTTParty's own. Its JSON branch passes `quirks_mode`, which json 3.0
|
|
26
|
+
# removed, so without this every response raises ArgumentError the moment a
|
|
27
|
+
# consumer resolves json 3. See JsonParser.
|
|
28
|
+
parser SleeperApi::JsonParser
|
|
29
|
+
|
|
24
30
|
# @param config [SleeperApi::Configuration] Client configuration
|
|
25
31
|
def initialize(config)
|
|
26
32
|
@config = config
|
|
@@ -209,6 +215,76 @@ module SleeperApi
|
|
|
209
215
|
end
|
|
210
216
|
end
|
|
211
217
|
|
|
218
|
+
# Get per-player statistics for one week, or for a whole season.
|
|
219
|
+
#
|
|
220
|
+
# Undocumented. Returns an object keyed by player id — plus `TEAM_XXX` keys
|
|
221
|
+
# for team-level rows — each holding raw counting stats (`rec`, `rush_yd`,
|
|
222
|
+
# `off_snp`, `rec_rz_tgt`…) alongside Sleeper's three canned point totals
|
|
223
|
+
# `pts_ppr` / `pts_half_ppr` / `pts_std`. 228 distinct fields were observed
|
|
224
|
+
# across one week of 2025.
|
|
225
|
+
#
|
|
226
|
+
# **Nothing here 404s.** An unplayed week, a week out of range, and an
|
|
227
|
+
# unrecognised season type all answer 200 with `{}`, so an empty result is
|
|
228
|
+
# a legitimate answer and is indistinguishable from a typo. Validate the
|
|
229
|
+
# arguments before you trust an empty body.
|
|
230
|
+
#
|
|
231
|
+
# **A week still being played answers with a partial set, and says nothing
|
|
232
|
+
# about being partial.** Mid-week-1 of 2026 this returned 301 rows with
|
|
233
|
+
# only 42 carrying a `pts_ppr` — the two teams whose games had finished or
|
|
234
|
+
# started. A caller that treats "the week's stats" as the whole week gets a
|
|
235
|
+
# half-filled answer for as long as the week is in progress, which for a
|
|
236
|
+
# regular-season week is most of five days. #schedule's per-game `status`
|
|
237
|
+
# is the only thing that can tell a missing row from a scoreless one.
|
|
238
|
+
#
|
|
239
|
+
# Omitting `week` requests season totals, which is a different resource at
|
|
240
|
+
# a shorter path rather than a default of week 1.
|
|
241
|
+
#
|
|
242
|
+
# `pre` and `post` restart week numbering at 1, exactly as #schedule does,
|
|
243
|
+
# so rows from different season types must never be pooled.
|
|
244
|
+
#
|
|
245
|
+
# @param season [Integer, String] Season year, e.g. 2025
|
|
246
|
+
# @param week [Integer, String, nil] Week number, or nil for season totals
|
|
247
|
+
# @param season_type [String] "regular" (default), "pre", or "post"
|
|
248
|
+
# @param sport [String] Sport code (default: "nfl")
|
|
249
|
+
# @return [HTTParty::Response] Player id to stats mapping
|
|
250
|
+
def stats(season, week: nil, season_type: "regular", sport: "nfl")
|
|
251
|
+
make_request(weekly_path("stats", sport, season_type, season, week))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Get per-player projections for one week, or for a whole season.
|
|
255
|
+
#
|
|
256
|
+
# Same shape and same caveats as #stats, with one of its own: for a season
|
|
257
|
+
# Sleeper has not projected, this still returns a full set of entries —
|
|
258
|
+
# 9,386 of them for 2030 — every one holding only `{"adp_dd_ppr" => 1000.0}`
|
|
259
|
+
# and no `pts_ppr` at all. **A row count is not evidence of a projection.**
|
|
260
|
+
# Filter on the field you actually want.
|
|
261
|
+
#
|
|
262
|
+
# `adp_dd_ppr` 1000.0 and `pos_rank_*` 999.0 are "unknown" sentinels rather
|
|
263
|
+
# than values.
|
|
264
|
+
#
|
|
265
|
+
# ⚠️ **This is a pre-game, whole-game projection and it does not move while
|
|
266
|
+
# the game is played.** Measured on 2026-09-10 across six hours spanning a
|
|
267
|
+
# kickoff: five players in that night's game held the same `pts_ppr` before
|
|
268
|
+
# it started and while it was in progress, to the decimal. Checked again
|
|
269
|
+
# against a game that had already finished — the projection still read
|
|
270
|
+
# 19.69 for a player who had scored 26.2, so it does not settle onto the
|
|
271
|
+
# final either.
|
|
272
|
+
#
|
|
273
|
+
# That matters because it is the natural thing to reach for and the wrong
|
|
274
|
+
# one: **there is no live projection here**, so nothing in this payload can
|
|
275
|
+
# say whether a player is on pace. A player on 8 points of a projected 12
|
|
276
|
+
# in the first quarter is ahead of schedule, and this endpoint will report
|
|
277
|
+
# 12 all afternoon.
|
|
278
|
+
#
|
|
279
|
+
# @param season [Integer, String] Season year, e.g. 2026
|
|
280
|
+
# @param week [Integer, String, nil] Week number, or nil for season totals
|
|
281
|
+
# @param season_type [String] "regular" (default), "pre", or "post"
|
|
282
|
+
# @param sport [String] Sport code (default: "nfl")
|
|
283
|
+
# @return [HTTParty::Response] Player id to projections mapping
|
|
284
|
+
def projections(season, week: nil, season_type: "regular", sport: "nfl")
|
|
285
|
+
make_request(weekly_path("projections", sport, season_type, season, week))
|
|
286
|
+
end
|
|
287
|
+
|
|
212
288
|
# Get a season's game schedule.
|
|
213
289
|
#
|
|
214
290
|
# Undocumented, and served from the host root rather than /v1 — hence the
|
|
@@ -220,6 +296,26 @@ module SleeperApi
|
|
|
220
296
|
# `post` (weeks 1-4) restart week numbering, so games from different season
|
|
221
297
|
# types must never be pooled.
|
|
222
298
|
#
|
|
299
|
+
# **`status` is `pre_game`, `in_game`, `complete` or `canceled`**, observed
|
|
300
|
+
# across the 2025 and 2026 regular seasons. `in_game` was read off a live
|
|
301
|
+
# game on 2026-09-10; the other three come from the published schedule. It
|
|
302
|
+
# is the only per-game signal of whether a game has been played — there is
|
|
303
|
+
# no kickoff time anywhere in this payload, only `date`, so a caller can
|
|
304
|
+
# know that a game is under way but never how far into it.
|
|
305
|
+
#
|
|
306
|
+
# **A week is not one event, and a caller reasoning from `date` alone will
|
|
307
|
+
# get that wrong.** Week 1 of 2026 held all three live states at the same
|
|
308
|
+
# moment: one `complete`, one `in_game`, fourteen `pre_game`.
|
|
309
|
+
#
|
|
310
|
+
# Treat the four as an open vocabulary. A postponement or a suspension
|
|
311
|
+
# would be a fifth value and none has been seen, so match with a fallback
|
|
312
|
+
# rather than a whitelist.
|
|
313
|
+
#
|
|
314
|
+
# **A team can carry two games in one week.** 2026 lists a canceled DAL/SEA
|
|
315
|
+
# in week 6 that was superseded rather than called off, and both teams play
|
|
316
|
+
# someone else that week. Order by status before picking one, or you will
|
|
317
|
+
# hand a team a bye it does not have.
|
|
318
|
+
#
|
|
223
319
|
# A season Sleeper has not scheduled yet answers 200 with an empty array
|
|
224
320
|
# rather than 404, so an empty result is a legitimate answer and not an
|
|
225
321
|
# error.
|
|
@@ -271,6 +367,20 @@ module SleeperApi
|
|
|
271
367
|
|
|
272
368
|
private
|
|
273
369
|
|
|
370
|
+
# Path for #stats and #projections. Segments are escaped because they are
|
|
371
|
+
# interpolated into a URI path: an unescaped one can walk out of the
|
|
372
|
+
# endpoint entirely, which is the bug the consuming app had to work around
|
|
373
|
+
# for #get_user.
|
|
374
|
+
#
|
|
375
|
+
# A nil week drops the segment rather than defaulting, because the shorter
|
|
376
|
+
# path is season totals.
|
|
377
|
+
def weekly_path(resource, sport, season_type, season, week)
|
|
378
|
+
segments = [resource, sport, season_type, season, week].compact
|
|
379
|
+
escaped = segments.map { |segment| ERB::Util.url_encode(segment.to_s) }
|
|
380
|
+
|
|
381
|
+
"/v1/#{escaped.join("/")}"
|
|
382
|
+
end
|
|
383
|
+
|
|
274
384
|
# Make an HTTP request with retry logic and logging.
|
|
275
385
|
#
|
|
276
386
|
# @param path [String] API endpoint path
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "httparty"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module SleeperApi
|
|
5
|
+
# HTTParty's JSON parser, minus the one argument that stops it working.
|
|
6
|
+
#
|
|
7
|
+
# HTTParty 0.24.2 parses with `JSON.parse(body, quirks_mode: true,
|
|
8
|
+
# allow_nan: true)`. **json 3.0 removed `quirks_mode`**, so every response
|
|
9
|
+
# this gem parses raises `ArgumentError: unknown keyword: quirks_mode` the
|
|
10
|
+
# moment a consumer resolves json 3 — which is total breakage, not a
|
|
11
|
+
# degradation, and it arrives through a transitive bump nobody asked for.
|
|
12
|
+
# httparty 0.24.2 is the newest release and has no fix.
|
|
13
|
+
#
|
|
14
|
+
# **Dropping the option rather than pinning json is what keeps this the
|
|
15
|
+
# gem's problem instead of its consumers'.** A `json < 3` constraint in the
|
|
16
|
+
# gemspec would fix the same crash by forbidding every app that uses this
|
|
17
|
+
# gem from upgrading json at all, for a flag none of them asked for.
|
|
18
|
+
#
|
|
19
|
+
# `quirks_mode: true` allowed a bare scalar at the top level. That is not
|
|
20
|
+
# academic here — Sleeper answers an unknown username with a literal `null`.
|
|
21
|
+
# Both majors parse that correctly without the flag, checked on 2026-09-11:
|
|
22
|
+
# `JSON.parse("null")` is `nil` under json 2.21.2 and under 3.0.2. The flag
|
|
23
|
+
# has been doing nothing for this gem for some time.
|
|
24
|
+
#
|
|
25
|
+
# **@api private.** It is public only because `parser` has to name something
|
|
26
|
+
# a consumer's Ruby can resolve. Nothing here is a promise: it exists to be
|
|
27
|
+
# deleted the day httparty parses without the flag, and it is why 1.3.1 is a
|
|
28
|
+
# patch rather than a minor — a new constant that is not API does not earn
|
|
29
|
+
# one, and a fix for total breakage should reach a `~> 1.3.0` pin.
|
|
30
|
+
#
|
|
31
|
+
# A subclass rather than a `parser` lambda, because everything else in
|
|
32
|
+
# `HTTParty::Parser#parse` still applies — the blank-body guard, the format
|
|
33
|
+
# detection, the supported-format table. One method is wrong; one method is
|
|
34
|
+
# overridden.
|
|
35
|
+
class JsonParser < HTTParty::Parser
|
|
36
|
+
def json
|
|
37
|
+
JSON.parse(body, allow_nan: true)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/sleeper_api/version.rb
CHANGED
data/lib/sleeper_api.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sleeper_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eruity1
|
|
@@ -148,6 +148,7 @@ files:
|
|
|
148
148
|
- lib/sleeper_api/client.rb
|
|
149
149
|
- lib/sleeper_api/draft.rb
|
|
150
150
|
- lib/sleeper_api/helpers.rb
|
|
151
|
+
- lib/sleeper_api/json_parser.rb
|
|
151
152
|
- lib/sleeper_api/league.rb
|
|
152
153
|
- lib/sleeper_api/user.rb
|
|
153
154
|
- lib/sleeper_api/version.rb
|