Passed
Pull Request — master (#650)
by
unknown
18:25
created

tabpy.tabpy_server.management.state   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 662
Duplicated Lines 0 %

Test Coverage

Coverage 41.18%

Importance

Changes 0
Metric Value
wmc 103
eloc 387
dl 0
loc 662
ccs 126
cts 306
cp 0.4118
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.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._check_and_set_is_public() 0 6 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
A TabPyState._add_update_endpoints_config() 0 22 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
        logger.log(logging.INFO, f"set is_public")
192
        if is_public is None:
193
            return defaultValue
194
195
        return is_public
196
197 1
    @state_lock
198 1
    def add_endpoint(
199
        self,
200
        name,
201
        description=None,
202
        docstring=None,
203
        endpoint_type=None,
204
        methods=None,
205
        target=None,
206
        dependencies=None,
207
        schema=None,
208
        is_public=None,
209
    ):
210
        """
211
        Add a new endpoint to the TabPy.
212
213
        Parameters
214
        ----------
215
        name : str
216
            Name of the endpoint
217
        description : str, optional
218
            Description of this endpoint
219
        doc_string : str, optional
220
            The doc string for this endpoint, if needed.
221
        endpoint_type : str
222
            The endpoint type (model, alias)
223
        target : str, optional
224
            The target endpoint name for the alias to be added.
225
226
        Note:
227
        The version of this endpoint will be set to 1 since it is a new
228
        endpoint.
229
230
        """
231
        try:
232
            if (self._check_endpoint_exists(name)):
233
                raise ValueError(f"endpoint {name} already exists.")
234
235
            endpoints = self.get_endpoints()
236
237
            description = self._check_and_set_endpoint_description(description, "")
238
            docstring = self._check_and_set_endpoint_docstring(
239
                docstring, "-- no docstring found in query function --")
240
            endpoint_type = self._check_and_set_endpoint_type(endpoint_type, None)
241
            dependencies = self._check_and_set_dependencies(dependencies, [])
242
            is_public = self._check_and_set_is_public(is_public, False)
243
244
            self._check_target(target)
245
            if target and target not in endpoints:
246
                raise ValueError("target endpoint is not valid.")
247
248
            endpoint_info = {
249
                "description": description,
250
                "docstring": docstring,
251
                "type": endpoint_type,
252
                "version": 1,
253
                "dependencies": dependencies,
254
                "target": target,
255
                "creation_time": int(time()),
256
                "last_modified_time": int(time()),
257
                "schema": schema,
258
                "is_public": is_public,
259
            }
260
261
            endpoints[name] = endpoint_info
262
            self._add_update_endpoints_config(endpoints)
263
        except Exception as e:
264
            logger.error(f"Error in add_endpoint: {e}")
265
            raise
266
267 1
    def _add_update_endpoints_config(self, endpoints):
268
        # save the endpoint info to config
269
        dstring = ""
270
        for endpoint_name in endpoints:
271
            try:
272
                info = endpoints[endpoint_name]
273
                dstring = str(
274
                    bytes(info["docstring"], "utf-8").decode("unicode_escape")
275
                )
276
                self._set_config_value(
277
                    _QUERY_OBJECT_DOCSTRING,
278
                    endpoint_name,
279
                    dstring,
280
                    _update_revision=False,
281
                )
282
                del info["docstring"]
283
                self._set_config_value(
284
                    _DEPLOYMENT_SECTION_NAME, endpoint_name, json.dumps(info)
285
                )
286
            except Exception as e:
287
                logger.error(f"Unable to write endpoints config: {e}")
288
                raise
289
290 1
    @state_lock
291 1
    def update_endpoint(
292
        self,
293
        name,
294
        description=None,
295
        docstring=None,
296
        endpoint_type=None,
297
        version=None,
298
        methods=None,
299
        target=None,
300
        dependencies=None,
301
        schema=None,
302
        is_public=None,
303
    ):
304
        """
305
        Update an existing endpoint on the TabPy.
306
307
        Parameters
308
        ----------
309
        name : str
310
            Name of the endpoint
311
        description : str, optional
312
            Description of this endpoint
313
        doc_string : str, optional
314
            The doc string for this endpoint, if needed.
315
        endpoint_type : str, optional
316
            The endpoint type (model, alias)
317
        version : str, optional
318
            The version of this endpoint
319
        dependencies=[]
320
            List of dependent endpoints for this existing endpoint
321
        target : str, optional
322
            The target endpoint name for the alias.
323
324
        Note:
325
        For those parameters that are not specified, those values will not
326
        get changed.
327
328
        """
329
        try:
330
            if (not self._check_endpoint_exists(name)):
331
                raise ValueError(f"endpoint {name} does not exist.")
332
333
            endpoints = self.get_endpoints()
334
            endpoint_info = endpoints[name]
335
336
            description = self._check_and_set_endpoint_description(
337
                description, endpoint_info["description"])
338
            docstring = self._check_and_set_endpoint_docstring(
339
                docstring, endpoint_info["docstring"])
340
            endpoint_type = self._check_and_set_endpoint_type(
341
                endpoint_type, endpoint_info["type"])
342
            dependencies = self._check_and_set_dependencies(
343
                dependencies, endpoint_info.get("dependencies", []))
344
            # Adding is_public means that some existing functions do not have the is_public attribute set. 
345
            # We need to check for this when updating and set to False by default 
346
            current_is_public = False
347
            if hasattr(endpoint_info, "is_public"):
348
                current_is_public = endpoint_info["is_public"]
349
            is_public = self._check_and_set_is_public(is_public, current_is_public)
350
351
            self._check_target(target)
352
            if target and target not in endpoints:
353
                raise ValueError("target endpoint is not valid.")
354
            elif not target:
355
                target = endpoint_info["target"]
356
357
            if version and not isinstance(version, int):
358
                raise ValueError("version must be an int.")
359
            elif not version:
360
                version = endpoint_info["version"]
361
362
            endpoint_info = {
363
                "description": description,
364
                "docstring": docstring,
365
                "type": endpoint_type,
366
                "version": version,
367
                "dependencies": dependencies,
368
                "target": target,
369
                "creation_time": endpoint_info["creation_time"],
370
                "last_modified_time": int(time()),
371
                "schema": schema,
372
                "is_public": is_public,
373
            }
374
375
            endpoints[name] = endpoint_info
376
            self._add_update_endpoints_config(endpoints)
377
        except Exception as e:
378
            logger.error(f"Error in update_endpoint: {e}")
379
            raise
380
381 1
    @state_lock
382 1
    def delete_endpoint(self, name):
383
        """
384
        Delete an existing endpoint on the TabPy
385
386
        Parameters
387
        ----------
388
        name : str
389
            The name of the endpoint to be deleted.
390
391
        Returns
392
        -------
393
        deleted endpoint object
394
395
        Note:
396
        Cannot delete this endpoint if other endpoints are currently
397
        depending on this endpoint.
398
399
        """
400
        if not name or name == "":
401
            raise ValueError("Name of the endpoint must be a valid string.")
402
        endpoints = self.get_endpoints()
403
        if name not in endpoints:
404
            raise ValueError(f"Endpoint {name} does not exist.")
405
406
        endpoint_to_delete = endpoints[name]
407
408
        # get dependencies and target
409
        deps = set()
410
        for endpoint_name in endpoints:
411
            if endpoint_name != name:
412
                deps_list = endpoints[endpoint_name].get("dependencies", [])
413
                if name in deps_list:
414
                    deps.add(endpoint_name)
415
416
        # check if other endpoints are depending on this endpoint
417
        if len(deps) > 0:
418
            raise ValueError(
419
                f"Cannot remove endpoint {name}, it is currently "
420
                f"used by {list(deps)} endpoints."
421
            )
422
423
        del endpoints[name]
424
425
        # delete the endpoint from state
426
        try:
427
            self._remove_config_option(
428
                _QUERY_OBJECT_DOCSTRING, name, _update_revision=False
429
            )
430
            self._remove_config_option(_DEPLOYMENT_SECTION_NAME, name)
431
432
            return endpoint_to_delete
433
        except Exception as e:
434
            logger.error(f"Unable to delete endpoint {e}")
435
            raise ValueError(f"Unable to delete endpoint: {e}")
436
437 1
    @property
438 1
    def name(self):
439
        """
440
        Returns the name of the TabPy service.
441
        """
442 1
        name = None
443 1
        try:
444 1
            name = self._get_config_value(_SERVICE_INFO_SECTION_NAME, "Name")
445
        except Exception as e:
446
            logger.error(f"Unable to get name: {e}")
447 1
        return name
448
449 1
    @property
450 1
    def creation_time(self):
451
        """
452
        Returns the creation time of the TabPy service.
453
        """
454 1
        creation_time = 0
455 1
        try:
456 1
            creation_time = self._get_config_value(
457
                _SERVICE_INFO_SECTION_NAME, "Creation Time"
458
            )
459
        except Exception as e:
460
            logger.error(f"Unable to get name: {e}")
461 1
        return creation_time
462
463 1
    @state_lock
464 1
    def set_name(self, name):
465
        """
466
        Set the name of this TabPy service.
467
468
        Parameters
469
        ----------
470
        name : str
471
            Name of TabPy service.
472
        """
473
        if not isinstance(name, str):
474
            raise ValueError("name must be a string.")
475
        try:
476
            self._set_config_value(_SERVICE_INFO_SECTION_NAME, "Name", name)
477
        except Exception as e:
478
            logger.error(f"Unable to set name: {e}")
479
480 1
    def get_description(self):
481
        """
482
        Returns the description of the TabPy service.
483
        """
484 1
        description = None
485 1
        try:
486 1
            description = self._get_config_value(
487
                _SERVICE_INFO_SECTION_NAME, "Description"
488
            )
489
        except Exception as e:
490
            logger.error(f"Unable to get description: {e}")
491 1
        return description
492
493 1
    @state_lock
494 1
    def set_description(self, description):
495
        """
496
        Set the description of this TabPy service.
497
498
        Parameters
499
        ----------
500
        description : str
501
            Description of TabPy service.
502
        """
503
        if not isinstance(description, str):
504
            raise ValueError("Description must be a string.")
505
        try:
506
            self._set_config_value(
507
                _SERVICE_INFO_SECTION_NAME, "Description", description
508
            )
509
        except Exception as e:
510
            logger.error(f"Unable to set description: {e}")
511
512 1
    def get_revision_number(self):
513
        """
514
        Returns the revision number of this TabPy service.
515
        """
516
        rev = -1
517
        try:
518
            rev = int(self._get_config_value(_META_SECTION_NAME, "Revision Number"))
519
        except Exception as e:
520
            logger.error(f"Unable to get revision number: {e}")
521
        return rev
522
523 1
    def get_access_control_allow_origin(self):
524
        """
525
        Returns Access-Control-Allow-Origin of this TabPy service.
526
        """
527 1
        _cors_origin = ""
528 1
        try:
529 1
            logger.debug("Collecting Access-Control-Allow-Origin from state file ...")
530 1
            _cors_origin = self._get_config_value(
531
                "Service Info", "Access-Control-Allow-Origin"
532
            )
533
        except Exception as e:
534
            logger.error(e)
535 1
        return _cors_origin
536
537 1
    def get_access_control_allow_headers(self):
538
        """
539
        Returns Access-Control-Allow-Headers of this TabPy service.
540
        """
541 1
        _cors_headers = ""
542 1
        try:
543 1
            _cors_headers = self._get_config_value(
544
                "Service Info", "Access-Control-Allow-Headers"
545
            )
546
        except Exception:
547
            pass
548 1
        return _cors_headers
549
550 1
    def get_access_control_allow_methods(self):
551
        """
552
        Returns Access-Control-Allow-Methods of this TabPy service.
553
        """
554 1
        _cors_methods = ""
555 1
        try:
556 1
            _cors_methods = self._get_config_value(
557
                "Service Info", "Access-Control-Allow-Methods"
558
            )
559
        except Exception:
560
            pass
561 1
        return _cors_methods
562
563 1
    def _set_revision_number(self, revision_number):
564
        """
565
        Set the revision number of this TabPy service.
566
        """
567
        if not isinstance(revision_number, int):
568
            raise ValueError("revision number must be an int.")
569
        try:
570
            self._set_config_value(
571
                _META_SECTION_NAME, "Revision Number", revision_number
572
            )
573
        except Exception as e:
574
            logger.error(f"Unable to set revision number: {e}")
575
576 1
    def _remove_config_option(
577
        self,
578
        section_name,
579
        option_name,
580
        logger=logging.getLogger(__name__),
581
        _update_revision=True,
582
    ):
583
        if not self.config:
584
            raise ValueError("State configuration not yet loaded.")
585
        self.config.remove_option(section_name, option_name)
586
        # update revision number
587
        if _update_revision:
588
            self._increase_revision_number()
589
        self._write_state(logger=logger)
590
591 1
    def _has_config_value(self, section_name, option_name):
592
        if not self.config:
593
            raise ValueError("State configuration not yet loaded.")
594
        return self.config.has_option(section_name, option_name)
595
596 1
    def _increase_revision_number(self):
597
        if not self.config:
598
            raise ValueError("State configuration not yet loaded.")
599
        cur_rev = int(self.config.get(_META_SECTION_NAME, "Revision Number"))
600
        self.config.set(_META_SECTION_NAME, "Revision Number", str(cur_rev + 1))
601
602 1
    def _set_config_value(
603
        self,
604
        section_name,
605
        option_name,
606
        option_value,
607
        logger=logging.getLogger(__name__),
608
        _update_revision=True,
609
    ):
610
        if not self.config:
611
            raise ValueError("State configuration not yet loaded.")
612
613
        if not self.config.has_section(section_name):
614
            logger.log(logging.DEBUG, f"Adding config section {section_name}")
615
            self.config.add_section(section_name)
616
617
        self.config.set(section_name, option_name, option_value)
618
        # update revision number
619
        if _update_revision:
620
            self._increase_revision_number()
621
        self._write_state(logger=logger)
622
623 1
    def _get_config_items(self, section_name):
624
        if not self.config:
625
            raise ValueError("State configuration not yet loaded.")
626
        return self.config.items(section_name)
627
628 1
    def _get_config_value(
629
        self, section_name, option_name, optional=False, default_value=None
630
    ):
631 1
        logger.log(
632
            logging.DEBUG,
633
            f"Loading option '{option_name}' from section [{section_name}]...")
634
635 1
        if not self.config:
636
            msg = "State configuration not yet loaded."
637
            logging.log(msg)
638
            raise ValueError(msg)
639
640 1
        res = None
641 1
        if not option_name:
642 1
            res = self.config.options(section_name)
643 1
        elif self.config.has_option(section_name, option_name):
644 1
            res = self.config.get(section_name, option_name)
645
        elif optional:
646
            res = default_value
647
        else:
648
            raise ValueError(
649
                f"Cannot find option name {option_name} "
650
                f"under section {section_name}"
651
            )
652
653 1
        logger.log(logging.DEBUG, f"Returning value '{res}'")
654 1
        return res
655
656 1
    def _write_state(self, logger=logging.getLogger(__name__)):
657
        """
658
        Write state (ConfigParser) to Consul
659
        """
660
        logger.log(logging.INFO, "Writing state to config")
661
        write_state_config(self.config, self.settings, logger=logger)
662