6844: Do not use inspect.getcallargs: it leaks memory. Passing num_retries as a posit...
[arvados.git] / sdk / python / arvados / api.py
1 import collections
2 import httplib2
3 import json
4 import logging
5 import os
6 import re
7 import types
8
9 import apiclient
10 from apiclient import discovery as apiclient_discovery
11 from apiclient import errors as apiclient_errors
12 import config
13 import errors
14 import util
15
16 _logger = logging.getLogger('arvados.api')
17
18 class OrderedJsonModel(apiclient.model.JsonModel):
19     """Model class for JSON that preserves the contents' order.
20
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::
23
24         from arvados.api import OrderedJsonModel
25         client = arvados.api('v1', ..., model=OrderedJsonModel())
26     """
27
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:
34             body = body['data']
35         return body
36
37
38 def _intercept_http_request(self, uri, **kwargs):
39     from httplib import BadStatusLine
40
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))
45
46     if 'headers' not in kwargs:
47         kwargs['headers'] = {}
48
49     if config.get("ARVADOS_EXTERNAL_CLIENT", "") == "true":
50         kwargs['headers']['X-External-Client'] = '1'
51
52     kwargs['headers']['Authorization'] = 'OAuth2 %s' % self.arvados_api_token
53     try:
54         return self.orig_http_request(uri, **kwargs)
55     except BadStatusLine:
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
61         # risky.
62         return self.orig_http_request(uri, **kwargs)
63
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)
69     return http
70
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):
75     global _cast_orig
76     if (type(value) != type('') and
77         (schema_type == 'object' or schema_type == 'array')):
78         return json.dumps(value)
79     else:
80         return _cast_orig(value, schema_type)
81 apiclient_discovery._cast = _cast_objects_too
82
83 # Convert apiclient's HttpErrors into our own API error subclass for better
84 # error reporting.
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)
91
92 def http_cache(data_type):
93     homedir = os.environ.get('HOME')
94     if not homedir or len(homedir) == 0:
95         return None
96     path = homedir + '/.cache/arvados/' + data_type
97     try:
98         util.mkdir_dash_p(path)
99     except OSError:
100         path = None
101     return path
102
103 def api(version=None, cache=True, host=None, token=None, insecure=False, **kwargs):
104     """Return an apiclient Resources object for an Arvados instance.
105
106     :version:
107       A string naming the version of the Arvados API to use (for
108       example, 'v1').
109
110     :cache:
111       Use a cache (~/.cache/arvados/discovery) for the discovery
112       document.
113
114     :host:
115       The Arvados API server host (and optional :port) to connect to.
116
117     :token:
118       The authentication token to send with each API call.
119
120     :insecure:
121       If True, ignore SSL certificate validation errors.
122
123     Additional keyword arguments will be passed directly to
124     `apiclient_discovery.build` if a new Resource object is created.
125     If the `discoveryServiceUrl` or `http` keyword arguments are
126     missing, this function will set default values for them, based on
127     the current Arvados configuration settings.
128
129     """
130
131     if not version:
132         version = 'v1'
133         _logger.info("Using default API version. " +
134                      "Call arvados.api('%s') instead." %
135                      version)
136     if 'discoveryServiceUrl' in kwargs:
137         if host:
138             raise ValueError("both discoveryServiceUrl and host provided")
139         # Here we can't use a token from environment, config file,
140         # etc. Those probably have nothing to do with the host
141         # provided by the caller.
142         if not token:
143             raise ValueError("discoveryServiceUrl provided, but token missing")
144     elif host and token:
145         pass
146     elif not host and not token:
147         return api_from_config(version=version, cache=cache, **kwargs)
148     else:
149         # Caller provided one but not the other
150         if not host:
151             raise ValueError("token argument provided, but host missing.")
152         else:
153             raise ValueError("host argument provided, but token missing.")
154
155     if host:
156         # Caller wants us to build the discoveryServiceUrl
157         kwargs['discoveryServiceUrl'] = (
158             'https://%s/discovery/v1/apis/{api}/{apiVersion}/rest' % (host,))
159
160     if 'http' not in kwargs:
161         http_kwargs = {'ca_certs': util.ca_certs_path()}
162         if cache:
163             http_kwargs['cache'] = http_cache('discovery')
164         if insecure:
165             http_kwargs['disable_ssl_certificate_validation'] = True
166         kwargs['http'] = httplib2.Http(**http_kwargs)
167
168     kwargs['http'] = _patch_http_request(kwargs['http'], token)
169
170     svc = apiclient_discovery.build('arvados', version, **kwargs)
171     svc.api_token = token
172     kwargs['http'].max_request_size = svc._rootDesc.get('maxRequestSize', 0)
173     kwargs['http'].cache = None
174     return svc
175
176 def api_from_config(version=None, apiconfig=None, **kwargs):
177     """Return an apiclient Resources object enabling access to an Arvados server
178     instance.
179
180     :version:
181       A string naming the version of the Arvados REST API to use (for
182       example, 'v1').
183
184     :apiconfig:
185       If provided, this should be a dict-like object (must support the get()
186       method) with entries for ARVADOS_API_HOST, ARVADOS_API_TOKEN, and
187       optionally ARVADOS_API_HOST_INSECURE.  If not provided, use
188       arvados.config (which gets these parameters from the environment by
189       default.)
190
191     Other keyword arguments such as `cache` will be passed along `api()`
192
193     """
194     # Load from user configuration or environment
195     if apiconfig is None:
196         apiconfig = config.settings()
197
198     for x in ['ARVADOS_API_HOST', 'ARVADOS_API_TOKEN']:
199         if x not in apiconfig:
200             raise ValueError("%s is not set. Aborting." % x)
201     host = apiconfig.get('ARVADOS_API_HOST')
202     token = apiconfig.get('ARVADOS_API_TOKEN')
203     insecure = config.flag_is_true('ARVADOS_API_HOST_INSECURE', apiconfig)
204
205     return api(version=version, host=host, token=token, insecure=insecure, **kwargs)