simple_http_service 0.2.0 → 3.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: 8a75ddb35b5f12bc409dfba441396f9341ee708894097afe93f1477095cf27cf
4
- data.tar.gz: 4feb1170c683ff594d20f27c8b970e9a17b70ecb4dc088ce4b5cb84dd72cbd46
3
+ metadata.gz: 2bca219ad4f3a280dfea6a35fa241481c04414f2e85f553756e7a8612c8f81da
4
+ data.tar.gz: 647ce34790aaea4de53b84f852415d274b26cb51bd093173b8d2b5612381bf92
5
5
  SHA512:
6
- metadata.gz: a80bacdcbaba0b520408918a547eb112c47cba2875a7bf4f64753d9f12287c3cb9bdb31de1890be53cc4d189c9db297dfcbdde93a929d7f6f1bf9f37ba184819
7
- data.tar.gz: b8b1c7527e13face8f5f45e60ae16957585c5c3e87525618467d0c6912705b48b0406c42d2b4095a69a23d938e37682ce7fed5c446cc199c9336f97ee674a21d
6
+ metadata.gz: 0c2dd8b1ea07c4a43612df855b64595447e219ffa950c3340c8fa271e4ac72323e7ccc507db62f6f4266924e2f8d194dff109075ebb6432dfbb2a02622266819
7
+ data.tar.gz: dfeea32e3a2db839a96165cd9b18da540d014d7a7115a3afb0ccc23f4272c87fdd0b901c71cae80c910a8b9bb49fbe94f3f569d1b19935c75b87701cf3dc7611
data/.gitignore CHANGED
@@ -9,4 +9,12 @@
9
9
  # rspec failure tracking
10
10
  .rspec_status
11
11
 
12
- .idea
12
+ .idea
13
+
14
+ # CCE (code-context-engine)
15
+ # CCE local cache (per-machine, not for version control)
16
+ .cce/
17
+ # Claude Code local settings written by cce init
18
+ .claude/settings.local.json
19
+ # .mcp.json contains absolute paths regenerated by `cce init`
20
+ .mcp.json
data/Gemfile.lock CHANGED
@@ -1,38 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_http_service (0.1.7)
5
- net-http (~> 0.4.1)
4
+ simple_http_service (3.0.0)
5
+ net-http (>= 0.4)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- diff-lcs (1.5.1)
11
- net-http (0.4.1)
12
- uri
13
- rake (10.5.0)
14
- rspec (3.13.0)
10
+ diff-lcs (1.6.2)
11
+ net-http (0.9.1)
12
+ uri (>= 0.11.1)
13
+ rake (13.4.2)
14
+ rspec (3.13.2)
15
15
  rspec-core (~> 3.13.0)
16
16
  rspec-expectations (~> 3.13.0)
17
17
  rspec-mocks (~> 3.13.0)
18
- rspec-core (3.13.2)
18
+ rspec-core (3.13.6)
19
19
  rspec-support (~> 3.13.0)
20
- rspec-expectations (3.13.0)
20
+ rspec-expectations (3.13.5)
21
21
  diff-lcs (>= 1.2.0, < 2.0)
22
22
  rspec-support (~> 3.13.0)
23
- rspec-mocks (3.13.1)
23
+ rspec-mocks (3.13.8)
24
24
  diff-lcs (>= 1.2.0, < 2.0)
25
25
  rspec-support (~> 3.13.0)
26
- rspec-support (3.13.1)
27
- uri (0.12.1)
28
- yard (0.9.37)
26
+ rspec-support (3.13.7)
27
+ uri (1.1.1)
28
+ yard (0.9.44)
29
29
 
30
30
  PLATFORMS
31
31
  ruby
32
32
 
33
33
  DEPENDENCIES
34
- bundler (~> 2.5.10)
35
- rake (~> 10.0)
34
+ bundler (~> 2.0)
35
+ rake (>= 10.0)
36
36
  rspec (~> 3.13.0)
37
37
  rspec-core (~> 3.13.1)
38
38
  simple_http_service!
data/README.md CHANGED
@@ -45,6 +45,10 @@ client = SimpleHttpService.new(
45
45
  max_retries: 3,
46
46
  additional_headers: {
47
47
  'X-Request-Id': '12345'
48
+ },
49
+ rate_limit: {
50
+ limit: 2,
51
+ interval: 60
48
52
  }
49
53
  )
