21910: Update all "Authorization: OAuth2 ..." usage to "Bearer".
[arvados.git] / sdk / python / arvados / api.py
index 8a17e42fcb3af881e517d8d580e3b5bdb4c25e41..13f946df48cb73fe6e814d886cb3cd3821d967f5 100644 (file)
@@ -10,6 +10,8 @@ client constructors are `api` and `api_from_config`.
 """
 
 import collections
+import errno
+import hashlib
 import httplib2
 import json
 import logging
@@ -19,6 +21,7 @@ import re
 import socket
 import ssl
 import sys
+import tempfile
 import threading
 import time
 import types
@@ -37,9 +40,10 @@ from apiclient import discovery as apiclient_discovery
 from apiclient import errors as apiclient_errors
 from . import config
 from . import errors
+from . import keep
 from . import retry
 from . import util
-from . import cache
+from ._internal import basedirs
 from .logging import GoogleHTTPClientFilter, log_handler
 
 _logger = logging.getLogger('arvados.api')
@@ -87,7 +91,7 @@ def _intercept_http_request(self, uri, method="GET", headers={}, **kwargs):
             self.max_request_size < len(kwargs['body'])):
             raise apiclient_errors.MediaUploadSizeError("Request size %i bytes exceeds published limit of %i bytes" % (len(kwargs['body']), self.max_request_size))
 
-        headers['Authorization'] = 'OAuth2 %s' % self.arvados_api_token
+        headers['Authorization'] = 'Bearer %s' % self.arvados_api_token
 
         if (time.time() - self._last_request_time) > self._max_keepalive_idle:
             # High probability of failure due to connection atrophy. Make
@@ -100,8 +104,8 @@ def _intercept_http_request(self, uri, method="GET", headers={}, **kwargs):
         self._last_request_time = time.time()
         try:
             response, body = self.orig_http_request(uri, method, headers=headers, **kwargs)
-        except ssl.SSLCertVerificationError as e:
-            raise ssl.SSLCertVerificationError(e.args[0], "Could not connect to %s\n%s\nPossible causes: remote SSL/TLS certificate expired, or was issued by an untrusted certificate authority." % (uri, e)) from None
+        except ssl.CertificateError as e:
+            raise ssl.CertificateError(e.args[0], "Could not connect to %s\n%s\nPossible causes: remote SSL/TLS certificate expired, or was issued by an untrusted certificate authority." % (uri, e)) from None
         # googleapiclient only retries 403, 429, and 5xx status codes.
         # If we got another 4xx status that we want to retry, convert it into
         # 5xx so googleapiclient handles it the way we want.
@@ -155,29 +159,155 @@ def _new_http_error(cls, *args, **kwargs):
         errors.ApiError, *args, **kwargs)
 apiclient_errors.HttpError.__new__ = staticmethod(_new_http_error)
 
-def http_cache(data_type: str) -> cache.SafeHTTPCache:
+class ThreadSafeHTTPCache:
+    """Thread-safe replacement for `httplib2.FileCache`
+
+    `arvados.api.http_cache` is the preferred way to construct this object.
+    Refer to that function's docstring for details.
+    """
+
+    def __init__(self, path=None, max_age=None):
+        self._dir = path
+        if max_age is not None:
+            try:
+                self._clean(threshold=time.time() - max_age)
+            except:
+                pass
+
+    def _clean(self, threshold=0):
+        for ent in os.listdir(self._dir):
+            fnm = os.path.join(self._dir, ent)
+            if os.path.isdir(fnm) or not fnm.endswith('.tmp'):
+                continue
+            stat = os.lstat(fnm)
+            if stat.st_mtime < threshold:
+                try:
+                    os.unlink(fnm)
+                except OSError as err:
+                    if err.errno != errno.ENOENT:
+                        raise
+
+    def __str__(self):
+        return self._dir
+
+    def _filename(self, url):
+        return os.path.join(self._dir, hashlib.md5(url.encode('utf-8')).hexdigest()+'.tmp')
+
+    def get(self, url):
+        filename = self._filename(url)
+        try:
+            with open(filename, 'rb') as f:
+                return f.read()
+        except (IOError, OSError):
+            return None
+
+    def set(self, url, content):
+        try:
+            fd, tempname = tempfile.mkstemp(dir=self._dir)
+        except:
+            return None
+        try:
+            try:
+                f = os.fdopen(fd, 'wb')
+            except:
+                os.close(fd)
+                raise
+            try:
+                f.write(content)
+            finally:
+                f.close()
+            os.rename(tempname, self._filename(url))
+            tempname = None
+        finally:
+            if tempname:
+                os.unlink(tempname)
+
+    def delete(self, url):
+        try:
+            os.unlink(self._filename(url))
+        except OSError as err:
+            if err.errno != errno.ENOENT:
+                raise
+
+
+class ThreadSafeAPIClient(object):
+    """Thread-safe wrapper for an Arvados API client
+
+    This class takes all the arguments necessary to build a lower-level
+    Arvados API client `googleapiclient.discovery.Resource`, then
+    transparently builds and wraps a unique object per thread. This works
+    around the fact that the client's underlying HTTP client object is not
+    thread-safe.
+
+    Arguments:
+
+    * apiconfig: Mapping[str, str] | None --- A mapping with entries for
+      `ARVADOS_API_HOST`, `ARVADOS_API_TOKEN`, and optionally
+      `ARVADOS_API_HOST_INSECURE`. If not provided, uses
+      `arvados.config.settings` to get these parameters from user
+      configuration.  You can pass an empty mapping to build the client
+      solely from `api_params`.
+
+    * keep_params: Mapping[str, Any] --- Keyword arguments used to construct
+      an associated `arvados.keep.KeepClient`.
+
+    * api_params: Mapping[str, Any] --- Keyword arguments used to construct
+      each thread's API client. These have the same meaning as in the
+      `arvados.api.api` function.
+
+    * version: str | None --- A string naming the version of the Arvados API
+      to use. If not specified, the code will log a warning and fall back to
+      `'v1'`.
+    """
+    def __init__(
+            self,
+            apiconfig: Optional[Mapping[str, str]]=None,
+            keep_params: Optional[Mapping[str, Any]]={},
+            api_params: Optional[Mapping[str, Any]]={},
+            version: Optional[str]=None,
+    ) -> None:
+        if apiconfig or apiconfig is None:
+            self._api_kwargs = api_kwargs_from_config(version, apiconfig, **api_params)
+        else:
+            self._api_kwargs = normalize_api_kwargs(version, **api_params)
+        self.api_token = self._api_kwargs['token']
+        self.request_id = self._api_kwargs.get('request_id')
+        self.local = threading.local()
+        self.keep = keep.KeepClient(api_client=self, **keep_params)
+
+    def localapi(self) -> 'googleapiclient.discovery.Resource':
+        try:
+            client = self.local.api
+        except AttributeError:
+            client = api_client(**self._api_kwargs)
+            client._http._request_id = lambda: self.request_id or util.new_request_id()
+            self.local.api = client
+        return client
+
+    def __getattr__(self, name: str) -> Any:
+        # Proxy nonexistent attributes to the thread-local API client.
+        return getattr(self.localapi(), name)
+
+
+def http_cache(data_type: str) -> Optional[ThreadSafeHTTPCache]:
     """Set up an HTTP file cache
 
