Passed
Push — develop ( 82f97f...71d0f8 )
by Plexxi
07:37 queued 03:48
created

jsexpose()   F

Complexity

Conditions 19

Size

Total Lines 105

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 19
c 3
b 0
f 0
dl 0
loc 105
rs 2

2 Methods

Rating   Name   Duplication   Size   Complexity  
A cast_value() 0 9 3
F callfunction() 0 79 17

How to fix   Long Method    Complexity   

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 jsexpose() 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
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import abc
17
import functools
18
import inspect
19
20
import jsonschema
21
import six
22
from six.moves import http_client
23
from webob import exc
24
import pecan
25
import traceback
26
from oslo_config import cfg
27
28
from st2common.constants.pack import DEFAULT_PACK_NAME
29
from st2common.util import mongoescape as util_mongodb
30
from st2common.util import schema as util_schema
31
from st2common.util.debugging import is_enabled as is_debugging_enabled
32
from st2common.util.jsonify import json_encode
33
from st2common.util.api import get_exception_for_type_error
34
from st2common import log as logging
35
36
__all__ = [
37
    'BaseAPI',
38
39
    'APIUIDMixin',
40
41
    'jsexpose'
42
]
43
44
45
LOG = logging.getLogger(__name__)
46
47
48
@six.add_metaclass(abc.ABCMeta)
49
class BaseAPI(object):
50
    schema = abc.abstractproperty
51
52
    def __init__(self, **kw):
53
        for key, value in kw.items():
54
            setattr(self, key, value)
55
56
    def __repr__(self):
57
        name = type(self).__name__
58
        attrs = ', '.join("'%s': %r" % item for item in six.iteritems(vars(self)))
59
        # The format here is so that eval can be applied.
60
        return "%s(**{%s})" % (name, attrs)
61
62
    def __str__(self):
63
        name = type(self).__name__
64
        attrs = ', '.join("%s=%r" % item for item in six.iteritems(vars(self)))
65
66
        return "%s[%s]" % (name, attrs)
67
68
    def __json__(self):
69
        return vars(self)
70
71
    def validate(self):
72
        """
73
        Perform validation and return cleaned object on success.
74
75
        Note: This method doesn't mutate this object in place, but it returns a new one.
76
77
        :return: Cleaned / validated object.
78
        """
79
        schema = getattr(self, 'schema', {})
80
        attributes = vars(self)
81
82
        cleaned = util_schema.validate(instance=attributes, schema=schema,
83
                                       cls=util_schema.CustomValidator, use_default=True,
84
                                       allow_default_none=True)
85
86
        return self.__class__(**cleaned)
87
88
    @classmethod
89
    def _from_model(cls, model, mask_secrets=False):
90
        doc = util_mongodb.unescape_chars(model.to_mongo())
91
92
        if '_id' in doc:
93
            doc['id'] = str(doc.pop('_id'))
94
95
        if mask_secrets and cfg.CONF.log.mask_secrets:
96
            doc = model.mask_secrets(value=doc)
97
98
        return doc
99
100
    @classmethod
101
    def from_model(cls, model, mask_secrets=False):
102
        """
103
        Create API model class instance for the provided DB model instance.
104
105
        :param model: DB model class instance.
106
        :type model: :class:`StormFoundationDB`
107
108
        :param mask_secrets: True to mask secrets in the resulting instance.
109
        :type mask_secrets: ``boolean``
110
        """
111
        doc = cls._from_model(model=model, mask_secrets=mask_secrets)
112
        attrs = {attr: value for attr, value in six.iteritems(doc) if value is not None}
113
114
        return cls(**attrs)
115
116
    @classmethod
117
    def to_model(cls, doc):
118
        """
119
        Create a model class instance for the provided MongoDB document.
120
121
        :param doc: MongoDB document.
122
        """
123
        raise NotImplementedError()
124
125
126
class APIUIDMixin(object):
127
    """"
128
    Mixin class for retrieving UID for API objects.
129
    """
130
131
    def get_uid(self):
132
        # TODO: This is not the most efficient approach - refactor this functionality into util
133
        # module and re-use it here and in the DB model
134
        resource_db = self.to_model(self)
135
        resource_uid = resource_db.get_uid()
136
        return resource_uid
137
138
    def get_pack_uid(self):
139
        # TODO: This is not the most efficient approach - refactor this functionality into util
140
        # module and re-use it here and in the DB model
141
        resource_db = self.to_model(self)
142
        pack_uid = resource_db.get_pack_uid()
143
        return pack_uid
144
145
146
def cast_argument_value(value_type, value):
147
    if value_type == bool:
148
        def cast_func(value):
149
            value = str(value)
150
            return value.lower() in ['1', 'true']
151
    else:
152
        cast_func = value_type
153
154
    result = cast_func(value)
