Class: Aspera::Rest::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/rest/client.rb

Overview

Make HTTP calls, equivalent to rest-client rest call errors are raised as exception CallError and error are analyzed in ErrorAnalyzer

Instance Attribute Summary collapse

CRUD collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_url:, auth: {type: :none}, not_auth_codes: ['401'], redirect_max: 0, headers: {}) ⇒ Client

Create a REST object for API calls HTTP sessions parameters can be modified using global parameters in Parameters For example, TLS verification can be skipped.

Parameters:

  • base_url (String) —

    base URL of REST API

  • auth (Hash) (defaults to: {type: :none}) —

    authentication parameters: :type (:none, :basic, :url, :oauth2) :username [:basic] :password [:basic] :url_query [:url] a hash :* [:oauth2] see OAuth::Factory class

  • not_auth_codes (Array) (defaults to: ['401']) —

    codes that trigger a refresh/regeneration of bearer token

  • redirect_max (Integer) (defaults to: 0) —

    max redirection allowed

  • headers (Hash) (defaults to: {}) —

    default headers to include in all calls



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/aspera/rest/client.rb', line 81

def initialize(
  base_url:,
  auth: {type: :none},
  not_auth_codes: ['401'],
  redirect_max: 0,
  headers: {}
)
  Aspera.assert_type(base_url, String)
  # base url with no trailing slashes (note: string may be frozen)
  @base_url = base_url.chomp('/')
  # remove trailing port if it is 443 and scheme is https
  @base_url = @base_url.gsub(/:443$/, '') if @base_url.start_with?('https://')
  @base_url = @base_url.gsub(/:80$/, '') if @base_url.start_with?('http://')
  Log.log.debug { "Client.new(#{@base_url})" }
  # default is no auth
  @auth_params = auth
  Aspera.assert_type(@auth_params, Hash)
  Aspera.assert(@auth_params.key?(:type), 'no auth type defined')
  @not_auth_codes = not_auth_codes
  Aspera.assert_type(@not_auth_codes, Array)
  # persistent session
  @http_session = nil
  @redirect_max = redirect_max
  Aspera.assert_type(@redirect_max, Integer)
  @headers = headers.clone
  Aspera.assert_type(@headers, Hash)
  @headers['User-Agent'] ||= Parameters.instance.user_agent
  # OAuth object (created on demand)
  @oauth = nil
end

Instance Attribute Details

#auth_params ⇒ Object (readonly)

All original constructor parameters



48
49
50
# File 'lib/aspera/rest/client.rb', line 48

def auth_params
  @auth_params
end

#base_url ⇒ Object (readonly)

The root URL for the API



51
52
53
# File 'lib/aspera/rest/client.rb', line 51

def base_url
  @base_url
end

#headers ⇒ Object (readonly)

Base common headers of API



54
55
56
# File 'lib/aspera/rest/client.rb', line 54

def headers
  @headers
end

Instance Method Details

#call(operation:, subpath: nil, query: nil, content_type: nil, body: nil, headers: nil, save_to: nil, exception: true, ret: :data) ⇒ Array(Hash, Net::HTTPResponse), ...

HTTP/S REST call

Parameters:

  • operation (String) —

    HTTP operation (GET, POST, PUT, DELETE)

  • subpath (String) (defaults to: nil) —

    subpath of REST API

  • query (Hash{String,Symbol => Object}) (defaults to: nil) —

    URL parameters

  • content_type (String, nil) (defaults to: nil) —

    Type of body parameters (one of MIME_*) and serialization, else use headers

  • body (Hash, String, nil) (defaults to: nil) —

    Body parameters

  • headers (Hash{String => String}) (defaults to: nil) —

    Additional headers (override Content-Type)

  • save_to (String, Pathname, IO, nil) (defaults to: nil) —

    File path or IO object to save response body; progress bar is used when set

  • exception (Boolean) (defaults to: true) —

    Whether to raise an exception on HTTP error

  • ret (Symbol) (defaults to: :data) —

    One of :data, :resp, :both - controls return value

