sleeper_api 1.0.0 → 1.1.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: 7343fb5de26fd02795df2ef4c91322eec909026cb792e52fcb24299c83cff901
4
- data.tar.gz: 5036b7dfa94981abe61d3204e6a1d265164e846961c1450eb92b03614a942ec9
3
+ metadata.gz: 8e64262b5b1ec981daba5a470cdaed3857b5b334b4b5b974b36369bb71d10fe6
4
+ data.tar.gz: 6e292f1ff2de695f2385736316d1d0329373cdfb8d944d399e42ffcda7014d2d
5
5
  SHA512:
6
- metadata.gz: ca60a9cfad9925c86afd87b1f2e219405dad22d80849130ee9b70c7b71321903e9cc27cb539ed9feb4b900b73b42cfcd6c0a284e3b8009b9578a900f7888f052
7
- data.tar.gz: 5559a1bffa0cbe150be4aff3789cc4ecc298e568c9c5e3f518293921efd0965b50ed6c440a56c98683ec62fed12707af16f51cef30c849ead3b34e76beeca8c2
6
+ metadata.gz: fcad44e534d0f99daa36897d56b228e432e5374d9e829915fed7cc71612588b9576a96147cedb07cc658cd0a590769161ccd74ef82f48ed7d1b0c2d7dc210e88
7
+ data.tar.gz: ca0a00e724c6b35182add239a7a80c9b8129c3a1af2805f216f21b50971842fa968a1c902ab97686819c676390ef89e3fa0fb4635013d9f3f8f7c5aec565d7e6
@@ -12,7 +12,7 @@ jobs:
12
12
 
13
13
  strategy:
14
14
  matrix:
15
- ruby-version: [3.2]
15
+ ruby-version: ["3.2", "3.4"]
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## [1.1.0] - 2026-08-22
2
+
3
+ ### Fixed
4
+
5
+ - `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.
6
+ - `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 }`).
7
+
8
+ ### Added
9
+
10
+ - `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.
11
+
12
+ ## [1.0.1] - 2026-08-12
13
+
14
+ ### Fixed
15
+
16
+ - `User#leagues` returned `nil` on every call after the first, because the memoization guard was the method's return value
17
+ - `League#users` raised `NoMethodError` when any league member had no `metadata` (Sleeper omits it for users who never set a team name), which also broke `#playoff_bracket` and `#toilet_bowl`
18
+ - `Client#make_request` raised `NameError: undefined local variable 'config'` when a retry fired with a logger configured — the retry path was only reachable with logging on, so it never surfaced in tests
19
+ - `League#matchups_by_week` returned a literal `nil` element for any roster with no `matchup_id` (bye weeks / unscheduled rosters)
20
+ - `League#matchups_by_week` raised `NoMethodError` when a matchup roster had no `players` or no `points`
21
+ - `League#users` memoization checked an undefined `@fetch_users` ivar instead of `@league_users`
22
+
23
+ ### Changed
24
+
25
+ - SimpleCov now starts before the library is required. It previously started after, so no lines were instrumented and the `minimum_coverage 90` gate passed vacuously on 0/0 lines. Real coverage at the time was 72%; `lib/` is now at 100%.
26
+
1
27
  ## [1.0.0] - 2025-01-17
2
28
 
