sleeper_api 1.1.0 → 1.3.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: 8e64262b5b1ec981daba5a470cdaed3857b5b334b4b5b974b36369bb71d10fe6
4
- data.tar.gz: 6e292f1ff2de695f2385736316d1d0329373cdfb8d944d399e42ffcda7014d2d
3
+ metadata.gz: 6fd4df8d68adb46808ea692bbf8c715f2218a07a8f93212f5efddfe404741a30
4
+ data.tar.gz: cd3b09acb61407c634f04d76f165a1d2e0b7b840b3564117136ecc9343bc286c
5
5
  SHA512:
6
- metadata.gz: fcad44e534d0f99daa36897d56b228e432e5374d9e829915fed7cc71612588b9576a96147cedb07cc658cd0a590769161ccd74ef82f48ed7d1b0c2d7dc210e88
7
- data.tar.gz: ca0a00e724c6b35182add239a7a80c9b8129c3a1af2805f216f21b50971842fa968a1c902ab97686819c676390ef89e3fa0fb4635013d9f3f8f7c5aec565d7e6
6
+ metadata.gz: a63777ce6255da38e963034bf9b016ca327d85dcaefc723ad508c85d58c9674939474ee00bd55b33b007e3268a043e81f8c175f06276cb77ae80b6a619871431
7
+ data.tar.gz: aed8fa84f731ddb1e8a01ef2cfb8987305826f665f338954e990fcb8c2c946c3d33f73edf510217ecee1923412bc6470a56931dbd9c60ba7369230c2c847627c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [1.3.0] - 2026-08-27
2
+
3
+ ### Added
4
+
5
+ - `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.
6
+
7
+ 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.
8
+
9
+ Three things a caller has to know, all confirmed live:
10
+
11
+ - **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.
12
+ - **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.
13
+ - **`pre` and `post` restart week numbering at 1**, exactly as `#schedule` does, so rows from different season types must never be pooled.
14
+
15
+ `adp_dd_ppr` 1000.0 and `pos_rank_*` 999.0 are "unknown" sentinels rather than values.
16
+
17
+ 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.
18
+
19
+ ### Fixed
20
+
21
+ - 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.
22
+
23
+ ## [1.2.0] - 2026-08-23
24
+
25
+ ### Added
26
+
27
+ - `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.
28
+
29
+ ### Changed
30
+
31
+ - **`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.
32
+
33
+ **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.
34
+
1
35
  ## [1.1.0] - 2026-08-22
2
36
 
3
37
  ### 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
 
