sleeper_api 1.5.0 → 1.6.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 +58 -0
- data/CLAUDE.md +3 -1
- data/README.md +7 -7
- data/lib/sleeper_api/client.rb +50 -30
- data/lib/sleeper_api/draft.rb +9 -2
- data/lib/sleeper_api/league.rb +30 -30
- data/lib/sleeper_api/user.rb +14 -13
- data/lib/sleeper_api/version.rb +1 -1
- data/lib/sleeper_api.rb +4 -0
- 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: 0375caa5f944f2f083cdc8cec7672f44fc9219969e8e733bde441c377e16ede4
|
|
4
|
+
data.tar.gz: a1d54df90f2ff3ba533d11756993513eca6bae884f1a1fc0e57e51d9be84fa12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba5338528abf3a7c924dd9fdfd7dbf6440a1ac75debc3426cca4d580520a65ce7986075c2d8a6709305a5e57720c08a9b0749710e83b46381628b2991bab702b
|
|
7
|
+
data.tar.gz: 84ff6c0503398ca629009b1976dfd2ead6a9594d63eabdf97fb0056ed29d3b4965e8f2a4d17091c1e6d02560c1b28f469afeffeacaccc36acb7b394c3415dc2a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
## [1.6.0] - 2026-09-28
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **`SleeperApi::UserNotFound`, a subclass of `SleeperApi::Error`.** Sleeper
|
|
6
|
+
answers an unknown user with `200` and a literal `null` body — checked live
|
|
7
|
+
2026-09-28 — so there was no error at all: `get_user` returned a response
|
|
8
|
+
whose `parsed_response` was nil. It now raises `UserNotFound`, and so does
|
|
9
|
+
`User.new` for a payload with no `user_id`. A caller can tell "no such user"
|
|
10
|
+
(permanent) from an outage without matching message strings; rescuing
|
|
11
|
+
`SleeperApi::Error` alone still sees both.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`League#matchups_by_week` returns rosters on a bye** instead of dropping
|
|
16
|
+
them. Each comes back unpaired as `{ matchup_id: nil, rosters: [roster] }`,
|
|
17
|
+
never grouped with another bye. A bye used to look exactly like a missing
|
|
18
|
+
team.
|
|
19
|
+
- **`get_user` raises for an unknown user** rather than returning a nil-bodied
|
|
20
|
+
response (see Added).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`User#leagues`, `#rosters` and `#drafts` answer for the season asked.** The
|
|
25
|
+
memo was not keyed by season, so `leagues(2025)` then `leagues(2024)` on one
|
|
26
|
+
instance returned 2025's leagues — silently wrong rows for a multi-season
|
|
27
|
+
caller.
|
|
28
|
+
- **Every value interpolated into a request path is escaped.** `get_user("a b")`
|
|
29
|
+
raised `URI::InvalidURIError`, which is not a `SleeperApi::Error`, and
|
|
30
|
+
`get_user("../../league/123")` requested `/league/123`. Every `Client` path is
|
|
31
|
+
now built from escaped segments.
|
|
32
|
+
- **Week 18 is reachable.** `matchups_by_week` and `transactions` accepted
|
|
33
|
+
weeks 1–17 and Sleeper's regular season runs to 18; `#matchups` now fetches
|
|
34
|
+
all eighteen.
|
|
35
|
+
- **`Draft#summary` no longer raises on a pick with no metadata**, and a pick
|
|
36
|
+
with one name part is not padded with a space.
|
|
37
|
+
- **The README** named `get_league_playoff_bracket` / `get_league_toilet_bowl`
|
|
38
|
+
(the methods are `get_playoff_bracket` / `get_toilet_bowl`) and described a
|
|
39
|
+
disk cache that does not exist.
|
|
40
|
+
|
|
41
|
+
## [1.5.1] - 2026-09-25
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **A connection that never got an answer now raises `SleeperApi::Error`.**
|
|
46
|
+
Before, only non-2xx responses and timeouts did: no DNS (`SocketError`), a
|
|
47
|
+
refused, reset or unreachable connection (`SystemCallError`), a failed TLS
|
|
48
|
+
handshake (`OpenSSL::SSL::SSLError`) and a socket closed mid-response
|
|
49
|
+
(`EOFError`) escaped `make_request` as themselves. A caller rescuing
|
|
50
|
+
`SleeperApi::Error` for "Sleeper is down" missed the commonest outage of all —
|
|
51
|
+
a laptop waking before its network — and one consumer recorded 196 failed
|
|
52
|
+
imports that its retry policy never saw.
|
|
53
|
+
|
|
54
|
+
The message is `"Could not reach <path>: <original message>"`, naming the host
|
|
55
|
+
for a `WEB_HOST` call. **These are not retried** whatever `retries` is set to:
|
|
56
|
+
they fail at once, and the caller's backoff is the one that can wait long
|
|
57
|
+
enough to matter. `retries` still governs timeouts only.
|
|
58
|
+
|
|
1
59
|
## [1.5.0] - 2026-09-25
|
|
2
60
|
|
|
3
61
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -28,7 +28,9 @@ Four layers, with a deliberate split between HTTP and modeling:
|
|
|
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
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
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.
|
|
31
|
+
Every call funnels through the private `make_request`, which handles retry-on-timeout, logging, and converts non-2xx into `SleeperApi::Error` — and, since 1.5.1, **every connection failure too** (`CONNECTION_ERRORS`: `SocketError`, `SystemCallError`, `OpenSSL::SSL::SSLError`, `EOFError`), unretried, as `"Could not reach <path>: …"`. A consumer can rescue `SleeperApi::Error` alone and see every way a call fails. **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.
|
|
32
|
+
|
|
33
|
+
⚠️ **Never interpolate a value into a request path; build it with the private `path(*segments)`**, which escapes each segment. An unescaped one raised `URI::InvalidURIError` — not a `SleeperApi::Error`, so it escaped every rescue — or walked out of the endpoint: `../../league/123` as a username requested `/league/123` (G.6, v1.6.0). An unknown user is `200` with a `null` body, which `get_user` turns into `SleeperApi::UserNotFound`.
|
|
32
34
|
|
|
33
35
|
⚠️ **There are two Sleeper hosts and v1.4.0 added the second.** `WEB_HOST`
|
|
34
36
|
(`https://api.sleeper.com`) is the one the web app uses, and it serves
|
data/README.md
CHANGED
|
@@ -339,8 +339,8 @@ season_stats = client.stats(2025)
|
|
|
339
339
|
real = projected.parsed_response.select { |_id, row| row.key?("pts_ppr") }
|
|
340
340
|
|
|
341
341
|
# Get playoff brackets
|
|
342
|
-
winners_bracket = client.
|
|
343
|
-
losers_bracket = client.
|
|
342
|
+
winners_bracket = client.get_playoff_bracket("league_id")
|
|
343
|
+
losers_bracket = client.get_toilet_bowl("league_id")
|
|
344
344
|
|
|
345
345
|
# Get league drafts
|
|
346
346
|
league_drafts = client.get_league_drafts("league_id")
|
|
@@ -366,7 +366,9 @@ end
|
|
|
366
366
|
|
|
367
367
|
### Error Types
|
|
368
368
|
|
|
369
|
-
- SleeperApi::Error - API-related errors (404, 500, timeouts,
|
|
369
|
+
- SleeperApi::Error - API-related errors (404, 500, timeouts, an unreachable host)
|
|
370
|
+
|
|
371
|
+
- SleeperApi::UserNotFound - a subclass of SleeperApi::Error: no Sleeper user has that identifier. Permanent, so rescue it before SleeperApi::Error if you retry the rest
|
|
370
372
|
|
|
371
373
|
- ArgumentError - Invalid parameters passed to methods
|
|
372
374
|
|
|
@@ -374,12 +376,10 @@ end
|
|
|
374
376
|
|
|
375
377
|
### Caching
|
|
376
378
|
|
|
377
|
-
- Player data is cached for 24 hours
|
|
379
|
+
- Player data is cached in memory for 24 hours, on the client instance — a new client downloads it again
|
|
378
380
|
|
|
379
381
|
- League/User/Draft data is cached per instance
|
|
380
382
|
|
|
381
|
-
- Cache files are stored in the current working directory
|
|
382
|
-
|
|
383
383
|
### Rate Limiting
|
|
384
384
|
|
|
385
385
|
- Be mindful of Sleeper's rate limits: stay under 1000 API calls per minute
|
|
@@ -390,7 +390,7 @@ end
|
|
|
390
390
|
|
|
391
391
|
### Memory Usage
|
|
392
392
|
|
|
393
|
-
-
|
|
393
|
+
- The all-players payload is several megabytes and held in memory; never fan it out
|
|
394
394
|
|
|
395
395
|
- League rosters and matchups are fetched lazily
|
|
396
396
|
|
data/lib/sleeper_api/client.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "httparty"
|
|
2
|
+
require "openssl"
|
|
2
3
|
require "json"
|
|
3
4
|
require "erb"
|
|
4
5
|
|
|
@@ -28,6 +29,12 @@ module SleeperApi
|
|
|
28
29
|
# because every other endpoint here lives on `.app`.
|
|
29
30
|
WEB_HOST = "https://api.sleeper.com".freeze
|
|
30
31
|
|
|
32
|
+
# The request never got an answer (1.5.1): no DNS, a refused or reset
|
|
33
|
+
# connection, a failed TLS handshake, a socket closed mid-response. Before,
|
|
34
|
+
# these escaped as themselves, so a caller rescuing SleeperApi::Error for
|
|
35
|
+
# an outage missed the commonest one. Timeouts are handled separately.
|
|
36
|
+
CONNECTION_ERRORS = [SocketError, SystemCallError, OpenSSL::SSL::SSLError, EOFError].freeze
|
|
37
|
+
|
|
31
38
|
# Not HTTParty's own. Its JSON branch passes `quirks_mode`, which json 3.0
|
|
32
39
|
# removed, so without this every response raises ArgumentError the moment a
|
|
33
40
|
# consumer resolves json 3. See JsonParser.
|
|
@@ -69,11 +76,18 @@ module SleeperApi
|
|
|
69
76
|
|
|
70
77
|
# Fetch user data by identifier.
|
|
71
78
|
#
|
|
79
|
+
# Sleeper answers an unknown user with 200 and a literal `null` body, not
|
|
80
|
+
# a 404, so that is turned into {SleeperApi::UserNotFound} here.
|
|
81
|
+
#
|
|
72
82
|
# @param identifier [String] Username or user ID
|
|
73
83
|
# @return [Hash] Raw user data
|
|
84
|
+
# @raise [SleeperApi::UserNotFound] If no user has that identifier
|
|
74
85
|
# @see https://docs.sleeper.com/#user
|
|
75
86
|
def get_user(identifier)
|
|
76
|
-
make_request("
|
|
87
|
+
response = make_request(path("v1", "user", identifier))
|
|
88
|
+
raise UserNotFound, "No Sleeper user #{identifier.to_s.inspect}" if response.parsed_response.nil?
|
|
89
|
+
|
|
90
|
+
response
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
# Get leagues for a user in a specific season.
|
|
@@ -84,7 +98,7 @@ module SleeperApi
|
|
|
84
98
|
# @return [Array<Hash>] League data
|
|
85
99
|
# @see https://docs.sleeper.com/#get-all-leagues-for-user
|
|
86
100
|
def get_user_leagues(user_id, sport: "nfl", season: Time.now.year)
|
|
87
|
-
make_request("
|
|
101
|
+
make_request(path("v1", "user", user_id, "leagues", sport, season))
|
|
88
102
|
end
|
|
89
103
|
|
|
90
104
|
# Get drafts for a user in a specific season.
|
|
@@ -95,7 +109,7 @@ module SleeperApi
|
|
|
95
109
|
# @return [Array<Hash>] Draft data
|
|
96
110
|
# @see https://docs.sleeper.com/#get-all-drafts-for-user
|
|
97
111
|
def get_user_drafts(user_id, sport: "nfl", season: Time.now.year)
|
|
98
|
-
make_request("
|
|
112
|
+
make_request(path("v1", "user", user_id, "drafts", sport, season))
|
|
99
113
|
end
|
|
100
114
|
|
|
101
115
|
# Fetch league details.
|
|
@@ -104,7 +118,7 @@ module SleeperApi
|
|
|
104
118
|
# @return [Hash] League metadata
|
|
105
119
|
# @see https://docs.sleeper.com/#get-a-specific-league
|
|
106
120
|
def get_league(league_id)
|
|
107
|
-
make_request("
|
|
121
|
+
make_request(path("v1", "league", league_id))
|
|
108
122
|
end
|
|
109
123
|
|
|
110
124
|
# Get all rosters in a league.
|
|
@@ -113,7 +127,7 @@ module SleeperApi
|
|
|
113
127
|
# @return [Array<Hash>] Roster data
|
|
114
128
|
# @see https://docs.sleeper.com/#getting-rosters-in-a-league
|
|
115
129
|
def get_league_rosters(league_id)
|
|
116
|
-
make_request("
|
|
130
|
+
make_request(path("v1", "league", league_id, "rosters"))
|
|
117
131
|
end
|
|
118
132
|
|
|
119
133
|
# Get all users in a league.
|
|
@@ -122,7 +136,7 @@ module SleeperApi
|
|
|
122
136
|
# @return [Array<Hash>] User data
|
|
123
137
|
# @see https://docs.sleeper.com/#getting-users-in-a-league
|
|
124
138
|
def get_league_users(league_id)
|
|
125
|
-
make_request("
|
|
139
|
+
make_request(path("v1", "league", league_id, "users"))
|
|
126
140
|
end
|
|
127
141
|
|
|
128
142
|
# Get matchups for a specific week.
|
|
@@ -132,7 +146,7 @@ module SleeperApi
|
|
|
132
146
|
# @return [Array<Hash>] Matchup data
|
|
133
147
|
# @see https://docs.sleeper.com/#getting-matchups-in-a-league
|
|
134
148
|
def get_league_matchups(league_id, week)
|
|
135
|
-
make_request("
|
|
149
|
+
make_request(path("v1", "league", league_id, "matchups", week))
|
|
136
150
|
end
|
|
137
151
|
|
|
138
152
|
# Get playoff winners bracket.
|
|
@@ -141,7 +155,7 @@ module SleeperApi
|
|
|
141
155
|
# @return [Array<Hash>] Bracket matchups
|
|
142
156
|
# @see https://docs.sleeper.com/#getting-the-playoff-bracket
|
|
143
157
|
def get_playoff_bracket(league_id)
|
|
144
|
-
make_request("
|
|
158
|
+
make_request(path("v1", "league", league_id, "winners_bracket"))
|
|
145
159
|
end
|
|
146
160
|
|
|
147
161
|
# Get toilet bowl (losers bracket).
|
|
@@ -150,7 +164,7 @@ module SleeperApi
|
|
|
150
164
|
# @return [Array<Hash>] Bracket matchups
|
|
151
165
|
# @see https://docs.sleeper.com/#getting-the-playoff-bracket
|
|
152
166
|
def get_toilet_bowl(league_id)
|
|
153
|
-
make_request("
|
|
167
|
+
make_request(path("v1", "league", league_id, "losers_bracket"))
|
|
154
168
|
end
|
|
155
169
|
|
|
156
170
|
# Get transactions for a specific week.
|
|
@@ -160,7 +174,7 @@ module SleeperApi
|
|
|
160
174
|
# @return [Array<Hash>] Transaction data
|
|
161
175
|
# @see https://docs.sleeper.com/#get-transactions
|
|
162
176
|
def get_transactions(league_id, week)
|
|
163
|
-
make_request("
|
|
177
|
+
make_request(path("v1", "league", league_id, "transactions", week))
|
|
164
178
|
end
|
|
165
179
|
|
|
166
180
|
# Get league drafts.
|
|
@@ -169,7 +183,7 @@ module SleeperApi
|
|
|
169
183
|
# @return [Array<Hash>] Draft data
|
|
170
184
|
# @see https://docs.sleeper.com/#get-all-drafts-for-a-league
|
|
171
185
|
def get_league_drafts(league_id)
|
|
172
|
-
make_request("
|
|
186
|
+
make_request(path("v1", "league", league_id, "drafts"))
|
|
173
187
|
end
|
|
174
188
|
|
|
175
189
|
# Get traded draft picks for a league.
|
|
@@ -178,7 +192,7 @@ module SleeperApi
|
|
|
178
192
|
# @return [Array<Hash>] Traded picks
|
|
179
193
|
# @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
|
|
180
194
|
def get_league_traded_picks(league_id)
|
|
181
|
-
make_request("
|
|
195
|
+
make_request(path("v1", "league", league_id, "traded_picks"))
|
|
182
196
|
end
|
|
183
197
|
|
|
184
198
|
# Fetch draft details.
|
|
@@ -187,7 +201,7 @@ module SleeperApi
|
|
|
187
201
|
# @return [Hash] Draft metadata
|
|
188
202
|
# @see https://docs.sleeper.com/#get-a-specific-draft
|
|
189
203
|
def get_draft(draft_id)
|
|
190
|
-
make_request("
|
|
204
|
+
make_request(path("v1", "draft", draft_id))
|
|
191
205
|
end
|
|
192
206
|
|
|
193
207
|
# Get draft picks.
|
|
@@ -196,7 +210,7 @@ module SleeperApi
|
|
|
196
210
|
# @return [Array<Hash>] Pick data
|
|
197
211
|
# @see https://docs.sleeper.com/#get-all-picks-in-a-draft
|
|
198
212
|
def get_draft_picks(draft_id)
|
|
199
|
-
make_request("
|
|
213
|
+
make_request(path("v1", "draft", draft_id, "picks"))
|
|
200
214
|
end
|
|
201
215
|
|
|
202
216
|
# Get traded draft picks for a draft.
|
|
@@ -205,7 +219,7 @@ module SleeperApi
|
|
|
205
219
|
# @return [Array<Hash>] Traded picks
|
|
206
220
|
# @see https://docs.sleeper.com/#get-traded-picks-in-a-draft
|
|
207
221
|
def get_draft_traded_picks(draft_id)
|
|
208
|
-
make_request("
|
|
222
|
+
make_request(path("v1", "draft", draft_id, "traded_picks"))
|
|
209
223
|
end
|
|
210
224
|
|
|
211
225
|
# Get NFL state (week, season status).
|
|
@@ -214,7 +228,7 @@ module SleeperApi
|
|
|
214
228
|
# @return [Hash] State data
|
|
215
229
|
# @see https://docs.sleeper.com/#get-nfl-state
|
|
216
230
|
def get_nfl_state(sport = "nfl")
|
|
217
|
-
nfl_state = make_request("
|
|
231
|
+
nfl_state = make_request(path("v1", "state", sport))
|
|
218
232
|
nfl_state.each_with_object({}) do |(k, v), result|
|
|
219
233
|
key = k.is_a?(String) ? k.to_sym : k
|
|
220
234
|
result[key] = v
|
|
@@ -417,7 +431,7 @@ module SleeperApi
|
|
|
417
431
|
# @param sport [String] Sport code (default: "nfl")
|
|
418
432
|
# @return [HTTParty::Response] Array of games
|
|
419
433
|
def schedule(season, season_type: "regular", sport: "nfl")
|
|
420
|
-
make_request("
|
|
434
|
+
make_request(path("schedule", sport, season_type, season))
|
|
421
435
|
end
|
|
422
436
|
|
|
423
437
|
# Get trending players.
|
|
@@ -429,7 +443,8 @@ module SleeperApi
|
|
|
429
443
|
# @return [Array<Hash>] Trending players
|
|
430
444
|
# @see https://docs.sleeper.com/#trending-players
|
|
431
445
|
def trending_players(sport = "nfl", type: "add", lookback_hours: 24, limit: 25)
|
|
432
|
-
|
|
446
|
+
query = URI.encode_www_form(lookback_hours: lookback_hours, limit: limit)
|
|
447
|
+
make_request("#{path("v1", "players", sport, "trending", type)}?#{query}")
|
|
433
448
|
end
|
|
434
449
|
|
|
435
450
|
# Get all player data (cached for 24 hours).
|
|
@@ -440,7 +455,7 @@ module SleeperApi
|
|
|
440
455
|
def get_players(sport = "nfl")
|
|
441
456
|
return @players_cache if @players_cache && @cache_timestamp && (Time.now - @cache_timestamp) < (3600 * 24)
|
|
442
457
|
|
|
443
|
-
response = make_request("
|
|
458
|
+
response = make_request(path("v1", "players", sport))
|
|
444
459
|
@players_cache = response.parsed_response
|
|
445
460
|
@cache_timestamp = Time.now
|
|
446
461
|
|
|
@@ -459,18 +474,18 @@ module SleeperApi
|
|
|
459
474
|
|
|
460
475
|
private
|
|
461
476
|
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def weekly_path(resource, sport, season_type, season, week)
|
|
470
|
-
segments = [resource, sport, season_type, season, week].compact
|
|
471
|
-
escaped = segments.map { |segment| ERB::Util.url_encode(segment.to_s) }
|
|
477
|
+
# Every request path is built here, from escaped segments. An unescaped
|
|
478
|
+
# one raised URI::InvalidURIError, which is not a SleeperApi::Error, or
|
|
479
|
+
# walked out of the endpoint entirely: `../../league/123` as a username
|
|
480
|
+
# requested /league/123.
|
|
481
|
+
def path(*segments)
|
|
482
|
+
"/#{segments.map { |segment| ERB::Util.url_encode(segment.to_s) }.join("/")}"
|
|
483
|
+
end
|
|
472
484
|
|
|
473
|
-
|
|
485
|
+
# Path for #stats and #projections. A nil week drops the segment rather
|
|
486
|
+
# than defaulting, because the shorter path is season totals.
|
|
487
|
+
def weekly_path(resource, sport, season_type, season, week)
|
|
488
|
+
path("v1", *[resource, sport, season_type, season, week].compact)
|
|
474
489
|
end
|
|
475
490
|
|
|
476
491
|
# Make an HTTP request with retry logic and logging.
|
|
@@ -516,6 +531,11 @@ module SleeperApi
|
|
|
516
531
|
@config.logger&.error("Request timed out for #{named} after #{retries} retries")
|
|
517
532
|
raise SleeperApi::Error, "Request timed out after #{retries} retries"
|
|
518
533
|
end
|
|
534
|
+
rescue *CONNECTION_ERRORS => e
|
|
535
|
+
# No retry: these fail at once, and the caller's backoff is the one
|
|
536
|
+
# that can wait long enough to matter.
|
|
537
|
+
@config.logger&.error("Could not reach #{named}: #{e.message}")
|
|
538
|
+
raise SleeperApi::Error, "Could not reach #{named}: #{e.message}"
|
|
519
539
|
end
|
|
520
540
|
end
|
|
521
541
|
end
|
data/lib/sleeper_api/draft.rb
CHANGED
|
@@ -109,8 +109,8 @@ module SleeperApi
|
|
|
109
109
|
top_picks: picks.select { |pick| pick[:round] == 1 }.map do |pick|
|
|
110
110
|
{
|
|
111
111
|
player_id: pick[:player_id],
|
|
112
|
-
player_name:
|
|
113
|
-
position: pick[:metadata]
|
|
112
|
+
player_name: pick_player_name(pick),
|
|
113
|
+
position: pick[:metadata]&.dig(:position),
|
|
114
114
|
picked_by: pick[:picked_by]
|
|
115
115
|
}
|
|
116
116
|
end
|
|
@@ -135,6 +135,13 @@ module SleeperApi
|
|
|
135
135
|
|
|
136
136
|
private
|
|
137
137
|
|
|
138
|
+
# A pick can arrive with null metadata, or a team defense with only a
|
|
139
|
+
# last name, so the name is built from whatever parts exist.
|
|
140
|
+
def pick_player_name(pick)
|
|
141
|
+
parts = [pick[:metadata]&.dig(:first_name), pick[:metadata]&.dig(:last_name)].compact
|
|
142
|
+
parts.empty? ? nil : parts.join(" ")
|
|
143
|
+
end
|
|
144
|
+
|
|
138
145
|
def fetch_draft_data
|
|
139
146
|
@draft_data ||= @client.get_draft(@draft_id)
|
|
140
147
|
end
|
data/lib/sleeper_api/league.rb
CHANGED
|
@@ -22,7 +22,7 @@ module SleeperApi
|
|
|
22
22
|
|
|
23
23
|
@league_id = league_id
|
|
24
24
|
@client = client
|
|
25
|
-
@weeks = 1..
|
|
25
|
+
@weeks = 1..18
|
|
26
26
|
@league_data = nil
|
|
27
27
|
@league_rosters = nil
|
|
28
28
|
@league_users = nil
|
|
@@ -105,7 +105,7 @@ module SleeperApi
|
|
|
105
105
|
|
|
106
106
|
# Get formatted matchups for a specific week.
|
|
107
107
|
#
|
|
108
|
-
# @param week [Integer] Week number (1-
|
|
108
|
+
# @param week [Integer] Week number (1-18)
|
|
109
109
|
# @return [Array<Hash>] Matchup data with scoring breakdown
|
|
110
110
|
# @raise [ArgumentError] If week is invalid
|
|
111
111
|
#
|
|
@@ -117,7 +117,7 @@ module SleeperApi
|
|
|
117
117
|
# end
|
|
118
118
|
# end
|
|
119
119
|
def matchups_by_week(week: nil)
|
|
120
|
-
raise ArgumentError, "Week must be between
|
|
120
|
+
raise ArgumentError, "Week must be between #{@weeks.min} and #{@weeks.max}" unless @weeks.include?(week)
|
|
121
121
|
|
|
122
122
|
fetch_matchups([week]) unless @matchups&.key?(week)
|
|
123
123
|
format_matchups(week)
|
|
@@ -140,7 +140,7 @@ module SleeperApi
|
|
|
140
140
|
|
|
141
141
|
# Get formatted transactions for a specific week.
|
|
142
142
|
#
|
|
143
|
-
# @param week [Integer] Week number (1-
|
|
143
|
+
# @param week [Integer] Week number (1-18)
|
|
144
144
|
# @return [Array<Hash>] Transaction data with adds/drops and draft picks
|
|
145
145
|
# @raise [ArgumentError] If week is invalid
|
|
146
146
|
#
|
|
@@ -150,7 +150,7 @@ module SleeperApi
|
|
|
150
150
|
# puts "#{tx[:type]}: #{tx[:adds]&.length || 0} adds, #{tx[:drops]&.length || 0} drops"
|
|
151
151
|
# end
|
|
152
152
|
def transactions(week: nil)
|
|
153
|
-
raise ArgumentError, "Week must be between
|
|
153
|
+
raise ArgumentError, "Week must be between #{@weeks.min} and #{@weeks.max}" unless @weeks.include?(week)
|
|
154
154
|
|
|
155
155
|
fetch_transactions([week]) unless @transactions&.key?(week)
|
|
156
156
|
format_transactions(week)
|
|
@@ -320,32 +320,32 @@ module SleeperApi
|
|
|
320
320
|
week_matchups = @matchups[week] || []
|
|
321
321
|
return [] if week_matchups.empty?
|
|
322
322
|
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
# A roster on a bye has a null matchup_id. Each one is its own unpaired
|
|
324
|
+
# entry: grouping the nils together would pair two teams that never met.
|
|
325
|
+
paired, byes = week_matchups.partition { |match| match["matchup_id"] }
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
end.compact
|
|
327
|
+
paired.group_by { |match| match["matchup_id"] }.map do |matchup_id, entries|
|
|
328
|
+
{ matchup_id: matchup_id, rosters: entries.map { |roster| format_matchup_roster(roster) } }
|
|
329
|
+
end + byes.map { |roster| { matchup_id: nil, rosters: [format_matchup_roster(roster)] } }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def format_matchup_roster(roster)
|
|
333
|
+
starters = roster["starters"] || []
|
|
334
|
+
bench = (roster["players"] || []) - starters
|
|
335
|
+
{
|
|
336
|
+
roster_id: roster["roster_id"],
|
|
337
|
+
points: roster["points"],
|
|
338
|
+
custom_points: roster["custom_points"],
|
|
339
|
+
total_points: (roster["points"] || 0) + (roster["custom_points"] || 0),
|
|
340
|
+
starters: starters,
|
|
341
|
+
bench: bench,
|
|
342
|
+
starter_points: player_points(roster, starters),
|
|
343
|
+
bench_points: player_points(roster, bench)
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def player_points(roster, player_ids)
|
|
348
|
+
player_ids.map { |player_id| { player_id => roster["players_points"]&.dig(player_id) || 0 } }
|
|
349
349
|
end
|
|
350
350
|
|
|
351
351
|
def format_users
|
data/lib/sleeper_api/user.rb
CHANGED
|
@@ -14,18 +14,20 @@ module SleeperApi
|
|
|
14
14
|
# @param identifier [String, Integer] Username or user ID
|
|
15
15
|
# @param client [SleeperApi::Client] HTTP client instance
|
|
16
16
|
# @raise [ArgumentError] If identifier is empty
|
|
17
|
-
# @raise [SleeperApi::
|
|
17
|
+
# @raise [SleeperApi::UserNotFound] If no user has that identifier
|
|
18
18
|
def initialize(identifier, client)
|
|
19
19
|
raise ArgumentError, "identifier must be a non-empty string" if identifier.to_s.empty?
|
|
20
20
|
|
|
21
21
|
@identifier = identifier
|
|
22
22
|
@client = client
|
|
23
23
|
@user_data = nil
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# Keyed by season: one instance is asked about several seasons, and a
|
|
25
|
+
# single memo answered every later season with the first one's data.
|
|
26
|
+
@leagues = {}
|
|
27
|
+
@drafts = {}
|
|
26
28
|
|
|
27
29
|
fetch_user_data
|
|
28
|
-
@user_id = @user_data["user_id"] || raise(SleeperApi::
|
|
30
|
+
@user_id = @user_data["user_id"] || raise(SleeperApi::UserNotFound, "Invalid user data: user_id not found")
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# Dynamically define attribute readers for user data.
|
|
@@ -51,7 +53,6 @@ module SleeperApi
|
|
|
51
53
|
raise ArgumentError, "season must be a valid year" unless season.is_a?(Integer)
|
|
52
54
|
|
|
53
55
|
fetch_leagues(season)
|
|
54
|
-
@leagues
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
# Get all rosters for this user across all leagues in a season.
|
|
@@ -62,8 +63,7 @@ module SleeperApi
|
|
|
62
63
|
def rosters(season = Time.now.year)
|
|
63
64
|
raise ArgumentError, "season must be a valid year" unless season.is_a?(Integer)
|
|
64
65
|
|
|
65
|
-
fetch_leagues(season)
|
|
66
|
-
(@leagues || []).flat_map do |league|
|
|
66
|
+
fetch_leagues(season).flat_map do |league|
|
|
67
67
|
league_instance = League.new(league[:league_id], @client, no_data: true)
|
|
68
68
|
league_instance.rosters(user_id: @user_id)
|
|
69
69
|
end
|
|
@@ -77,8 +77,7 @@ module SleeperApi
|
|
|
77
77
|
def drafts(season = Time.now.year)
|
|
78
78
|
raise ArgumentError, "season must be a valid year" unless season.is_a?(Integer)
|
|
79
79
|
|
|
80
|
-
fetch_drafts(season)
|
|
81
|
-
format_drafts
|
|
80
|
+
format_drafts(fetch_drafts(season))
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
# Get a summary of user's league participation.
|
|
@@ -115,17 +114,19 @@ module SleeperApi
|
|
|
115
114
|
end
|
|
116
115
|
|
|
117
116
|
def fetch_leagues(season)
|
|
118
|
-
@leagues ||= (@client.get_user_leagues(@user_id, season: season) || []).map do |league|
|
|
117
|
+
@leagues[season] ||= (@client.get_user_leagues(@user_id, season: season) || []).map do |league|
|
|
119
118
|
deep_symbolize_keys(league)
|
|
120
119
|
end
|
|
121
120
|
end
|
|
122
121
|
|
|
123
122
|
def fetch_drafts(season)
|
|
124
|
-
@drafts ||= (@client.get_user_drafts(@user_id, season: season) || []).map
|
|
123
|
+
@drafts[season] ||= (@client.get_user_drafts(@user_id, season: season) || []).map do |draft|
|
|
124
|
+
deep_symbolize_keys(draft)
|
|
125
|
+
end
|
|
125
126
|
end
|
|
126
127
|
|
|
127
|
-
def format_drafts
|
|
128
|
-
|
|
128
|
+
def format_drafts(drafts)
|
|
129
|
+
drafts.map do |draft|
|
|
129
130
|
draft.merge(
|
|
130
131
|
league_name: draft.dig(:metadata, :name),
|
|
131
132
|
scoring_type: draft.dig(:metadata, :scoring_type),
|
data/lib/sleeper_api/version.rb
CHANGED
data/lib/sleeper_api.rb
CHANGED
|
@@ -9,6 +9,10 @@ require_relative "sleeper_api/draft"
|
|
|
9
9
|
module SleeperApi
|
|
10
10
|
class Error < StandardError; end
|
|
11
11
|
|
|
12
|
+
# The identifier names no Sleeper user. Permanent, unlike the rest of
|
|
13
|
+
# Error — retrying will not help — so a caller can rescue it on its own.
|
|
14
|
+
class UserNotFound < Error; end
|
|
15
|
+
|
|
12
16
|
class << self
|
|
13
17
|
attr_writer :configuration
|
|
14
18
|
end
|