10 from apiclient import discovery as apiclient_discovery
11 from apiclient import errors as apiclient_errors
16 _logger = logging.getLogger('arvados.api')
18 class OrderedJsonModel(apiclient.model.JsonModel):
19 """Model class for JSON that preserves the contents' order.
21 API clients that care about preserving the order of fields in API
22 server responses can use this model to do so, like this::
24 from arvados.api import OrderedJsonModel
25 client = arvados.api('v1', ..., model=OrderedJsonModel())
28 def deserialize(self, content):
29 # This is a very slightly modified version of the parent class'
30 # implementation. Copyright (c) 2010 Google.
31 content = content.decode('utf-8')
32 body = json.loads(content, object_pairs_hook=collections.OrderedDict)
33 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
38 def _intercept_http_request(self, uri, **kwargs):
39 from httplib import BadStatusLine
41 if (self.max_request_size and
42 kwargs.get('body') and
43 self.max_request_size < len(kwargs['body'])):
44 raise apiclient_errors.MediaUploadSizeError("Request size %i bytes exceeds published limit of %i bytes" % (len(kwargs['body']), self.max_request_size))
46 if 'headers' not in kwargs:
47 kwargs['headers'] = {}
49 if config.get("ARVADOS_EXTERNAL_CLIENT", "") == "true":
50 kwargs['headers']['X-External-Client'] = '1'
52 kwargs['headers']['Authorization'] = 'OAuth2 %s' % self.arvados_api_token
54 return self.orig_http_request(uri, **kwargs)
56 # This is how httplib tells us that it tried to reuse an
57 # existing connection but it was already closed by the
58 # server. In that case, yes, we would like to retry.
59 # Unfortunately, we are not absolutely certain that the
60 # previous call did not succeed, so this is slightly
62 return self.orig_http_request(uri, **kwargs)
64 def _patch_http_request(http, api_token):
65 http.arvados_api_token = api_token
66 http.max_request_size = 0
67 http.orig_http_request = http.request
68 http.request = types.MethodType(_intercept_http_request, http)
71 # Monkey patch discovery._cast() so objects and arrays get serialized
72 # with json.dumps() instead of str().
73 _cast_orig = apiclient_discovery._cast
74 def _cast_objects_too(value, schema_type):
76 if (type(value) != type('') and
77 (schema_type == 'object' or schema_type == 'array')):
78 return json.dumps(value)
80 return _cast_orig(value, schema_type)
81 apiclient_discovery._cast = _cast_objects_too
83 # Convert apiclient's HttpErrors into our own API error subclass for better
85 # Reassigning apiclient_errors.HttpError is not sufficient because most of the
86 # apiclient submodules import the class into their own namespace.
87 def _new_http_error(cls, *args, **kwargs):
88 return super(apiclient_errors.HttpError, cls).__new__(
89 errors.ApiError, *args, **kwargs)
90 apiclient_errors.HttpError.__new__ = staticmethod(_new_http_error)
92 def http_cache(data_type):
93 path = os.environ['HOME'] + '/.cache/arvados/' + data_type
95 util.mkdir_dash_p(path)
100 def api(version=None, cache=True, host=None, token=None, insecure=False, **kwargs):
101 """Return an apiclient Resources object for an Arvados instance.
104 A string naming the version of the Arvados API to use (for
108 Use a cache (~/.cache/arvados/discovery) for the discovery
112 The Arvados API server host (and optional :port) to connect to.
115 The authentication token to send with each API call.
118 If True, ignore SSL certificate validation errors.
120 Additional keyword arguments will be passed directly to
121 `apiclient_discovery.build` if a new Resource object is created.
122 If the `discoveryServiceUrl` or `http` keyword arguments are
123 missing, this function will set default values for them, based on
124 the current Arvados configuration settings.
130 _logger.info("Using default API version. " +
131 "Call arvados.api('%s') instead." %
133 if 'discoveryServiceUrl' in kwargs:
135 raise ValueError("both discoveryServiceUrl and host provided")
136 # Here we can't use a token from environment, config file,
137 # etc. Those probably have nothing to do with the host
138 # provided by the caller.
140 raise ValueError("discoveryServiceUrl provided, but token missing")
143 elif not host and not token:
144 return api_from_config(version=version, cache=cache, **kwargs)
146 # Caller provided one but not the other
148 raise ValueError("token argument provided, but host missing.")
150 raise ValueError("host argument provided, but token missing.")
153 # Caller wants us to build the discoveryServiceUrl
154 kwargs['discoveryServiceUrl'] = (
155 'https://%s/discovery/v1/apis/{api}/{apiVersion}/rest' % (host,))
157 if 'http' not in kwargs:
159 # Prefer system's CA certificates (if available) over httplib2's.
160 certs_path = '/etc/ssl/certs/ca-certificates.crt'
161 if os.path.exists(certs_path):
162 http_kwargs['ca_certs'] = certs_path
164 http_kwargs['cache'] = http_cache('discovery')
166 http_kwargs['disable_ssl_certificate_validation'] = True
167 kwargs['http'] = httplib2.Http(**http_kwargs)
169 kwargs['http'] = _patch_http_request(kwargs['http'], token)
171 svc = apiclient_discovery.build('arvados', version, **kwargs)
172 svc.api_token = token
173 kwargs['http'].max_request_size = svc._rootDesc.get('maxRequestSize', 0)
174 kwargs['http'].cache = None
177 def api_from_config(version=None, apiconfig=None, **kwargs):
178 """Return an apiclient Resources object enabling access to an Arvados server
182 A string naming the version of the Arvados REST API to use (for
186 If provided, this should be a dict-like object (must support the get()
187 method) with entries for ARVADOS_API_HOST, ARVADOS_API_TOKEN, and
188 optionally ARVADOS_API_HOST_INSECURE. If not provided, use
189 arvados.config (which gets these parameters from the environment by
192 Other keyword arguments such as `cache` will be passed along `api()`
195 # Load from user configuration or environment
196 if apiconfig is None:
197 apiconfig = config.settings()
199 for x in ['ARVADOS_API_HOST', 'ARVADOS_API_TOKEN']:
200 if x not in apiconfig:
201 raise ValueError("%s is not set. Aborting." % x)
202 host = apiconfig.get('ARVADOS_API_HOST')
203 token = apiconfig.get('ARVADOS_API_TOKEN')
204 insecure = config.flag_is_true('ARVADOS_API_HOST_INSECURE', apiconfig)
206 return api(version=version, host=host, token=token, insecure=insecure, **kwargs)