• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vintasoftware / tapioca-wrapper / #351

pending completion
#351

push

coveralls-python

web-flow
Add devcontainer (#178)

* Remove Travis in favor of GHA

* Add Devcontainer for development

* Remove the pin on arrow lib

* Use flake8 inside tox

* Remove support for Python 3.6 and 3.7

341 of 374 relevant lines covered (91.18%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.15
/tapioca/tapioca.py
1
# coding: utf-8
2

3
from __future__ import unicode_literals
1✔
4

5
import copy
1✔
6

7
import requests
1✔
8
import webbrowser
1✔
9

10
import json
1✔
11
from collections import OrderedDict
1✔
12

13
from .exceptions import ResponseProcessException
1✔
14

15

16
class TapiocaInstantiator(object):
1✔
17

18
    def __init__(self, adapter_class):
1✔
19
        self.adapter_class = adapter_class
1✔
20

21
    def __call__(self, serializer_class=None, session=None, **kwargs):
1✔
22
        refresh_token_default = kwargs.pop('refresh_token_by_default', False)
1✔
23
        return TapiocaClient(
1✔
24
            self.adapter_class(serializer_class=serializer_class),
25
            api_params=kwargs, refresh_token_by_default=refresh_token_default,
26
            session=session)
27

28

29
class TapiocaClient(object):
1✔
30

31
    def __init__(self, api, data=None, response=None, request_kwargs=None,
1✔
32
                 api_params=None, resource=None, refresh_token_by_default=False,
33
                 refresh_data=None, session=None, *args, **kwargs):
34
        self._api = api
1✔
35
        self._data = data
1✔
36
        self._response = response
1✔
37
        self._api_params = api_params or {}
1✔
38
        self._request_kwargs = request_kwargs
1✔
39
        self._resource = resource
1✔
40
        self._refresh_token_default = refresh_token_by_default
1✔
41
        self._refresh_data = refresh_data
1✔
42
        self._session = session or requests.Session()
1✔
43

44
    def _instatiate_api(self):
1✔
45
        serializer_class = None
1✔
46
        if self._api.serializer:
1✔
47
            serializer_class = self._api.serializer.__class__
1✔
48
        return self._api.__class__(
1✔
49
            serializer_class=serializer_class)
50

51
    def _wrap_in_tapioca(self, data, *args, **kwargs):
1✔
52
        request_kwargs = kwargs.pop('request_kwargs', self._request_kwargs)
1✔
53
        return TapiocaClient(self._instatiate_api(), data=data,
1✔
54
                             api_params=self._api_params,
55
                             request_kwargs=request_kwargs,
56
                             refresh_token_by_default=self._refresh_token_default,
57
                             refresh_data=self._refresh_data,
58
                             session=self._session,
59
                             *args, **kwargs)
60

61
    def _wrap_in_tapioca_executor(self, data, *args, **kwargs):
1✔
62
        request_kwargs = kwargs.pop('request_kwargs', self._request_kwargs)
1✔
63
        return TapiocaClientExecutor(self._instatiate_api(), data=data,
1✔
64
                                     api_params=self._api_params,
65
                                     request_kwargs=request_kwargs,
66
                                     refresh_token_by_default=self._refresh_token_default,
67
                                     refresh_data=self._refresh_data,
68
                                     session=self._session,
69
                                     *args, **kwargs)
70

71
    def _get_doc(self):
1✔
72
        resources = copy.copy(self._resource)
1✔
73
        docs = ("Automatic generated __doc__ from resource_mapping.\n"
1✔
74
                "Resource: %s\n"
75
                "Docs: %s\n" % (resources.pop('resource', ''),
76
                                resources.pop('docs', '')))
77
        for key, value in sorted(resources.items()):
1✔
78
            docs += "%s: %s\n" % (key.title(), value)
1✔
79
        docs = docs.strip()
1✔
80
        return docs
1✔
81

82
    __doc__ = property(_get_doc)
1✔
83

84
    def __call__(self, *args, **kwargs):
1✔
85
        data = self._data
1✔
86

87
        url_params = self._api_params.get('default_url_params', {})
1✔
88
        url_params.update(kwargs)
1✔
89
        if self._resource and url_params:
1✔
90
            data = self._api.fill_resource_template_url(self._data, url_params)
1✔
91

92
        return self._wrap_in_tapioca_executor(data, resource=self._resource,
1✔
93
                                              response=self._response)
94
    """
95
    Convert a snake_case string in CamelCase.
96
    http://stackoverflow.com/questions/19053707/convert-snake-case-snake-case-to-lower-camel-case-lowercamelcase-in-python
97
    """
98
    def _to_camel_case(self, name):
1✔
99
        if isinstance(name, int):
1✔
100
            return name
×
101
        components = name.split('_')
1✔
102
        return components[0] + "".join(x.title() for x in components[1:])
1✔
103

104
    def _get_client_from_name(self, name):
1✔
105
        if (isinstance(self._data, list) and isinstance(name, int) or
1✔
106
                hasattr(self._data, '__iter__') and name in self._data):
107
            return self._wrap_in_tapioca(data=self._data[name])
1✔
108

109
        # if could not access, falback to resource mapping
110
        resource_mapping = self._api.resource_mapping
1✔
111
        if name in resource_mapping:
1✔
112
            resource = resource_mapping[name]
1✔
113
            api_root = self._api.get_api_root(
1✔
114
                self._api_params, resource_name=name
115
            )
116

117
            url = api_root.rstrip('/') + '/' + resource['resource'].lstrip('/')
1✔
118
            return self._wrap_in_tapioca(url, resource=resource)
1✔
119

120
        return None
1✔
121

122
    def _get_client_from_name_or_fallback(self, name):
1✔
123
        client = self._get_client_from_name(name)
1✔
124
        if client is not None:
1✔
125
            return client
1✔
126

127
        camel_case_name = self._to_camel_case(name)
1✔
128
        client = self._get_client_from_name(camel_case_name)
1✔
129
        if client is not None:
1✔
130
            return client
1✔
131

132
        normal_camel_case_name = camel_case_name[0].upper()
1✔
133
        normal_camel_case_name += camel_case_name[1:]
1✔
134

135
        client = self._get_client_from_name(normal_camel_case_name)
1✔
136
        if client is not None:
1✔
137
            return client
1✔
138

139
        return None
×
140

141
    def __getattr__(self, name):
1✔
142
        # Fix to be pickle-able:
143
        # return None for all unimplemented dunder methods
144
        if name.startswith('__') and name.endswith('__'):
1✔
145
            raise AttributeError(name)
1✔
146
        ret = self._get_client_from_name_or_fallback(name)
1✔
147
        if ret is None:
1✔
148
            raise AttributeError(name)
×
149
        return ret
1✔
150

151
    def __getitem__(self, key):
1✔
152
        ret = self._get_client_from_name_or_fallback(key)
1✔
153
        if ret is None:
1✔
154
            raise KeyError(key)
×
155
        return ret
1✔
156

157
    def __dir__(self):
1✔
158
        if self._api and self._data is None:
×
159
            return [key for key in self._api.resource_mapping.keys()]
×
160

161
        if isinstance(self._data, dict):
×
162
            return self._data.keys()
×
163

164
        return []
×
165

166
    def __str__(self):
1✔
167
        if type(self._data) == OrderedDict:
×
168
            return ("<{} object, printing as dict:\n"
×
169
                    "{}>").format(
170
                self.__class__.__name__, json.dumps(self._data, indent=4))
171
        else:
172
            import pprint
×
173
            pp = pprint.PrettyPrinter(indent=4)
×
174
            return ("<{} object\n"
×
175
                    "{}>").format(
176
                self.__class__.__name__, pp.pformat(self._data))
177

178
    def _repr_pretty_(self, p, cycle):
1✔
179
        p.text(self.__str__())
×
180

181
    def __len__(self):
1✔
182
        return len(self._data)
1✔
183

184
    def __contains__(self, key):
1✔
185
        return key in self._data
1✔
186

187

188
class TapiocaClientExecutor(TapiocaClient):
1✔
189

190
    def __init__(self, api, *args, **kwargs):
1✔
191
        super(TapiocaClientExecutor, self).__init__(api, *args, **kwargs)
1✔
192

193
    def __getitem__(self, key):
1✔
194
        raise Exception("This operation cannot be done on a" +
1✔
195
                        " TapiocaClientExecutor object")
196

197
    def __iter__(self):
1✔
198
        raise Exception("Cannot iterate over a TapiocaClientExecutor object")
1✔
199

200
    def __getattr__(self, name):
1✔
201
        # Fix to be pickle-able:
202
        # return None for all unimplemented dunder methods
203
        if name.startswith('__') and name.endswith('__'):
1✔
204
            raise AttributeError(name)
×
205
        if name.startswith('to_'):  # deserializing
1✔
206
            return self._api._get_to_native_method(name, self._data)
1✔
207
        return self._wrap_in_tapioca_executor(getattr(self._data, name))
1✔
208

209
    def __call__(self, *args, **kwargs):
1✔
210
        return self._wrap_in_tapioca(self._data.__call__(*args, **kwargs))
1✔
211

212
    @property
1✔
213
    def data(self):
1✔
214
        return self._data
1✔
215

216
    @property
1✔
217
    def response(self):
1✔
218
        if self._response is None:
1✔
219
            raise Exception("This instance has no response object")
1✔
220
        return self._response
1✔
221

222
    @property
1✔
223
    def status_code(self):
1✔
224
        return self.response.status_code
1✔
225

226
    @property
1✔
227
    def refresh_data(self):
1✔
228
        return self._refresh_data
1✔
229

230
    def _make_request(self, request_method, refresh_token=None, *args, **kwargs):
1✔
231
        if 'url' not in kwargs:
1✔
232
            kwargs['url'] = self._data
1✔
233

234
        request_kwargs = self._api.get_request_kwargs(
1✔
235
            self._api_params, request_method, *args, **kwargs)
236

237
        response = self._session.request(request_method, **request_kwargs)
1✔
238

239
        try:
1✔
240
            data = self._api.process_response(response)
1✔
241
        except ResponseProcessException as e:
1✔
242
            client = self._wrap_in_tapioca(e.data, response=response,
1✔
243
                                           request_kwargs=request_kwargs)
244

245
            error_message = self._api.get_error_message(data=e.data,
1✔
246
                                                        response=response)
247
            tapioca_exception = e.tapioca_exception(message=error_message,
1✔
248
                                                    client=client)
249

250
            should_refresh_token = (refresh_token is not False and
1✔
251
                                    self._refresh_token_default)
252
            auth_expired = self._api.is_authentication_expired(tapioca_exception)
1✔
253

254
            propagate_exception = True
1✔
255

256
            if should_refresh_token and auth_expired:
1✔
257
                self._refresh_data = self._api.refresh_authentication(self._api_params)
1✔
258
                if self._refresh_data:
1✔
259
                    propagate_exception = False
1✔
260
                    return self._make_request(request_method,
1✔
261
                                              refresh_token=False, *args, **kwargs)
262

263
            if propagate_exception:
1✔
264
                raise tapioca_exception
1✔
265

266
        return self._wrap_in_tapioca(data, response=response,
1✔
267
                                     request_kwargs=request_kwargs)
268

269
    def get(self, *args, **kwargs):
1✔
270
        return self._make_request('GET', *args, **kwargs)
1✔
271

272
    def post(self, *args, **kwargs):
1✔
273
        return self._make_request('POST', *args, **kwargs)
1✔
274

275
    def options(self, *args, **kwargs):
1✔
276
        return self._make_request('OPTIONS', *args, **kwargs)
×
277

278
    def put(self, *args, **kwargs):
1✔
279
        return self._make_request('PUT', *args, **kwargs)
1✔
280

281
    def patch(self, *args, **kwargs):
1✔
282
        return self._make_request('PATCH', *args, **kwargs)
1✔
283

284
    def delete(self, *args, **kwargs):
1✔
285
        return self._make_request('DELETE', *args, **kwargs)
1✔
286

287
    def _get_iterator_list(self):
1✔
288
        return self._api.get_iterator_list(self._data)
1✔
289

290
    def _get_iterator_next_request_kwargs(self):
1✔
291
        return self._api.get_iterator_next_request_kwargs(
1✔
292
            self._request_kwargs, self._data, self._response)
293

294
    def _reached_max_limits(self, page_count, item_count, max_pages,
1✔
295
                            max_items):
296
        reached_page_limit = max_pages is not None and max_pages <= page_count
1✔
297
        reached_item_limit = max_items is not None and max_items <= item_count
1✔
298
        return reached_page_limit or reached_item_limit
1✔
299

300
    def pages(self, max_pages=None, max_items=None, **kwargs):
1✔
301
        executor = self
1✔
302
        iterator_list = executor._get_iterator_list()
1✔
303
        page_count = 0
1✔
304
        item_count = 0
1✔
305

306
        while iterator_list:
1✔
307
            if self._reached_max_limits(page_count, item_count, max_pages,
1✔
308
                                        max_items):
309
                break
1✔
310
            for item in iterator_list:
1✔
311
                if self._reached_max_limits(page_count, item_count, max_pages,
1✔
312
                                            max_items):
313
                    break
1✔
314
                yield self._wrap_in_tapioca(item)
1✔
315
                item_count += 1
1✔
316

317
            page_count += 1
1✔
318

319
            next_request_kwargs = executor._get_iterator_next_request_kwargs()
1✔
320

321
            if not next_request_kwargs:
1✔
322
                break
1✔
323

324
            response = self.get(**next_request_kwargs)
1✔
325
            executor = response()
1✔
326
            iterator_list = executor._get_iterator_list()
1✔
327

328
    def open_docs(self):
1✔
329
        if not self._resource:
×
330
            raise KeyError()
×
331

332
        new = 2  # open in new tab
×
333
        webbrowser.open(self._resource['docs'], new=new)
×
334

335
    def open_in_browser(self):
1✔
336
        new = 2  # open in new tab
×
337
        webbrowser.open(self._data, new=new)
×
338

339
    def __dir__(self):
1✔
340
        methods = [m for m in TapiocaClientExecutor.__dict__.keys() if not m.startswith('_')]
1✔
341
        methods += [m for m in dir(self._api.serializer) if m.startswith('to_')]
1✔
342

343
        return methods
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc