Passed
Push — master ( 227024...1d0698 )
by Oleksandr
02:44
created

TabPyState.add_endpoint()   F

Complexity

Conditions 21

Size

Total Lines 68
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 68
rs 0
c 0
b 0
f 0
cc 21
nop 9

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like tabpy.tabpy_server.management.state.TabPyState.add_endpoint() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
try:
2
    from ConfigParser import ConfigParser
3
except ImportError:
4
    from configparser import ConfigParser
5
import json
6
import logging
7
import sys
8
from tabpy.tabpy_server.management.util import write_state_config
9
from threading import Lock
10
from time import time
11
12
13
logger = logging.getLogger(__name__)
14
15
# State File Config Section Names
16
_DEPLOYMENT_SECTION_NAME = 'Query Objects Service Versions'
17
_QUERY_OBJECT_DOCSTRING = 'Query Objects Docstrings'
18
_SERVICE_INFO_SECTION_NAME = 'Service Info'
19
_META_SECTION_NAME = 'Meta'
20
21
# Directory Names
22
_QUERY_OBJECT_DIR = 'query_objects'
23
24
'''
25
Lock to change the TabPy State.
26
'''
27
_PS_STATE_LOCK = Lock()
28
29
30
def state_lock(func):
31
    '''
32
    Mutex for changing PS state
33
    '''
34
    def wrapper(self, *args, **kwargs):
35
        try:
36
            _PS_STATE_LOCK.acquire()
37
            return func(self, *args, **kwargs)
38
        finally:
39
            # ALWAYS RELEASE LOCK
40
            _PS_STATE_LOCK.release()
41
    return wrapper
42
43
44
def _get_root_path(state_path):
45
    if state_path[-1] != '/':
46
        return state_path + '/'
47
    else:
48
        return state_path
49
50
51
def get_query_object_path(state_file_path, name, version):
52
    '''
53
    Returns the query object path
54
55
    If the version is None, a path without the version will be returned.
56
    '''
57
    root_path = _get_root_path(state_file_path)
58
    if version is not None:
59
        full_path = root_path + \
60
                '/'.join([_QUERY_OBJECT_DIR, name, str(version)])
61
    else:
62
        full_path = root_path + \
63
                '/'.join([_QUERY_OBJECT_DIR, name])
64
    return full_path
65
66
67
class TabPyState(object):
68
    '''
69
    The TabPy state object that stores attributes
70
    about this TabPy and perform GET/SET on these
71
    attributes.
72
73
    Attributes:
74
        - name
75
        - description
76
        - endpoints (name, description, docstring, version, target)
77
        - revision number
78
79
    When the state object is initialized, the state is saved as a ConfigParser.
80
    There is a config to any attribute.
81
82
    '''
83
    def __init__(self, settings, config=None):
84
        self.settings = settings
85
        self.set_config(config, _update=False)
86
87
    @state_lock
88
    def set_config(self, config,
89
                   logger=logging.getLogger(__name__),
90
                   _update=True):
91
        '''
92
        Set the local ConfigParser manually.
93
        This new ConfigParser will be used as current state.
94
        '''
95
        if not isinstance(config, ConfigParser):
96
            raise ValueError("Invalid config")
97
        self.config = config
98
        if _update:
99
            self._write_state(logger)
100
101
    def get_endpoints(self, name=None):
102
        '''
103
        Return a dictionary of endpoints
104
105
        Parameters
106
        ----------
107
        name : str
108
            The name of the endpoint.
109
            If "name" is specified, only the information about that endpoint
110
            will be returned.
111
112
        Returns
113
        -------
114
        endpoints : dict
115
            The dictionary containing information about each endpoint.
116
            The keys are the endpoint names.
117
            The values for each include:
118
                - description
119
                - doc string
120
                - type
121
                - target
122
123
        '''
124
        endpoints = {}
125
        try:
126
            endpoint_names = self._get_config_value(
127
                _DEPLOYMENT_SECTION_NAME, name)
