Passed
Pull Request — master (#650)
by
unknown
18:25
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
        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