sleeper_api 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/CLAUDE.md +6 -1
- data/lib/sleeper_api/client.rb +49 -0
- 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: c1ac943353dae30f0fd6efa95f54f3287a85e116abd8408e843e980103557e29
|
|
4
|
+
data.tar.gz: 11569ff23125b08872ead08956c3395612a0092cf254a93450997fef069b2f19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cf5797fea22593f99b428469ea2b99089f9f463f170c86bf7536c9a50398996d6a2edf2f06707b2ae6e3077ccce00d28c1bfe3ee2799943708f65a56090a536d
|
|
7
|
+
data.tar.gz: '08b1850e3dfb474167c1bc8df50700cab7669c7ac77c8d83ad6d757a92968ac345a82c5ecfc05b22b5238351d448782b48d297372acc33448dce0d1f4e9f1618'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## [1.5.1] - 2026-09-25
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **A connection that never got an answer now raises `SleeperApi::Error`.**
|
|
6
|
+
Before, only non-2xx responses and timeouts did: no DNS (`SocketError`), a
|
|
7
|
+
refused, reset or unreachable connection (`SystemCallError`), a failed TLS
|
|
8
|
+
handshake (`OpenSSL::SSL::SSLError`) and a socket closed mid-response
|
|
9
|
+
(`EOFError`) escaped `make_request` as themselves. A caller rescuing
|
|
10
|
+
`SleeperApi::Error` for "Sleeper is down" missed the commonest outage of all —
|
|
11
|
+
a laptop waking before its network — and one consumer recorded 196 failed
|
|
12
|
+
imports that its retry policy never saw.
|
|
13
|
+
|
|
14
|
+
The message is `"Could not reach <path>: <original message>"`, naming the host
|
|
15
|
+
for a `WEB_HOST` call. **These are not retried** whatever `retries` is set to:
|
|
16
|
+
they fail at once, and the caller's backoff is the one that can wait long
|
|
17
|
+
enough to matter. `retries` still governs timeouts only.
|
|
18
|
+
|
|
19
|
+
## [1.5.0] - 2026-09-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`Client#scores(season, week = nil)` — games with their kickoff times.** Each
|
|
24
|
+
game carries `start_time`, epoch milliseconds, which is **the only kickoff
|
|
25
|
+
time Sleeper publishes**: `#schedule` has a `date` and nothing finer. Measured
|
|
26
|
+
2026-09-25 against ATL @ GB, the previous night's Thursday game — its
|
|
27
|
+
`start_time` is 2026-09-25T00:15Z and agrees with `metadata.date_time` on
|
|
28
|
+
every game of the week.
|
|
29
|
+
|
|
30
|
+
⚠️ **Also on `api.sleeper.com`**, like `#stats_with_context`, and with a
|
|
31
|
+
different shape: the season type is a **path segment**
|
|
32
|
+
(`/scores/nfl/regular/2026/3`), and the query-parameter form answers 200 with
|
|
33
|
+
nothing.
|
|
34
|
+
|
|
35
|
+
**Leave the week off for the whole season** — all 272 games of 2026 in one
|
|
36
|
+
~610 KB call (~93 KB gzipped), against ~57 KB a week. `metadata` is the live
|
|
37
|
+
game (`quarter`, `time_remaining`, quarter scores, `is_in_progress`) plus the
|
|
38
|
+
TV channel, spread and forecast. Its `status` vocabulary (`scheduled`,
|
|
39
|
+
`closed`) differs from the top level's (`pre_game`, `complete`), and neither
|
|
40
|
+
value a live game carries has been read yet. Nothing 404s: week 19 and a
|
|
41
|
+
garbage season type answer `[]`.
|
|
42
|
+
|
|
1
43
|
## [1.4.0] - 2026-09-14
|
|
2
44
|
|
|
3
45
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -28,7 +28,7 @@ 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
32
|
|
|
33
33
|
⚠️ **There are two Sleeper hosts and v1.4.0 added the second.** `WEB_HOST`
|
|
34
34
|
(`https://api.sleeper.com`) is the one the web app uses, and it serves
|
|
@@ -46,6 +46,11 @@ Four layers, with a deliberate split between HTTP and modeling:
|
|
|
46
46
|
default**, because `/stats/nfl/2021/16` is a real path on both and they
|
|
47
47
|
return different things; paths on `.app` quote exactly as they did before.
|
|
48
48
|
|
|
49
|
+
v1.5.0 added a second `WEB_HOST` endpoint, `#scores`, the only source of a
|
|
50
|
+
**kickoff time** — and its season type is a path segment where
|
|
51
|
+
`#stats_with_context` takes a query parameter. The host does not have one
|
|
52
|
+
convention; each path is measured on its own.
|
|
53
|
+
|
|
49
54
|
**When an endpoint almost has what you need, try the other host before
|
|
50
55
|
concluding the data does not exist.** Four epics of stats work in the
|
|
51
56
|
consuming app never surfaced the per-week team, because the documented-looking
|
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.
|
|
@@ -340,6 +347,43 @@ module SleeperApi
|
|
|
340
347
|
make_request("/stats/#{segments.join("/")}?season_type=#{query}", host: WEB_HOST)
|
|
341
348
|
end
|
|
342
349
|
|
|
350
|
+
# Get games with their kickoff times — undocumented, on the web host.
|
|
351
|
+
#
|
|
352
|
+
# **The only kickoff time Sleeper publishes.** #schedule carries a `date`
|
|
353
|
+
# and nothing finer; each game here has `start_time`, epoch milliseconds —
|
|
354
|
+
# 2026's Thursday opener of week 3, ATL @ GB, is 1790295300000, which is
|
|
355
|
+
# 2026-09-25T00:15Z and agrees with `metadata.date_time` on every game.
|
|
356
|
+
# Measured 2026-09-25, the morning after that game.
|
|
357
|
+
#
|
|
358
|
+
# `metadata` is the live game: `quarter`, `time_remaining`, the score by
|
|
359
|
+
# quarter, `is_in_progress`, `is_over`, plus the TV `channel`, the spread and
|
|
360
|
+
# a forecast. Its `status` is a different vocabulary from the top level's —
|
|
361
|
+
# `scheduled`/`closed` against `pre_game`/`complete` — and neither value a
|
|
362
|
+
# live game carries has been read here yet. Match with a fallback.
|
|
363
|
+
#
|
|
364
|
+
# **Leave the week off for the whole season** — all 272 games of 2026 in one
|
|
365
|
+
# call, ~610 KB (~93 KB gzipped), against ~57 KB a week. Unlike
|
|
366
|
+
# #stats_with_context, the season form is useful.
|
|
367
|
+
#
|
|
368
|
+
# The season type is a **path segment**, not a query parameter —
|
|
369
|
+
# `/scores/nfl/2026/3?season_type=regular` answers 200 with nothing. Shares
|
|
370
|
+
# the host's traps: nothing 404s (week 19, a garbage season type all answer
|
|
371
|
+
# `200` with `[]`), and `pre`/`post` restart week numbering.
|
|
372
|
+
#
|
|
373
|
+
# @param season [Integer, String] Season year, e.g. 2026
|
|
374
|
+
# @param week [Integer, String, nil] Week number; nil for the whole season
|
|
375
|
+
# @param season_type [String] "regular" (default), "pre", or "post"
|
|
376
|
+
# @param sport [String] Sport code (default: "nfl")
|
|
377
|
+
# @return [HTTParty::Response] Array of games, each with `game_id`, `week`,
|
|
378
|
+
# `status`, `start_time`, `date` and a nested `metadata`
|
|
379
|
+
def scores(season, week = nil, season_type: "regular", sport: "nfl")
|
|
380
|
+
segments = [sport, season_type, season, week].compact.map do |segment|
|
|
381
|
+
ERB::Util.url_encode(segment.to_s)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
make_request("/scores/#{segments.join("/")}", host: WEB_HOST)
|
|
385
|
+
end
|
|
386
|
+
|
|
343
387
|
# Get a season's game schedule.
|
|
344
388
|
#
|
|
345
389
|
# Undocumented, and served from the host root rather than /v1 — hence the
|
|
@@ -479,6 +523,11 @@ module SleeperApi
|
|
|
479
523
|
@config.logger&.error("Request timed out for #{named} after #{retries} retries")
|
|
480
524
|
raise SleeperApi::Error, "Request timed out after #{retries} retries"
|
|
481
525
|
end
|
|
526
|
+
rescue *CONNECTION_ERRORS => e
|
|
527
|
+
# No retry: these fail at once, and the caller's backoff is the one
|
|
528
|
+
# that can wait long enough to matter.
|
|
529
|
+
@config.logger&.error("Could not reach #{named}: #{e.message}")
|
|
530
|
+
raise SleeperApi::Error, "Could not reach #{named}: #{e.message}"
|
|
482
531
|
end
|
|
483
532
|
end
|
|
484
533
|
end
|
data/lib/sleeper_api/version.rb
CHANGED