Merge branch '21666-provision-test-improvement'
[arvados.git] / sdk / python / arvados / safeapi.py
index 5c5c87250ac678f9b9bf93cbf56cb335707c6554..56b92e8f08ea38990de09c60394fb49b78b8f2a6 100644 (file)
@@ -1,31 +1,81 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+"""Thread-safe wrapper for an Arvados API client
+
+This module provides `ThreadSafeApiCache`, a thread-safe, API-compatible
+Arvados API client.
+"""
+
+import sys
 import threading
-import api
-import keep
-import config
-import copy
+
+from typing import (
+    Any,
+    Mapping,
+    Optional,
+)
+
+from . import config
+from . import keep
+from . import util
+
+api = sys.modules['arvados.api']
 
 class ThreadSafeApiCache(object):
-    """Threadsafe wrapper for API objects.
+    """Thread-safe wrapper for an Arvados API client
 
-    This stores and returns a different api object per thread, because httplib2
-    which underlies apiclient is not threadsafe.
+    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`.
 
-    def __init__(self, apiconfig=None, keep_params={}):
-        if apiconfig is None:
-            apiconfig = config.settings()
-        self.apiconfig = copy.copy(apiconfig)
+    * 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.api_kwargs_from_config(version, apiconfig, **api_params)
+        else:
+            self._api_kwargs = api.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):
-        if 'api' not in self.local.__dict__:
-            self.local.api = api.api_from_config('v1', apiconfig=self.apiconfig)
-        return self.local.api
+    def localapi(self) -> 'googleapiclient.discovery.Resource':
+        try:
+            client = self.local.api
+        except AttributeError:
+            client = api.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):
+    def __getattr__(self, name: str) -> Any:
         # Proxy nonexistent attributes to the thread-local API client.
-        if name == "api_token":
-            return self.apiconfig['ARVADOS_API_TOKEN']
         return getattr(self.localapi(), name)