128
        except Exception as e:
129
            logger.error(f'error in get_endpoints: {str(e)}')
130
            return {}
131
132
        if name:
133
            endpoint_info = json.loads(endpoint_names)
134
            docstring = self._get_config_value(_QUERY_OBJECT_DOCSTRING, name)
135
            endpoint_info['docstring'] = str(
136
                bytes(docstring, "utf-8").decode('unicode_escape'))
137
            endpoints = {name: endpoint_info}
138
        else:
139
            for endpoint_name in endpoint_names:
140
                endpoint_info = json.loads(self._get_config_value(
141
                    _DEPLOYMENT_SECTION_NAME, endpoint_name))
142
                docstring = self._get_config_value(_QUERY_OBJECT_DOCSTRING,
143
                                                   endpoint_name, True, '')
144
                endpoint_info['docstring'] = str(
145
                    bytes(docstring, "utf-8").decode('unicode_escape'))
146
                endpoints[endpoint_name] = endpoint_info
147
        logger.debug(f'Collected endpoints: {endpoints}')
148
        return endpoints
149
150
    @state_lock
151
    def add_endpoint(self, name, description=None,
152
                     docstring=None, endpoint_type=None,
153
                     methods=None, target=None, dependencies=None,
154
                     schema=None):
155
        '''
156
        Add a new endpoint to the TabPy.
157
158
        Parameters
159
        ----------
160
        name : str
161
            Name of the endpoint
162
        description : str, optional
163
            Description of this endpoint
164
        doc_string : str, optional
165
            The doc string for this endpoint, if needed.
166
        endpoint_type : str
167
            The endpoint type (model, alias)
168
        target : str, optional
169
            The target endpoint name for the alias to be added.
170
171
        Note:
172
        The version of this endpoint will be set to 1 since it is a new
173
        endpoint.
174
175
        '''
176
        try:
177
            endpoints = self.get_endpoints()
178
            if name is None or not isinstance(
179
                    name, str) or len(name) == 0:
180
                raise ValueError(
181
                    "name of the endpoint must be a valid string.")
182
            elif name in endpoints:
183
                raise ValueError(f'endpoint {name} already exists.')
184
            if description and not isinstance(description, str):
185
                raise ValueError("description must be a string.")
186
            elif not description:
187
                description = ''
188
            if docstring and not isinstance(docstring, str):
189
                raise ValueError("docstring must be a string.")
190
            elif not docstring:
191
                docstring = '-- no docstring found in query function --'
192
            if not endpoint_type or not isinstance(endpoint_type, str):
193
                raise ValueError("endpoint type must be a string.")
194
            if dependencies and not isinstance(dependencies, list):
195
                raise ValueError("dependencies must be a list.")
196
            elif not dependencies:
197
                dependencies = []
198
            if target and not isinstance(target, str):
199
                raise ValueError("target must be a string.")
200
            elif target and target not in endpoints:
201
                raise ValueError("target endpoint is not valid.")
202
203
            endpoint_info = {"description": description,
204
                             "docstring": docstring,
205
                             "type": endpoint_type,
206
                             "version": 1,
207
                             "dependencies": dependencies,
208
                             "target": target,
209
                             "creation_time": int(time()),
210
                             "last_modified_time": int(time()),
211
                             "schema": schema}
212
213
            endpoints[name] = endpoint_info
214
            self._add_update_endpoints_config(endpoints)
215
        except Exception as e:
216
            logger.error(f'Error in add_endpoint: {e}')
217
            raise
218
219
    def _add_update_endpoints_config(self, endpoints):
220
        # save the endpoint info to config
221
        dstring = ''
222
        for endpoint_name in endpoints:
223
            try:
224
                info = endpoints[endpoint_name]
225
                dstring = str(bytes(info['docstring'], "utf-8").decode(
226
                    'unicode_escape'))
227
                self._set_config_value(_QUERY_OBJECT_DOCSTRING,
228
                                       endpoint_name,
229
                                       dstring,
230
                                       _update_revision=False)