Returns:

  • (Array(Hash, Net::HTTPResponse)) —

    When ret is :both

  • (Net::HTTPResponse) —

    When ret is :resp

  • (Hash) —

    When ret is :data

Raises:

  • (CallError) —

    on error if exception is true



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/aspera/rest/client.rb', line 138

def call(
  operation:,
  subpath: nil,
  query: nil,
  content_type: nil,
  body: nil,
  headers: nil,
  save_to: nil,
  exception: true,
  ret: :data
)
  subpath = subpath.to_s if subpath.is_a?(Symbol)
  subpath = '' if subpath.nil?
  # File path (String or Pathname) or stream (responds to `write`)
  # Pathname also responds to `write` (overwrites file), so it must not be taken as a stream
  save_to = save_to.to_s if save_to.is_a?(Pathname)
  Aspera.assert(save_to.nil? || save_to.is_a?(String) || save_to.respond_to?(:write)) { "save_to: unsupported type #{save_to.class}" }
  Log.log.debug { "call #{operation} [#{subpath}]".red.bold.bg(:green) }
  Log.dump(:body, body, level: :trace1)
  Log.dump(:query, query, level: :trace1)
  Log.dump(:headers, headers, level: :trace1)
  Aspera.assert_type(subpath, String)
  # We must have a way to check return code
  Aspera.assert(exception || !ret.eql?(:data), 'ret: :data requires exception handler')
  req_headers, req_query = prepare_call(headers, query)
  result_http = nil
  result_data = nil
  # number of tries on error (first call included)
  error_tries = 1 + Parameters.instance.retry_max
  # OAuth token is renewed only once, independently of error retries
  token_renewed = false
  # start a block to be able to retry the actual HTTP request in case of OAuth token expiration
  begin
    Log.log.debug("send request (redirects=#{@redirect_max})")
    req = build_request(operation, subpath, req_query, content_type, body, req_headers)
    result_mime = nil
    file_saved = false
    # make http request (pipelined)
    http_session.request(req) do |response|
      result_http = response
      result_mime = Rest.parse_header(result_http['Content-Type'] || Mime::TEXT)[:type]
      Log.log.debug { "response: code=#{result_http.code}, mime=#{result_mime}, content-type=#{response['Content-Type']}" }
      # JSON data needs to be parsed, in case it contains an error code
      file_saved = save_response(response, result_mime, save_to)
    end
    Log.log.debug { "result: code=#{result_http.code} mime=#{result_mime}" }
    # sometimes there is a UTF8 char (e.g. (c) )
    # TODO : related to mime type encoding ?
    # result_http.body.force_encoding('UTF-8') if result_http.body.is_a?(String)
    # Log.log.debug{"result: body=#{result_http.body}"}
    result_data = parse_response(result_http, result_mime)
    ErrorAnalyzer.instance.raise_on_error(req, result_data, result_http)
    unless file_saved || save_to.nil?
      raise 'save_to: IO object requires a streaming response' if save_to.respond_to?(:write)
      FileUtils.mkdir_p(File.dirname(save_to))
      File.write(save_to, result_http.body, binmode: true)
    end
  rescue *NETWORK_ERRORS => e
    raise unless retry_error?(e) && (error_tries -= 1).positive?
    Log.log.warn { "#{e.class}: #{e.message}: retrying" }
    retry_sleep
    retry
  rescue CallError => e
    # not authorized: OAuth token expired
    if !token_renewed && @not_auth_codes.include?(result_http.code.to_s) && @auth_params[:type].eql?(:oauth2)
      token_renewed = true
      new_authorization = renew_oauth_authorization
      unless new_authorization.nil?
        Log.log.debug('using new token')
        req_headers['Authorization'] = new_authorization
        retry
      end
    end
    if retry_error?(e) && (error_tries -= 1).positive?
      retry_sleep
      retry
    end
    # redirect ? (any code beginning with 3)
    if e.response.is_a?(Net::HTTPRedirection) && @redirect_max.positive?
      return redirect_call(
        req.uri,
        e.response['Location'],
        operation:    operation,
        body:         body,
        content_type: content_type,
        save_to:      save_to,
        exception:    exception,
        headers:      headers,
        ret:          ret
      )
    end
    # raise exception if could not retry and not return error in result
    raise e if exception
  end
  Log.log.debug { "result=http:#{result_http}, data:#{result_data.class}" }
  return case ret
         when :data then result_data
         when :resp then result_http
         when :both then [result_data, result_http]
         else Aspera.error_unexpected_value(ret) { 'Type of result for REST' }
         end
