Passed
Pull Request — master (#650)
by
unknown
15:46
created

TabPyState.update_endpoint()   C

Complexity

Conditions 10

Size

Total Lines 90
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 91.2949

Importance

Changes 0
Metric Value
eloc 55
dl 0
loc 90
ccs 2
cts 30
cp 0.0667
rs 5.6727
c 0
b 0
f 0
cc 10
nop 11
crap 91.2949

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.update_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 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 is_public 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