231
                del info['docstring']
232
                self._set_config_value(_DEPLOYMENT_SECTION_NAME,
233
                                       endpoint_name, json.dumps(info))
234
            except Exception as e:
235
                logger.error(f'Unable to write endpoints config: {e}')
236
                raise
237
238
    @state_lock
239
    def update_endpoint(self, name, description=None,
240
                        docstring=None, endpoint_type=None,
241
                        version=None, methods=None,
242
                        target=None, dependencies=None,
243
                        schema=None):
244
        '''
245
        Update an existing endpoint on the TabPy.
246
247
        Parameters
248
        ----------
249
        name : str
250
            Name of the endpoint
251
        description : str, optional
252
            Description of this endpoint
253
        doc_string : str, optional
254
            The doc string for this endpoint, if needed.
255
        endpoint_type : str, optional
256
            The endpoint type (model, alias)
257
        version : str, optional
258
            The version of this endpoint
259
        dependencies=[]
260
            List of dependent endpoints for this existing endpoint
261
        target : str, optional
262
            The target endpoint name for the alias.
263
264
        Note:
265
        For those parameters that are not specified, those values will not
266
        get changed.
267
268
        '''
269
        try:
270
            endpoints = self.get_endpoints()
271
            if not name or not isinstance(name, str):
272
                raise ValueError("name of the endpoint must be string.")
273
            elif name not in endpoints:
274
                raise ValueError(f'endpoint {name} does not exist.')
275
276
            endpoint_info = endpoints[name]
277
278
            if description and not isinstance(description, str):
279
                raise ValueError("description must be a string.")
280
            elif not description:
281
                description = endpoint_info['description']
282
            if docstring and not isinstance(docstring, str):
283
                raise ValueError("docstring must be a string.")
284
            elif not docstring:
285
                docstring = endpoint_info['docstring']
286
            if endpoint_type and not isinstance(endpoint_type, str):
287
                raise ValueError("endpoint type must be a string.")
288
            elif not endpoint_type:
289
                endpoint_type = endpoint_info['type']
290
            if version and not isinstance(version, int):
291
                raise ValueError("version must be an int.")
292
            elif not version:
293
                version = endpoint_info['version']
294
            if dependencies and not isinstance(dependencies, list):
295
                raise ValueError("dependencies must be a list.")
296
            elif not dependencies:
297
                if 'dependencies' in endpoint_info:
298
                    dependencies = endpoint_info['dependencies']
299
                else:
300
                    dependencies = []
301
            if target and not isinstance(target, str):
302
                raise ValueError("target must be a string.")
303
            elif target and target not in endpoints:
304
                raise ValueError("target endpoint is not valid.")
305
            elif not target:
306
                target = endpoint_info['target']
307
            endpoint_info = {'description': description,
308
                             'docstring': docstring,
309
                             'type': endpoint_type,
310
                             'version': version,
311
                             'dependencies': dependencies,
312
                             'target': target,
313
                             'creation_time': endpoint_info['creation_time'],
314
                             'last_modified_time': int(time()),
315
                             'schema': schema}
316
317
            endpoints[name] = endpoint_info
318
            self._add_update_endpoints_config(endpoints)
319
        except Exception as e:
320
            logger.error(f'Error in update_endpoint: {e}')
321
            raise
322
323
    @state_lock
324
    def delete_endpoint(self, name):
325
        '''
326
        Delete an existing endpoint on the TabPy
327
328
        Parameters
329
        ----------
330
        name : str
331
            The name of the endpoint to be deleted.
332
333
        Returns
334
        -------
335
        deleted endpoint object
336
337
        Note:
338
        Cannot delete this endpoint if other endpoints are currently
339
        depending on this endpoint.
340
341
        '''
342
        if not name or name == '':
343
            raise ValueError("Name of the endpoint must be a valid string.")
344
        endpoints = self.get_endpoints()
