sleeper_api 1.3.1 → 1.4.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: 3bd436fd0363716d6908e51d9137e89725230afb2c8a71479582a8894f12db57
4
- data.tar.gz: b5933518c66d46edfc540ac261c0d473f62fc6c93f6cc03c9d7e426edebc9ed3
3
+ metadata.gz: c89b9383f35e517efd9ce353f9c14b1463f37375cc7dc4c30310c5c57370cdab
4
+ data.tar.gz: 336f7a25653bdbad75ec3094d150389fb5abba4244ef80ffbe97ce702e6a7ce9
5
5
  SHA512:
6
- metadata.gz: 7bdf8104296937896fdf11dcd71d12008e5801d9197552827293d734dd360032ac99dacaa5d7ff656e34ba2938f206bf8f864385d20b6f30230e12d819dc2039
7
- data.tar.gz: 12eb88e92e6f27af1734b059b24313248ccf1888e6354437b391439920d0e3d2c5c00e263d7a957ad67cc791503978a92786b67b12eb6e871152a1b274e814c3
6
+ metadata.gz: 49c0c6f2843e597c53cf8c8c548a348f7c03255e6a38306a8e43190523f2ad485125145e9dc844b4f6b1ea326b74286e8a26cffa34c8c8522caeb70822477f0b
7
+ data.tar.gz: 45c2ebd6bc9b2e9ef3cd4f5f53c6635cd00f8199f9f5da7d0061d740c99f27aa94a1894ac682d072fb542e5c0498996ba799d1ee8c094689281188befdce33f3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ ## [1.4.0] - 2026-09-14
2
+
3
+ ### Added
4
+
5
+ - **`Client#stats_with_context(season, week)` — one week's stat lines carrying
6
+ the week they were played in.** Each row has the player's team *that week*,
7
+ their `opponent`, the `game_id` and the `date`, which the existing `#stats`
8
+ endpoint does not return at all.
9
+
10
+ ⚠️ **It is served from a different host: `api.sleeper.com`, not
11
+ `api.sleeper.app`.** Same sport and season in the path, no `/v1`, and the
12
+ season type is a query parameter rather than a segment. The `.app` endpoint
13
+ answers the *same stat lines with none of this metadata*, which is how it sat
14
+ unfound through four epics of stats work in the consuming app.
15
+
16
+ **What is genuinely per-week, measured** over the 2,275 players present in
17
+ both week 1 and week 16 of 2021: `team` differs on **164** of them and
18
+ `opponent` on **2,262**. ⚠️ **The nested `player` object is not history** — it
19
+ is the current catalog record stapled on, with `team` null inside it and
20
+ `injury_status` differing on 1 of the 204 rows carrying one across those
21
+ fifteen weeks. A 2021 row reporting "Questionable" is reporting that he is
22
+ questionable *now*.
23
+
24
+ ⚠️ **Pass a week.** Dropping it answers season totals — 8,251 rows for 2021 —
25
+ with `week` and `opponent` null on every one, so the season form carries none
26
+ of the context the endpoint exists for. Shares `#stats`' traps otherwise:
27
+ nothing 404s, and `pre`/`post` restart week numbering. An array of rows, not
28
+ a hash keyed by player id.
29
+
30
+ ### Changed
31
+
32
+ - **`make_request` takes an optional `host:`**, which is how the second host is
33
+ reached. ⚠️ **Passing an absolute URL instead does not work**: HTTParty 0.24
34
+ raises `UnsafeURIError` for any URL whose host differs from the configured
35
+ `base_uri` — "this request could send credentials to an unintended server" —
36
+ so a second host has to arrive as its own per-request `base_uri`.
37
+
38
+ **Errors and logs now name the host whenever it is not the default.**
39
+ `/stats/nfl/2021/16` is a real path on both hosts and they return different
40
+ things, so a message quoting the path alone cannot say which one failed.
41
+ Messages for paths on `api.sleeper.app` are byte-for-byte what they were.
42
+
1
43
  ## [1.3.1] - 2026-09-10
2
44
 
3
45
  ### Fixed
