pyUSIrest.client   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 495
Duplicated Lines 10.1 %

Importance

Changes 0
Metric Value
eloc 164
dl 50
loc 495
rs 9.36
c 0
b 0
f 0
wmc 38

1 Function

Rating   Name   Duplication   Size   Complexity  
A is_date() 0 13 2

18 Methods

Rating   Name   Duplication   Size   Complexity  
A Client.auth() 0 5 2
A Client.__init__() 0 16 1
A Client.check_headers() 0 21 3
A Client.check_status() 0 23 4
A Client.post() 0 26 1
A Document.follow_tag() 0 29 1
A Client.delete() 0 25 1
A Document.__update_key() 0 22 5
A Client.put() 26 26 1
A Document.__init__() 0 15 3
A Document.clean_url() 0 17 2
A Document.paginate() 0 22 2
A Document.read_url() 0 22 1
A Client.get() 24 24 1
A Document.follow_self_url() 0 21 1
A Client.patch() 0 26 1
A Document.get() 0 23 1
A Document.read_data() 0 19 4

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Thu Dec 19 16:28:46 2019
5
6
@author: Paolo Cozzi <[email protected]>
7
"""
8
9
import requests
10
import logging
11
12
from dateutil.parser import parse as parse_date
13
14
from . import __version__
15
from .auth import Auth
16
from .exceptions import USIConnectionError, USIDataError, TokenExpiredError
17
18
logger = logging.getLogger(__name__)
19
20
21
# https://stackoverflow.com/a/25341965/4385116
22
def is_date(string, fuzzy=False):
23
    """
24
    Return whether the string can be interpreted as a date.
25
26
    :param string: str, string to check for date
27
    :param fuzzy: bool, ignore unknown tokens in string if True
28
    """
29
    try:
30
        parse_date(string, fuzzy=fuzzy)
31
        return True
32
33
    except ValueError:
34
        return False
35
36
37
class Client():
38
    """A class to deal with EBI submission API. It perform request
39
    modelling user token in request headers. You need to call this class after
40
    instantiating an :py:class:`Auth <pyUSIrest.auth.Auth>` object::
41
42
        import getpass
43
        from pyUSIrest.auth import Auth
44
        from pyUSIrest.client import Client
45
        auth = Auth(user=<you_aap_user>, password=getpass.getpass())
46
        client = Client(auth)
47
        response = client.get("https://submission-test.ebi.ac.uk/api/")
48
49
    Attributes:
50
        headers (dict): default headers for requests
51
        last_response (requests.Response): last response object read by this
52
            class
53
        last_satus_code (int): last status code read by this class
54
        session (request.Session): a session object
55
        auth (Auth): a pyUSIrest Auth object
56
    """
57
58
    headers = {
59
        'Accept': 'application/hal+json',
60
        'User-Agent': 'pyUSIrest %s' % (__version__)
61
    }
62
63
    def __init__(self, auth):
64
        """Instantiate the class
65
66
        Args:
67
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
68
69
        """
70
71
        # my attributes
72
        self._auth = None
73
        self.last_response = None
74
        self.last_status_code = None
75
        self.session = requests.Session()
76
77
        # setting auth object
78
        self.auth = auth
79
80
    @property
81
    def auth(self):
82
        """Get/Set :py:class:`Auth <pyUSIrest.auth.Auth>` object"""
83
84
        return self._auth
85
86
    @auth.setter
87
    def auth(self, auth):
88
        logger.debug("Auth type is %s" % (type(auth)))
89
90
        # assign Auth object or create a new one
91
        if isinstance(auth, Auth):
92
            logger.debug("Assigning an Auth object")
93
            self._auth = auth
94
95
        else:
96
            logger.debug("Creating an Auth object")
97
            self._auth = Auth(token=auth)
98
99
        logger.debug("Updating headers with token")
100
        self.headers['Authorization'] = "Bearer {token}".format(
101
            token=self._auth.token)
102
103
    def check_headers(self, headers=None):
104
        """Checking headers and token
105
106
        Args:
107
            headers (dict): custom header for request
108
109
        Returns:
110
            headers (dict): an update headers tocken"""
111
112
        if self.auth.is_expired():
113
            raise TokenExpiredError("Your token is expired")
114
115
        if not headers:
116
            logger.debug("Using default headers")
117
            new_headers = self.headers
118
119
        else:
120
            new_headers = self.headers.copy()
121
            new_headers.update(headers)
122
123
        return new_headers
124
125
    def check_status(self, response, expected_status=200):
126
        """Check response status. See `HTTP status codes <https://submission.
127
        ebi.ac.uk/api/docs/ref_overview.html#_http_status_codes>`_
128
129
        Args:
130
            response (requests.Reponse): the reponse returned by requests
131
            method
