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

TabPyState.delete_endpoint()   C

Complexity

Conditions 9

Size

Total Lines 53
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 53
rs 6.6666
c 0
b 0
f 0
cc 9
nop 2

How to fix   Long Method   

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:

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