ApiManager   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 29
c 1
b 0
f 0
dl 0
loc 150
ccs 0
cts 82
cp 0
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A call() 0 8 2
A get_service() 0 14 3
A build_response() 0 4 1
A register() 0 11 2
A build_error() 0 12 2
F decode() 0 33 10
C process() 0 54 9
1
from plugin.api.core.exceptions import ApiError
2
from plugin.preferences import Preferences
3
4
from threading import Lock
5
import logging
6
import six
7
8
log = logging.getLogger(__name__)
9
10
11
class ApiContext(object):
12
    def __init__(self, method, headers, body):
13
        self.method = method
14
        self.headers = headers
15
        self.body = body
16
17
        # Validate token
18
        self.token = self._validate_token()
19
20
    def _validate_token(self):
21
        token = self.headers.get('X-Channel-Token') if self.headers else None
22
23
        if not token:
24
            return None
25
26
        # Validate `token`
27
        system = ApiManager.get_service('system')
28
29
        try:
30
            return system.validate(token)
31
        except:
0 ignored issues
show
Coding Style Best Practice introduced by
General except handlers without types should be used sparingly.

Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.

Loading history...
32
            return None
33
34
35
class ApiManager(object):
36
    service_classes = {}
37
    services = {}
38
39
    # Call attributes
40
    lock = Lock()
41
    context = None
42
43
    @classmethod
44
    def process(cls, method, headers, body, key, *args, **kwargs):
45
        log.debug('Handling API %s request %r - args: %r, kwargs: %r', method, key, len(args), len(kwargs.keys()))
46
47
        if not Preferences.get('api.enabled'):
48
            log.debug('Unable to process request, API is currently disabled')
49
            return cls.build_error('disabled', 'Unable to process request, API is currently disabled')
50
51
        k_service, k_method = key.rsplit('.', 1)
52
53
        # Try find matching service
54
        service = cls.get_service(k_service)
55
56
        if service is None:
57
            log.warn('Unable to find service: %r', k_service)
58
            return cls.build_error('unknown.service', 'Unable to find service: %r' % k_service)
59
60
        func = getattr(service, k_method, None)
61
62
        if func is None:
63
            log.warn('Unable to find method: %r', k_method)
64
            return cls.build_error('unknown.method', 'Unable to find method: %r' % k_method)
65
66
        # Validate
67
        meta = getattr(func, '__meta__', {})
68
69
        if not meta.get('exposed', False):
70
            log.warn('Method is not exposed: %r', k_method)
71
            return cls.build_error('restricted.method', 'Method is not exposed: %r' % k_method)
72
73
        # Decode strings in the `args` parameter
74
        try:
75
            args = cls.decode(args)
76
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
77
            return cls.build_error('args.decode_error', 'Unable to decode provided args')
78
79
        # Decode strings in the `kwargs` parameter
80
        try:
81
            kwargs = cls.decode(kwargs)
82
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
83
            return cls.build_error('kwargs.decode_error', 'Unable to decode provided kwargs')
84
85
        # Execute request handler
86
        try:
87
            result = cls.call(method, headers, body, func, args, kwargs)
88
        except ApiError as ex:
89
            log.warn('Error returned while handling request %r: %r', key, ex, exc_info=True)
90
            return cls.build_error('error.%s' % ex.code, ex.message)
91
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
92
            log.error('Exception raised while handling request %r: %s', key, ex, exc_info=True)
93
            return cls.build_error('exception', 'Exception raised while handling the request')
94
95
        # Build response
96
        return cls.build_response(result)
97
98
    @classmethod
99
    def call(cls, method, headers, body, func, args, kwargs):
100
        with cls.lock:
101
            # Construct context
102
            cls.context = ApiContext(method, headers, body)
103
104
            # Call function
105
            return func(*args, **kwargs)
106
107
    @classmethod
108
    def register(cls, service):
109
        key = service.__key__
110
111
        if not key:
112
            log.warn('Service %r has an invalid "__key__" attribute', service)
113
            return
114
115
        cls.service_classes[key] = service
116
117
        log.debug('Registered service: %r (%r)', key, service)
118
119
    @classmethod
120
    def get_service(cls, key):
121
        if key in cls.services:
122
            # Service already constructed
123
            return cls.services[key]
124
125
        if key not in cls.service_classes:
126
            # Service doesn't exist
127
            return None
128
129
        # Construct service
130
        cls.services[key] = cls.service_classes[key](cls)
131
132
        return cls.services[key]
133
134
    @classmethod
135
    def decode(cls, data):
136
        if not data:
137
            return data
138
139
        # Strings
140
        if isinstance(data, six.string_types):
141
            try:
142
                return data.decode('unicode-escape')
143
            except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
144
                log.warn('Unable to decode string: %s', ex, exc_info=True)
145
                return data
146
147
        # Collections
148
        if type(data) is dict:
149
            return dict([
150
                (cls.decode(key), cls.decode(value))
151
                for key, value in data.items()
152
            ])
153
154
        if type(data) is list:
155
            return [
156
                cls.decode(value)
157
                for value in data
158
            ]
159
160
        if type(data) is tuple:
161
            return tuple([
162
                cls.decode(value)
163
                for value in list(data)
164
            ])
165
166
        return data
167
168
    @classmethod
169
    def build_error(cls, code, message=None):
170
        result = {
171
            'error': {
172
                'code': code
173
            }
174
        }
175
176
        if message:
177
            result['error']['message'] = message
178
179
        return result
180
181
    @classmethod
182
    def build_response(cls, result):
183
        return {
184
            'result': result
185
        }
186