sleeper_api 1.0.1 → 1.2.0
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 +23 -0
- data/CLAUDE.md +9 -3
- data/lib/sleeper_api/client.rb +46 -19
- data/lib/sleeper_api/league.rb +18 -2
- data/lib/sleeper_api/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49f189b10709caa46c846f8733372cfd50d1dff0f66548aba5cb7ce0d32508f5
|
|
4
|
+
data.tar.gz: a0670835fa5f56541d041292b498722d0597a0599c724e95f9e3ed193151872c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd18ff49145f5c1848857f7af5e9b15f02a8aac6f7940fe85517d333a213707f524d3f54d5d211ce94de28be9e955e6e366a3db66101ce329d39264f6eabf983
|
|
7
|
+
data.tar.gz: 0b254fa2078732333ee53f3f1074f3700c85c28733478234cd3c303b6b2698967889ad2db33d9802b1a93d00d7dee378387bfdb1d18403f55d0104daedc934b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [1.2.0] - 2026-08-23
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `Client#schedule(season, season_type:, sport:)` — a season's game list, each entry `{status, date, home, away, week, game_id}`. A team's bye week is the week it appears in no game, which derives exactly: checked against 2024, 2025 and 2026, every one gives exactly one bye per team. Note `pre` (weeks 1-3) and `post` (weeks 1-4) restart week numbering, so games from different season types must never be pooled, and a season Sleeper has not scheduled yet answers 200 with `[]` rather than 404.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`base_uri` is now the bare host, `https://api.sleeper.app`, and every path carries its own `/v1`.** The schedule endpoint is served from the host root, so while the version lived in `base_uri` it was unreachable at any path a caller could pass to `make_request`. Moving the version into the paths means one mechanism for every endpoint rather than an escape hatch for the exceptions.
|
|
10
|
+
|
|
11
|
+
**This changes the text of `SleeperApi::Error`**, which quotes the path it failed on: `"Failed to fetch /user/x: 404"` is now `"Failed to fetch /v1/user/x: 404"`. Anything matching on that string needs updating — though matching on it was never sound, since the same error covers a missing user, a 5xx and a timeout alike.
|
|
12
|
+
|
|
13
|
+
## [1.1.0] - 2026-08-22
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `League#rosters` truncated every score. Sleeper splits a score across two integer fields — `fpts: 1617` with `fpts_decimal: 78` is 1617.78 — and `total_points` read `fpts` alone. The loss was invisible because what remained was still a plausible score, and no fixture carried a `fpts_decimal` to catch it. **This changes `total_points` from an Integer to a Float** for any roster whose score has a fractional part.
|
|
18
|
+
- `League#rosters` returned `co_owners: nil` for every roster, always. It read `roster["co_owner"]`, singular; no Sleeper payload has ever contained that key. Now reads the plural spelling, and still yields `nil` when the field is absent — Sleeper's documented roster object does not include co-owners, though the published docs are partial (the league `settings` object is rendered only as `{ settings object }`).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `League#rosters` now returns `points_against`, combining `fpts_against` with `fpts_against_decimal` the same way. Previously reachable only by digging into the raw `settings` hash that passes through wholesale.
|
|
23
|
+
|
|
1
24
|
## [1.0.1] - 2026-08-12
|
|
2
25
|
|
|
3
26
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -26,7 +26,9 @@ CI (`.github/workflows/ci.yml`) runs `bundle exec rake ci` on Ruby 3.2 only. The
|
|
|
26
26
|
Four layers, with a deliberate split between HTTP and modeling:
|
|
27
27
|
|
|
28
28
|
- **`SleeperApi`** (`lib/sleeper_api.rb`) — module-level config + memoized global `SleeperApi.client`. `Configuration` validates `timeout` (10–60) and `retries` (0–5), raising `SleeperApi::Error` outside those bounds.
|
|
29
|
-
- **`Client`** — the only thing that talks HTTP. `include HTTParty` with `base_uri "https://api.sleeper.app
|
|
29
|
+
- **`Client`** — the only thing that talks HTTP. `include HTTParty` with `base_uri "https://api.sleeper.app"` — **the bare host; the `/v1` lives in each path**. That is deliberate and load-bearing: not every Sleeper endpoint is versioned. `/schedule/{sport}/{season_type}/{season}` is served from the host root, and while `base_uri` carried the version that endpoint was unreachable at any path a caller could pass in. A new endpoint spells out where it lives; do not move the version back into `base_uri` to shorten the paths.
|
|
30
|
+
|
|
31
|
+
Every call funnels through the private `make_request`, which handles retry-on-timeout, logging, and converts non-2xx into `SleeperApi::Error`. **That error quotes the path**, so the path prefix is part of a public string — v1.2.0 changed it from `"Failed to fetch /user/x: 404"` to `"Failed to fetch /v1/user/x: 404"`. `Client` also owns the 24-hour in-memory player cache.
|
|
30
32
|
- **`League` / `User` / `Draft`** — resource objects. Each takes `(id, client)`, fetches eagerly in the constructor, memoizes into ivars, and exposes formatted hashes.
|
|
31
33
|
- **`Helpers`** — mixed into all four. `deep_symbolize_keys` plus `player_details`, which reaches through `@client` — so any class including it must define `@client`.
|
|
32
34
|
|
|
@@ -54,6 +56,10 @@ RSpec + WebMock, with `WebMock.disable_net_connect!` — **specs must never hit
|
|
|
54
56
|
|
|
55
57
|
## Release
|
|
56
58
|
|
|
57
|
-
Version lives in `lib/sleeper_api/version.rb`. `bundle exec rake release` tags and
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
**The API key needs the `push_rubygem` scope.** `gem signin` defaults to `index_rubygems` only — read access — and answering `n` to "Do you want to customise scopes?" produces a key that fails the upload with `This API key cannot perform the specified action on this gem`. Answer `y` and enable `push_rubygem`, or edit the key's scopes at https://rubygems.org/profile/api_keys. Credentials land in `~/.local/share/gem/credentials` (the XDG path, not `~/.gem/credentials`).
|
|
62
|
+
|
|
63
|
+
**`rake release` is not atomic.** It tags and pushes the tag *before* uploading, so a failed upload leaves the tag published and the gem unreleased. Recovering means `gem push pkg/sleeper_api-<version>.gem` on the existing artifact — re-running `rake release` fails on the tag that already exists.
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
Consumers do not get a fix until it is published *and* they bump their lockfile.
|
data/lib/sleeper_api/client.rb
CHANGED
|
@@ -15,7 +15,11 @@ module SleeperApi
|
|
|
15
15
|
include Helpers
|
|
16
16
|
include HTTParty
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# The host only. The API version belongs in the paths because not every
|
|
19
|
+
# endpoint has one: /schedule is served from the root, and while base_uri
|
|
20
|
+
# carried "/v1" that endpoint was unreachable at any path a caller could
|
|
21
|
+
# pass to make_request.
|
|
22
|
+
base_uri "https://api.sleeper.app"
|
|
19
23
|
|
|
20
24
|
# @param config [SleeperApi::Configuration] Client configuration
|
|
21
25
|
def initialize(config)
|
|
@@ -57,7 +61,7 @@ module SleeperApi
|
|
|
57
61
|
# @return [Hash] Raw user data
|
|
58
62
|
# @see https://docs.sleeper.com/#user
|
|
59
63
|
def get_user(identifier)
|
|
60
|
-
make_request("/user/#{identifier}")
|
|
64
|
+
make_request("/v1/user/#{identifier}")
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
# Get leagues for a user in a specific season.
|
|
@@ -68,7 +72,7 @@ module SleeperApi
|
|
|
68
72
|
# @return [Array<Hash>] League data
|
|
69
73
|
# @see https://docs.sleeper.com/#get-all-leagues-for-user
|
|
70
74
|
def get_user_leagues(user_id, sport: "nfl", season: Time.now.year)
|
|
71
|
-
make_request("/user/#{user_id}/leagues/#{sport}/#{season}")
|
|
75
|
+
make_request("/v1/user/#{user_id}/leagues/#{sport}/#{season}")
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
# Get drafts for a user in a specific season.
|
|
@@ -79,7 +83,7 @@ module SleeperApi
|
|
|
79
83
|
# @return [Array<Hash>] Draft data
|
|
80
84
|
# @see https://docs.sleeper.com/#get-all-drafts-for-user
|
|
81
85
|
def get_user_drafts(user_id, sport: "nfl", season: Time.now.year)
|
|
82
|
-
make_request("/user/#{user_id}/drafts/#{sport}/#{season}")
|
|
86
|
+
make_request("/v1/user/#{user_id}/drafts/#{sport}/#{season}")
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
# Fetch league details.
|
|
@@ -88,7 +92,7 @@ module SleeperApi
|
|
|
88
92
|
# @return [Hash] League metadata
|
|
89
93
|
# @see https://docs.sleeper.com/#get-a-specific-league
|
|
90
94
|
def get_league(league_id)
|
|
91
|
-
make_request("/league/#{league_id}")
|
|
95
|
+
make_request("/v1/league/#{league_id}")
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
# Get all rosters in a league.
|
|
@@ -97,7 +101,7 @@ module SleeperApi
|
|
|
97
101
|
# @return [Array<Hash>] Roster data
|
|
98
102
|
# @see https://docs.sleeper.com/#getting-rosters-in-a-league
|
|
99
103
|
def get_league_rosters(league_id)
|
|
100
|
-
make_request("/league/#{league_id}/rosters")
|
|
104
|
+
make_request("/v1/league/#{league_id}/rosters")
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
# Get all users in a league.
|
|
@@ -106,7 +110,7 @@ module SleeperApi
|
|
|
106
110
|
# @return [Array<Hash>] User data
|
|
107
111
|
# @see https://docs.sleeper.com/#getting-users-in-a-league
|
|
108
112
|
def get_league_users(league_id)
|
|
109
|
-
make_request("/league/#{league_id}/users")
|
|
113
|
+
make_request("/v1/league/#{league_id}/users")
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
# Get matchups for a specific week.
|
|
@@ -116,7 +120,7 @@ module SleeperApi
|
|
|
116
120
|
# @return [Array<Hash>] Matchup data
|
|
117
121
|
# @see https://docs.sleeper.com/#getting-matchups-in-a-league
|
|
118
122
|
def get_league_matchups(league_id, week)
|
|
119
|
-
make_request("/league/#{league_id}/matchups/#{week}")
|
|
123
|
+
make_request("/v1/league/#{league_id}/matchups/#{week}")
|
|
120
124
|
end
|
|
121
125
|
|
|
122
126
|
# Get playoff winners bracket.
|
|
@@ -125,7 +129,7 @@ module SleeperApi
|
|
|
125
129
|
# @return [Array<Hash>] Bracket matchups
|
|
126
130
|
# @see https://docs.sleeper.com/#getting-the-playoff-bracket
|
|
127
131
|
def get_playoff_bracket(league_id)
|
|
128
|
-
make_request("/league/#{league_id}/winners_bracket")
|
|
132
|
+
make_request("/v1/league/#{league_id}/winners_bracket")
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
# Get toilet bowl (losers bracket).
|
|
@@ -134,7 +138,7 @@ module SleeperApi
|
|
|
134
138
|
# @return [Array<Hash>] Bracket matchups
|
|
135
139
|
# @see https://docs.sleeper.com/#getting-the-playoff-bracket
|
|
136
140
|
def get_toilet_bowl(league_id)
|
|
137
|
-
make_request("/league/#{league_id}/losers_bracket")
|
|
141
|
+
make_request("/v1/league/#{league_id}/losers_bracket")
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
# Get transactions for a specific week.
|
|
@@ -144,7 +148,7 @@ module SleeperApi
|
|
|
144
148
|
# @return [Array<Hash>] Transaction data
|
|
145
149
|
# @see https://docs.sleeper.com/#get-transactions
|
|
146
150
|
def get_transactions(league_id, week)
|
|
147
|
-
make_request("/league/#{league_id}/transactions/#{week}")
|
|
151
|
+
make_request("/v1/league/#{league_id}/transactions/#{week}")
|
|
148
152
|
end
|
|
149
153
|
|
|
150
154
|
# Get league drafts.
|
|
@@ -153,7 +157,7 @@ module SleeperApi
|
|
|
153
157
|
# @return [Array<Hash>] Draft data
|
|
154
158
|
# @see https://docs.sleeper.com/#get-all-drafts-for-a-league
|
|
155
159
|
def get_league_drafts(league_id)
|
|
156
|
-
make_request("/league/#{league_id}/drafts")
|
|
160
|
+
make_request("/v1/league/#{league_id}/drafts")
|
|
157
161
|
end
|
|
158
162
|
|
|
159
163
|
# Get traded draft picks for a league.
|
|
@@ -162,7 +166,7 @@ module SleeperApi
|
|
|
162
166
|
# @return [Array<Hash>] Traded picks
|
|
163
167
|
# @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
|
|
164
168
|
def get_league_traded_picks(league_id)
|
|
165
|
-
make_request("/league/#{league_id}/traded_picks")
|
|
169
|
+
make_request("/v1/league/#{league_id}/traded_picks")
|
|
166
170
|
end
|
|
167
171
|
|
|
168
172
|
# Fetch draft details.
|
|
@@ -171,7 +175,7 @@ module SleeperApi
|
|
|
171
175
|
# @return [Hash] Draft metadata
|
|
172
176
|
# @see https://docs.sleeper.com/#get-a-specific-draft
|
|
173
177
|
def get_draft(draft_id)
|
|
174
|
-
make_request("/draft/#{draft_id}")
|
|
178
|
+
make_request("/v1/draft/#{draft_id}")
|
|
175
179
|
end
|
|
176
180
|
|
|
177
181
|
# Get draft picks.
|
|
@@ -180,7 +184,7 @@ module SleeperApi
|
|
|
180
184
|
# @return [Array<Hash>] Pick data
|
|
181
185
|
# @see https://docs.sleeper.com/#get-all-picks-in-a-draft
|
|
182
186
|
def get_draft_picks(draft_id)
|
|
183
|
-
make_request("/draft/#{draft_id}/picks")
|
|
187
|
+
make_request("/v1/draft/#{draft_id}/picks")
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
# Get traded draft picks for a draft.
|
|
@@ -189,7 +193,7 @@ module SleeperApi
|
|
|
189
193
|
# @return [Array<Hash>] Traded picks
|
|
190
194
|
# @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
|
|
191
195
|
def get_draft_traded_picks(draft_id)
|
|
192
|
-
make_request("/draft/#{draft_id}/traded_picks")
|
|
196
|
+
make_request("/v1/draft/#{draft_id}/traded_picks")
|
|
193
197
|
end
|
|
194
198
|
|
|
195
199
|
# Get NFL state (week, season status).
|
|
@@ -198,13 +202,36 @@ module SleeperApi
|
|
|
198
202
|
# @return [Hash] State data
|
|
199
203
|
# @see https://docs.sleeper.com/#get-nfl-state
|
|
200
204
|
def get_nfl_state(sport = "nfl")
|
|
201
|
-
nfl_state = make_request("/state/#{sport}")
|
|
205
|
+
nfl_state = make_request("/v1/state/#{sport}")
|
|
202
206
|
nfl_state.each_with_object({}) do |(k, v), result|
|
|
203
207
|
key = k.is_a?(String) ? k.to_sym : k
|
|
204
208
|
result[key] = v
|
|
205
209
|
end
|
|
206
210
|
end
|
|
207
211
|
|
|
212
|
+
# Get a season's game schedule.
|
|
213
|
+
#
|
|
214
|
+
# Undocumented, and served from the host root rather than /v1 — hence the
|
|
215
|
+
# version living in the paths rather than in base_uri.
|
|
216
|
+
#
|
|
217
|
+
# Returns a flat array of games, each `{status, date, home, away, week,
|
|
218
|
+
# game_id}`. A team's bye week is the week it appears in no game; that
|
|
219
|
+
# derives exactly, but only within one season type — `pre` (weeks 1-3) and
|
|
220
|
+
# `post` (weeks 1-4) restart week numbering, so games from different season
|
|
221
|
+
# types must never be pooled.
|
|
222
|
+
#
|
|
223
|
+
# A season Sleeper has not scheduled yet answers 200 with an empty array
|
|
224
|
+
# rather than 404, so an empty result is a legitimate answer and not an
|
|
225
|
+
# error.
|
|
226
|
+
#
|
|
227
|
+
# @param season [Integer, String] Season year, e.g. 2026
|
|
228
|
+
# @param season_type [String] "regular" (default), "pre", or "post"
|
|
229
|
+
# @param sport [String] Sport code (default: "nfl")
|
|
230
|
+
# @return [HTTParty::Response] Array of games
|
|
231
|
+
def schedule(season, season_type: "regular", sport: "nfl")
|
|
232
|
+
make_request("/schedule/#{sport}/#{season_type}/#{season}")
|
|
233
|
+
end
|
|
234
|
+
|
|
208
235
|
# Get trending players.
|
|
209
236
|
#
|
|
210
237
|
# @param sport [String] Sport code (default: "nfl")
|
|
@@ -214,7 +241,7 @@ module SleeperApi
|
|
|
214
241
|
# @return [Array<Hash>] Trending players
|
|
215
242
|
# @see https://docs.sleeper.com/#trending-players
|
|
216
243
|
def trending_players(sport = "nfl", type: "add", lookback_hours: 24, limit: 25)
|
|
217
|
-
make_request("/players/#{sport}/trending/#{type}?lookback_hours=#{lookback_hours}&limit=#{limit}")
|
|
244
|
+
make_request("/v1/players/#{sport}/trending/#{type}?lookback_hours=#{lookback_hours}&limit=#{limit}")
|
|
218
245
|
end
|
|
219
246
|
|
|
220
247
|
# Get all player data (cached for 24 hours).
|
|
@@ -225,7 +252,7 @@ module SleeperApi
|
|
|
225
252
|
def get_players(sport = "nfl")
|
|
226
253
|
return @players_cache if @players_cache && @cache_timestamp && (Time.now - @cache_timestamp) < (3600 * 24)
|
|
227
254
|
|
|
228
|
-
response = make_request("/players/#{sport}")
|
|
255
|
+
response = make_request("/v1/players/#{sport}")
|
|
229
256
|
@players_cache = response.parsed_response
|
|
230
257
|
@cache_timestamp = Time.now
|
|
231
258
|
|
data/lib/sleeper_api/league.rb
CHANGED
|
@@ -281,7 +281,8 @@ module SleeperApi
|
|
|
281
281
|
injured_reserve: roster["reserve"] || [],
|
|
282
282
|
taxi: roster["taxi"] || [],
|
|
283
283
|
bench: (roster["players"] || []) - (roster["starters"] || []) - (roster["reserve"] || []) - (roster["taxi"] || []),
|
|
284
|
-
total_points: roster_settings
|
|
284
|
+
total_points: combined_points(roster_settings, "fpts"),
|
|
285
|
+
points_against: combined_points(roster_settings, "fpts_against"),
|
|
285
286
|
wins: roster_settings&.dig("wins"),
|
|
286
287
|
ties: roster_settings&.dig("ties"),
|
|
287
288
|
losses: roster_settings&.dig("losses"),
|
|
@@ -290,7 +291,7 @@ module SleeperApi
|
|
|
290
291
|
faab_used: roster_settings&.dig("waiver_budget_used"),
|
|
291
292
|
waiver_position: roster_settings&.dig("waiver_position"),
|
|
292
293
|
streak: roster_metadata&.dig("streak"),
|
|
293
|
-
co_owners: roster["
|
|
294
|
+
co_owners: roster["co_owners"],
|
|
294
295
|
keepers: roster["keepers"],
|
|
295
296
|
players_map: roster["player_map"],
|
|
296
297
|
players: roster["players"],
|
|
@@ -300,6 +301,21 @@ module SleeperApi
|
|
|
300
301
|
end
|
|
301
302
|
end
|
|
302
303
|
|
|
304
|
+
# Sleeper splits a score across two integer fields: fpts 1617 with
|
|
305
|
+
# fpts_decimal 78 is 1617.78. Reading fpts alone truncates every score in
|
|
306
|
+
# the league, and the loss is invisible because what remains is still a
|
|
307
|
+
# plausible number.
|
|
308
|
+
#
|
|
309
|
+
# Recombined as (whole * 100 + fraction) / 100.0 rather than
|
|
310
|
+
# whole + fraction / 100.0 — the latter accumulates two rounding steps and
|
|
311
|
+
# lands on 1617.7800000000002.
|
|
312
|
+
def combined_points(settings, key)
|
|
313
|
+
whole = settings&.dig(key)
|
|
314
|
+
return nil if whole.nil?
|
|
315
|
+
|
|
316
|
+
((whole * 100) + (settings["#{key}_decimal"] || 0)) / 100.0
|
|
317
|
+
end
|
|
318
|
+
|
|
303
319
|
def format_matchups(week)
|
|
304
320
|
week_matchups = @matchups[week] || []
|
|
305
321
|
return [] if week_matchups.empty?
|
data/lib/sleeper_api/version.rb
CHANGED