ezclient 1.0.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 127a10e9346fcbb5498bf8a4d82781f95e1765d7c5208c4337881204f10dd290
4
- data.tar.gz: 2a09e878beed9ed75c9aeaf78b07d1e3e6cd6d1e96456775ecc8062489ee01ea
3
+ metadata.gz: c4f9418b8f7468e16261c321cf17ea9ee34e8c2168fda6b936a56b8c88db20ad
4
+ data.tar.gz: a82af3a1afcd6baa5cdc8e04521db679670b854631843fe3171714f3f4900488
5
5
  SHA512:
6
- metadata.gz: 7d562d84a39c415c90b417f5b49afc5330eef0f59112b282e34a69138124f1d4bc73f98ee806b400e92f30ede925f287a0ece9c12f2c136199848ef2a528f649
7
- data.tar.gz: 9ef56bc7f181fcd779f0ef1767b94b7304c2e28e6be4b1e5994ceebcd00db62cfe30762a7d81069324c924bc6e6a01387e8bf339a101050ea739420f1f623c60
6
+ metadata.gz: b0c28a22832ce80a471d1fa16483fb0fcbab35550879e8132b6ee4849633489ce49cac1c03dc377ee0936033c9db9d144892644a598313e6d6a20efbf8eda5e4
7
+ data.tar.gz: 60fc3ff9026c5984d9548a9f420e65d7c3bd223efb29fd96daafbbcb3d725ac3ddd680d94d3121a60fe5b0a21c9ea8d08fa14128ca6883305e921a151360f270
@@ -4,6 +4,14 @@ inherit_gem:
4
4
  AllCops:
5
5
  DisplayCopNames: true
6
6
  TargetRubyVersion: 2.5
7
+ Include:
8
+ - bin/console
9
+ - Gemfile
10
+ - ezclient.gemspec
11
+ - Rakefile
12
+ - lib/**/*
13
+ - spec/**/*
14
+ - gemfile/**/*
7
15
  Exclude:
8
16
  - vendor/**/*
9
17
  - gemfiles/**/*
@@ -1,30 +1,17 @@
1
1
  language: ruby
2
2
 
3
- sudo: false
3
+ os: linux
4
4
 
5
- rvm:
6
- - 2.3
7
- - 2.4
8
- - 2.5
9
- - ruby-head
10
-
11
- gemfile:
12
- - gemfiles/http3.gemfile
13
- - Gemfile
5
+ dist: xenial
14
6
 
15
7
  before_install: gem install bundler
16
8
 
17
- env: SUITE="rspec"
18
-
19
- script: bundle exec $SUITE
20
-
21
- matrix:
9
+ jobs:
22
10
  fast_finish: true
23
- # Only run RuboCop on the latest Ruby
24
11
  include:
25
- - rvm: 2.5
26
- env: SUITE="rubocop"
12
+ - rvm: 2.5
13
+ - rvm: 2.6
14
+ - rvm: 2.7
15
+ - rvm: ruby-head
27
16
  allow_failures:
