21700: Install Bundler system-wide in Rails postinst
[arvados.git] / sdk / python / arvados / safeapi.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4 """Thread-safe wrapper for an Arvados API client
5
6 This module provides `ThreadSafeApiCache`, a thread-safe, API-compatible
7 Arvados API client.
8 """
9
10 import sys
11 import threading
12
13 from typing import (
14     Any,
15     Mapping,
16     Optional,
17 )
18
19 from . import config
20 from . import keep
21 from . import util
22
23 api = sys.modules['arvados.api']
24
25 class ThreadSafeApiCache(object):
26     """Thread-safe wrapper for an Arvados API client
27
28     This class takes all the arguments necessary to build a lower-level
29     Arvados API client `googleapiclient.discovery.Resource`, then
30     transparently builds and wraps a unique object per thread. This works
31     around the fact that the client's underlying HTTP client object is not
32     thread-safe.
33
34     Arguments:
35
36     * apiconfig: Mapping[str, str] | None --- A mapping with entries for
37       `ARVADOS_API_HOST`, `ARVADOS_API_TOKEN`, and optionally
38       `ARVADOS_API_HOST_INSECURE`. If not provided, uses
39       `arvados.config.settings` to get these parameters from user
40       configuration.  You can pass an empty mapping to build the client
41       solely from `api_params`.
42
43     * keep_params: Mapping[str, Any] --- Keyword arguments used to construct
44       an associated `arvados.keep.KeepClient`.
45
46     * api_params: Mapping[str, Any] --- Keyword arguments used to construct
47       each thread's API client. These have the same meaning as in the
48       `arvados.api.api` function.
49
50     * version: str | None --- A string naming the version of the Arvados API
51       to use. If not specified, the code will log a warning and fall back to
52       `'v1'`.
53     """
54     def __init__(
55             self,
56             apiconfig: Optional[Mapping[str, str]]=None,
57             keep_params: Optional[Mapping[str, Any]]={},
58             api_params: Optional[Mapping[str, Any]]={},
59             version: Optional[str]=None,
60     ) -> None:
61         if apiconfig or apiconfig is None:
62             self._api_kwargs = api.api_kwargs_from_config(version, apiconfig, **api_params)
63         else:
64             self._api_kwargs = api.normalize_api_kwargs(version, **api_params)
65         self.api_token = self._api_kwargs['token']
66         self.request_id = self._api_kwargs.get('request_id')
67         self.local = threading.local()
68         self.keep = keep.KeepClient(api_client=self, **keep_params)
69
70     def localapi(self) -> 'googleapiclient.discovery.Resource':
71         try:
72             client = self.local.api
73         except AttributeError:
74             client = api.api_client(**self._api_kwargs)
75             client._http._request_id = lambda: self.request_id or util.new_request_id()
76             self.local.api = client
77         return client
78
79     def __getattr__(self, name: str) -> Any:
80         # Proxy nonexistent attributes to the thread-local API client.
81         return getattr(self.localapi(), name)