13 from apiclient import discovery as apiclient_discovery
14 from apiclient import errors as apiclient_errors
19 _logger = logging.getLogger('arvados.api')
21 MAX_IDLE_CONNECTION_DURATION = 30
22 RETRY_DELAY_INITIAL = 2
23 RETRY_DELAY_BACKOFF = 2
26 class OrderedJsonModel(apiclient.model.JsonModel):
27 """Model class for JSON that preserves the contents' order.
29 API clients that care about preserving the order of fields in API
30 server responses can use this model to do so, like this::
32 from arvados.api import OrderedJsonModel
33 client = arvados.api('v1', ..., model=OrderedJsonModel())
36 def deserialize(self, content):
37 # This is a very slightly modified version of the parent class'
38 # implementation. Copyright (c) 2010 Google.
39 content = content.decode('utf-8')
40 body = json.loads(content, object_pairs_hook=collections.OrderedDict)
41 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
46 def _intercept_http_request(self, uri, **kwargs):
47 if (self.max_request_size and
48 kwargs.get('body') and
49 self.max_request_size < len(kwargs['body'])):
50 raise apiclient_errors.MediaUploadSizeError("Request size %i bytes exceeds published limit of %i bytes" % (len(kwargs['body']), self.max_request_size))
52 if 'headers' not in kwargs:
53 kwargs['headers'] = {}
55 if config.get("ARVADOS_EXTERNAL_CLIENT", "") == "true":
56 kwargs['headers']['X-External-Client'] = '1'
58 kwargs['headers']['Authorization'] = 'OAuth2 %s' % self.arvados_api_token
60 retryable = kwargs.get('method', 'GET') in [
61 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT']
62 retry_count = self._retry_count if retryable else 0
65 time.time() - self._last_request_time > self._max_keepalive_idle):
66 # High probability of failure due to connection atrophy. Make
67 # sure this request [re]opens a new connection by closing and
68 # forgetting all cached connections first.
69 for conn in self.connections.itervalues():
71 self.connections.clear()
73 delay = self._retry_delay_initial
74 for _ in range(retry_count):
75 self._last_request_time = time.time()
77 return self.orig_http_request(uri, **kwargs)
78 except httplib.HTTPException:
79 _logger.debug("Retrying API request in %d s after HTTP error",
82 # This is the one case where httplib2 doesn't close the
83 # underlying connection first. Close all open
84 # connections, expecting this object only has the one
85 # connection to the API server. This is safe because
86 # httplib2 reopens connections when needed.
87 _logger.debug("Retrying API request in %d s after socket error",
89 for conn in self.connections.itervalues():
92 delay = delay * self._retry_delay_backoff
94 self._last_request_time = time.time()
95 return self.orig_http_request(uri, **kwargs)
97 def _patch_http_request(http, api_token):
98 http.arvados_api_token = api_token
99 http.max_request_size = 0
100 http.orig_http_request = http.request
101 http.request = types.MethodType(_intercept_http_request, http)
102 http._last_request_time = 0
103 http._max_keepalive_idle = MAX_IDLE_CONNECTION_DURATION
104 http._retry_delay_initial = RETRY_DELAY_INITIAL
105 http._retry_delay_backoff = RETRY_DELAY_BACKOFF
106 http._retry_count = RETRY_COUNT
109 # Monkey patch discovery._cast() so objects and arrays get serialized
110 # with json.dumps() instead of str().
111 _cast_orig = apiclient_discovery._cast
112 def _cast_objects_too(value, schema_type):
114 if (type(value) != type('') and
115 (schema_type == 'object' or schema_type == 'array')):
116 return json.dumps(value)
118 return _cast_orig(value, schema_type)
119 apiclient_discovery._cast = _cast_objects_too
121 # Convert apiclient's HttpErrors into our own API error subclass for better
123 # Reassigning apiclient_errors.HttpError is not sufficient because most of the
124 # apiclient submodules import the class into their own namespace.
125 def _new_http_error(cls, *args, **kwargs):
126 return super(apiclient_errors.HttpError, cls).__new__(
127 errors.ApiError, *args, **kwargs)
128 apiclient_errors.HttpError.__new__ = staticmethod(_new_http_error)
130 def http_cache(data_type):
131 homedir = os.environ.get('HOME')
132 if not homedir or len(homedir) == 0:
134 path = homedir + '/.cache/arvados/' + data_type
136 util.mkdir_dash_p(path)
141 def api(version=None, cache=True, host=None, token=None, insecure=False, **kwargs):
142 """Return an apiclient Resources object for an Arvados instance.
145 A string naming the version of the Arvados API to use (for
149 Use a cache (~/.cache/arvados/discovery) for the discovery
153 The Arvados API server host (and optional :port) to connect to.
156 The authentication token to send with each API call.
159 If True, ignore SSL certificate validation errors.
161 Additional keyword arguments will be passed directly to
162 `apiclient_discovery.build` if a new Resource object is created.
163 If the `discoveryServiceUrl` or `http` keyword arguments are
164 missing, this function will set default values for them, based on
165 the current Arvados configuration settings.
171 _logger.info("Using default API version. " +
172 "Call arvados.api('%s') instead." %
174 if 'discoveryServiceUrl' in kwargs:
176 raise ValueError("both discoveryServiceUrl and host provided")
177 # Here we can't use a token from environment, config file,
178 # etc. Those probably have nothing to do with the host
179 # provided by the caller.
181 raise ValueError("discoveryServiceUrl provided, but token missing")
184 elif not host and not token:
185 return api_from_config(version=version, cache=cache, **kwargs)
187 # Caller provided one but not the other
189 raise ValueError("token argument provided, but host missing.")
191 raise ValueError("host argument provided, but token missing.")
194 # Caller wants us to build the discoveryServiceUrl
195 kwargs['discoveryServiceUrl'] = (
196 'https://%s/discovery/v1/apis/{api}/{apiVersion}/rest' % (host,))
198 if 'http' not in kwargs:
199 http_kwargs = {'ca_certs': util.ca_certs_path()}
201 http_kwargs['cache'] = http_cache('discovery')
203 http_kwargs['disable_ssl_certificate_validation'] = True
204 kwargs['http'] = httplib2.Http(**http_kwargs)
206 kwargs['http'] = _patch_http_request(kwargs['http'], token)
208 svc = apiclient_discovery.build('arvados', version, **kwargs)
209 svc.api_token = token
210 kwargs['http'].max_request_size = svc._rootDesc.get('maxRequestSize', 0)
211 kwargs['http'].cache = None
214 def api_from_config(version=None, apiconfig=None, **kwargs):
215 """Return an apiclient Resources object enabling access to an Arvados server
219 A string naming the version of the Arvados REST API to use (for
223 If provided, this should be a dict-like object (must support the get()
224 method) with entries for ARVADOS_API_HOST, ARVADOS_API_TOKEN, and
225 optionally ARVADOS_API_HOST_INSECURE. If not provided, use
226 arvados.config (which gets these parameters from the environment by
229 Other keyword arguments such as `cache` will be passed along `api()`
232 # Load from user configuration or environment
233 if apiconfig is None:
234 apiconfig = config.settings()
236 for x in ['ARVADOS_API_HOST', 'ARVADOS_API_TOKEN']:
237 if x not in apiconfig:
238 raise ValueError("%s is not set. Aborting." % x)
239 host = apiconfig.get('ARVADOS_API_HOST')
240 token = apiconfig.get('ARVADOS_API_TOKEN')
241 insecure = config.flag_is_true('ARVADOS_API_HOST_INSECURE', apiconfig)
243 return api(version=version, host=host, token=token, insecure=insecure, **kwargs)