Passed
Pull Request — master (#650)
by
unknown
34:59 queued 17:21
created

tabpy.tabpy_server.management.state   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 661
Duplicated Lines 0 %

Test Coverage

Coverage 41.31%

Importance

Changes 0
Metric Value
wmc 103
eloc 386
dl 0
loc 661
ccs 126
cts 305
cp 0.4131
rs 2
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A TabPyState.__init__() 0 3 1
A TabPyState.get_endpoints() 0 51 4
A TabPyState.set_config() 0 11 3
A TabPyState._check_and_set_endpoint_docstring() 0 2 1
A TabPyState._check_and_set_dependencies() 0 8 4
A TabPyState._check_and_set_endpoint_description() 0 2 1
A TabPyState._check_endpoint_exists() 0 6 4
A TabPyState._check_and_set_endpoint_type() 0 3 1
A TabPyState._check_and_set_endpoint_str_value() 0 8 5
A TabPyState._check_target() 0 3 3
B TabPyState.add_endpoint() 0 69 5
A TabPyState._check_and_set_is_public() 0 5 2
A TabPyState._add_update_endpoints_config() 0 22 3
A TabPyState.get_description() 0 12 2
A TabPyState.name() 0 11 2
A TabPyState.creation_time() 0 13 2
A TabPyState._remove_config_option() 0 14 3
B TabPyState._get_config_value() 0 27 5
A TabPyState.get_access_control_allow_headers() 0 12 2
A TabPyState._has_config_value() 0 4 2
A TabPyState._set_config_value() 0 20 4
A TabPyState.get_access_control_allow_methods() 0 12 2
A TabPyState.get_access_control_allow_origin() 0 13 2
C TabPyState.delete_endpoint() 0 55 9
A TabPyState._write_state() 0 6 1
A TabPyState.set_description() 0 18 3
C TabPyState.update_endpoint() 0 90 10
A TabPyState._get_config_items() 0 4 2
A TabPyState._set_revision_number() 0 12 3
A TabPyState.get_revision_number() 0 10 2
A TabPyState.set_name() 0 16 3
A TabPyState._increase_revision_number() 0 5 2

3 Functions

Rating   Name   Duplication   Size   Complexity  
A state_lock() 0 14 1
A _get_root_path() 0 5 2
A get_query_object_path() 0 12 2

How to fix   Complexity   

Complexity

Complex classes like tabpy.tabpy_server.management.state 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.

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