data/CLAUDE.md CHANGED
@@ -29,6 +29,27 @@ Four layers, with a deliberate split between HTTP and modeling:
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
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.
32
+
33
+ ⚠️ **There are two Sleeper hosts and v1.4.0 added the second.** `WEB_HOST`
34
+ (`https://api.sleeper.com`) is the one the web app uses, and it serves
35
+ **richer rows from same-looking paths**: `#stats_with_context` returns the
36
+ player's team *that week* and their opponent, while `#stats` on `.app`
37
+ returns the same stat lines with neither. Reached with `make_request(path,
38
+ host: WEB_HOST)` rather than by moving `base_uri`, because every other
39
+ endpoint lives on `.app`.
40
+
41
+ **Passing an absolute URL instead does not work**, and the failure is not
42
+ subtle: HTTParty 0.24 raises `UnsafeURIError` for any URL whose host differs
43
+ from the configured `base_uri` — *"this request could send credentials to an
44
+ unintended server"* — so a second host must arrive as its own per-request
45
+ `base_uri`. **The error and the log name the host whenever it is not the
46
+ default**, because `/stats/nfl/2021/16` is a real path on both and they
47
+ return different things; paths on `.app` quote exactly as they did before.
48
+
49
+ **When an endpoint almost has what you need, try the other host before
50
+ concluding the data does not exist.** Four epics of stats work in the
51
+ consuming app never surfaced the per-week team, because the documented-looking
52
+ path on `.app` answers 200 with a payload that simply omits it.
32
53
  - **`League` / `User` / `Draft`** — resource objects. Each takes `(id, client)`, fetches eagerly in the constructor, memoizes into ivars, and exposes formatted hashes.
33
54
  - **`Helpers`** — mixed into all four. `deep_symbolize_keys` plus `player_details`, which reaches through `@client` — so any class including it must define `@client`.
34
55
 
@@ -22,6 +22,12 @@ module SleeperApi
22
22
  # pass to make_request.
23
23
  base_uri "https://api.sleeper.app"
24
24
 
25
+ # ⚠️ **The other Sleeper host.** The web app talks to `api.sleeper.com`,
26
+ # which serves *richer rows from same-looking paths* — see
27
+ # #stats_with_context. Reached per-request rather than by moving base_uri,
28
+ # because every other endpoint here lives on `.app`.
29
+ WEB_HOST = "https://api.sleeper.com".freeze
30
+
25
31
  # Not HTTParty's own. Its JSON branch passes `quirks_mode`, which json 3.0
26
32
  # removed, so without this every response raises ArgumentError the moment a
27
33
  # consumer resolves json 3. See JsonParser.
@@ -285,6 +291,55 @@ module SleeperApi
285
291
  make_request(weekly_path("projections", sport, season_type, season, week))
286
292
  end
287
293
 
294
+ # Get one week's stat lines **with the context of the week they were played
295
+ # in** — the player's team *that week*, their opponent, the game and its
296
+ # date. Probed 2026-09-08, re-probed 2026-09-14.
297
+ #
298
+ # ⚠️ **A different host: `api.sleeper.com`, not `api.sleeper.app`.** The
299
+ # `.app` endpoint #stats calls returns the *same stat lines with none of
300
+ # this metadata*, which is why it can be there for years without anyone
301
+ # finding it. Same sport and season in the path, but no `/v1` and the
302
+ # season type is a query parameter rather than a segment.
303
+ #
304
+ # **What is genuinely per-week, measured** over the 2,275 players present in
305
+ # both week 1 and week 16 of 2021: `team` differs on **164** of them (a
306
+ # midseason trade is a real change of team) and `opponent` on **2,262**. So
307
+ # both are history rather than today's catalog.
308
+ #
309
+ # ⚠️ **The nested `player` object is NOT history.** It is the current
310
+ # catalog record stapled on — `player.team` is null inside it, and
311
+ # `injury_status` differed on 1 of the 204 rows carrying one across fifteen
312
+ # weeks, which is the signature of a field that does not vary by week at
313
+ # all. A 2021 row reporting "Questionable" is reporting that he is
314
+ # questionable *now*. The top-level `status` is null on every row seen.
315
+ #
316
+ # ⚠️ **Pass a week.** Dropping it answers season totals — 8,251 rows for
317
+ # 2021 — with `week` and `opponent` null on every one, so the season form
318
+ # carries none of the context this endpoint exists for.
319
+ #
320
+ # Shares #stats' traps: nothing 404s (an unplayed 2026 week, a week out of
321
+ # range, a season before Sleeper's history and a garbage season type all
322
+ # answer `200` with `[]`), and `pre`/`post` restart week numbering — 2021
323
+ # `post` week 1 is the wildcard round, played 2022-01-15.
324
+ #
325
+ # **An array, not a hash.** #stats keys by player id; this returns a list of
326
+ # rows each carrying `player_id`, so a caller indexing it must build its own
327
+ # map. `/projections/nfl/{season}/{week}` on the same host answers the same
328
+ # shape (9,420 rows for 2021 week 16) and has no wrapper here yet.
329
+ #
330
+ # @param season [Integer, String] Season year, e.g. 2021
331
+ # @param week [Integer, String] Week number — required, see above
332
+ # @param season_type [String] "regular" (default), "pre", or "post"
333
+ # @param sport [String] Sport code (default: "nfl")
334
+ # @return [HTTParty::Response] Array of stat lines, each with `team`,
335
+ # `opponent`, `game_id`, `date`, `week`, `season` and a nested `player`
336
+ def stats_with_context(season, week, season_type: "regular", sport: "nfl")
337
+ segments = [sport, season, week].map { |segment| ERB::Util.url_encode(segment.to_s) }
338
+ query = ERB::Util.url_encode(season_type.to_s)
339
+
340
+ make_request("/stats/#{segments.join("/")}?season_type=#{query}", host: WEB_HOST)
341
+ end
342
+
288
343
  # Get a season's game schedule.