28
- - rvm: ruby-head
29
-
30
- cache: bundler
17
+ - rvm: ruby-head
data/Gemfile CHANGED
@@ -2,5 +2,3 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
-
6
- gem "http", github: "httprb/http"
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Yuri Smirnov
3
+ Copyright (c) 2018-2019 Yuri Smirnov
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # EzClient   [![Gem Version](https://badge.fury.io/rb/ezclient.svg)](https://badge.fury.io/rb/ezclient) [![Build Status](https://travis-ci.org/umbrellio/ezclient.svg?branch=master)](https://travis-ci.org/umbrellio/ezclient) [![Coverage Status](https://coveralls.io/repos/github/umbrellio/ezclient/badge.svg?branch=master)](https://coveralls.io/github/umbrellio/ezclient?branch=master)
2
+
2
3
  EzClient is [HTTP gem](https://github.com/httprb/http) wrapper for easy persistent connections and more.
3
4
 
4
5
  ## Installation
6
+
5
7
  Add this line to your application's Gemfile:
6
8
 
7
9
  ```ruby
@@ -9,6 +11,7 @@ gem "ezclient"
9
11
  ```
10
12
 
11
13
  ## Usage
14
+
12
15
  ```ruby
13
16
  url = "http://example.com"
14
17
 
@@ -29,6 +32,7 @@ response = client.perform!(:get, url, request_options) # => EzClient::Response o
29
32
  ```
30
33
 
31
34
  Valid client options are:
35
+
32
36
  - `api_auth` – arguments for `ApiAuth.sign!` (see https://github.com/mgomes/api_auth)
33
37
  - `basic_auth` – arguments for basic authentication (either a hash with `:user` and `:pass` keys or a two-element array)
34
38
  - `headers` – a hash of headers for requests
@@ -39,11 +43,13 @@ Valid client options are:
39
43
  - `on_retry` – callback called on request retry
40
44
  - `retry_exceptions` – an array of exception classes to retry
41
45
  - `ssl_context` – ssl context for requests (an `OpenSSL::SSL::SSLContext` instance)
42
- - `timeout` – timeout for requests in seconds
46
+ - `timeout` – timeout for requests in seconds or hash like `{ read: 5, write: 5, connect: 1 }`
47
+ - `follow` - enable following redirects (`true` or hash with options – e.g. `{ max_hops: 1, strict: false}`)
43
48
 
44
49
  All these options are passed to each request made by this client but can be overriden on per-request basis.
45
50
 
46
51
  Extra per-request only options are:
52
+
47
53
  - `body` – raw request body
48
54
  - `form` – hash for urlencoded body
49
55
  - `json` – data for json (also adds `application/json` content-type header)
@@ -52,14 +58,32 @@ Extra per-request only options are:
52
58
  - `query` – hash for uri query
53
59
 
54
60
  ## Persistent connections
61
+
55
62
  If you provide `keep_alive` option to the client or particular request, the connection will be stored in the client and then
56
63
  reused for all following requests to the same origin within specified amount of time.
57
64
 
58
- Note that, as of now, EzClient will
65
+ Note that if you are using persistent connections, you shouldn't store your client in a variable that is accessable by different threads. See the example:
66
+
67
+ ```ruby
68
+ module MyApp
69
+ # Bad: multiple threads will use the same socket
70
+ def self.bad_client
71
+ @ezclient ||= EzClient.new(keep_alive: 100)
72
+ end
73
+
74
+ # Good: each thread has it's own socket
75
+ def self.good_client
76
+ Thread.current[:ezclient] ||= EzClient.new(keep_alive: 100)
77
+ end
78
+ end
79
+ ```
80
+
81
+ Alose note that, as of now, EzClient will
59
82
  automatically retry the request on any `HTTP::ConnectionError` exception in this case which may possibly result in two requests
60
83
  received by a server (see https://github.com/httprb/http/issues/459).
61
84
 
62
85
  ## Callbacks and retrying
86
+
63
87
  You can provide `on_complete`, `on_error` and `on_retry` callbacks like this:
64
88
 
65
89
  ```ruby
@@ -79,12 +103,14 @@ response = client.perform!(:get, url, metadata: :hello)
79
103
  ```
80
104
 
81
105
  The arguments passed into callbacks are:
106
+
82
107
  - `request` – an `EzClient::Request` instance
83
108
  - `response` – an `EzClient::Response` instance
84
109
  - `error` – an exception instance
85
110
  - `metadata` - the `metadata` option passed into a request
86
111
 
87
112
  ## Request object
113
+
88
114
  ```ruby
89
115
  request = client.request(:post, "http://example.com", json: { a: 1 }, timeout: 15)
90
116
 
@@ -95,6 +121,7 @@ request.headers # => { "Content-Type" => "application/json; charset=UTF-8", ...
95
121
  ```
96
122
 
97
123
  ## Response object
124
+
98
125
  ```ruby
99
126
  response = request.perform(...)
100
127
 
@@ -110,12 +137,15 @@ response.error? # Returns if request was 4xx or 5xx status
110
137
  ```
111
138
 
112
139
  ## Contributing
140
+
113
141
  Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/ezclient.
114
142
 
115
143
  ## License
144
+
116
145
  Released under MIT License.
117
146
 
118
147
  ## Authors
148
+
119
149
  Created by Yuri Smirnov.
120
150
 
121
151
  <a href="https://github.com/umbrellio/">
data/Rakefile CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new(:lint)
7
9
 
8
- task default: :spec
10
+ task default: %i[lint spec]
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ezclient"
6
+
7
+ require "pry"
8
+ Pry.start
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("bundle", __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require "ezclient/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = "ezclient"
9
- spec.version = EzClient::VERSION
10
- spec.authors = ["Yuri Smirnov"]
11
- spec.email = ["tycooon@yandex.ru"]
8
+ spec.required_ruby_version = ">= 2.5.0"
12
9
 
13
- spec.summary = "An HTTP gem wrapper for easy persistent connections and more."
14
- spec.homepage = "https://github.com/umbrellio/ezclient"
15
- spec.license = "MIT"
10
+ spec.name = "ezclient"
11
+ spec.version = EzClient::VERSION
12
+ spec.authors = ["Yuri Smirnov"]
13
+ spec.email = ["tycooon@yandex.ru", "oss@umbrellio.biz"]
16
14
 
17
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
15
+ spec.summary = "An HTTP gem wrapper for easy persistent connections and more."
16
+ spec.homepage = "https://github.com/umbrellio/ezclient"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
18
20
  spec.require_paths = ["lib"]
19
21
 
20
- spec.add_runtime_dependency "http", ">= 3.3"
22
+ spec.add_runtime_dependency "http", ">= 4"
21
23
 
22
24
  spec.add_development_dependency "bundler"
23
25
  spec.add_development_dependency "coveralls"
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "http"
4
- require "ezclient/version"
5
- require "ezclient/client"
6
- require "ezclient/request"
7
- require "ezclient/response"
8
- require "ezclient/errors"
9
- require "ezclient/check_options"
4
+
5
+ require_relative "ezclient/version"
6
+ require_relative "ezclient/client"
7
+ require_relative "ezclient/persistent_client"
8
+ require_relative "ezclient/persistent_client_registry"
9
+ require_relative "ezclient/request"
10
+ require_relative "ezclient/response"
11
+ require_relative "ezclient/errors"
12
+ require_relative "ezclient/check_options"
10
13
 
11
14
  module EzClient
12
15
  def self.new(*args)
13
16
  Client.new(*args)
14
17
  end
18
+
19
+ def self.get_time
20
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
21
+ end
15
22
  end
@@ -14,11 +14,11 @@ class EzClient::Client
14
14
  retry_exceptions
15
15
  ssl_context
16
16
  timeout
17
+ follow
17
18
  ].freeze
18
19
 
19
20
  def initialize(options = {})
20
21
  self.request_options = options
21
- self.clients = {}
22
22
  EzClient::CheckOptions.call(options, REQUEST_OPTION_KEYS)
23
23
  end
24
24
 
@@ -29,7 +29,7 @@ class EzClient::Client
29
29
  api_auth = options.delete(:api_auth)
30
30
 
31
31
  if keep_alive_timeout
32
- client = persistent_client_for(url, timeout: keep_alive_timeout)
32
+ client = persistent_client_registry.for(url, timeout: keep_alive_timeout)
33
33
  else
34
34
  client = HTTP::Client.new
35
35
  end
@@ -39,20 +39,19 @@ class EzClient::Client
39
39
  end
40
40
  end
41
41
 
42
- def perform(*args)
43
- request(*args).perform
42
+ def perform(*args, **kwargs)
43
+ request(*args, **kwargs).perform
44
44
  end
45
45
 
46
- def perform!(*args)
47
- request(*args).perform!
46
+ def perform!(*args, **kwargs)
47
+ request(*args, **kwargs).perform!
48
48
  end
49
49
 
50
50
  private
51
51
 
52
- attr_accessor :request_options, :clients
52
+ attr_accessor :request_options
53
53
 
54
- def persistent_client_for(url, timeout: 600)
55
- uri = HTTP::URI.parse(url)
56
- clients[uri.origin] ||= HTTP.persistent(uri.origin, timeout: timeout)
54
+ def persistent_client_registry
55
+ @persistent_client_registry ||= EzClient::PersistentClientRegistry.new
57
56
  end
58
57
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EzClient::PersistentClient
4
+ extend Forwardable
5
+
6
+ def_delegators :http_client, :build_request, :default_options, :timeout
7
+
8
+ attr_accessor :origin, :keep_alive_timeout, :last_request_at
9
+
10
+ def initialize(origin, keep_alive_timeout)
11
+ self.origin = origin
12
+ self.keep_alive_timeout = keep_alive_timeout
13
+ self.last_request_at = nil
14
+ end
15
+
16
+ def perform(*args)
17
+ http_client.perform(*args).tap do
18
+ self.last_request_at = EzClient.get_time
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def http_client
25
+ @http_client ||= HTTP.persistent(origin, timeout: keep_alive_timeout)
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EzClient::PersistentClientRegistry
4
+ def initialize
5
+ self.registry = {}
6
+ end
7
+
8
+ def for(url, timeout:)
9
+ cleanup_registry!
10
+ uri = HTTP::URI.parse(url)
11
+ registry[uri.origin] ||= EzClient::PersistentClient.new(uri.origin, timeout)
12
+ end
13
+
14
+ private
15
+
16
+ attr_accessor :registry
17
+
18
+ def cleanup_registry!
19
+ registry.delete_if do |_key, value|
20
+ EzClient.get_time - value.last_request_at >= value.keep_alive_timeout
21
+ end
22
+ end
23
+ end
@@ -47,6 +47,10 @@ class EzClient::Request
47
47
  self
48
48
  end
49
49
 
50
+ def uri
51
+ http_request.uri
52
+ end
53
+
50
54
  def body
51
55
  body = +""
52
56
  http_request.body.each { |chunk| body << chunk }
@@ -97,13 +101,22 @@ class EzClient::Request
97
101
  end
98
102
 
99
103
  def perform_request
104
+ with_retry do
105
+ # Use original client so that connection can be reused
106
+ res = client.perform(http_request, http_options)
107
+ return res unless follow
108
+
109
+ HTTP::Redirector.new(follow).perform(http_request, res) do |request|
110
+ client.perform(request, http_options)
111
+ end
112
+ end
113
+ end
114
+
115
+ def with_retry(&block)
100
116
  retries = 0
101
117
 
102
118
  begin
103
- retry_on_connection_error do
104
- # Use original client so that connection can be reused
105
- client.perform(http_request, http_options)
106
- end
119
+ retry_on_connection_error(&block)
107
120
  rescue *retried_exceptions => error
108
121
  if retries < max_retries.to_i
109
122
  retries += 1
@@ -119,14 +132,18 @@ class EzClient::Request
119
132
  # This may result in 2 requests reaching the server so I hope HTTP fixes it
120
133
  # https://github.com/httprb/http/issues/459
121
134
  yield
122
- rescue HTTP::ConnectionError, IOError, SocketError, SystemCallError => error
123
- # TODO: remove IOError, SocketError, SystemCallError after we drop HTTP v3 support
135
+ rescue HTTP::ConnectionError => error
124
136
  on_retry.call(self, error, options[:metadata])
125
137
  yield
126
138
  end
127
139
 
128
140
  def timeout
129
- options[:timeout]&.to_f
141
+ case options[:timeout]
142
+ when Hash
143
+ options[:timeout].transform_values! { |value| value&.to_f }
144
+ else
145
+ options[:timeout]&.to_f
146
+ end
130
147
  end
131
148
 
132
149
  def on_complete
@@ -149,6 +166,11 @@ class EzClient::Request
149
166
  options[:max_retries] || 1
150
167
  end
151
168
 
169
+ def follow
170
+ return unless options[:follow]
171
+ options[:follow].is_a?(Hash) ? options[:follow] : {}
172
+ end
173
+
152
174
  def prepare_headers(headers)
153
175
  headers = HTTP::Headers.coerce(headers)
154
176
  headers[:user_agent] ||= "ezclient/#{EzClient::VERSION}"
@@ -172,17 +194,7 @@ class EzClient::Request
172
194
  end
173
195
 
174
196
  def set_timeout(client)
175
- return client unless timeout
176
-
177
- timeout_args =
178
- # for HTTP v3
179
- if HTTP::Timeout::Global.ancestors.include?(HTTP::Timeout::PerOperation)
180
- [:global, read: timeout, write: 0, connect: 0]
181
- else
182
- [timeout]
183
- end
184
-
185
- client.timeout(*timeout_args)
197
+ timeout ? client.timeout(timeout) : client
186
198
  end
187
199
 
188
200
  def basic_auth
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EzClient
4
- VERSION = "1.0.0"
4
+ VERSION = "1.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ezclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Smirnov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-21 00:00:00.000000000 Z
11
+ date: 2020-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.3'
19
+ version: '4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.3'
26
+ version: '4'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -136,9 +136,10 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- description:
139
+ description:
140
140
  email:
141
141
  - tycooon@yandex.ru
142
+ - oss@umbrellio.biz
142
143
  executables: []
143
144
  extensions: []
144
145
  extra_rdoc_files: []
@@ -151,12 +152,16 @@ files:
151
152
  - LICENSE.txt
152
153
  - README.md
153
154
  - Rakefile
155
+ - bin/console
156
+ - bin/rspec
154
157
  - ezclient.gemspec
155
158
  - gemfiles/http3.gemfile
156
159
  - lib/ezclient.rb
157
160
  - lib/ezclient/check_options.rb
158
161
  - lib/ezclient/client.rb
159
162
  - lib/ezclient/errors.rb
163
+ - lib/ezclient/persistent_client.rb
164
+ - lib/ezclient/persistent_client_registry.rb
160
165
  - lib/ezclient/request.rb
161
166
  - lib/ezclient/response.rb
162
167
  - lib/ezclient/version.rb
@@ -164,7 +169,7 @@ homepage: https://github.com/umbrellio/ezclient
164
169
  licenses:
165
170
  - MIT
166
171
  metadata: {}
167
- post_install_message:
172
+ post_install_message:
168
173
  rdoc_options: []
169
174
  require_paths:
170
175
  - lib
@@ -172,16 +177,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
177
  requirements:
173
178
  - - ">="
174
179
  - !ruby/object:Gem::Version
175
- version: '0'
180
+ version: 2.5.0
176
181
  required_rubygems_version: !ruby/object:Gem::Requirement
177
182
  requirements:
178
183
  - - ">="
179
184
  - !ruby/object:Gem::Version
180
185
  version: '0'
181
186
  requirements: []
182
- rubyforge_project:
183
- rubygems_version: 2.7.7
184
- signing_key:
187
+ rubygems_version: 3.1.2
188
+ signing_key:
185
189
  specification_version: 4
186
190
  summary: An HTTP gem wrapper for easy persistent connections and more.
187
191
  test_files: []