httpx 0.0.1

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +191 -0
  3. data/README.md +119 -0
  4. data/lib/httpx.rb +50 -0
  5. data/lib/httpx/buffer.rb +34 -0
  6. data/lib/httpx/callbacks.rb +32 -0
  7. data/lib/httpx/chainable.rb +51 -0
  8. data/lib/httpx/channel.rb +222 -0
  9. data/lib/httpx/channel/http1.rb +220 -0
  10. data/lib/httpx/channel/http2.rb +224 -0
  11. data/lib/httpx/client.rb +173 -0
  12. data/lib/httpx/connection.rb +74 -0
  13. data/lib/httpx/errors.rb +7 -0
  14. data/lib/httpx/extensions.rb +52 -0
  15. data/lib/httpx/headers.rb +152 -0
  16. data/lib/httpx/io.rb +240 -0
  17. data/lib/httpx/loggable.rb +11 -0
  18. data/lib/httpx/options.rb +138 -0
  19. data/lib/httpx/plugins/authentication.rb +14 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +20 -0
  21. data/lib/httpx/plugins/compression.rb +123 -0
  22. data/lib/httpx/plugins/compression/brotli.rb +55 -0
  23. data/lib/httpx/plugins/compression/deflate.rb +50 -0
  24. data/lib/httpx/plugins/compression/gzip.rb +59 -0
  25. data/lib/httpx/plugins/cookies.rb +63 -0
  26. data/lib/httpx/plugins/digest_authentication.rb +141 -0
  27. data/lib/httpx/plugins/follow_redirects.rb +72 -0
  28. data/lib/httpx/plugins/h2c.rb +85 -0
  29. data/lib/httpx/plugins/proxy.rb +108 -0
  30. data/lib/httpx/plugins/proxy/http.rb +115 -0
  31. data/lib/httpx/plugins/proxy/socks4.rb +110 -0
  32. data/lib/httpx/plugins/proxy/socks5.rb +152 -0
  33. data/lib/httpx/plugins/push_promise.rb +67 -0
  34. data/lib/httpx/plugins/stream.rb +33 -0
  35. data/lib/httpx/registry.rb +88 -0
  36. data/lib/httpx/request.rb +222 -0
  37. data/lib/httpx/response.rb +225 -0
  38. data/lib/httpx/selector.rb +155 -0
  39. data/lib/httpx/timeout.rb +68 -0
  40. data/lib/httpx/transcoder.rb +12 -0
  41. data/lib/httpx/transcoder/body.rb +56 -0
  42. data/lib/httpx/transcoder/chunker.rb +38 -0
  43. data/lib/httpx/transcoder/form.rb +41 -0
  44. data/lib/httpx/transcoder/json.rb +36 -0
  45. data/lib/httpx/version.rb +5 -0
  46. metadata +150 -0
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class Client
5
+ include Loggable
6
+ include Chainable
7
+
8
+ def initialize(options = {})
9
+ @options = self.class.default_options.merge(options)
10
+ @connection = Connection.new(@options)
11
+ @responses = {}
12
+ return unless block_given?
13
+ begin
14
+ @keep_open = true
15
+ yield self
16
+ ensure
17
+ @keep_open = false
18
+ close
19
+ end
20
+ end
21
+
22
+ def close
23
+ @connection.close
24
+ end
25
+
26
+ def request(*args, keep_open: @keep_open, **options)
27
+ requests = __build_reqs(*args, **options)
28
+ responses = __send_reqs(*requests, **options)
29
+ return responses.first if responses.size == 1
30
+ responses
31
+ ensure
32
+ close unless keep_open
33
+ end
34
+
35
+ private
36
+
37
+ def on_response(request, response)
38
+ @responses[request] = response
39
+ end
40
+
41
+ def on_promise(_, stream)
42
+ log(2, "#{stream.id}: ") { "refusing stream!" }
43
+ stream.refuse
44
+ # TODO: policy for handling promises
45
+ end
46
+
47
+ def fetch_response(request)
48
+ response = @responses.delete(request)
49
+ if response.is_a?(ErrorResponse) && response.retryable?
50
+ channel = find_channel(request)
51
+ channel.send(request, retries: response.retries - 1)
52
+ return
53
+ end
54
+ response
55
+ end
56
+
57
+ def find_channel(request, **options)
58
+ uri = URI(request.uri)
59
+ @connection.find_channel(uri) || begin
60
+ channel = @connection.build_channel(uri, **options)
61
+ set_channel_callbacks(channel)
62
+ channel
63
+ end
64
+ end
65
+
66
+ def set_channel_callbacks(channel)
67
+ channel.on(:response, &method(:on_response))
68
+ channel.on(:promise, &method(:on_promise))
69
+ end
70
+
71
+ def __build_reqs(*args, **options)
72
+ case args.size
73
+ when 1
74
+ reqs = args.first
75
+ reqs.map do |verb, uri|
76
+ __build_req(verb, uri, options)
77
+ end
78
+ when 2, 3
79
+ verb, uris = args
80
+ if uris.respond_to?(:each)
81
+ uris.map do |uri|
82
+ __build_req(verb, uri, options)
83
+ end
84
+ else
85
+ [__build_req(verb, uris, options)]
86
+ end
87
+ else
88
+ raise ArgumentError, "unsupported number of arguments"
89
+ end
90
+ end
91
+
92
+ def __send_reqs(*requests, **options)
93
+ requests.each do |request|
94
+ channel = find_channel(request, **options)
95
+ channel.send(request)
96
+ end
97
+ responses = []
98
+
99
+ # guarantee ordered responses
100
+ loop do
101
+ begin
102
+ request = requests.first
103
+ @connection.next_tick until (response = fetch_response(request))
104
+
105
+ responses << response
106
+ requests.shift
107
+
108
+ break if requests.empty?
109
+ rescue TimeoutError => e
110
+ responses << ErrorResponse.new(e.message, 0) while requests.shift
111
+ @connection.reset
112
+ break
113
+ end
114
+ end
115
+ requests.size == 1 ? responses.first : responses
116
+ end
117
+
118
+ def __build_req(verb, uri, options = {})
119
+ rklass = @options.request_class
120
+ rklass.new(verb, uri, @options.merge(options))
121
+ end
122
+
123
+ @default_options = Options.new
124
+ @plugins = []
125
+
126
+ class << self
127
+ attr_reader :default_options
128
+
129
+ def inherited(klass)
130
+ super
131
+ klass.instance_variable_set(:@default_options, @default_options.dup)
132
+ klass.instance_variable_set(:@plugins, @plugins.dup)
133
+ end
134
+
135
+ def plugin(pl, *args, &block)
136
+ # raise Error, "Cannot add a plugin to a frozen config" if frozen?
137
+ pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol)
138
+ unless @plugins.include?(pl)
139
+ @plugins << pl
140
+ pl.load_dependencies(self, *args, &block) if pl.respond_to?(:load_dependencies)
141
+ include(pl::InstanceMethods) if defined?(pl::InstanceMethods)
142
+ extend(pl::ClassMethods) if defined?(pl::ClassMethods)
143
+ if defined?(pl::OptionsMethods) || defined?(pl::OptionsClassMethods)
144
+ options_klass = Class.new(@default_options.class)
145
+ options_klass.extend(pl::OptionsClassMethods) if defined?(pl::OptionsClassMethods)
146
+ options_klass.__send__(:include, pl::OptionsMethods) if defined?(pl::OptionsMethods)
147
+ @default_options = options_klass.new
148
+ end
149
+ opts = default_options
150
+ opts.request_class.__send__(:include, pl::RequestMethods) if defined?(pl::RequestMethods)
151
+ opts.request_class.extend(pl::RequestClassMethods) if defined?(pl::RequestClassMethods)
152
+ opts.response_class.__send__(:include, pl::ResponseMethods) if defined?(pl::ResponseMethods)
153
+ opts.response_class.extend(pl::ResponseClassMethods) if defined?(pl::ResponseClassMethods)
154
+ opts.headers_class.__send__(:include, pl::HeadersMethods) if defined?(pl::HeadersMethods)
155
+ opts.headers_class.extend(pl::HeadersClassMethods) if defined?(pl::HeadersClassMethods)
156
+ opts.request_body_class.__send__(:include, pl::RequestBodyMethods) if defined?(pl::RequestBodyMethods)
157
+ opts.request_body_class.extend(pl::RequestBodyClassMethods) if defined?(pl::RequestBodyClassMethods)
158
+ opts.response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
159
+ opts.response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
160
+ pl.configure(self, *args, &block) if pl.respond_to?(:configure)
161
+ end
162
+ self
163
+ end
164
+
165
+ def plugins(pls)
166
+ pls.each do |pl, *args|
167
+ plugin(pl, *args)
168
+ end
169
+ self
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx/selector"
4
+ require "httpx/channel"
5
+
6
+ module HTTPX
7
+ class Connection
8
+ def initialize(options)
9
+ @options = Options.new(options)
10
+ @timeout = options.timeout
11
+ @selector = Selector.new
12
+ @channels = []
13
+ end
14
+
15
+ def running?
16
+ !@channels.empty?
17
+ end
18
+
19
+ def next_tick(timeout: @timeout.timeout)
20
+ @selector.select(timeout) do |monitor|
21
+ if (channel = monitor.value)
22
+ consume(channel)
23
+ end
24
+ monitor.interests = channel.interests
25
+ end
26
+ end
27
+
28
+ def close(channel = nil)
29
+ if channel
30
+ channel.close
31
+ else
32
+ @channels.each(&:close)
33
+ next_tick until @selector.empty?
34
+ end
35
+ end
36
+
37
+ def reset
38
+ @channels.each(&:reset)
39
+ end
40
+
41
+ def build_channel(uri, **options)
42
+ channel = Channel.by(uri, @options.merge(options))
43
+ register_channel(channel)
44
+ channel
45
+ end
46
+
47
+ # opens a channel to the IP reachable through +uri+.
48
+ # Many hostnames are reachable through the same IP, so we try to
49
+ # maximize pipelining by opening as few channels as possible.
50
+ #
51
+ def find_channel(uri)
52
+ @channels.find do |channel|
53
+ channel.match?(uri)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def register_channel(channel)
60
+ monitor = @selector.register(channel, :w)
61
+ monitor.value = channel
62
+ channel.on(:close) do
63
+ @channels.delete(channel)
64
+ @selector.deregister(channel)
65
+ end
66
+ @channels << channel
67
+ end
68
+
69
+ def consume(channel)
70
+ ch = catch(:close) { channel.call }
71
+ close(ch) if ch
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ Error = Class.new(StandardError)
5
+
6
+ TimeoutError = Class.new(Error)
7
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless Method.method_defined?(:curry)
4
+
5
+ # Backport
6
+ #
7
+ # Ruby 2.1 and lower implement curry only for Procs.
8
+ #
9
+ # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
10
+ #
11
+ module CurryMethods # :nodoc:
12
+ # Backport for the Method#curry method, which is part of ruby core since 2.2 .
13
+ #
14
+ def curry(*args)
15
+ to_proc.curry(*args)
16
+ end
17
+ end
18
+ Method.__send__(:include, CurryMethods)
19
+ end
20
+
21
+ unless String.method_defined?(:+@)
22
+ # Backport for +"", to initialize unfrozen strings from the string literal.
23
+ #
24
+ module LiteralStringExtensions
25
+ def +@
26
+ frozen? ? dup : self
27
+ end
28
+ end
29
+ String.__send__(:include, LiteralStringExtensions)
30
+ end
31
+
32
+ unless Numeric.method_defined?(:positive?)
33
+ # Ruby 2.3 Backport (Numeric#positive?)
34
+ #
35
+ module PosMethods
36
+ def positive?
37
+ self > 0
38
+ end
39
+ end
40
+ Numeric.__send__(:include, PosMethods)
41
+ end
42
+
43
+ unless Numeric.method_defined?(:negative?)
44
+ # Ruby 2.3 Backport (Numeric#negative?)
45
+ #
46
+ module NegMethods
47
+ def negative?
48
+ self < 0
49
+ end
50
+ end
51
+ Numeric.__send__(:include, NegMethods)
52
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class Headers
5
+ EMPTY = [].freeze # :nodoc:
6
+
7
+ class << self
8
+ def new(h = nil)
9
+ return h if h.is_a?(self)
10
+ super
11
+ end
12
+ end
13
+
14
+ def initialize(h = nil)
15
+ @headers = {}
16
+ return unless h
17
+ h.each do |field, value|
18
+ array_value(value).each do |v|
19
+ add(downcased(field), v)
20
+ end
21
+ end
22
+ end
23
+
24
+ # cloned initialization
25
+ def initialize_clone(orig)
26
+ super
27
+ @headers = orig.instance_variable_get(:@headers).clone
28
+ end
29
+
30
+ # dupped initialization
31
+ def initialize_dup(orig)
32
+ super
33
+ @headers = orig.instance_variable_get(:@headers).dup
34
+ end
35
+
36
+ # freezes the headers hash
37
+ def freeze
38
+ @headers.freeze
39
+ super
40
+ end
41
+
42
+ # merges headers with another header-quack.
43
+ # the merge rule is, if the header already exists,
44
+ # ignore what the +other+ headers has. Otherwise, set
45
+ #
46
+ def merge(other)
47
+ # TODO: deep-copy
48
+ headers = dup
49
+ other.each do |field, value|
50
+ headers[field] = value
51
+ end
52
+ headers
53
+ end
54
+
55
+ # returns the comma-separated values of the header field
56
+ # identified by +field+, or nil otherwise.
57
+ #
58
+ def [](field)
59
+ a = @headers[downcased(field)] || return
60
+ a.join(",")
61
+ end
62
+
63
+ # sets +value+ (if not nil) as single value for the +field+ header.
64
+ #
65
+ def []=(field, value)
66
+ return unless value
67
+ @headers[downcased(field)] = array_value(value)
68
+ end
69
+
70
+ # deletes all values associated with +field+ header.
71
+ #
72
+ def delete(field)
73
+ canonical = downcased(field)
74
+ @headers.delete(canonical) if @headers.key?(canonical)
75
+ end
76
+
77
+ # adds additional +value+ to the existing, for header +field+.
78
+ #
79
+ def add(field, value)
80
+ (@headers[downcased(field)] ||= []) << String(value)
81
+ end
82
+
83
+ # helper to be used when adding an header field as a value to another field
84
+ #
85
+ # h2_headers.add_header("vary", "accept-encoding")
86
+ # h2_headers["vary"] #=> "accept-encoding"
87
+ # h1_headers.add_header("vary", "accept-encoding")
88
+ # h1_headers["vary"] #=> "Accept-Encoding"
89
+ #
90
+ alias_method :add_header, :add
91
+
92
+ # returns the enumerable headers store in pairs of header field + the values in
93
+ # the comma-separated string format
94
+ #
95
+ def each
96
+ return enum_for(__method__) { @headers.size } unless block_given?
97
+ @headers.each do |field, value|
98
+ yield(field, value.join(", ")) unless value.empty?
99
+ end
100
+ end
101
+
102
+ def ==(other)
103
+ to_hash == Headers.new(other).to_hash
104
+ end
105
+
106
+ # the headers store in Hash format
107
+ def to_hash
108
+ Hash[to_a]
109
+ end
110
+
111
+ # the headers store in array of pairs format
112
+ def to_a
113
+ Array(each)
114
+ end
115
+
116
+ # headers as string
117
+ def to_s
118
+ @headers.to_s
119
+ end
120
+
121
+ # this is internal API and doesn't abide to other public API
122
+ # guarantees, like downcasing strings.
123
+ # Please do not use this outside of core!
124
+ #
125
+ def key?(downcased_key)
126
+ @headers.key?(downcased_key)
127
+ end
128
+
129
+ # returns the values for the +field+ header in array format.
130
+ # This method is more internal, and for this reason doesn't try
131
+ # to "correct" the user input, i.e. it doesn't downcase the key.
132
+ #
133
+ def get(field)
134
+ @headers[field] || EMPTY
135
+ end
136
+
137
+ private
138
+
139
+ def array_value(value)
140
+ case value
141
+ when Array
142
+ value.map { |val| String(val) }
143
+ else
144
+ [String(value)]
145
+ end
146
+ end
147
+
148
+ def downcased(field)
149
+ String(field).downcase
150
+ end
151
+ end
152
+ end