data/README.md CHANGED
@@ -2,71 +2,71 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/sleeper_api.svg)](https://badge.fury.io/rb/sleeper_api)
4
4
 
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.
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 - Users, leagues, drafts, players, matchups, transactions
9
+ - Complete API Coverage - Users, leagues, drafts, players, matchups, transactions
10
10
 
11
- - Performance Optimized - Smart caching, connection pooling, rate limiting
11
+ - Performance Optimized - Smart caching, connection pooling, rate limiting
12
12
 
13
- - Robust Error Handling - Automatic retries, timeout management, detailed error messages
13
+ - Robust Error Handling - Automatic retries, timeout management, detailed error messages
14
14
 
15
- - Well Tested - 90%+ test coverage with RSpec
15
+ - Well Tested - 90%+ test coverage with RSpec
16
16
 
17
- - Highly Configurable - Custom timeouts, retries, logging
17
+ - Highly Configurable - Custom timeouts, retries, logging
18
18
 
19
- - Production Ready - Type signatures, CI/CD, code quality tools
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 'sleeper_api'
26
+ gem 'sleeper_api'
27
27
  ```
28
28
 
29
29
  ## Quick Start
30
30
 
31
31
  ```ruby
32
- require 'sleeper_api'
32
+ require 'sleeper_api'
33
33
 
34
- # Basic usage with default configuration
35
- client = SleeperApi.client
36
- league = client.league("123456789012345678")
34
+ # Basic usage with default configuration
35
+ client = SleeperApi.client
36
+ league = client.league("123456789012345678")
37
37
 
38
- # Access league information
39
- puts league.name          # "My Fantasy League"
40
- puts league.total_rosters # 12
41
- puts league.status        # "in_season"
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
- # Get rosters
44
- rosters = league.rosters
45
- rosters.each do |roster|
46
-   puts "#{roster[:owner_display_name]}: #{roster[:wins]}-#{roster[:losses]}"
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 behavior:
52
+ Customize the gem's behavior:
53
53
 
54
54
  ```ruby
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
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
- # Configuration is applied to all subsequent client instances
62
- client = SleeperApi.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 their leagues/drafts:
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 data, rosters, matchups, and transactions:
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 traded picks:
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 caching:
240
+ Access player data with automatic caching:
241
241
 
242
242
  ```ruby
243
- # Get all players (cached for 24 hours)
244
- players = client.get_players
243
+ # Get all players (cached for 24 hours)
244
+ players = client.get_players
245
245
 
246
- # Find specific player
247
- player = client.get_player_by_id("1234")
248
- puts "#{player['first_name']} #{player['last_name']} - #{player['position']}"
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
- # Get trending players
251
- trending_adds = client.trending_players(type: "add", limit: 10)
252
- trending_drops = client.trending_players(type: "drop", limit: 10)
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 Endpoints
316
+ ### Additional Endpoints
317
317
 
318
318
  ```ruby
319
- # Get NFL state
320
- state = client.get_nfl_state
321
- puts "Current week: #{state['week']}"
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
- # Get playoff brackets
324
- winners_bracket = client.get_league_playoff_bracket("league_id")
325
- losers_bracket = client.get_league_toilet_bowl("league_id")
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
- # Get league drafts
328
- league_drafts = client.get_league_drafts("league_id")
345
+ # Get league drafts
346
+ league_drafts = client.get_league_drafts("league_id")
329
347
 
330
- # Get traded picks
331
- traded_picks = client.get_league_traded_picks("league_id")
348
+ # Get traded picks
349
+ traded_picks = client.get_league_traded_picks("league_id")
332
350
  ```
333
351
 
334
- ## Error Handling
352
+ ## Error Handling
335
353
 
336
354
  The gem provides comprehensive error handling:
337
355
 
338
356
  ```ruby
339
357
  begin
340
-   league = client.league("invalid_id")
341
-   # Process league data
342
- rescue SleeperApi::Error => e
343
-   puts "API Error: #{e.message}"
344
- rescue ArgumentError => e
345
-   puts "Invalid parameter: #{e.message}"
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 - API-related errors (404, 500, timeouts, etc.)
369
+ - SleeperApi::Error - API-related errors (404, 500, timeouts, etc.)
352
370
 
353
- - ArgumentError - Invalid parameters passed to methods
371
+ - ArgumentError - Invalid parameters passed to methods
354
372
 
355
373
  ## Performance Considerations
356
374
 
357
375
  ### Caching
358
376
 
359
- - Player data is cached for 24 hours to reduce API calls
377
+ - Player data is cached for 24 hours to reduce API calls
360
378
 
361
- - League/User/Draft data is cached per instance
379
+ - League/User/Draft data is cached per instance
362
380
 
363
- - Cache files are stored in the current working directory
381
+ - Cache files are stored in the current working directory
364
382
 
365
- ### Rate Limiting
383
+ ### Rate Limiting
366
384
 
367
- - Be mindful of Sleeper's rate limits: stay under 1000 API calls per minute
385
+ - Be mindful of Sleeper's rate limits: stay under 1000 API calls per minute
368
386
 
369
- - The gem automatically handles timeouts and retries
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 (like all players) are cached to disk
393
+ - Large datasets (like all players) are cached to disk
376
394
 
377
- - League rosters and matchups are fetched lazily
395
+ - League rosters and matchups are fetched lazily
378
396
 
379
- - Use no_data: true when initializing leagues if you don't need immediate data
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
- # Run all tests
387
- bundle exec rspec
388
- # Run with coverage
389
- bundle exec rspec --coverage
390
- # Run specific test file
391
- bundle exec rspec spec/sleeper_api/client_spec.rb
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 clone https://github.com/eruity1/sleeper_api.git
398
- cd sleeper_api
399
- bundle install
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
- # Run all checks (tests + linting)
406
- bundle exec rake ci
423
+ # Run all checks (tests + linting)
424
+ bundle exec rake ci
407
425
 
408
- # Run RuboCop
409
- bundle exec rubocop
426
+ # Run RuboCop
427
+ bundle exec rubocop
410
428
 
411
- # Auto-fix RuboCop issues
412
- bundle exec rubocop -a
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 - Testing framework
447
+ - rspec - Testing framework
430
448
 
431
- - rubocop - Code style and quality
449
+ - rubocop - Code style and quality
432
450
 
433
- - simplecov - Test coverage
451
+ - simplecov - Test coverage
434
452
 
435
- - webmock - HTTP request mocking
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 (HTTParty is bundled)
459
+ - No external dependencies (HTTParty is bundled)
442
460
 
443
461
  ## License
444
462
 
445
- The gem is available as open source under the terms of the MIT License.
463
+ The gem is available as open source under the terms of the MIT License.
446
464
 
447
465
  ## Changelog
448
466
 
449
- See CHANGELOG.md for version history and updates.
467
+ See CHANGELOG.md for version history and updates.
@@ -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.
@@ -15,7 +16,11 @@ module SleeperApi
15
16
  include Helpers
16
17
  include HTTParty
17
18
 
18
- base_uri "https://api.sleeper.app/v1"
19
+ # The host only. The API version belongs in the paths because not every
20
+ # endpoint has one: /schedule is served from the root, and while base_uri
21
+ # carried "/v1" that endpoint was unreachable at any path a caller could
22
+ # pass to make_request.
23
+ base_uri "https://api.sleeper.app"
19
24
 
20
25
  # @param config [SleeperApi::Configuration] Client configuration
21
26
  def initialize(config)
@@ -57,7 +62,7 @@ module SleeperApi
57
62
  # @return [Hash] Raw user data
58
63
  # @see https://docs.sleeper.com/#user
59
64
  def get_user(identifier)
60
- make_request("/user/#{identifier}")
65
+ make_request("/v1/user/#{identifier}")
61
66
  end
62
67
 
63
68
  # Get leagues for a user in a specific season.
@@ -68,7 +73,7 @@ module SleeperApi
68
73
  # @return [Array<Hash>] League data
69
74
  # @see https://docs.sleeper.com/#get-all-leagues-for-user
70
75
  def get_user_leagues(user_id, sport: "nfl", season: Time.now.year)
71
- make_request("/user/#{user_id}/leagues/#{sport}/#{season}")
76
+ make_request("/v1/user/#{user_id}/leagues/#{sport}/#{season}")
72
77
  end
73
78
 
74
79
  # Get drafts for a user in a specific season.
@@ -79,7 +84,7 @@ module SleeperApi
79
84
  # @return [Array<Hash>] Draft data
80
85
  # @see https://docs.sleeper.com/#get-all-drafts-for-user
81
86
  def get_user_drafts(user_id, sport: "nfl", season: Time.now.year)
82
- make_request("/user/#{user_id}/drafts/#{sport}/#{season}")
87
+ make_request("/v1/user/#{user_id}/drafts/#{sport}/#{season}")
83
88
  end
84
89
 
85
90
  # Fetch league details.
@@ -88,7 +93,7 @@ module SleeperApi
88
93
  # @return [Hash] League metadata
89
94
  # @see https://docs.sleeper.com/#get-a-specific-league
90
95
  def get_league(league_id)
91
- make_request("/league/#{league_id}")
96
+ make_request("/v1/league/#{league_id}")
92
97
  end
93
98
 
94
99
  # Get all rosters in a league.
@@ -97,7 +102,7 @@ module SleeperApi
97
102
  # @return [Array<Hash>] Roster data
98
103
  # @see https://docs.sleeper.com/#getting-rosters-in-a-league
99
104
  def get_league_rosters(league_id)
100
- make_request("/league/#{league_id}/rosters")
105
+ make_request("/v1/league/#{league_id}/rosters")
101
106
  end
102
107
 
103
108
  # Get all users in a league.
@@ -106,7 +111,7 @@ module SleeperApi
106
111
  # @return [Array<Hash>] User data
107
112
  # @see https://docs.sleeper.com/#getting-users-in-a-league
108
113
  def get_league_users(league_id)
109
- make_request("/league/#{league_id}/users")
114
+ make_request("/v1/league/#{league_id}/users")
110
115
  end
111
116
 
112
117
  # Get matchups for a specific week.
@@ -116,7 +121,7 @@ module SleeperApi
116
121
  # @return [Array<Hash>] Matchup data
117
122
  # @see https://docs.sleeper.com/#getting-matchups-in-a-league
118
123
  def get_league_matchups(league_id, week)
119
- make_request("/league/#{league_id}/matchups/#{week}")
124
+ make_request("/v1/league/#{league_id}/matchups/#{week}")
120
125
  end
121
126
 
122
127
  # Get playoff winners bracket.
@@ -125,7 +130,7 @@ module SleeperApi
125
130
  # @return [Array<Hash>] Bracket matchups
126
131
  # @see https://docs.sleeper.com/#getting-the-playoff-bracket
127
132
  def get_playoff_bracket(league_id)
128
- make_request("/league/#{league_id}/winners_bracket")
133
+ make_request("/v1/league/#{league_id}/winners_bracket")
129
134
  end
130
135
 
131
136
  # Get toilet bowl (losers bracket).
@@ -134,7 +139,7 @@ module SleeperApi
134
139
  # @return [Array<Hash>] Bracket matchups
135
140
  # @see https://docs.sleeper.com/#getting-the-playoff-bracket
136
141
  def get_toilet_bowl(league_id)
137
- make_request("/league/#{league_id}/losers_bracket")
142
+ make_request("/v1/league/#{league_id}/losers_bracket")
138
143
  end
139
144
 
140
145
  # Get transactions for a specific week.
@@ -144,7 +149,7 @@ module SleeperApi
144
149
  # @return [Array<Hash>] Transaction data
145
150
  # @see https://docs.sleeper.com/#get-transactions
146
151
  def get_transactions(league_id, week)
147
- make_request("/league/#{league_id}/transactions/#{week}")
152
+ make_request("/v1/league/#{league_id}/transactions/#{week}")
148
153
  end
149
154
 
150
155
  # Get league drafts.
@@ -153,7 +158,7 @@ module SleeperApi
153
158
  # @return [Array<Hash>] Draft data
154
159
  # @see https://docs.sleeper.com/#get-all-drafts-for-a-league
155
160
  def get_league_drafts(league_id)
156
- make_request("/league/#{league_id}/drafts")
161
+ make_request("/v1/league/#{league_id}/drafts")
157
162
  end
158
163
 
159
164
  # Get traded draft picks for a league.
@@ -162,7 +167,7 @@ module SleeperApi
162
167
  # @return [Array<Hash>] Traded picks
163
168
  # @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
164
169
  def get_league_traded_picks(league_id)
165
- make_request("/league/#{league_id}/traded_picks")
170
+ make_request("/v1/league/#{league_id}/traded_picks")
166
171
  end
167
172
 
168
173
  # Fetch draft details.
@@ -171,7 +176,7 @@ module SleeperApi
171
176
  # @return [Hash] Draft metadata
172
177
  # @see https://docs.sleeper.com/#get-a-specific-draft
173
178
  def get_draft(draft_id)
174
- make_request("/draft/#{draft_id}")
179
+ make_request("/v1/draft/#{draft_id}")
175
180
  end
176
181
 
177
182
  # Get draft picks.
@@ -180,7 +185,7 @@ module SleeperApi
180
185
  # @return [Array<Hash>] Pick data
181
186
  # @see https://docs.sleeper.com/#get-all-picks-in-a-draft
182
187
  def get_draft_picks(draft_id)
183
- make_request("/draft/#{draft_id}/picks")
188
+ make_request("/v1/draft/#{draft_id}/picks")
184
189
  end
185
190
 
186
191
  # Get traded draft picks for a draft.
@@ -189,7 +194,7 @@ module SleeperApi
189
194
  # @return [Array<Hash>] Traded picks
190
195
  # @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
191
196
  def get_draft_traded_picks(draft_id)
192
- make_request("/draft/#{draft_id}/traded_picks")
197
+ make_request("/v1/draft/#{draft_id}/traded_picks")
193
198
  end
194
199
 
195
200
  # Get NFL state (week, season status).
@@ -198,13 +203,84 @@ module SleeperApi
198
203
  # @return [Hash] State data
199
204
  # @see https://docs.sleeper.com/#get-nfl-state
200
205
  def get_nfl_state(sport = "nfl")
201
- nfl_state = make_request("/state/#{sport}")
206
+ nfl_state = make_request("/v1/state/#{sport}")
202
207
  nfl_state.each_with_object({}) do |(k, v), result|
203
208
  key = k.is_a?(String) ? k.to_sym : k
204
209
  result[key] = v
205
210
  end
206
211
  end
207
212
 
213
+ # Get per-player statistics for one week, or for a whole season.
214
+ #
215
+ # Undocumented. Returns an object keyed by player id — plus `TEAM_XXX` keys
216
+ # for team-level rows — each holding raw counting stats (`rec`, `rush_yd`,
217
+ # `off_snp`, `rec_rz_tgt`…) alongside Sleeper's three canned point totals
218
+ # `pts_ppr` / `pts_half_ppr` / `pts_std`. 228 distinct fields were observed
219
+ # across one week of 2025.
220
+ #
221
+ # **Nothing here 404s.** An unplayed week, a week out of range, and an
222
+ # unrecognised season type all answer 200 with `{}`, so an empty result is
223
+ # a legitimate answer and is indistinguishable from a typo. Validate the
224
+ # arguments before you trust an empty body.
225
+ #
226
+ # Omitting `week` requests season totals, which is a different resource at
227
+ # a shorter path rather than a default of week 1.
228
+ #
229
+ # `pre` and `post` restart week numbering at 1, exactly as #schedule does,
230
+ # so rows from different season types must never be pooled.
231
+ #
232
+ # @param season [Integer, String] Season year, e.g. 2025
233
+ # @param week [Integer, String, nil] Week number, or nil for season totals
234
+ # @param season_type [String] "regular" (default), "pre", or "post"
235
+ # @param sport [String] Sport code (default: "nfl")
236
+ # @return [HTTParty::Response] Player id to stats mapping
237
+ def stats(season, week: nil, season_type: "regular", sport: "nfl")
238
+ make_request(weekly_path("stats", sport, season_type, season, week))
239
+ end
240
+
241
+ # Get per-player projections for one week, or for a whole season.
242
+ #
243
+ # Same shape and same caveats as #stats, with one of its own: for a season
244
+ # Sleeper has not projected, this still returns a full set of entries —
245
+ # 9,386 of them for 2030 — every one holding only `{"adp_dd_ppr" => 1000.0}`
246
+ # and no `pts_ppr` at all. **A row count is not evidence of a projection.**
247
+ # Filter on the field you actually want.
248
+ #
249
+ # `adp_dd_ppr` 1000.0 and `pos_rank_*` 999.0 are "unknown" sentinels rather
250
+ # than values.
251
+ #
252
+ # @param season [Integer, String] Season year, e.g. 2026
253
+ # @param week [Integer, String, nil] Week number, or nil for season totals
254
+ # @param season_type [String] "regular" (default), "pre", or "post"
255
+ # @param sport [String] Sport code (default: "nfl")
256
+ # @return [HTTParty::Response] Player id to projections mapping
257
+ def projections(season, week: nil, season_type: "regular", sport: "nfl")
258
+ make_request(weekly_path("projections", sport, season_type, season, week))
259
+ end
260
+
261
+ # Get a season's game schedule.
262
+ #
263
+ # Undocumented, and served from the host root rather than /v1 — hence the
264
+ # version living in the paths rather than in base_uri.
265
+ #
266
+ # Returns a flat array of games, each `{status, date, home, away, week,
267
+ # game_id}`. A team's bye week is the week it appears in no game; that
268
+ # derives exactly, but only within one season type — `pre` (weeks 1-3) and
269
+ # `post` (weeks 1-4) restart week numbering, so games from different season
270
+ # types must never be pooled.
271
+ #
272
+ # A season Sleeper has not scheduled yet answers 200 with an empty array
273
+ # rather than 404, so an empty result is a legitimate answer and not an
274
+ # error.
275
+ #
276
+ # @param season [Integer, String] Season year, e.g. 2026
277
+ # @param season_type [String] "regular" (default), "pre", or "post"
278
+ # @param sport [String] Sport code (default: "nfl")
279
+ # @return [HTTParty::Response] Array of games
280
+ def schedule(season, season_type: "regular", sport: "nfl")
281
+ make_request("/schedule/#{sport}/#{season_type}/#{season}")
282
+ end
283
+
208
284
  # Get trending players.
209
285
  #
210
286
  # @param sport [String] Sport code (default: "nfl")
@@ -214,7 +290,7 @@ module SleeperApi
214
290
  # @return [Array<Hash>] Trending players
215
291
  # @see https://docs.sleeper.com/#trending-players
216
292
  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}")
293
+ make_request("/v1/players/#{sport}/trending/#{type}?lookback_hours=#{lookback_hours}&limit=#{limit}")
218
294
  end
219
295
 
220
296
  # Get all player data (cached for 24 hours).
@@ -225,7 +301,7 @@ module SleeperApi
225
301
  def get_players(sport = "nfl")
226
302
  return @players_cache if @players_cache && @cache_timestamp && (Time.now - @cache_timestamp) < (3600 * 24)
227
303
 
228
- response = make_request("/players/#{sport}")
304
+ response = make_request("/v1/players/#{sport}")
229
305
  @players_cache = response.parsed_response
230
306
  @cache_timestamp = Time.now
231
307
 
@@ -244,6 +320,20 @@ module SleeperApi
244
320
 
245
321
  private
246
322
 
323
+ # Path for #stats and #projections. Segments are escaped because they are
324
+ # interpolated into a URI path: an unescaped one can walk out of the
325
+ # endpoint entirely, which is the bug the consuming app had to work around
326
+ # for #get_user.
327
+ #
328
+ # A nil week drops the segment rather than defaulting, because the shorter
329
+ # path is season totals.
330
+ def weekly_path(resource, sport, season_type, season, week)
331
+ segments = [resource, sport, season_type, season, week].compact
332
+ escaped = segments.map { |segment| ERB::Util.url_encode(segment.to_s) }
333
+
334
+ "/v1/#{escaped.join("/")}"
335
+ end
336
+
247
337
  # Make an HTTP request with retry logic and logging.
248
338
  #
249
339
  # @param path [String] API endpoint path
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SleeperApi
4
- VERSION = "1.1.0"
4
+ VERSION = "1.3.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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eruity1