-    This function constructs and returns an `arvados.cache.SafeHTTPCache`
-    backed by the filesystem under `~/.cache/arvados/`, or `None` if the
-    directory cannot be set up. The return value can be passed to
+    This function constructs and returns an `arvados.api.ThreadSafeHTTPCache`
+    backed by the filesystem under a cache directory from the environment, or
+    `None` if the directory cannot be set up. The return value can be passed to
     `httplib2.Http` as the `cache` argument.
 
     Arguments:
 
-    * data_type: str --- The name of the subdirectory under `~/.cache/arvados`
+    * data_type: str --- The name of the subdirectory
       where data is cached.
     """
     try:
-        homedir = pathlib.Path.home()
-    except RuntimeError:
-        return None
-    path = pathlib.Path(homedir, '.cache', 'arvados', data_type)
-    try:
-        path.mkdir(parents=True, exist_ok=True)
-    except OSError:
+        path = basedirs.BaseDirectories('CACHE').storage_path(data_type)
+    except (OSError, RuntimeError):
         return None
-    return cache.SafeHTTPCache(str(path), max_age=60*60*24*2)
+    else:
+        return ThreadSafeHTTPCache(str(path), max_age=60*60*24*2)
 
 def api_client(
         version: str,
@@ -211,8 +341,7 @@ def api_client(
     Keyword-only arguments:
 
     * cache: bool --- If true, loads the API discovery document from, or
-      saves it to, a cache on disk (located at
-      `~/.cache/arvados/discovery`).
+      saves it to, a cache on disk.
 
     * http: httplib2.Http | None --- The HTTP client object the API client
       object will use to make requests.  If not provided, this function will
@@ -412,7 +541,7 @@ def api(
         *,
         discoveryServiceUrl: Optional[str]=None,
         **kwargs: Any,
-) -> 'arvados.safeapi.ThreadSafeApiCache':
+) -> ThreadSafeAPIClient:
     """Dynamically build an Arvados API client
 
     This function provides a high-level "do what I mean" interface to build an
