nb_api_client 0.3.1 → 1.0.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: f412b933b609da683b5885317338a0d6c09a86ae8614207a2eebd27a4aea6450
4
- data.tar.gz: 90778567fd7a94e88a3daf97b50320d8919b9436afb4ed0abb9f85146a4791f3
3
+ metadata.gz: c8a34771eec831f76ebbcab4057adab19e3ed2f0f18a7ea1fefdf4f6cb9c75c8
4
+ data.tar.gz: d9c17c3d66fe7250dcb06c267530feda1255841509724b6ac061137c1584ff47
5
5
  SHA512:
6
- metadata.gz: c53d7b630005c8ab3819ccc85194a865296fc1bb38707c36dc06d474aaa55536d350717428a25d2ef78f599777ba9db37710426a621801110567fbffac217978
7
- data.tar.gz: 3ee2477602ef28d7ca26d4432e1694ec33f148e2458c2476a42872c0f63c0cc62f1e11f3e16a2b5199485ee2b80e055df3bdae2ee0221e6c832fbd80b6b454e5
6
+ metadata.gz: e2215188c41fa8820c2b6372cb94e29efabbdd98717b7806b107d8e0e28820daa137118a9de1dfab8e0ee2bb0e5fe4a2a848a9a2a3e56f98cab34a001ce0fff2
7
+ data.tar.gz: 957fafb9f78368eeff8e0a42e9b01b8e1f5c0cfc8d7112bbcfb2247a62f4fad70632536c290d0f5af147740cccc48dba5165a64a00d430dc06d430cd3d9f7f71
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.0.0] - 2026-07-16
8
+
9
+ - **Breaking:** Removed the `oauth2` gem dependency. `NbApiClient::Request` now
10
+ raises `NbApiClient::Request::OAuthError` (a plain `StandardError` carrying
11
+ the failed `response`) instead of `OAuth2::Error` on token-refresh failure
12
+ or an unrecognized JSON error response. `OAuth2::Error` was only ever used
13
+ as a generic error wrapper here — this gem never used `OAuth2::Client` or
14
+ `OAuth2::AccessToken`. Update any `rescue OAuth2::Error` (or exception
15
+ allow-lists, e.g. Sidekiq retry/discard lists) around calls into this gem
16
+ to `rescue NbApiClient::Request::OAuthError` instead.
17
+
7
18
  ## [0.3.1] - 2026-07-16
8
19
 
9
20
  - Updated `UrlBuilder` to build nation's URL based on it's slug, instead of requirng a separate `url` argument.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # NbApiClient
1
+ # NationBuilder API Client
2
2
 
3
3
  A rate-limited, token-refreshing HTTP client for the NationBuilder API, for use in Rails apps that integrate with NationBuilder on behalf of many OAuth-connected accounts ("nations").
4
4
 
@@ -25,24 +25,59 @@ gem "nb_api_client", path: "gems/nb_api_client"
25
25
 
26
26
  ```ruby
27
27
  NbApiClient::Request.call(nation, :get, "/api/v2/signups/123")
28
- NbApiClient::Request.call(nation, :post, "/api/v2/signups", "data": {"type": "signups", "attributes": {{"email": "email@example.com"}}})
28
+ NbApiClient::Request.call(nation, :post, "/api/v2/signups", {
29
+ "data": {
30
+ "type": "signups", "attributes": {"email": "email@example.com"}
31
+ }
32
+ })
29
33
  ```
30
34
 
31
35
  ### The `nation` interface
32
36
 
33
37
  `nation` can be any object — it does not need to be an ActiveRecord model — that responds to:
34
38
 