50
54
  ```
@@ -65,6 +69,50 @@ puts response.body
65
69
  - `max_retries`: The number of times to retry the request in case of failure.
66
70
  - `request_body`: The body of the request (used for POST and PUT requests).
67
71
  - `additional_headers`: Additional headers to include in the request.
72
+ - `rate_limit`: A hash enabling client side throttling. Omit it and no limiting is applied.
73
+ - `limit` (required to enable): Maximum number of requests allowed per window.
74
+ - `interval`: Length of the window in seconds (default is `60`).
75
+ - `wait`: When `true`, `call` sleeps until a slot frees up instead of raising (default is `false`).
76
+ - `key`: Bucket the window under a custom key (default is `scheme://host:port`).
77
+
78
+ ### Rate Limiting
79
+ Rate limiting is opt in. Pass a `rate_limit` hash to cap how often the client fires requests:
80
+
81
+ ```ruby
82
+ client = SimpleHttpService.new(
83
+ url: 'https://api.example.com/endpoint',
84
+ http_method: :get,
85
+ rate_limit: { limit: 2, interval: 60 }
86
+ )
87
+
88
+ 2.times { client.call }
89
+ client.call # raises SimpleHttpService::RateLimitExceeded
90
+ ```
91
+
92
+ The window is a sliding one, shared across every client in the process that resolves to the same
93
+ key, so two clients pointing at the same host draw from the same budget. Use `key` to share (or
94
+ separate) buckets explicitly.
95
+
96
+ To block instead of raising when the budget is used up:
97
+
98
+ ```ruby
99
+ client = SimpleHttpService.new(
100
+ url: 'https://api.example.com/endpoint',
101
+ http_method: :get,
102
+ rate_limit: { limit: 2, interval: 60, wait: true }
103
+ )
104
+ ```
105
+
106
+ `SimpleHttpService::RateLimitExceeded` inherits from `SimpleHttpService::Error` and exposes
107
+ `retry_after`, the number of seconds until the next slot opens:
108
+
109
+ ```ruby
110
+ begin
111
+ client.call
112
+ rescue SimpleHttpService::RateLimitExceeded => e
113
+ puts "Throttled, retry in #{e.retry_after.ceil}s"
114
+ end
115
+ ```
68
116
 
69
117
  ### Example
70
118
  Here's a complete example of using `SimpleHttpService` to make a `GET` request:
@@ -1,8 +1,10 @@
1
1
  require 'net/http'
2
+ require 'simple_http_service/rate_limiter'
3
+
2
4
  module SimpleHttpService
3
5
  class Client
4
6
  attr_accessor :uri, :headers, :http_method, :open_timeout, :read_timeout, :write_timeout,
5
- :max_retries, :request_body, :additional_headers
7
+ :max_retries, :request_body, :additional_headers, :rate_limit
6
8
 
7
9
  def initialize(opts)
8
10
  raise 'URL must be present' unless opts[:url]
@@ -17,9 +19,11 @@ module SimpleHttpService
17
19
  @write_timeout = opts[:write_timeout]
18
20
  @max_retries = opts[:max_retries] || 1
19
21
  @additional_headers = opts[:additional_headers] || {}
22
+ @rate_limit = opts[:rate_limit] || {}
20
23
  end
21
24
 
22
25
  def call
26
+ throttle
23
27
  enable_ssl
24
28
  set_headers
25
29
  set_timeout
@@ -27,8 +31,23 @@ module SimpleHttpService
27
31
  http.request(request)
28
32
  end
29
33
 
34
+ # Limiter backing this client, or nil unless rate_limit[:limit] was passed in.
35
+ def rate_limiter
36
+ return unless rate_limit[:limit]
37
+
38
+ @rate_limiter ||= RateLimiter.for(
39
+ rate_limit[:key] || "#{uri.scheme}://#{uri.host}:#{uri.port}",
40
+ limit: rate_limit[:limit],
41
+ interval: rate_limit[:interval] || RateLimiter::DEFAULT_INTERVAL
42
+ )
43
+ end
44
+
30
45
  private
31
46
 
47
+ def throttle
48
+ rate_limiter&.acquire(wait: rate_limit[:wait] || false)
49
+ end
50
+
32
51
  def set_headers
33
52
  request["Accept"] = headers[:accept] if headers[:accept]
34
53
  request["Authorization"] = headers[:authorization] if headers[:authorization]