345
        if name not in endpoints:
346
            raise ValueError(f'Endpoint {name} does not exist.')
347
348
        endpoint_to_delete = endpoints[name]
349
350
        # get dependencies and target
351
        deps = set()
352
        for endpoint_name in endpoints:
353
            if endpoint_name != name:
354
                deps_list = endpoints[endpoint_name].get('dependencies', [])
355
                if name in deps_list:
356
                    deps.add(endpoint_name)
357
358
        # check if other endpoints are depending on this endpoint
359
        if len(deps) > 0:
360
            raise ValueError(
361
                f'Cannot remove endpoint {name}, it is currently '
362
                f'used by {list(deps)} endpoints.')
363
364
        del endpoints[name]
365
366
        # delete the endpoint from state
367
        try:
368
            self._remove_config_option(_QUERY_OBJECT_DOCSTRING, name,
369
                                       _update_revision=False)
370
            self._remove_config_option(_DEPLOYMENT_SECTION_NAME, name)
371
372
            return endpoint_to_delete
373
        except Exception as e:
374
            logger.error(f'Unable to delete endpoint {e}')
375
            raise ValueError(f'Unable to delete endpoint: {e}')
376
377
    @property
378
    def name(self):
379
        '''
380
        Returns the name of the TabPy service.
381
        '''
382
        name = None
383
        try:
384
            name = self._get_config_value(_SERVICE_INFO_SECTION_NAME, 'Name')
385
        except Exception as e:
386
            logger.error(f'Unable to get name: {e}')
387
        return name
388
389
    @property
390
    def creation_time(self):
391
        '''
392
        Returns the creation time of the TabPy service.
393
        '''
394
        creation_time = 0
395
        try:
396
            creation_time = self._get_config_value(
397
                _SERVICE_INFO_SECTION_NAME, 'Creation Time')
398
        except Exception as e:
399
            logger.error(f'Unable to get name: {e}')
400
        return creation_time
401
402
    @state_lock
403
    def set_name(self, name):
404
        '''
405
        Set the name of this TabPy service.
406
407
        Parameters
408
        ----------
409
        name : str
410
            Name of TabPy service.
411
        '''
412
        if not isinstance(name, str):
413
            raise ValueError("name must be a string.")
414
        try:
415
            self._set_config_value(_SERVICE_INFO_SECTION_NAME, 'Name', name)
416
        except Exception as e:
417
            logger.error(f'Unable to set name: {e}')
418
419
    def get_description(self):
420
        '''
421
        Returns the description of the TabPy service.
422
        '''
423
        description = None
424
        try:
425
            description = self._get_config_value(
426
                _SERVICE_INFO_SECTION_NAME, 'Description')
427
        except Exception as e:
428
            logger.error(f'Unable to get description: {e}')
429
        return description
430
431
    @state_lock
432
    def set_description(self, description):
433
        '''
434
        Set the description of this TabPy service.
435
436
        Parameters
437
        ----------
438
        description : str
439
            Description of TabPy service.
440
        '''
441
        if not isinstance(description, str):
442
            raise ValueError("Description must be a string.")
443
        try:
444
            self._set_config_value(
445
                _SERVICE_INFO_SECTION_NAME, 'Description', description)
446
        except Exception as e:
447
            logger.error(f'Unable to set description: {e}')
448
449
    def get_revision_number(self):
450
        '''
451
        Returns the revision number of this TabPy service.
452
        '''
453
        rev = -1
454
        try:
455
            rev = int(self._get_config_value(
456
                _META_SECTION_NAME, 'Revision Number'))
457
        except Exception as e:
458
            logger.error(f'Unable to get revision number: {e}')
459
        return rev
460
461
    def get_access_control_allow_origin(self):
462
        '''
463
        Returns Access-Control-Allow-Origin of this TabPy service.
464
        '''
465
        _cors_origin = ''
466
        try:
467
            logger.debug("Collecting Access-Control-Allow-Origin from "
468
                         "state file...")