132
        """
133
134
        # check with status code. deal with 50X statuses (internal error)
135
        if int(response.status_code / 100) == 5:
136
            raise USIConnectionError(
137
                "Problems with API endpoints: %s" % response.text)
138
139
        if int(response.status_code / 100) == 4:
140
            raise USIDataError(
141
                "Error with request: %s" % response.text)
142
143
        # TODO: evaluate a list of expected status?
144
        if response.status_code != expected_status:
145
            raise USIConnectionError(
146
                "Got a status code different than expected: %s (%s)" % (
147
                    response.status_code, response.text))
148
149 View Code Duplication
    def get(self, url, headers={}, params={}):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
150
        """Generic GET method
151
152
        Args:
153
            url (str): url to request
154
            headers (dict): custom headers for get request
155
            params (dict): custom params for get request
156
157
        Returns:
158
            requests.Response: a response object
159
        """
160
161
        logger.debug("Getting %s" % (url))
162
        headers = self.check_headers(headers)
163
        response = self.session.get(url, headers=headers, params=params)
164
165
        # track last response
166
        self.last_response = response
167
        self.last_status_code = response.status_code
168
169
        # check response status code
170
        self.check_status(response)
171
172
        return response
173
174
    def post(self, url, payload={}, headers={}, params={}):
175
        """Generic POST method
176
177
        Args:
178
            url (str): url to request
179
            payload (dict): data to send
180
            headers (dict): custom header for request
181
            params (dict): custom params for request
182
183
        Returns:
184
            requests.Response: a response object
185
        """
186
187
        logger.debug("Posting data to %s" % (url))
188
        headers = self.check_headers(headers)
189
        response = self.session.post(
190
            url, json=payload, headers=headers, params=params)
191
192
        # track last response
193
        self.last_response = response
194
        self.last_status_code = response.status_code
195
196
        # check response status code
197
        self.check_status(response, expected_status=201)
198
199
        return response
200
201
    def patch(self, url, payload={}, headers={}, params={}):
202
        """Generic PATCH method
203
204
        Args:
205
            url (str): url to request
206
            payload (dict): data to send
207
            headers (dict): custom header for request
208
            params (dict): custom params for request
209
210
        Returns:
211
            requests.Response: a response object
212
        """
213
214
        logger.debug("Patching data to %s" % (url))
215
        headers = self.check_headers(headers)
216
        response = self.session.patch(
217
            url, json=payload, headers=headers, params=params)
218
219
        # track last response
220
        self.last_response = response
221
        self.last_status_code = response.status_code
222
223
        # check response status code
224
        self.check_status(response)
225
226
        return response
227
228
    def delete(self, url, headers={}, params={}):
229
        """Generic DELETE method
230
231
        Args:
232
            url (str): url to request
233
            headers (dict): custom header for request
234
            params (dict): custom params for request
235
236
        Returns:
237
            requests.Response: a response object
238
        """
239
240
        logger.debug("Deleting %s" % (url))
241
242
        headers = self.check_headers(headers)
243
        response = self.session.delete(url, headers=headers, params=params)
244
245
        # track last response
246
        self.last_response = response
247
        self.last_status_code = response.status_code
248
249
        # check response status code
250
        self.check_status(response, expected_status=204)
251
252
        return response
253
254 View Code Duplication
    def put(self, url, payload={}, headers={}, params={}):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
255
        """Generic PUT method
256
257
        Args:
258
            url (str): url to request
259
            payload (dict): data to send
260
            params (dict): custom params for request
261
            headers (dict): custom header for request
262
263
        Returns:
264
            requests.Response: a response object
265
        """
266
267
        logger.debug("Putting data to %s" % (url))
268
        headers = self.check_headers(headers)
269
        response = self.session.put(
270
            url, json=payload, headers=headers, params=params)
271
272
        # track last response
273
        self.last_response = response
274
        self.last_status_code = response.status_code
275
276
        # check response status code
277
        self.check_status(response)
278
279
        return response
280
281
282
class Document(Client):
283
    """Base class for pyUSIrest classes. It models common methods and
284
    attributes by calling :py:class:`Client` and reading json response from
285
    biosample API
286
287
    Attributes:
288
        _link (dict): ``_links`` data read from USI response
289
        _embeddedd (dict): ``_embedded`` data read from USI response
290
        page (dict): ``page`` data read from USI response
291
        name (str): name of this object
292
        data (dict): data from USI read with
293
            :py:meth:`response.json() <requests.Response.json>`
294
295
    """
296
297
    def __init__(self, auth=None, data=None):
298
        # if I get auth, setting appropriate method
299
        if auth:
300
            Client.__init__(self, auth)
301
302
        # my class attributes
303
        self._links = {}
304
        self._embedded = {}
305
        self.page = {}
306
        self.name = None
307
        self.data = {}
308
309
        # if I get data, read data into myself
310
        if data:
311
            self.read_data(data)
312
313
    def get(self, url, force_keys=True):
314
        """Override the Client.get method and read data into object::