289
344
  #
290
345
  # Undocumented, and served from the host root rather than /v1 — hence the
@@ -383,29 +438,45 @@ module SleeperApi
383
438
 
384
439
  # Make an HTTP request with retry logic and logging.
385
440
  #
441
+ # ⚠️ **`host:` cannot be done by passing an absolute URL instead.** HTTParty
442
+ # 0.24 raises `UnsafeURIError` for any URL whose host differs from the
443
+ # configured `base_uri` — "this request could send credentials to an
444
+ # unintended server" — so a second host has to arrive as its own
445
+ # `base_uri`, which is a per-request option it supports.
446
+ #
447
+ # **The error and the log name the host whenever it is not the default.**
448
+ # `/stats/nfl/2021/16` is a real path on both hosts and they return
449
+ # different things, so a message quoting the path alone cannot say which
450
+ # one failed. Paths on the default host quote exactly as they always have.
451
+ #
386
452
  # @param path [String] API endpoint path
453
+ # @param host [String, nil] a different host, e.g. WEB_HOST
387
454
  # @return [HTTParty::Response]
388
455
  # @raise [SleeperApi::Error] On HTTP errors or timeouts
389
- def make_request(path)
390
- @config.logger&.info("Making request to #{self.class.base_uri}#{path}")
456
+ def make_request(path, host: nil)
457
+ named = host ? "#{host}#{path}" : path
458
+ options = { timeout: @config.timeout }
459
+ options[:base_uri] = host if host
460
+
461
+ @config.logger&.info("Making request to #{host || self.class.base_uri}#{path}")
391
462
  retries = 0
392
463
  begin
393
- response = self.class.get(path, timeout: @config.timeout)
464
+ response = self.class.get(path, **options)
394
465
  if response.success?
395
- @config.logger&.info("Successful response for #{path}")
466
+ @config.logger&.info("Successful response for #{named}")
396
467
  response
397
468
  else
398
- @config.logger&.error("Failed to fetch #{path}: #{response.code}")
399
- raise SleeperApi::Error, "Failed to fetch #{path}: #{response.code}"
469
+ @config.logger&.error("Failed to fetch #{named}: #{response.code}")
470
+ raise SleeperApi::Error, "Failed to fetch #{named}: #{response.code}"
400
471
  end
401
472
  rescue Net::OpenTimeout, Net::ReadTimeout => e
402
473
  retries += 1
403
474
  if retries <= @config.retries
404
- @config.logger&.warn("Retrying #{path} (attempt #{retries}/#{@config.retries}) due to #{e}")
475
+ @config.logger&.warn("Retrying #{named} (attempt #{retries}/#{@config.retries}) due to #{e}")
405
476
  sleep(1)
406
477
  retry
407
478
  else
408
- @config.logger&.error("Request timed out for #{path} after #{retries} retries")
479
+ @config.logger&.error("Request timed out for #{named} after #{retries} retries")
409
480
  raise SleeperApi::Error, "Request timed out after #{retries} retries"
410
481
  end
411
482
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SleeperApi
4
- VERSION = "1.3.1"
4
+ VERSION = "1.4.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.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eruity1