35
- | Method | Returns |
36
- | --------------------- | ------------------------------------------------------------------------------------------------------ |
39
+ | Method | Returns |
40
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
41
  | `slug` | A string uniquely identifying the account (used as a cache-key/log prefix, and to build the account's base URL, e.g. `"myorg"` becomes `"https://myorg.nationbuilder.com"`). |
38
- | `active?` | Whether requests should be allowed (checked before every call, except `/oauth/token`). |
39
- | `token` | The current OAuth access token, appended as a query param on every request. |
40
- | `refresh_oauth_token` | Refreshes the OAuth token in place; returns truthy on success, falsy on failure. |
41
- | `deauthorize` | Called after too many consecutive "unauthorized" responses (see `unauthorized_error_threshold` below). |
42
+ | `active?` | Whether requests should be allowed (checked before every call, except `/oauth/token`). |
43
+ | `token` | The current OAuth access token, appended as a query param on every request. |
44
+ | `refresh_oauth_token` | Refreshes the OAuth token in place; returns truthy on success, falsy on failure. |
45
+ | `deauthorize` | Called after too many consecutive "unauthorized" responses (see `unauthorized_error_threshold` below). |
46
+
47
+ A typical `refresh_oauth_token`, using the [`oauth2`](https://github.com/oauth-xx/oauth2) gem (a separate dependency of your app, not of this gem) and an ActiveRecord model with `token`, `refresh_token`, and `token_expires_at` columns:
48
+
49
+ ```ruby
50
+ def refresh_oauth_token
51
+ with_lock do
52
+ # Re-check after acquiring the lock in case another process already
53
+ # refreshed the token while this one was waiting.
54
+ return true if updated_at > 5.seconds.ago
55
+
56
+ client = OAuth2::Client.new(ENV["NB_CLIENT_ID"], ENV["NB_CLIENT_SECRET"], site: "https://#{slug}.nationbuilder.com")
57
+ old_token = OAuth2::AccessToken.new(client, token, refresh_token: refresh_token)
58
+ new_token = old_token.refresh!
59
+
60
+ update!(token: new_token.token,
61
+ refresh_token: new_token.refresh_token,
62
+ token_expires_at: new_token.expires_at)
63
+ end
64
+ rescue OAuth2::Error => e
65
+ Rails.logger.error("Failed to refresh OAuth token for #{slug}: #{e.message}")
66
+ false
67
+ end
68
+ ```
69
+
70
+ The `with_lock` (a `SELECT ... FOR UPDATE` row lock) and `updated_at` re-check guard against multiple concurrent requests for the same nation refreshing the token at once.
42
71
 
43
72
  ### Configuration
44
73
 
45
74
  ```ruby
75
+ NB_API_CLIENT_CACHE_STORE = ActiveSupport::Cache::RedisCacheStore.new(
76
+ url: ENV["REDIS_URL"],
77
+ pool: {size: ENV.fetch("RATE_LIMITER_POOL_SIZE", 6).to_i, timeout: 5},
78
+ namespace: "nb_api_client"
79
+ )
80
+
46
81
  NbApiClient.configure do |config|
47
82
  # Called after `nation.deauthorize` when a nation has failed authorization
48
83
  # too many times in a row. Use this to notify the account owner or schedule
@@ -61,6 +96,13 @@ NbApiClient.configure do |config|
61
96
  # respond to #increment(key, amount, options), #decrement, and #read.
62
97
  config.cache_store = Rails.cache
63
98
 
99
+ # Example of a more advanced cache store, using its own Redis store with pooling.
100
+ # config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(
101
+ # url: ENV["REDIS_URL"],
102
+ # pool: {size: ENV.fetch("RATE_LIMITER_POOL_SIZE", 6).to_i, timeout: 5},
103
+ # namespace: "nb_api_client"
104
+ # )
105
+
64
106
  # Logger for rate-limit/retry warnings. Defaults to Rails.logger.
65
107
  config.logger = Rails.logger
66
108
 
@@ -75,6 +117,8 @@ NbApiClient.configure do |config|
75
117
  # cache_store to a raw Redis/ConnectionPool) and its client supports Lua
76
118
  # scripts, this is enforced as an exact sliding window via a Lua script
77
119
  # instead of the bucketed approximation used for other cache stores.
120
+ # Refer to https://support.nationbuilder.com/en/articles/9868960-api-rate-limit-policy
121
+ # for NationBuilder's rate limit policy.
78
122
  config.rate_limit = 200 # requests
79
123
  config.rate_limit_window_seconds = 10 # per this many seconds
80
124
  config.rate_limit_max_wait_seconds = 120
@@ -87,13 +131,13 @@ All of the above have working defaults (matching NationBuilder's published limit
87
131
 
88
132
  On success, `NbApiClient::Request.call` returns the raw [`HTTParty::Response`](https://www.rubydoc.info/gems/httparty/HTTParty/Response) object from the underlying call — it is not parsed or unwrapped for you. Useful methods on it include:
89
133
 
90
- | Method | Returns |
91
- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
92
- | `parsed_response` | The JSON body parsed into a `Hash`/`Array`. |
93
- | `code` | The HTTP status code, as an integer. |
94
- | `headers` | An [`HTTParty::Response::Headers`](https://www.rubydoc.info/gems/httparty/HTTParty/Response/Headers) (delegates to `Net::HTTPHeader`). |
95
- | `body` | The raw response body, as a string. |
96
- | `success?` | Whether the response was a 2xx. |
134
+ | Method | Returns |
135
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `parsed_response` | The JSON body parsed into a `Hash`/`Array`. |
137
+ | `code` | The HTTP status code, as an integer. |
138
+ | `headers` | An [`HTTParty::Response::Headers`](https://www.rubydoc.info/gems/httparty/HTTParty/Response/Headers) (delegates to `Net::HTTPHeader`). |
139
+ | `body` | The raw response body, as a string. |
140
+ | `success?` | Whether the response was a 2xx. |
97
141
 
98
142
  `HTTParty::Response` also delegates most `Hash`/`Array` methods (`[]`, `fetch`, `each`, …) straight to `parsed_response`, so e.g. `response["data"]` or `response.fetch("code", "")` works directly on the response without calling `parsed_response` first. See the [HTTParty README](https://github.com/jnunemaker/httparty#readme) for the full API.
99
143
 
@@ -107,7 +151,7 @@ Under `Rails.env.test?` (with the default `short_circuit_in_test`, see below), `
107
151
  - `NbApiClient::Request::RateLimitedError` — NationBuilder returned 429 more than `max_rate_limit_retries` times in a row.
108
152
  - `NbApiClient::Request::InvalidContentType` — the API returned a non-JSON error body.
109
153
  - `NbApiClient::RateLimiter::RateLimitExhausted` — the local rate limiter couldn't get a slot within `rate_limit_max_wait_seconds`.
110
- - `OAuth2::Error` — token refresh failed, or the API returned an unrecognized JSON error.
154
+ - `NbApiClient::Request::OAuthError` — token refresh failed, or the API returned an unrecognized JSON error. (In 0.x this was `OAuth2::Error`; the gem no longer depends on `oauth2` — see CHANGELOG.)
111
155
 
112
156
  These are ordinary Ruby exception classes, so they compose naturally with e.g. Sidekiq's `sidekiq_retry_in`/`sidekiq_retries_exhausted` hooks.
113
157
 
@@ -30,6 +30,17 @@ module NbApiClient
30
30
  end
31
31
  end
32
32
 
33
+ # Replaces OAuth2::Error: this gem never talks OAuth2::Client/AccessToken,
34
+ # it only wraps an HTTParty response body as an error message.
35
+ class OAuthError < StandardError
36
+ attr_reader :response
37
+
38
+ def initialize(response)
39
+ @response = response
40
+ super(response.body)
41
+ end
42
+ end
43
+
33
44
  def self.call(...)
34
45
  new(...).call
35
46
  end
@@ -73,12 +84,12 @@ module NbApiClient
73
84
  sleep wait_seconds
74
85
  next
75
86
  elsif expired_token_error?
76
- raise OAuth2::Error.new(@response) if (token_retries += 1) > config.max_token_retries
77
- raise OAuth2::Error.new(@response) unless @nation.refresh_oauth_token
87
+ raise OAuthError.new(@response) if (token_retries += 1) > config.max_token_retries
88
+ raise OAuthError.new(@response) unless @nation.refresh_oauth_token
78
89
 
79
90
  next
80
91
  elsif unauthorized_error?
81
- raise OAuth2::Error.new(@response) if (token_retries += 1) > config.max_token_retries
92
+ raise OAuthError.new(@response) if (token_retries += 1) > config.max_token_retries
82
93
 
83
94
  if too_many_unauthorized_errors?(config)
84
95
  @nation.deauthorize
@@ -89,7 +100,7 @@ module NbApiClient
89
100
  @nation.refresh_oauth_token
90
101
  next
91
102
  elsif @response.content_type.in?(["application/json", "application/vnd.api+json"])
92
- raise OAuth2::Error.new(@response)
103
+ raise OAuthError.new(@response)
93
104
  else
94
105
  raise InvalidContentType.new(nation: @nation.slug, body: @response.body, content_type: @response.content_type)
95
106
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NbApiClient
4
- VERSION = "0.3.1"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/nb_api_client.rb CHANGED
@@ -9,7 +9,6 @@ require "active_support/core_ext/object/inclusion"
9
9
 
10
10
  require "httparty"
11
11
  require "addressable/uri"
12
- require "oauth2"
13
12
 
14
13
  require_relative "nb_api_client/version"
15
14
  require_relative "nb_api_client/configuration"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nb_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Flint
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-16 00:00:00.000000000 Z
11
+ date: 2026-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.8'
55
- - !ruby/object:Gem::Dependency
56
- name: oauth2
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '2.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '2.0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: rake
71
57
  requirement: !ruby/object:Gem::Requirement