end

#cancel(subpath, **kwargs) ⇒ Object

CANCEL

Parameters:

  • subpath (String) —

    Subpath of REST API

  • kwargs (Hash) —

    Other arguments of call

Returns:

  • (Object) —

    Result of call



485
# File 'lib/aspera/rest/client.rb', line 485

def cancel(subpath, **kwargs) = call(operation: 'CANCEL', subpath: subpath, **json_call_args(kwargs))

#create(subpath, params, **kwargs) ⇒ Object

POST JSON body

Parameters:

  • subpath (String) —

    Subpath of REST API

  • params (Hash) —

    Body

  • kwargs (Hash) —

    Other arguments of call

Returns:

  • (Object) —

    Result of call



458
# File 'lib/aspera/rest/client.rb', line 458

def create(subpath, params, **kwargs) = call(operation: 'POST', subpath: subpath, body: params, **json_call_args(kwargs, body: true))

#delete(subpath, params = nil, **kwargs) ⇒ Object

DELETE

Parameters:

  • subpath (String) —

    Subpath of REST API

  • params (Hash, nil) (defaults to: nil) —

    Query

  • kwargs (Hash) —

    Other arguments of call

Returns:

  • (Object) —

    Result of call



479
# File 'lib/aspera/rest/client.rb', line 479

def delete(subpath, params = nil, **kwargs) = call(operation: 'DELETE', subpath: subpath, query: params, **json_call_args(kwargs))

#oauth ⇒ OAuth::Base

OAuth object used for authorization, when auth type is :oauth2

Returns:

  • (OAuth::Base) —

    the OAuth object (create, or cached if already created)



114
115
116
117
118
119
120
121
122
# File 'lib/aspera/rest/client.rb', line 114

def oauth
  if @oauth.nil?
    Aspera.assert(@auth_params[:type].eql?(:oauth2), 'no OAuth defined')
    oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
    Log.dump(:oauth_parameters, oauth_parameters)
    @oauth = OAuth::Factory.instance.create(**oauth_parameters)
  end
  return @oauth
end

#params ⇒ Hash

Parameters to create a copy of this object, e.g. Rest::Client.new(**api.params)

Returns:

  • (Hash) —

    Creation parameters (copy)



58
59
60
61
62
63
64
65
66
# File 'lib/aspera/rest/client.rb', line 58

def params
  return {
    base_url:       @base_url,           # String
    auth:           @auth_params.dup,    # Hash
    not_auth_codes: @not_auth_codes.dup, # Array
    redirect_max:   @redirect_max,       # Integer
    headers:        @headers.dup         # Hash
  }
end

#read(subpath, query = nil, **kwargs) ⇒ Object

GET

Parameters:

  • subpath (String) —

    Subpath of REST API

  • query (Hash, nil) (defaults to: nil) —

    Query

  • kwargs (Hash) —

    Other arguments of call

Returns:

  • (Object) —

    Result of call



465
# File 'lib/aspera/rest/client.rb', line 465

def read(subpath, query = nil, **kwargs) = call(operation: 'GET', subpath: subpath, query: query, **json_call_args(kwargs))

#update(subpath, params, **kwargs) ⇒ Object

PUT JSON body

Parameters:

  • subpath (String) —

    Subpath of REST API

  • params (Hash) —

    Body

  • kwargs (Hash) —

    Other arguments of call

Returns:

  • (Object) —

    Result of call



472
# File 'lib/aspera/rest/client.rb', line 472

def update(subpath, params, **kwargs) = call(operation: 'PUT', subpath: subpath, body: params, **json_call_args(kwargs, body: true))