315
316
            document = Document(auth)
317
            document.get(settings.ROOT_URL + "/api/")
318
319
        Args:
320
            url (str): url to request
321
            force_keys (bool): If True, define a new class attribute from data
322
                keys
323
324
        Returns:
325
            requests.Response: a response object
326
        """
327
328
        # call the base method
329
        response = super().get(url)
330
331
        # read data
332
        self.read_data(response.json(), force_keys)
333
334
        # act like client object
335
        return response
336
337
    def read_data(self, data, force_keys=False):
338
        """Read data from a dictionary object and set class attributes
339
340
        Args:
341
            data (dict): a data dictionary object read with
342
                :py:meth:`response.json() <requests.Response.json>`
343
            force_keys (bool): If True, define a new class attribute from data
344
                keys
345
        """
346
347
        # dealing with this type of documents
348
        for key in data.keys():
349
            if "date" in key.lower() and is_date(data[key]):
350
                self.__update_key(key, parse_date(data[key]), force_keys)
351
352
            else:
353
                self.__update_key(key, data[key], force_keys)
354
355
        self.data = data
356
357
    def __update_key(self, key, value, force_keys=False):
358
        """Helper function to update keys"""
359
360
        if hasattr(self, key):
361
            if getattr(self, key) and getattr(self, key) != '':
362
                # when I reload data, I do a substitution
363
                logger.debug("Found %s -> %s" % (key, getattr(self, key)))
364
                logger.debug("Updating %s -> %s" % (key, value))
365
366
            else:
367
                # don't have this attribute set
368
                logger.debug("Setting %s -> %s" % (key, value))
369
370
            setattr(self, key, value)
371
372
        else:
373
            if force_keys is True:
374
                logger.debug("Forcing %s -> %s" % (key, value))
375
                setattr(self, key, value)
376
377
            else:
378
                logger.warning("key %s not implemented" % (key))
379
380
    @classmethod
381
    def clean_url(cls, url):
382
        """Remove stuff like ``{?projection}`` from url
383
384
        Args:
385
            url (str): a string url
386
387
        Returns:
388
            str: the cleaned url
389
        """
390
391
        # remove {?projection} from self url. This is unreachable
392
        if '{?projection}' in url:
393
            logger.debug("removing {?projection} from url")
394
            url = url.replace("{?projection}", "")
395
396
        return url
397
398
    @classmethod
399
    def read_url(cls, auth, url):
400
        """Read a url and returns a :py:class:`Document` object
401
402
        Args:
403
            auth (Auth): an Auth object to pass to result
404
            url (str): url to request
405
406
        Returns:
407
            Document: a document object
408
        """
409
410
        # clean url
411
        url = cls.clean_url(url)
412
413
        # create a new document
414
        document = cls(auth=auth)
415
416
        # get url and load data
417
        document.get(url)
418
419
        return document
420
421
    def paginate(self):
422
        """Follow all the pages. Return an iterator of document objects
423
424
        Args:
425
            response (requests.Response): a response object
426
427
        Yield:
428
            Document: a new Document instance
429
        """
430
431
        # return myself
432
        yield self
433
434
        # track the current document
435
        document = self
436
437
        while 'next' in document._links:
438
            url = document._links['next']['href']
439
            document = Document.read_url(self.auth, url)
440
441
            # return the last document
442
            yield document
443
444
    def follow_tag(self, tag, force_keys=True):
445
        """Pick a url from data attribute relying on tag, perform a request
446
        and returns a document object. For instance::
447
448
            document.follow_tag('userSubmissions')
449
450
        will return a document instance by requesting with
451
        :py:meth:`Client.get` using
452
        ``document._links['userSubmissions']['href']`` as url
453
454
        Args:
455
            tag (str): a key from USI response dictionary
456
            force_keys (bool): set a new class attribute if not present
457
458
        Returns:
459
            Document: a document object
460
        """
461
462
        logger.debug("Following %s url" % (tag))
463
464
        url = self._links[tag]['href']
465
466
        # create a new document
467
        document = Document(auth=self.auth)
468
469
        # read data
470
        document.get(url, force_keys)
471
472
        return document
473
474
    def follow_self_url(self):
475
        """Follow *self* url and update class attributes. For instance::
476
477
            document.follow_self_url()
478
479
        will reload document instance by requesting with
480
        :py:meth:`Client.get` using
481
        ``document.data['_links']['self']['href']`` as url"""
482
483
        logger.debug("Following self url")
484
485
        # get a url to follow
486
        url = self._links['self']['href']
487
488
        # clean url
489
        url = self.clean_url(url)
490
491
        logger.debug("Updating self")
492
493
        # now follow self url and load data to self
494
        self.get(url)
495