155
    return result
156
157
158
def get_controller_args_for_types(func, arg_types, args, kwargs):
159
    """
160
    Build a list of arguments and dictionary of keyword arguments which are passed to the
161
    controller method based on the arg_types specification.
162
163
    Note: args argument is mutated in place.
164
    """
165
    result_args = []
166
    result_kwargs = {}
167
168
    argspec = inspect.getargspec(func)
169
    names = argspec.args[1:]  # Note: we skip "self"
170
171
    for index, name in enumerate(names):
172
        # 1. Try kwargs first
173
        if name in kwargs:
174
            try:
175
                value = kwargs[name]
176
                value_type = arg_types[index]
177
                value = cast_argument_value(value_type=value_type, value=value)
178
                result_kwargs[name] = value
179
            except IndexError:
180
                LOG.warning("Type definition for '%s' argument of '%s' is missing.",
181
                            name, func.__name__)
182
183
            continue
184
185
        # 2. Try positional args
186
        try:
187
            value = args.pop(0)
188
            value_type = arg_types[index]
189
            value = cast_argument_value(value_type=value_type, value=value)
190
            result_args.append(value)
191
        except IndexError:
192
            LOG.warning("Type definition for '%s' argument of '%s' is missing.",
193
                        name, func.__name__)
194
195
    return result_args, result_kwargs
196
197
198
def jsexpose(arg_types=None, body_cls=None, status_code=None, content_type='application/json'):
199
    """
200
    :param arg_types: A list of types for the function arguments (e.g. [str, str, int, bool]).
201
    :type arg_types: ``list``
202
203
    :param body_cls: Request body class. If provided, this class will be used to create an instance
204
                     out of the request body.
205
    :type body_cls: :class:`object`
206
207
    :param status_code: Response status code.
208
    :type status_code: ``int``
209
210
    :param content_type: Response content type.
211
    :type content_type: ``str``
212
    """
213
    pecan_json_decorate = pecan.expose(
214
        content_type=content_type,
215
        generic=False)
216
217
    def decorate(f):
218
        @functools.wraps(f)
219
        def callfunction(*args, **kwargs):
220
            function_name = f.__name__
221
            args = list(args)
222
            more = [args.pop(0)]
223
224
            def cast_value(value_type, value):
225
                if value_type == bool:
226
                    def cast_func(value):
227
                        return value.lower() in ['1', 'true']
228
                else:
229
                    cast_func = value_type
230
231
                result = cast_func(value)
232
                return result
233
234
            if arg_types:
235
                # Cast and transform arguments based on the provided arg_types specification
236
                result_args, result_kwargs = get_controller_args_for_types(func=f,
237
                                                                           arg_types=arg_types,
238
                                                                           args=args,
239
                                                                           kwargs=kwargs)
240
                more = more + result_args
241
                kwargs.update(result_kwargs)
242
243
            if body_cls:
244
                if pecan.request.body:
245
                    data = pecan.request.json
246
                else:
247
                    data = {}
248
249
                obj = body_cls(**data)
250
                try:
251
                    obj = obj.validate()
252
                except (jsonschema.ValidationError, ValueError) as e:
253
                    raise exc.HTTPBadRequest(detail=e.message,
254
                                             comment=traceback.format_exc())
255
                except Exception as e:
256
                    raise exc.HTTPInternalServerError(detail=e.message,
257
                                                      comment=traceback.format_exc())
258
259
                # Set default pack if one is not provided for resource create
260
                if function_name == 'post' and not hasattr(obj, 'pack'):
261
                    extra = {
262
                        'resource_api': obj,
263
                        'default_pack_name': DEFAULT_PACK_NAME
264
                    }
265
                    LOG.debug('Pack not provided in the body, setting a default pack name',
266
                              extra=extra)
267
                    setattr(obj, 'pack', DEFAULT_PACK_NAME)
268
269
                more.append(obj)
270
271
            args = tuple(more) + tuple(args)
272
273
            noop_codes = [http_client.NOT_IMPLEMENTED,
274
                          http_client.METHOD_NOT_ALLOWED,
275
                          http_client.FORBIDDEN]
276
277
            if status_code and status_code in noop_codes:
278
                pecan.response.status = status_code
279
                return json_encode(None)
280
281
            try:
282
                result = f(*args, **kwargs)
283
            except TypeError as e:
284
                e = get_exception_for_type_error(func=f, exc=e)
285
                raise e
286
287
            if status_code:
288
                pecan.response.status = status_code
289
            if content_type == 'application/json':
290
                if is_debugging_enabled():
291
                    indent = 4
292
                else:
293
                    indent = None
294
                return json_encode(result, indent=indent)
295
            else:
296
                return result
297
298
        pecan_json_decorate(callfunction)
299
300
        return callfunction
301
302
    return decorate
303