nb_api_client 0.1.0 → 0.3.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 +23 -0
- data/README.md +37 -17
- data/lib/nb_api_client/configuration.rb +2 -5
- data/lib/nb_api_client/rate_limiter.rb +82 -15
- data/lib/nb_api_client/version.rb +1 -1
- data/lib/nb_api_client.rb +0 -11
- metadata +14 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03ba7132dde127d46367c5a7e9731862657586a3a16da516a951a108ab1b89ed
|
|
4
|
+
data.tar.gz: 4d6e1c174a2ff5a1efe821ab92cbad01902271a18bfdc6c87724c83d1e79ef43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03efb68afa45a268187f5d464d6f46f049c8c206400da5d9effcfb26ffaa3f901cae7e3ec40505f6a267b27ca3c3e332c110ce074006dd34ee87ec37ec541fb7
|
|
7
|
+
data.tar.gz: 928da3e0894dc85da016d3a35e1cc8c75614e5a863119a9aef40d6ca54d01c158f4b163aecf75942f93c4cb017baa506981c6db4996b2446a8c4b13eb620b430
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-07-16
|
|
8
|
+
|
|
9
|
+
- `NbApiClient::RateLimiter` now enforces an exact sliding window via a Redis
|
|
10
|
+
Lua script (ZADD/ZREMRANGEBYSCORE/ZCARD on a sorted set) when
|
|
11
|
+
`configuration.cache_store` is Redis-backed and its client supports Lua
|
|
12
|
+
scripts (true for `ActiveSupport::Cache::RedisCacheStore`, or a raw
|
|
13
|
+
Redis/`ConnectionPool` passed in directly as `cache_store`). This is
|
|
14
|
+
detected automatically at runtime — no configuration change is needed. Every
|
|
15
|
+
other `cache_store` still falls back to the bucketed sliding-window-counter
|
|
16
|
+
approximation introduced in 0.2.0. `redis` is added as a development-only
|
|
17
|
+
dependency, used solely by the gem's own integration test.
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-07-16
|
|
20
|
+
|
|
21
|
+
- **Breaking:** Removed the `redis` and `connection_pool` gem dependencies, along with
|
|
22
|
+
`config.redis_url` and `config.rate_limit_pool_size`. `NbApiClient::RateLimiter` now
|
|
23
|
+
implements its sliding window (a bucketed sliding-window-counter algorithm) on top of
|
|
24
|
+
the existing `configuration.cache_store` (defaults to `Rails.cache`) instead of a
|
|
25
|
+
dedicated Redis connection pool and Lua script. Rate limiting is disabled entirely if
|
|
26
|
+
no `cache_store` is configured.
|
|
27
|
+
|
|
5
28
|
## [0.1.1] - 2026-07-16
|
|
6
29
|
|
|
7
30
|
- Publish to rubygems.org instead of GitLab's RubyGems registry (which is
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A rate-limited, token-refreshing HTTP client for the NationBuilder API, for use
|
|
|
5
5
|
It provides two pieces:
|
|
6
6
|
|
|
7
7
|
- `NbApiClient::Request` — makes a single authenticated API call, transparently retrying on rate limits and refreshing expired OAuth tokens.
|
|
8
|
-
- `NbApiClient::RateLimiter` — a
|
|
8
|
+
- `NbApiClient::RateLimiter` — a sliding-window rate limiter, backed by your configured `cache_store` (defaults to `Rails.cache`), shared across every process/thread in your fleet, so you don't blow through NationBuilder's server-side rate limit when running many workers. If no `cache_store` is configured, rate limiting is disabled entirely. If `cache_store` is Redis-backed and its client supports Lua scripts, an exact sliding window is enforced atomically via a Lua script; otherwise a bucketed sliding-window-counter approximation is used, so any `ActiveSupport::Cache::Store` works.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
@@ -24,22 +24,22 @@ gem "nb_api_client", path: "gems/nb_api_client"
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
|
-
NbApiClient::Request.call(nation, :get, "/api/v2/
|
|
28
|
-
NbApiClient::Request.call(nation, :post, "/api/v2/
|
|
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"}}})
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
### The `nation` interface
|
|
32
32
|
|
|
33
33
|
`nation` can be any object — it does not need to be an ActiveRecord model — that responds to:
|
|
34
34
|
|
|
35
|
-
| Method
|
|
36
|
-
|
|
|
37
|
-
| `slug`
|
|
38
|
-
| `active?`
|
|
39
|
-
| `url`
|
|
40
|
-
| `token`
|
|
41
|
-
| `refresh_oauth_token` | Refreshes the OAuth token in place; returns truthy on success, falsy on failure.
|
|
42
|
-
| `deauthorize`
|
|
35
|
+
| Method | Returns |
|
|
36
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| `slug` | A string uniquely identifying the account (used as a cache-key/log prefix). |
|
|
38
|
+
| `active?` | Whether requests should be allowed (checked before every call, except `/oauth/token`). |
|
|
39
|
+
| `url` | The base URL for the account, e.g. `"https://myorg.nationbuilder.com"`. |
|
|
40
|
+
| `token` | The current OAuth access token, appended as a query param on every request. |
|
|
41
|
+
| `refresh_oauth_token` | Refreshes the OAuth token in place; returns truthy on success, falsy on failure. |
|
|
42
|
+
| `deauthorize` | Called after too many consecutive "unauthorized" responses (see `unauthorized_error_threshold` below). |
|
|
43
43
|
|
|
44
44
|
### Configuration
|
|
45
45
|
|
|
@@ -57,8 +57,9 @@ NbApiClient.configure do |config|
|
|
|
57
57
|
config.unauthorized_error_threshold = 25
|
|
58
58
|
config.unauthorized_error_window = 5 * 60
|
|
59
59
|
|
|
60
|
-
# Cache store used to count those unauthorized responses
|
|
61
|
-
#
|
|
60
|
+
# Cache store used to count those unauthorized responses, and (below) to
|
|
61
|
+
# back the sliding-window rate limiter. Defaults to Rails.cache; must
|
|
62
|
+
# respond to #increment(key, amount, options), #decrement, and #read.
|
|
62
63
|
config.cache_store = Rails.cache
|
|
63
64
|
|
|
64
65
|
# Logger for rate-limit/retry warnings. Defaults to Rails.logger.
|
|
@@ -69,17 +70,36 @@ NbApiClient.configure do |config|
|
|
|
69
70
|
# rather stub HTTP at a lower level (e.g. WebMock/VCR) in your test suite.
|
|
70
71
|
config.short_circuit_in_test = true
|
|
71
72
|
|
|
72
|
-
# Sliding-window rate limit enforced via
|
|
73
|
+
# Sliding-window rate limit enforced via `cache_store` across every process.
|
|
74
|
+
# Disabled entirely if `cache_store` is nil. If `cache_store` is Redis-backed
|
|
75
|
+
# (e.g. Rails.cache is an ActiveSupport::Cache::RedisCacheStore, or you set
|
|
76
|
+
# cache_store to a raw Redis/ConnectionPool) and its client supports Lua
|
|
77
|
+
# scripts, this is enforced as an exact sliding window via a Lua script
|
|
78
|
+
# instead of the bucketed approximation used for other cache stores.
|
|
73
79
|
config.rate_limit = 200 # requests
|
|
74
80
|
config.rate_limit_window_seconds = 10 # per this many seconds
|
|
75
81
|
config.rate_limit_max_wait_seconds = 120
|
|
76
|
-
config.rate_limit_pool_size = 6 # Redis connection pool size
|
|
77
|
-
config.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
|
|
78
82
|
end
|
|
79
83
|
```
|
|
80
84
|
|
|
81
85
|
All of the above have working defaults (matching NationBuilder's published limits with a safety margin), so a new app can start with zero configuration beyond `on_repeated_unauthorized`.
|
|
82
86
|
|
|
87
|
+
### Responses
|
|
88
|
+
|
|
89
|
+
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:
|
|
90
|
+
|
|
91
|
+
| Method | Returns |
|
|
92
|
+
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
93
|
+
| `parsed_response` | The JSON body parsed into a `Hash`/`Array`. |
|
|
94
|
+
| `code` | The HTTP status code, as an integer. |
|
|
95
|
+
| `headers` | An [`HTTParty::Response::Headers`](https://www.rubydoc.info/gems/httparty/HTTParty/Response/Headers) (delegates to `Net::HTTPHeader`). |
|
|
96
|
+
| `body` | The raw response body, as a string. |
|
|
97
|
+
| `success?` | Whether the response was a 2xx. |
|
|
98
|
+
|
|
99
|
+
`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.
|
|
100
|
+
|
|
101
|
+
Under `Rails.env.test?` (with the default `short_circuit_in_test`, see below), `call` returns a plain `{}` instead of a real response.
|
|
102
|
+
|
|
83
103
|
### Error handling
|
|
84
104
|
|
|
85
105
|
`NbApiClient::Request.call` raises:
|
|
@@ -99,7 +119,7 @@ bundle install
|
|
|
99
119
|
bundle exec rake test
|
|
100
120
|
```
|
|
101
121
|
|
|
102
|
-
The
|
|
122
|
+
The Lua-script integration test in `rate_limiter_test.rb` is skipped automatically if no Redis is reachable at `REDIS_URL` (defaults to `redis://localhost:6379/0`).
|
|
103
123
|
|
|
104
124
|
## Releasing a new version
|
|
105
125
|
|
|
@@ -31,14 +31,13 @@ module NbApiClient
|
|
|
31
31
|
attr_accessor :max_token_retries
|
|
32
32
|
|
|
33
33
|
# Sliding-window rate limit: at most `rate_limit` requests per `rate_limit_window_seconds`,
|
|
34
|
-
# enforced across all processes via
|
|
34
|
+
# enforced across all processes via `cache_store` (above). If `cache_store` is nil,
|
|
35
|
+
# rate limiting is disabled entirely.
|
|
35
36
|
attr_accessor :rate_limit
|
|
36
37
|
attr_accessor :rate_limit_window_seconds
|
|
37
38
|
attr_accessor :rate_limit_max_wait_seconds
|
|
38
39
|
attr_accessor :rate_limit_poll_interval
|
|
39
40
|
attr_accessor :rate_limit_max_jitter
|
|
40
|
-
attr_accessor :rate_limit_pool_size
|
|
41
|
-
attr_accessor :redis_url
|
|
42
41
|
|
|
43
42
|
def initialize
|
|
44
43
|
@cache_store = (defined?(Rails) && Rails.respond_to?(:cache)) ? Rails.cache : nil
|
|
@@ -55,8 +54,6 @@ module NbApiClient
|
|
|
55
54
|
@rate_limit_max_wait_seconds = ENV.fetch("API_RATE_MAX_WAIT_SECONDS", 120).to_i
|
|
56
55
|
@rate_limit_poll_interval = 0.25
|
|
57
56
|
@rate_limit_max_jitter = 0.25
|
|
58
|
-
@rate_limit_pool_size = ENV.fetch("RATE_LIMITER_POOL_SIZE", 6).to_i
|
|
59
|
-
@redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
|
|
60
57
|
end
|
|
61
58
|
end
|
|
62
59
|
end
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NbApiClient
|
|
4
|
-
# A global (cross-process, cross-thread) distributed rate limiter backed by
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# A global (cross-process, cross-thread) distributed rate limiter backed by
|
|
5
|
+
# configuration.cache_store (the same store used for unauthorized-response
|
|
6
|
+
# tracking; defaults to Rails.cache). All callers share one window (configure
|
|
7
|
+
# via NbApiClient.configure).
|
|
8
|
+
#
|
|
9
|
+
# If cache_store is Redis-backed and its underlying client supports Lua
|
|
10
|
+
# scripts (i.e. responds to #eval — true for ActiveSupport::Cache::RedisCacheStore
|
|
11
|
+
# and for a bare Redis/ConnectionPool passed in directly), a true sliding
|
|
12
|
+
# window is enforced atomically via a Lua script (ZADD/ZREMRANGEBYSCORE/ZCARD
|
|
13
|
+
# on a sorted set).
|
|
14
|
+
#
|
|
15
|
+
# Otherwise, a "sliding window counter" algorithm is used instead: each window
|
|
16
|
+
# is divided into fixed buckets, and the previous bucket's count is weighted
|
|
17
|
+
# by how much of it still overlaps the sliding window. This approximates a
|
|
18
|
+
# true sliding window without requiring sorted-set support, so it works with
|
|
19
|
+
# any ActiveSupport::Cache::Store (memory, Memcached, ...).
|
|
20
|
+
#
|
|
21
|
+
# If no cache_store is configured, rate limiting is disabled entirely (every
|
|
22
|
+
# request is allowed through immediately).
|
|
8
23
|
class RateLimiter
|
|
9
24
|
class RateLimitExhausted < StandardError; end
|
|
10
25
|
|
|
@@ -66,10 +81,12 @@ module NbApiClient
|
|
|
66
81
|
waited += sleep_time
|
|
67
82
|
end
|
|
68
83
|
end
|
|
69
|
-
rescue
|
|
70
|
-
|
|
84
|
+
rescue RateLimitExhausted
|
|
85
|
+
raise
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
# Fail open: if the cache store is unavailable, allow the request through.
|
|
71
88
|
# NbApiClient::Request's 429 handling acts as a safety net.
|
|
72
|
-
config.logger&.warn("[NbApiClient::RateLimiter]
|
|
89
|
+
config.logger&.warn("[NbApiClient::RateLimiter] cache store error, failing open: #{e.class} - #{e.message}")
|
|
73
90
|
yield
|
|
74
91
|
end
|
|
75
92
|
|
|
@@ -77,22 +94,72 @@ module NbApiClient
|
|
|
77
94
|
|
|
78
95
|
# Attempts to acquire a slot in the sliding window.
|
|
79
96
|
# Returns the new count (positive) on success, or -1 if the window is full.
|
|
97
|
+
# Always succeeds (returns 1) if no cache_store is configured, i.e. rate limiting is disabled.
|
|
80
98
|
def try_acquire_slot
|
|
81
99
|
config = NbApiClient.configuration
|
|
100
|
+
store = config.cache_store
|
|
101
|
+
return 1 unless store
|
|
102
|
+
|
|
103
|
+
redis = redis_client(store)
|
|
104
|
+
redis ? try_acquire_slot_via_lua(redis, config) : try_acquire_slot_via_cache_store(store, config)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def try_acquire_slot_via_lua(redis, config)
|
|
82
108
|
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
83
109
|
member = "#{now}:#{SecureRandom.hex(4)}"
|
|
110
|
+
argv = [config.rate_limit.to_s, config.rate_limit_window_seconds.to_s, now.to_s, member]
|
|
111
|
+
|
|
112
|
+
run = ->(conn) { conn.eval(ACQUIRE_SLOT_SCRIPT, keys: [@key], argv: argv) }
|
|
113
|
+
redis.respond_to?(:with) ? redis.with(&run) : run.call(redis)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Approximates a sliding window via fixed buckets, weighting the previous
|
|
117
|
+
# bucket's count by how much of it still overlaps the sliding window.
|
|
118
|
+
def try_acquire_slot_via_cache_store(store, config)
|
|
119
|
+
window = config.rate_limit_window_seconds
|
|
120
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
121
|
+
current_bucket = (now / window).floor
|
|
122
|
+
previous_bucket = current_bucket - 1
|
|
123
|
+
elapsed_in_current = now - (current_bucket * window)
|
|
124
|
+
previous_weight = [(window - elapsed_in_current) / window, 0.0].max
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
# Safety-net expiry so bucket keys don't live forever if traffic stops.
|
|
127
|
+
current_count = store.increment("#{@key}:#{current_bucket}", 1, expires_in: window * 2)
|
|
128
|
+
previous_count = store.read("#{@key}:#{previous_bucket}") || 0
|
|
129
|
+
|
|
130
|
+
weighted_count = current_count + (previous_count * previous_weight)
|
|
131
|
+
|
|
132
|
+
if weighted_count <= config.rate_limit
|
|
133
|
+
weighted_count
|
|
134
|
+
else
|
|
135
|
+
# Undo the increment so this rejected attempt doesn't count against the window.
|
|
136
|
+
store.decrement("#{@key}:#{current_bucket}", 1)
|
|
137
|
+
-1
|
|
91
138
|
end
|
|
92
139
|
end
|
|
93
140
|
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
# Returns the Redis client (or ConnectionPool of them) backing `store`, if
|
|
142
|
+
# it -- or a checked-out connection from it -- supports Lua scripts (#eval).
|
|
143
|
+
# Covers ActiveSupport::Cache::RedisCacheStore (whose #redis may itself be a
|
|
144
|
+
# bare client or a ConnectionPool) as well as a bare Redis/ConnectionPool
|
|
145
|
+
# object passed in directly as cache_store. Returns nil otherwise, so the
|
|
146
|
+
# caller falls back to the generic cache_store algorithm. Memoized per
|
|
147
|
+
# RateLimiter instance since cache_store doesn't change mid-request.
|
|
148
|
+
def redis_client(store)
|
|
149
|
+
return @redis_client if defined?(@redis_client)
|
|
150
|
+
|
|
151
|
+
@redis_client = begin
|
|
152
|
+
candidate = store.respond_to?(:redis) ? store.redis : store
|
|
153
|
+
if candidate.nil?
|
|
154
|
+
nil
|
|
155
|
+
elsif candidate.respond_to?(:with)
|
|
156
|
+
candidate.with { |conn| conn.respond_to?(:eval) } ? candidate : nil
|
|
157
|
+
elsif candidate.respond_to?(:eval)
|
|
158
|
+
candidate
|
|
159
|
+
end
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
96
163
|
end
|
|
97
164
|
end
|
|
98
165
|
end
|
data/lib/nb_api_client.rb
CHANGED
|
@@ -10,8 +10,6 @@ require "active_support/core_ext/object/inclusion"
|
|
|
10
10
|
require "httparty"
|
|
11
11
|
require "addressable/uri"
|
|
12
12
|
require "oauth2"
|
|
13
|
-
require "redis"
|
|
14
|
-
require "connection_pool"
|
|
15
13
|
|
|
16
14
|
require_relative "nb_api_client/version"
|
|
17
15
|
require_relative "nb_api_client/configuration"
|
|
@@ -28,14 +26,5 @@ module NbApiClient
|
|
|
28
26
|
def configure
|
|
29
27
|
yield(configuration)
|
|
30
28
|
end
|
|
31
|
-
|
|
32
|
-
# Lazily built so it always reflects the redis_url/pool_size in effect the
|
|
33
|
-
# first time a request needs it (i.e. after any NbApiClient.configure block
|
|
34
|
-
# has run).
|
|
35
|
-
def connection_pool
|
|
36
|
-
@connection_pool ||= ConnectionPool.new(size: configuration.rate_limit_pool_size, timeout: 3) {
|
|
37
|
-
Redis.new(url: configuration.redis_url)
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
29
|
end
|
|
41
30
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nb_api_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Flint
|
|
@@ -67,63 +67,49 @@ dependencies:
|
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '2.0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '5.0'
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '5.0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: connection_pool
|
|
70
|
+
name: rake
|
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
|
86
72
|
requirements:
|
|
87
|
-
- - "
|
|
73
|
+
- - "~>"
|
|
88
74
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
90
|
-
type: :
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
91
77
|
prerelease: false
|
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
79
|
requirements:
|
|
94
|
-
- - "
|
|
80
|
+
- - "~>"
|
|
95
81
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
82
|
+
version: '13.0'
|
|
97
83
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
84
|
+
name: minitest
|
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
|
100
86
|
requirements:
|
|
101
87
|
- - "~>"
|
|
102
88
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
89
|
+
version: '5.0'
|
|
104
90
|
type: :development
|
|
105
91
|
prerelease: false
|
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
93
|
requirements:
|
|
108
94
|
- - "~>"
|
|
109
95
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
96
|
+
version: '5.0'
|
|
111
97
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
98
|
+
name: redis
|
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
|
114
100
|
requirements:
|
|
115
|
-
- - "
|
|
101
|
+
- - ">="
|
|
116
102
|
- !ruby/object:Gem::Version
|
|
117
103
|
version: '5.0'
|
|
118
104
|
type: :development
|
|
119
105
|
prerelease: false
|
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
107
|
requirements:
|
|
122
|
-
- - "
|
|
108
|
+
- - ">="
|
|
123
109
|
- !ruby/object:Gem::Version
|
|
124
110
|
version: '5.0'
|
|
125
111
|
description: Wraps NationBuilder API requests with OAuth token refresh, deauthorization
|
|
126
|
-
handling, and a
|
|
112
|
+
handling, and a cache-store-backed sliding-window rate limiter shared across processes.
|
|
127
113
|
email:
|
|
128
114
|
executables: []
|
|
129
115
|
extensions: []
|