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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92a31d66b4e0b110e6daf96f0689fdcc626b341bc7550ac8f1d982eaae8796bf
4
- data.tar.gz: 14b9b6d090c28a8087edb9a45d61eadeae4c8fd6027995b206456cc45c95c9f5
3
+ metadata.gz: 49f189b10709caa46c846f8733372cfd50d1dff0f66548aba5cb7ce0d32508f5
4
+ data.tar.gz: a0670835fa5f56541d041292b498722d0597a0599c724e95f9e3ed193151872c
5
5
  SHA512:
6
- metadata.gz: d24b6c42b978e3a14c70ff6c9db5c08f6747ff73c64206ccb8a7374741b56e6f034a9451a03f4d93670743414a5c30011c311a709a02743219f593e9cf8299e1
7
- data.tar.gz: 16e2f0ab8bb4fa2053cc01f0e35b4a3a6e2bde9d8a55173b8781329378323045da0c826711a33c13117ccb8e17e58b04572c34591d9e123235f21d2378ee51d9
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/v1"`. Every call funnels through the private `make_request`, which handles retry-on-timeout, logging, and converts non-2xx into `SleeperApi::Error`. It also owns the 24-hour in-memory player cache.
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 pushes to rubygems. Update `CHANGELOG.md` first.
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
- v1.0.0 is published with six bugs that are fixed in the working tree but not yet released see the unreleased section of `CHANGELOG.md`. Consumers on the published gem still hit them.
65
+ Consumers do not get a fix until it is published *and* they bump their lockfile.
@@ -15,7 +15,11 @@ module SleeperApi
15
15
  include Helpers
16
16
  include HTTParty
17
17
 
18
- base_uri "https://api.sleeper.app/v1"
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
 
@@ -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&.dig("fpts"),
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["co_owner"],
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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SleeperApi
4
- VERSION = "1.0.1"
4
+ VERSION = "1.2.0"
5
5
  end
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.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eruity1