3
29
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,63 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ `sleeper_api` — a published Ruby gem (rubygems.org, v1.0.0) wrapping [Sleeper's fantasy football API](https://docs.sleeper.com/). Read-only public API: no auth, no API key, no write endpoints.
8
+
9
+ Consumed by the `fantasy-football-manager` project in the sibling directory, which is the primary real-world user.
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ bin/setup # install deps
15
+ bundle exec rake ci # rubocop + rspec — this is what CI runs
16
+ bundle exec rspec
17
+ bundle exec rspec spec/sleeper_api/league_spec.rb:42 # single example
18
+ bundle exec rubocop -A # autocorrect
19
+ bin/console # IRB with the gem loaded
20
+ ```
21
+
22
+ CI (`.github/workflows/ci.yml`) runs `bundle exec rake ci` on Ruby 3.2 only. The gemspec claims `required_ruby_version >= 2.6.0` and RuboCop targets 2.6, but nothing tests below 3.2 — treat 2.6 compatibility as unverified.
23
+
24
+ ## Architecture
25
+
26
+ Four layers, with a deliberate split between HTTP and modeling:
27
+
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.
30
+ - **`League` / `User` / `Draft`** — resource objects. Each takes `(id, client)`, fetches eagerly in the constructor, memoizes into ivars, and exposes formatted hashes.
31
+ - **`Helpers`** — mixed into all four. `deep_symbolize_keys` plus `player_details`, which reaches through `@client` — so any class including it must define `@client`.
32
+
33
+ ### Conventions that matter
34
+
35
+ **Raw data is string-keyed; formatted output is symbol-keyed.** The `ATTRIBUTES` readers (defined via `define_method` on each class) return raw API values straight off the string-keyed hash. The `format_*` private methods return symbol-keyed hashes. Don't mix the two — inside `format_rosters` you're indexing `roster["starters"]` but returning `starters:`.
36
+
37
+ **`Client` returns `HTTParty::Response`, not Hash.** `make_request` returns the response object, which delegates `[]` and `dig` to `parsed_response`, so it usually behaves like a Hash. `get_players` is the exception — it explicitly stores `.parsed_response`. If you add code that needs a real Hash (e.g. `.merge`, `.transform_keys`), call `.parsed_response` yourself; `fetch_playoff_bracket` already does.
38
+
39
+ **Eager construction.** `League.new` and `User.new` hit the network in the constructor. `League.new(id, client, no_data: true)` skips it — used by `User#rosters` to avoid N redundant league fetches. Be aware `no_data: true` leaves `@league_data` nil, so `format_rosters`' `remaining_faab` falls back to `0 - waiver_budget_used`.
40
+
41
+ **Guard nil defensively.** Sleeper omits fields freely — users without `metadata`, rosters without `players`, matchups without `points`. Real leagues hit these; the spec fixtures often don't. Use `&.dig` rather than `[]` on anything nested off an API response.
42
+
43
+ **Never fan out `get_players`.** It's a multi-megabyte payload of every NFL player. It's cached for 24h in an ivar on the client instance — which means the cache dies with the client. Consumers holding a short-lived client re-download it every time.
44
+
45
+ ## Testing
46
+
47
+ RSpec + WebMock, with `WebMock.disable_net_connect!` — **specs must never hit the network**. Stub with `stub_request` (see `client_spec.rb`) or `instance_double(SleeperApi::Client)` for resource-object specs (see `league_spec.rb`).
48
+
49
+ `spec_helper.rb` resets `SleeperApi`'s `@client` and `@configuration` ivars before each example, since both are module-level memoized state that would otherwise leak between tests.
50
+
51
+ **SimpleCov must start before `require "sleeper_api"`.** It previously started after, which meant zero lines were instrumented and `minimum_coverage 90` passed vacuously on 0/0. Don't reorder those requires back — it silently disables the gate rather than failing loudly.
52
+
53
+ `lib/` is at 100% line coverage. Every bug found in this gem so far has been in a `League#format_*` method handling a field Sleeper omitted, exercised only by real data. When adding a formatter, write the nil-field case first.
54
+
55
+ ## Release
56
+
57
+ 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.
58
+
59
+ **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`).
60
+
61
+ **`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.
62
+
63
+ Consumers do not get a fix until it is published *and* they bump their lockfile.
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # SleeperApi
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/sleeper_api.svg)](https://badge.fury.io/rb/sleeper_api)
4
+
3
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.
4
6
 
5
7
  ## Features
@@ -264,7 +264,7 @@ module SleeperApi
264
264
  rescue Net::OpenTimeout, Net::ReadTimeout => e
265
265
  retries += 1
266
266
  if retries <= @config.retries
267
- @config.logger&.warn("Retrying #{path} (attempt #{retries}/#{config.retries}) due to #{e}")
267
+ @config.logger&.warn("Retrying #{path} (attempt #{retries}/#{@config.retries}) due to #{e}")
268
268
  sleep(1)
269
269
  retry
270
270
  else
@@ -134,7 +134,7 @@ module SleeperApi
134
134
  # puts "#{user[:display_name]} #{role}"
135
135
  # end
136
136
  def users
137
- fetch_users unless @fetch_users
137
+ fetch_users unless @league_users
138
138
  format_users
139
139
  end
140
140
 
@@ -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?
@@ -310,14 +326,14 @@ module SleeperApi
310
326
  {
311
327
  matchup_id: matchup_id,
312
328
  rosters: matchup_entries.map do |roster|
313
- starters = roster["starters"]
329
+ starters = roster["starters"] || []
314
330
 
315
- bench = roster["players"] - starters
331
+ bench = (roster["players"] || []) - starters
316
332
  {
317
333
  roster_id: roster["roster_id"],
318
334
  points: roster["points"],
319
335
  custom_points: roster["custom_points"],
320
- total_points: roster["points"] + (roster["custom_points"] || 0),
336
+ total_points: (roster["points"] || 0) + (roster["custom_points"] || 0),
321
337
  starters: starters,
322
338
  bench: bench,
323
339
  starter_points: (starters || []).map do |starter_id|
@@ -329,7 +345,7 @@ module SleeperApi
329
345
  }
330
346
  end
331
347
  }
332
- end
348
+ end.compact
333
349
  end
334
350
 
335
351
  def format_users
@@ -342,7 +358,7 @@ module SleeperApi
342
358
  username: user["username"],
343
359
  display_name: user["display_name"],
344
360
  avatar_id: user["avatar"],
345
- team_name: user_metadata["team_name"],
361
+ team_name: user_metadata&.dig("team_name"),
346
362
  commissioner: user["is_owner"],
347
363
  is_bot: user["is_bot"],
348
364
  metadata: user_metadata.is_a?(Hash) ? user_metadata.transform_keys(&:to_sym) : user_metadata,
@@ -50,7 +50,8 @@ module SleeperApi
50
50
  def leagues(season = Time.now.year)
51
51
  raise ArgumentError, "season must be a valid year" unless season.is_a?(Integer)
52
52
 
53
- fetch_leagues(season) unless @leagues
53
+ fetch_leagues(season)
54
+ @leagues
54
55
  end
55
56
 
56
57
  # Get all rosters for this user across all leagues in a season.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SleeperApi
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sleeper_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eruity1
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-09-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: httparty
@@ -138,6 +137,7 @@ files:
138
137
  - ".rubocop.yml"
139
138
  - ".rubocop_todo.yml"
140
139
  - CHANGELOG.md
140
+ - CLAUDE.md
141
141
  - Gemfile
142
142
  - LICENSE.txt
143
143
  - README.md
@@ -162,7 +162,6 @@ metadata:
162
162
  source_code_uri: https://github.com/eruity1/sleeper_api
163
163
  changelog_uri: https://github.com/eruity1/sleeper_api/blob/main/CHANGELOG.md
164
164
  documentation_uri: https://github.com/eruity1/sleeper_api/blob/main/README.md
165
- post_install_message:
166
165
  rdoc_options: []
167
166
  require_paths:
168
167
  - lib
@@ -177,8 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
176
  - !ruby/object:Gem::Version
178
177
  version: '0'
179
178
  requirements: []
180
- rubygems_version: 3.4.10
181
- signing_key:
179
+ rubygems_version: 3.6.7
182
180
  specification_version: 4
183
181
  summary: Comprehensive Ruby wrapper for the Sleeper's fantasy sports API
184
182
  test_files: []