X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/a1fc5b9e889f8359a32470c3a7d91190d0894899..HEAD:/sdk/python/arvados/safeapi.py diff --git a/sdk/python/arvados/safeapi.py b/sdk/python/arvados/safeapi.py index 5c5c87250a..56b92e8f08 100644 --- a/sdk/python/arvados/safeapi.py +++ b/sdk/python/arvados/safeapi.py @@ -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)