@@ -421,7 +550,7 @@ def api(
     like you would write in user configuration; or pass additional arguments
     for lower-level control over the client.
 
-    This function returns a `arvados.safeapi.ThreadSafeApiCache`, an
+    This function returns a `arvados.api.ThreadSafeAPIClient`, an
     API-compatible wrapper around `googleapiclient.discovery.Resource`. If
     you're handling concurrency yourself and/or your application is very
     performance-sensitive, consider calling `api_client` directly.
@@ -460,22 +589,20 @@ def api(
     else:
         kwargs.update(api_kwargs_from_config(version))
     version = kwargs.pop('version')
-    # We do the import here to avoid a circular import at the top level.
-    from .safeapi import ThreadSafeApiCache
-    return ThreadSafeApiCache({}, {}, kwargs, version)
+    return ThreadSafeAPIClient({}, {}, kwargs, version)
 
 def api_from_config(
         version: Optional[str]=None,
         apiconfig: Optional[Mapping[str, str]]=None,
         **kwargs: Any
-) -> 'arvados.safeapi.ThreadSafeApiCache':
+) -> ThreadSafeAPIClient:
     """Build an Arvados API client from a configuration mapping
 
     This function builds an Arvados API client from a mapping with user
     configuration. It accepts that mapping as an argument, so you can use a
     configuration that's different from what the user has set up.
 
-    This function returns a `arvados.safeapi.ThreadSafeApiCache`, an
+    This function returns a `arvados.api.ThreadSafeAPIClient`, an
     API-compatible wrapper around `googleapiclient.discovery.Resource`. If
     you're handling concurrency yourself and/or your application is very
     performance-sensitive, consider calling `api_client` directly.
@@ -496,49 +623,3 @@ def api_from_config(
     docstring for more information about their meaning.
     """
     return api(**api_kwargs_from_config(version, apiconfig, **kwargs))
-
-class OrderedJsonModel(apiclient.model.JsonModel):
-    """Model class for JSON that preserves the contents' order
-
-    .. WARNING:: Deprecated
-       This model is redundant now that Python dictionaries preserve insertion
-       ordering. Code that passes this model to API constructors can remove it.
-
-    In Python versions before 3.6, API clients that cared about preserving the
-    order of fields in API server responses could use this model to do so.
-    Typical usage looked like:
-
-        from arvados.api import OrderedJsonModel
-        client = arvados.api('v1', ..., model=OrderedJsonModel())
-    """
-    @util._deprecated(preferred="the default model and rely on Python's built-in dictionary ordering")
-    def __init__(self, data_wrapper=False):
-        return super().__init__(data_wrapper)
-
-
-RETRY_DELAY_INITIAL = 0
-"""
-.. WARNING:: Deprecated
-   This constant was used by retry code in previous versions of the Arvados SDK.
-   Changing the value has no effect anymore.
-   Prefer passing `num_retries` to an API client constructor instead.
-   Refer to the constructor docstrings for details.
-"""
-
-RETRY_DELAY_BACKOFF = 0
-"""
-.. WARNING:: Deprecated
-   This constant was used by retry code in previous versions of the Arvados SDK.
-   Changing the value has no effect anymore.
-   Prefer passing `num_retries` to an API client constructor instead.
-   Refer to the constructor docstrings for details.
-"""
-
-RETRY_COUNT = 0
-"""
-.. WARNING:: Deprecated
-   This constant was used by retry code in previous versions of the Arvados SDK.
-   Changing the value has no effect anymore.
-   Prefer passing `num_retries` to an API client constructor instead.
-   Refer to the constructor docstrings for details.
-"""