diff options
| author | mo khan <mo@mokhan.ca> | 2025-10-08 10:42:46 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-10-08 10:42:46 -0600 |
| commit | 4072dedb8f17040f3dbe1ec8400e2eb87b044c2d (patch) | |
| tree | cd7f30fc71793a914e6d9c4d3d3878b51200d064 | |
| parent | 58d4e9be69cf5fbc951bc63858ac899dded47bee (diff) | |
feat: add support for streaming responses
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | README.md | 34 | ||||
| -rw-r--r-- | lib/net/hippie/client.rb | 21 | ||||
| -rw-r--r-- | lib/net/hippie/connection.rb | 10 | ||||
| -rw-r--r-- | lib/net/hippie/version.rb | 2 | ||||
| -rw-r--r-- | test/net/client_test.rb | 38 |
6 files changed, 103 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1ede0..7f21f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.4.0] - 2025-10-08 +### Added +- Streaming response support via block parameter +- Backward compatible with existing block API (arity-based detection) + ## [1.3.0] - 2025-04-30 ### Changed - Ruby 2.3+ required @@ -95,7 +100,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - with\_retry. - authorization header helpers -[Unreleased]: https://github.com/xlgmokha/net-hippie/compare/v1.3.0...HEAD +[Unreleased]: https://github.com/xlgmokha/net-hippie/compare/v1.4.0...HEAD +[1.4.0]: https://github.com/xlgmokha/net-hippie/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/xlgmokha/net-hippie/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/xlgmokha/net-hippie/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/xlgmokha/net-hippie/compare/v1.1.0...v1.1.1 @@ -90,6 +90,40 @@ headers = { 'Authorization' => Net::Hippie.bearer_auth('token') } Net::Hippie.get('https://www.example.org', headers: headers) ``` +### Streaming Responses + +Net::Hippie supports streaming responses by passing a block that accepts the response object: + +```ruby +Net::Hippie.post('https://api.example.com/stream', body: { prompt: 'Hello' }) do |response| + response.read_body do |chunk| + print chunk + end +end +``` + +This is useful for Server-Sent Events (SSE) or other streaming APIs: + +```ruby +client = Net::Hippie::Client.new +client.post('https://api.openai.com/v1/chat/completions', + headers: { + 'Authorization' => Net::Hippie.bearer_auth(ENV['OPENAI_API_KEY']), + 'Content-Type' => 'application/json' + }, + body: { model: 'gpt-4', messages: [{ role: 'user', content: 'Hi' }], stream: true } +) do |response| + buffer = "" + response.read_body do |chunk| + buffer += chunk + while (line = buffer.slice!(/.*\n/)) + next if line.strip.empty? + puts line if line.start_with?('data: ') + end + end +end +``` + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. diff --git a/lib/net/hippie/client.rb b/lib/net/hippie/client.rb index 340531b..f8610c2 100644 --- a/lib/net/hippie/client.rb +++ b/lib/net/hippie/client.rb @@ -26,13 +26,22 @@ module Net def execute(uri, request, limit: follow_redirects, &block) connection = connection_for(uri) - response = connection.run(request) - if limit.positive? && response.is_a?(Net::HTTPRedirection) - url = connection.build_url_for(response['location']) - request = request_for(Net::HTTP::Get, url) - execute(url, request, limit: limit - 1, &block) + if block_given? + if block.arity == 2 + response = connection.run(request) + yield(request, response) + else + connection.run(request, &block) + end else - block_given? ? yield(request, response) : response + response = connection.run(request) + if limit.positive? && response.is_a?(Net::HTTPRedirection) + url = connection.build_url_for(response['location']) + request = request_for(Net::HTTP::Get, url) + execute(url, request, limit: limit - 1) + else + response + end end end diff --git a/lib/net/hippie/connection.rb b/lib/net/hippie/connection.rb index 9b86dc5..392914b 100644 --- a/lib/net/hippie/connection.rb +++ b/lib/net/hippie/connection.rb @@ -15,8 +15,14 @@ module Net @http = http end - def run(request) - @http.request(request) + def run(request, &block) + if block_given? + @http.request(request) do |response| + yield response + end + else + @http.request(request) + end end def build_url_for(path) diff --git a/lib/net/hippie/version.rb b/lib/net/hippie/version.rb index 07de45b..bb07582 100644 --- a/lib/net/hippie/version.rb +++ b/lib/net/hippie/version.rb @@ -2,6 +2,6 @@ module Net module Hippie - VERSION = '1.3.0' + VERSION = '1.4.0' end end diff --git a/test/net/client_test.rb b/test/net/client_test.rb index 45ea419..4a688d1 100644 --- a/test/net/client_test.rb +++ b/test/net/client_test.rb @@ -317,4 +317,42 @@ class ClientTest < Minitest::Test end end end + + def test_post_with_streaming + chunks = ["chunk1", "chunk2", "chunk3"] + response_body = chunks.join + + WebMock.stub_request(:post, "https://example.org/stream") + .to_return(status: 200, body: response_body) + + uri = URI.parse('https://example.org/stream') + received_chunks = [] + + subject.post(uri, body: { test: true }) do |response| + response.read_body do |chunk| + received_chunks << chunk + end + end + + assert_equal chunks.join, received_chunks.join + end + + def test_get_with_streaming + chunks = ["data1\n", "data2\n", "data3\n"] + response_body = chunks.join + + WebMock.stub_request(:get, "https://example.org/stream") + .to_return(status: 200, body: response_body) + + uri = URI.parse('https://example.org/stream') + received_data = [] + + subject.get(uri) do |response| + response.read_body do |chunk| + received_data << chunk + end + end + + assert_equal chunks.join, received_data.join + end end |