@@ -0,0 +1,107 @@
1
+ module SimpleHttpService
2
+ class Error < StandardError; end
3
+
4
+ # Raised when a request would exceed the configured rate limit and
5
+ # the client is not configured to wait for a free slot.
6
+ class RateLimitExceeded < Error
7
+ attr_reader :retry_after
8
+
9
+ def initialize(msg, retry_after: nil)
10
+ @retry_after = retry_after
11
+ super(msg)
12
+ end
13
+ end
14
+
15
+ # Sliding-window rate limiter, shared per key across all Client instances
16
+ # in the process. Thread safe.
17
+ class RateLimiter
18
+ DEFAULT_INTERVAL = 60
19
+
20
+ attr_reader :limit, :interval
21
+
22
+ class << self
23
+ # Returns the limiter registered for +key+, creating it on first use.
24
+ def for(key, limit:, interval: DEFAULT_INTERVAL)
25
+ registry_mutex.synchronize do
26
+ registry[key] ||= new(limit: limit, interval: interval)
27
+ end
28
+ end
29
+
30
+ # Drops every registered limiter. Mainly useful in tests.
31
+ def reset!
32
+ registry_mutex.synchronize { registry.clear }
33
+ end
34
+
35
+ private
36
+
37
+ def registry
38
+ @registry ||= {}
39
+ end
40
+
41
+ def registry_mutex
42
+ @registry_mutex ||= Mutex.new
43
+ end
44
+ end
45
+
46
+ def initialize(limit:, interval: DEFAULT_INTERVAL)
47
+ raise 'rate limit must be a positive integer' unless limit.to_i.positive?
48
+ raise 'rate limit interval must be positive' unless interval.to_f.positive?
49
+
50
+ @limit = limit.to_i
51
+ @interval = interval.to_f
52
+ @timestamps = []
53
+ @mutex = Mutex.new
54
+ end
55
+
56
+ # Consumes one slot. Raises RateLimitExceeded when the window is full,
57
+ # unless +wait+ is true, in which case it sleeps until a slot frees up.
58
+ def acquire(wait: false)
59
+ loop do
60
+ retry_after = try_acquire
61
+ return true unless retry_after
62
+
63
+ unless wait
64
+ raise RateLimitExceeded.new(
65
+ "rate limit of #{limit} request(s) per #{interval.round} seconds exceeded, " \
66
+ "retry in #{retry_after.ceil} second(s)",
67
+ retry_after: retry_after
68
+ )
69
+ end
70
+
71
+ sleep(retry_after)
72
+ end
73
+ end
74
+
75
+ # Slots still available in the current window.
76
+ def remaining
77
+ @mutex.synchronize do
78
+ prune
79
+ limit - @timestamps.size
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Returns nil when a slot was taken, otherwise seconds until the next one.
86
+ def try_acquire
87
+ @mutex.synchronize do
88
+ prune
89
+ if @timestamps.size < limit
90
+ @timestamps << now
91
+ return nil
92
+ end
93
+
94
+ [@timestamps.first + interval - now, 0.001].max
95
+ end
96
+ end
97
+
98
+ def prune
99
+ cutoff = now - interval
100
+ @timestamps.shift while @timestamps.first && @timestamps.first <= cutoff
101
+ end
102
+
103
+ def now
104
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
105
+ end
106
+ end
107
+ end
@@ -1,3 +1,3 @@
1
1
  module SimpleHttpService
2
- VERSION = "0.2.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -12,6 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'SimpleHttpService is a simple Ruby library to make HTTP requests with customizable options for headers, timeouts, and retries. It provides a convenient way to create and send HTTP requests using a clean and simple interface.'
13
13
  spec.homepage = "https://github.com/gklsan/simple_http_service"
14
14
  spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.5"
15
16
 
16
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
@@ -38,8 +39,8 @@ Gem::Specification.new do |spec|
38
39
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
40
  spec.require_paths = ["lib"]
40
41
 
41
- spec.add_dependency "net-http", "~> 0.4.1"
42
- spec.add_development_dependency "bundler", "~> 2.5.10"
43
- spec.add_development_dependency "rake", "~> 10.0"
42
+ spec.add_dependency "net-http", ">= 0.4"
43
+ spec.add_development_dependency "bundler", "~> 2.0"
44
+ spec.add_development_dependency "rake", ">= 10.0"
44
45
  spec.add_development_dependency "rspec", "~> 3.0"
45
46
  end
metadata CHANGED
@@ -1,54 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_http_service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gokul (gklsan)
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-06-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: net-http
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.4.1
18
+ version: '0.4'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.4.1
25
+ version: '0.4'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 2.5.10
32
+ version: '2.0'
33
33
  type: :development
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 2.5.10
39
+ version: '2.0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rake
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '10.0'
47
47
  type: :development
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '10.0'
54
54
  - !ruby/object:Gem::Dependency
@@ -105,6 +105,7 @@ files:
105
105
  - doc/top-level-namespace.html
106
106
  - lib/simple_http_service.rb
107
107
  - lib/simple_http_service/client.rb
108
+ - lib/simple_http_service/rate_limiter.rb
108
109
  - lib/simple_http_service/version.rb
109
110
  - simple_http_service.gemspec
110
111
  homepage: https://github.com/gklsan/simple_http_service
@@ -123,14 +124,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
124
  requirements:
124
125
  - - ">="
125
126
  - !ruby/object:Gem::Version
126
- version: '0'
127
+ version: '2.5'
127
128
  required_rubygems_version: !ruby/object:Gem::Requirement
128
129
  requirements:
129
130
  - - ">="
130
131
  - !ruby/object:Gem::Version
131
132
  version: '0'
132
133
  requirements: []
133
- rubygems_version: 3.6.6
134
+ rubygems_version: 4.0.11
134
135
  specification_version: 4
135
136
  summary: SimpleHttpService is a simple Ruby library to make HTTP requests with customizable
136
137
  options for headers, timeouts, and retries. It provides a convenient way to create