ruby-requests 0.0.1.a1

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.
@@ -0,0 +1,124 @@
1
+ require 'net/http'
2
+ require 'openssl'
3
+
4
+
5
+ ##
6
+ # Initial implementation here will be very simple and lack features like
7
+ # connection pooling.
8
+
9
+ module Requests
10
+ class BaseAdapter
11
+ attr_reader :logger
12
+ private :logger
13
+
14
+ def initialize
15
+ @logger = Requests.logger
16
+ end
17
+
18
+ # :nocov:
19
+ # rubocop:disable Lint/UnusedMethodArgument
20
+ def send_request(_request, stream: false, timeout: nil, verify: true,
21
+ cert: nil, proxies: {})
22
+ raise(NotImplementedError, "send_request must be implemented by " \
23
+ "subclasses of #{self.class.name}")
24
+ end
25
+ # rubocop:enable Lint/UnusedMethodArgument
26
+
27
+ def close
28
+ raise(NotImplementedError, "close must be implemented by " \
29
+ "subclasses of #{self.class.name}")
30
+ end
31
+ # :nocov:
32
+
33
+ def determine_proxy(uri, proxies)
34
+ scheme_host = "#{uri.scheme}://#{uri.host}"
35
+ proxy = proxies[scheme_host] || proxies[uri.scheme]
36
+ proxy ? URI(proxy) : nil
37
+ end
38
+ end
39
+
40
+ class HTTPAdapter < BaseAdapter
41
+ def send_request(request, stream: false, timeout: nil, verify: true,
42
+ cert: nil, proxies: {})
43
+ logger.info("Request URI: #{request.uri.inspect}")
44
+
45
+ if stream
46
+ raise(NotImplementedError,
47
+ "streaming response body is not yet implemented")
48
+ end
49
+
50
+ proxy = determine_proxy(request.uri, proxies)
51
+ http = create_http(request.uri, proxy, verify, cert)
52
+
53
+ if timeout.is_a?(Array)
54
+ http.open_timeout, http.read_timeout = timeout
55
+ else
56
+ http.open_timeout = timeout
57
+ http.read_timeout = timeout
58
+ end
59
+
60
+ # :nocov:
61
+ if ENV['REQUESTS_HTTP_DEBUG'] == 'true'
62
+ logger.debug("Setting Net::HTTP debug output to stdout")
63
+ http.set_debug_output($stdout)
64
+ end
65
+ # :nocov:
66
+
67
+ begin
68
+ resp = http.start do
69
+ http.request(request.raw)
70
+ end
71
+ rescue Net::OpenTimeout => e
72
+ raise ConnectionTimeout.new('Connection timed out.',
73
+ from_error: e, request: request)
74
+ rescue Net::ReadTimeout => e
75
+ raise ReadTimeout.new('Read timed out.',
76
+ from_error: e, request: request)
77
+ rescue Net::HTTPServerException => e
78
+ if e.message == '407 "Proxy Authentication Required"'
79
+ resp = Response.from_net_http_response(e.response,
80
+ request)
81
+ raise ProxyAuthError.new(e.message,
82
+ from_error: e,
83
+ request: request,
84
+ response: resp)
85
+ else
86
+ raise e
87
+ end
88
+ end
89
+
90
+ body_size = resp.body ? resp.body.bytesize : 0
91
+ logger.debug("Response body: #{body_size} bytes")
92
+
93
+ # TODO: handle redirects
94
+
95
+ response = Response.from_net_http_response(resp, request)
96
+ logger.info("Response: #{response.inspect}")
97
+ response
98
+ end
99
+
100
+ def create_http(uri, proxy, verify, _cert)
101
+ if proxy
102
+ logger.info("Using proxy: #{proxy.scheme}://#{proxy.host}:" \
103
+ "#{proxy.port}")
104
+ http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port)
105
+ http.proxy_user = proxy.user
106
+ http.proxy_pass = proxy.password
107
+ else
108
+ http = Net::HTTP.new(uri.host, uri.port, nil, nil)
109
+ end
110
+
111
+ http.use_ssl = (uri.scheme == 'https')
112
+
113
+ if verify == false
114
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
115
+ elsif verify.is_a?(String)
116
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
117
+ http.ca_file = verify
118
+ else
119
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
120
+ end
121
+ http
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,98 @@
1
+ require 'http-cookie'
2
+
3
+
4
+ module Requests
5
+ class Cookie < HTTP::Cookie
6
+ end
7
+
8
+ class CookieJar < HTTP::CookieJar
9
+ ##
10
+ # Return Hash of cookies from this jar for a given uri.
11
+ # If provided no argument, it returns a Hash of all cookie
12
+ # key/value pairs in the jar, but this can lose information
13
+ # if the same cookie key was stored for different URIs.
14
+ #
15
+ # @param uri [URI String] optional
16
+
17
+ def to_hash(uri=nil)
18
+ hash = {}
19
+ cookies(uri).each do |c|
20
+ hash[c.name] = c.value
21
+ end
22
+ hash
23
+ end
24
+
25
+ ##
26
+ # Given another CookieJar, copy the cookies with this one
27
+
28
+ def merge!(jar)
29
+ jar.each do |cookie|
30
+ add(cookie.dup)
31
+ end
32
+ self
33
+ end
34
+
35
+ ##
36
+ # Given an array of Set-Cookie header values, parse each
37
+ # and add them to this cookie jar.
38
+ #
39
+ # @param header [Array]
40
+ # @param uri [URI String] URI these cookies belong to
41
+
42
+ def set_cookies_from_header(header, uri)
43
+ header.each do |value|
44
+ parse(value, uri)
45
+ end
46
+ self
47
+ end
48
+
49
+ def self.from_hash(hash, domain)
50
+ jar = new
51
+ hash.each do |key, value|
52
+ cookie = Cookie.new(key, value, domain: domain,
53
+ path: '/', for_domain: true)
54
+ jar.add(cookie)
55
+ end
56
+ jar
57
+ end
58
+ end
59
+
60
+ module CookiesMixin
61
+ ##
62
+ # The following methods depend on the instance variable
63
+ # @uri being defined
64
+
65
+ def cookies=(cookies)
66
+ if cookies.is_a?(Hash)
67
+ domain = @uri.hostname.downcase
68
+ @cookies = CookieJar.from_hash(cookies, domain)
69
+ else
70
+ @cookies = cookies
71
+ end
72
+ end
73
+
74
+ def cookies
75
+ @cookies ||= CookieJar.new
76
+ @cookies
77
+ end
78
+
79
+ def cookies_hash
80
+ @cookies.to_hash(@uri)
81
+ end
82
+
83
+ def cookie_header
84
+ Cookie.cookie_value(@cookies.cookies(@uri))
85
+ end
86
+
87
+ ##
88
+ # Given an array of Set-Cookie header values, add them
89
+ # to @cookies
90
+
91
+ def set_cookies_from_header(header, uri)
92
+ return if header.nil? || header.empty?
93
+ @cookies ||= CookieJar.new
94
+ @cookies.set_cookies_from_header(header, uri)
95
+ @cookies
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,32 @@
1
+ require 'net/http'
2
+
3
+
4
+ module Requests
5
+ class RequestException < RuntimeError
6
+ attr_reader :request, :response, :message, :from_error
7
+
8
+ def initialize(msg, from_error: nil, request: nil, response: nil)
9
+ super(msg)
10
+ @message = msg
11
+ @from_error = from_error
12
+ @request = request
13
+ @response = response
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class.name} #{@message}>"
18
+ end
19
+ end
20
+
21
+ class TimeoutError < RequestException
22
+ end
23
+
24
+ class ConnectionTimeout < TimeoutError
25
+ end
26
+
27
+ class ReadTimeout < TimeoutError
28
+ end
29
+
30
+ class ProxyAuthError < RequestException
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ module Requests
2
+ module HttpMethods
3
+ def get(url, params: {}, **kwargs)
4
+ kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, true)
5
+ request('get', url, params: params, **kwargs)
6
+ end
7
+
8
+ def post(url, params: {}, data: nil, json: nil, **kwargs)
9
+ request('post', url, params: params, data: data,
10
+ json: json, **kwargs)
11
+ end
12
+
13
+ def put(url, params: {}, data: nil, json: nil, **kwargs)
14
+ request('put', url, params: params, data: data,
15
+ json: json, **kwargs)
16
+ end
17
+
18
+ def delete(url, **kwargs)
19
+ request('delete', url, **kwargs)
20
+ end
21
+
22
+ def head(url, **kwargs)
23
+ kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, false)
24
+ request('head', url, **kwargs)
25
+ end
26
+
27
+ def options(url, **kwargs)
28
+ kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, true)
29
+ request('options', url, **kwargs)
30
+ end
31
+
32
+ def patch(url, **kwargs)
33
+ request('patch', url, **kwargs)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ require 'logger'
2
+
3
+
4
+ module Requests
5
+ class LogFormatter
6
+ FORMAT = "%<severity>s [%<time>s] %<msg>s\n".freeze
7
+
8
+ def call(severity, time, _, msg)
9
+ FORMAT % {:severity => severity.ljust(5),
10
+ :time => format_time(time),
11
+ :msg => msg}
12
+ end
13
+
14
+ def format_time(time)
15
+ time.iso8601
16
+ end
17
+ end
18
+
19
+ class Logger < ::Logger
20
+ DEFAULT_LOG_DEV = STDOUT
21
+ DEFAULT_FORMATTER = LogFormatter.new
22
+
23
+ attr_accessor :enabled
24
+
25
+ def initialize(logdev, shift_age=0, shift_size=1048576, level: DEBUG,
26
+ enabled: true)
27
+ super(logdev, shift_age, shift_size)
28
+ @enabled = enabled
29
+ self.formatter = formatter || DEFAULT_FORMATTER
30
+ self.level = level
31
+ end
32
+
33
+ def add(severity, message=nil, progname=nil)
34
+ super(severity, message, progname) if @enabled
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+
6
+ module Requests
7
+ class Request
8
+ include CookiesMixin
9
+ include Utils::HeadersMixin
10
+
11
+ attr_accessor :method, :url, :uri, :files, :data, :params, :auth,
12
+ :json, :body
13
+ attr_reader :logger, :raw
14
+ private :logger
15
+
16
+ def initialize(method, url, headers: {}, files: nil, data: nil,
17
+ params: {}, auth: nil, cookies: nil, json: nil)
18
+ @logger = Requests.logger
19
+ @method = method
20
+ @url = url
21
+ @uri = URI(@url)
22
+ @headers = headers
23
+
24
+ @files = files
25
+ if @files
26
+ raise(NotImplementedError,
27
+ "files keyword param is not yet implemented")
28
+ end
29
+
30
+ @data = data
31
+ @params = params
32
+ @auth = auth
33
+ self.cookies = cookies
34
+ @json = json
35
+ end
36
+
37
+ def inspect
38
+ "#<Requests::Request #{@method}>"
39
+ end
40
+
41
+ def prepare
42
+ if @params && !@params.empty?
43
+ @uri.query = URI.encode_www_form(@params)
44
+ end
45
+ set_body_and_content_type
46
+ @raw = build_request
47
+ set_request_auth(@raw)
48
+ self.headers = @raw.to_hash
49
+ end
50
+
51
+ def set_body_and_content_type
52
+ if @json
53
+ @headers['Content-Type'] = 'application/json'
54
+ @body = @json.to_json
55
+ elsif @data.is_a?(String)
56
+ @headers['Content-Type'] ||= 'text/plain'
57
+ @body = @data
58
+ elsif @data.is_a?(Array)
59
+ @headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
60
+ @body = URI.encode_www_form(Hash[@data])
61
+ elsif @data.is_a?(Hash)
62
+ @headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
63
+ @body = URI.encode_www_form(@data)
64
+ end
65
+ end
66
+
67
+ def build_request
68
+ req = http_method_class.new(uri.request_uri)
69
+ req.body = body
70
+ @headers['cookie'] = cookie_header if !cookies.empty?
71
+
72
+ @headers.each do |key, value|
73
+ req[key] = value
74
+ end
75
+ req
76
+ end
77
+
78
+ ##
79
+ # @param request: Net::HTTPRequest object to set authentication on
80
+ # Currently only basic auth is supported
81
+
82
+ def set_request_auth(request)
83
+ if @auth && @auth.is_a?(Array)
84
+ user, pass = @auth
85
+ request.basic_auth(user, pass)
86
+ end
87
+ request
88
+ end
89
+
90
+ def http_method_class
91
+ Requests::Utils.http_method_class(@method)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,93 @@
1
+ require 'json'
2
+ require 'zlib'
3
+
4
+
5
+ module Requests
6
+ class Response
7
+ include CookiesMixin
8
+ include Utils::HeadersMixin
9
+
10
+ attr_accessor :status_code, :body, :request, :reason, :cookie_jar,
11
+ :uri, :raw
12
+
13
+ attr_reader :logger, :raw_headers
14
+ private :logger
15
+
16
+ def initialize(status_code:, reason:, headers:, body:, uri:,
17
+ request:, raw: nil)
18
+ @logger = Requests.logger
19
+ @status_code = status_code
20
+ @reason = reason
21
+ @raw_headers = Requests::Utils::InsensitiveDict.new(headers)
22
+ self.headers = headers
23
+ @body = body ? decode_content(body) : ''
24
+ @uri = URI(uri)
25
+ @request = request
26
+ @raw = raw
27
+ set_cookies_from_header(@raw_headers['set-cookie'], @request.uri)
28
+ fix_string_encoding
29
+ end
30
+
31
+ def inspect
32
+ "#<Requests::Response #{@status_code} #{@reason}>"
33
+ end
34
+
35
+ def url
36
+ uri.to_s
37
+ end
38
+
39
+ def text
40
+ body
41
+ end
42
+
43
+ def json
44
+ JSON.parse(body)
45
+ end
46
+
47
+ def decode_content(body)
48
+ if content_encoding == 'identity'
49
+ body
50
+ elsif content_encoding == 'gzip'
51
+ str_io = StringIO.new(body)
52
+ gz = Zlib::GzipReader.new(str_io)
53
+ gz.read
54
+ elsif content_encoding == 'deflate'
55
+ Zlib::Inflate.inflate(body)
56
+ end
57
+ end
58
+
59
+ def content_encoding
60
+ @headers['content-encoding'] || 'identity'
61
+ end
62
+
63
+ def fix_string_encoding
64
+ charset = charset_from_content_type
65
+
66
+ if charset
67
+ begin
68
+ encoding = Encoding.find(charset)
69
+ logger.debug("Forcing encoding for response body " \
70
+ "using charset: #{charset.inspect}")
71
+ @body.force_encoding(encoding)
72
+ rescue ArgumentError
73
+ logger.error("No such encoding: #{charset.inspect}")
74
+ end
75
+ end
76
+ end
77
+
78
+ def charset_from_content_type
79
+ Requests::Utils.charset_from_content_type(headers['Content-Type'])
80
+ end
81
+
82
+ def self.from_net_http_response(resp, request)
83
+ body = resp.body rescue nil
84
+ self.new(status_code: resp.code.to_i,
85
+ reason: resp.message,
86
+ headers: resp.to_hash,
87
+ body: body,
88
+ uri: request.uri,
89
+ request: request,
90
+ raw: resp)
91
+ end
92
+ end
93
+ end