469
            _cors_origin = self._get_config_value(
470
                'Service Info', 'Access-Control-Allow-Origin')
471
        except Exception as e:
472
            logger.error(e)
473
            pass
474
        return _cors_origin
475
476
    def get_access_control_allow_headers(self):
477
        '''
478
        Returns Access-Control-Allow-Headers of this TabPy service.
479
        '''
480
        _cors_headers = ''
481
        try:
482
            _cors_headers = self._get_config_value(
483
                'Service Info', 'Access-Control-Allow-Headers')
484
        except Exception:
485
            pass
486
        return _cors_headers
487
488
    def get_access_control_allow_methods(self):
489
        '''
490
        Returns Access-Control-Allow-Methods of this TabPy service.
491
        '''
492
        _cors_methods = ''
493
        try:
494
            _cors_methods = self._get_config_value(
495
                'Service Info', 'Access-Control-Allow-Methods')
496
        except Exception:
497
            pass
498
        return _cors_methods
499
500
    def _set_revision_number(self, revision_number):
501
        '''
502
        Set the revision number of this TabPy service.
503
        '''
504
        if not isinstance(revision_number, int):
505
            raise ValueError("revision number must be an int.")
506
        try:
507
            self._set_config_value(_META_SECTION_NAME,
508
                                   'Revision Number', revision_number)
509
        except Exception as e:
510
            logger.error(f'Unable to set revision number: {e}')
511
512
    def _remove_config_option(self, section_name, option_name,
513
                              logger=logging.getLogger(__name__),
514
                              _update_revision=True):
515
        if not self.config:
516
            raise ValueError("State configuration not yet loaded.")
517
        self.config.remove_option(section_name, option_name)
518
        # update revision number
519
        if _update_revision:
520
            self._increase_revision_number()
521
        self._write_state(logger=logger)
522
523
    def _has_config_value(self, section_name, option_name):
524
        if not self.config:
525
            raise ValueError("State configuration not yet loaded.")
526
        return self.config.has_option(section_name, option_name)
527
528
    def _increase_revision_number(self):
529
        if not self.config:
530
            raise ValueError("State configuration not yet loaded.")
531
        cur_rev = int(self.config.get(_META_SECTION_NAME, 'Revision Number'))
532
        self.config.set(_META_SECTION_NAME, 'Revision Number',
533
                        str(cur_rev + 1))
534
535
    def _set_config_value(self, section_name, option_name, option_value,
536
                          logger=logging.getLogger(__name__),
537
                          _update_revision=True):
538
        if not self.config:
539
            raise ValueError("State configuration not yet loaded.")
540
541
        if not self.config.has_section(section_name):
542
            logger.log(logging.DEBUG, f'Adding config section {section_name}')
543
            self.config.add_section(section_name)
544
545
        self.config.set(section_name, option_name, option_value)
546
        # update revision number
547
        if _update_revision:
548
            self._increase_revision_number()
549
        self._write_state(logger=logger)
550
551
    def _get_config_items(self, section_name):
552
        if not self.config:
553
            raise ValueError("State configuration not yet loaded.")
554
        return self.config.items(section_name)
555
556
    def _get_config_value(self, section_name, option_name, optional=False,
557
                          default_value=None):
558
        if not self.config:
559
            raise ValueError("State configuration not yet loaded.")
560
561
        if not option_name:
562
            return self.config.options(section_name)
563
564
        if self.config.has_option(section_name, option_name):
565
            return self.config.get(section_name, option_name)
566
        elif optional:
567
            return default_value
568
        else:
569
            raise ValueError(
570
                f'Cannot find option name {option_name} '
571
                f'under section {section_name}')
572
573
    def _write_state(self, logger=logging.getLogger(__name__)):
574
        '''
575
        Write state (ConfigParser) to Consul
576
        '''
577
        logger.log(logging.INFO, 'Writing state to config')
578
        write_state_config(self.config, self.settings, logger=logger)
579