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 | from __future__ import absolute_import |
||
17 | import copy |
||
18 | import functools |
||
19 | import re |
||
20 | import six |
||
21 | import sys |
||
22 | import traceback |
||
23 | |||
24 | from flex.core import validate |
||
25 | import jsonschema |
||
26 | from oslo_config import cfg |
||
27 | import routes |
||
28 | from six.moves.urllib import parse as urlparse # pylint: disable=import-error |
||
29 | import webob |
||
30 | from webob import cookies, exc, Request |
||
31 | from webob.compat import url_unquote |
||
32 | |||
33 | from st2common.exceptions import rbac as rbac_exc |
||
34 | from st2common.exceptions import auth as auth_exc |
||
35 | from st2common import log as logging |
||
36 | from st2common.persistence.auth import User |
||
37 | from st2common.rbac import resolvers |
||
38 | from st2common.util import date as date_utils |
||
39 | from st2common.util.jsonify import json_encode |
||
40 | from st2common.util.jsonify import get_json_type_for_python_value |
||
41 | from st2common.util.http import parse_content_type_header |
||
42 | |||
43 | __all__ = [ |
||
44 | 'Router', |
||
45 | |||
46 | 'Response', |
||
47 | |||
48 | 'NotFoundException', |
||
49 | |||
50 | 'abort', |
||
51 | 'abort_unauthorized', |
||
52 | 'exc' |
||
53 | ] |
||
54 | |||
55 | LOG = logging.getLogger(__name__) |
||
56 | |||
57 | |||
58 | def op_resolver(op_id): |
||
59 | module_name, func_name = op_id.split(':', 1) |
||
60 | __import__(module_name) |
||
61 | module = sys.modules[module_name] |
||
62 | return functools.reduce(getattr, func_name.split('.'), module) |
||
63 | |||
64 | |||
65 | def abort(status_code=exc.HTTPInternalServerError.code, message='Unhandled exception'): |
||
66 | raise exc.status_map[status_code](message) |
||
67 | |||
68 | |||
69 | def abort_unauthorized(msg=None): |
||
70 | raise exc.HTTPUnauthorized('Unauthorized - %s' % msg if msg else 'Unauthorized') |
||
71 | |||
72 | |||
73 | def extend_with_default(validator_class): |
||
74 | validate_properties = validator_class.VALIDATORS["properties"] |
||
75 | |||
76 | def set_defaults(validator, properties, instance, schema): |
||
77 | for property, subschema in six.iteritems(properties): |
||
0 ignored issues
–
show
|
|||
78 | if "default" in subschema: |
||
79 | instance.setdefault(property, subschema["default"]) |
||
80 | |||
81 | for error in validate_properties( |
||
82 | validator, properties, instance, schema, |
||
83 | ): |
||
84 | yield error |
||
85 | |||
86 | return jsonschema.validators.extend( |
||
87 | validator_class, {"properties": set_defaults}, |
||
88 | ) |
||
89 | |||
90 | |||
91 | def extend_with_additional_check(validator_class): |
||
92 | def set_additional_check(validator, properties, instance, schema): |
||
93 | ref = schema.get("x-additional-check") |
||
94 | func = op_resolver(ref) |
||
95 | for error in func(validator, properties, instance, schema): |
||
96 | yield error |
||
97 | |||
98 | return jsonschema.validators.extend( |
||
99 | validator_class, {"x-additional-check": set_additional_check}, |
||
100 | ) |
||
101 | |||
102 | |||
103 | def extend_with_nullable(validator_class): |
||
104 | validate_type = validator_class.VALIDATORS["type"] |
||
105 | |||
106 | def set_type_draft4(validator, types, instance, schema): |
||
107 | is_nullable = schema.get("x-nullable", False) |
||
108 | |||
109 | if is_nullable and instance is None: |
||
110 | return |
||
111 | |||
112 | for error in validate_type(validator, types, instance, schema): |
||
113 | yield error |
||
114 | |||
115 | return jsonschema.validators.extend( |
||
116 | validator_class, {"type": set_type_draft4}, |
||
117 | ) |
||
118 | |||
119 | |||
120 | CustomValidator = jsonschema.Draft4Validator |
||
121 | CustomValidator = extend_with_nullable(CustomValidator) |
||
122 | CustomValidator = extend_with_additional_check(CustomValidator) |
||
123 | CustomValidator = extend_with_default(CustomValidator) |
||
124 | |||
125 | |||
126 | class NotFoundException(Exception): |
||
127 | pass |
||
128 | |||
129 | |||
130 | class Response(webob.Response): |
||
131 | def __init__(self, body=None, status=None, headerlist=None, app_iter=None, content_type=None, |
||
132 | *args, **kwargs): |
||
133 | # Do some sanity checking, and turn json_body into an actual body |
||
134 | if app_iter is None and body is None and ('json_body' in kwargs or 'json' in kwargs): |
||
135 | if 'json_body' in kwargs: |
||
136 | json_body = kwargs.pop('json_body') |
||
137 | else: |
||
138 | json_body = kwargs.pop('json') |
||
139 | body = json_encode(json_body).encode('UTF-8') |
||
140 | |||
141 | if content_type is None: |
||
142 | content_type = 'application/json' |
||
143 | |||
144 | super(Response, self).__init__(body, status, headerlist, app_iter, content_type, |
||
145 | *args, **kwargs) |
||
146 | |||
147 | def _json_body__get(self): |
||
148 | return super(Response, self)._json_body__get() |
||
149 | |||
150 | def _json_body__set(self, value): |
||
151 | self.body = json_encode(value).encode('UTF-8') |
||
152 | |||
153 | def _json_body__del(self): |
||
154 | return super(Response, self)._json_body__del() |
||
155 | |||
156 | json = json_body = property(_json_body__get, _json_body__set, _json_body__del) |
||
157 | |||
158 | |||
159 | class Router(object): |
||
160 | def __init__(self, arguments=None, debug=False, auth=True, is_gunicorn=True): |
||
161 | self.debug = debug |
||
162 | self.auth = auth |
||
163 | self.is_gunicorn = is_gunicorn |
||
164 | |||
165 | self.arguments = arguments or {} |
||
166 | |||
167 | self.spec = {} |
||
168 | self.spec_resolver = None |
||
169 | self.routes = routes.Mapper() |
||
170 | |||
171 | def add_spec(self, spec, transforms): |
||
172 | info = spec.get('info', {}) |
||
173 | LOG.debug('Adding API: %s %s', info.get('title', 'untitled'), info.get('version', '0.0.0')) |
||
174 | |||
175 | self.spec = spec |
||
176 | self.spec_resolver = jsonschema.RefResolver('', self.spec) |
||
177 | |||
178 | validate(copy.deepcopy(self.spec)) |
||
179 | |||
180 | for filter in transforms: |
||
0 ignored issues
–
show
|
|||
181 | for (path, methods) in six.iteritems(spec['paths']): |
||
182 | if not re.search(filter, path): |
||
183 | continue |
||
184 | |||
185 | for (method, endpoint) in six.iteritems(methods): |
||
186 | conditions = { |
||
187 | 'method': [method.upper()] |
||
188 | } |
||
189 | |||
190 | connect_kw = {} |
||
191 | if 'x-requirements' in endpoint: |
||
192 | connect_kw['requirements'] = endpoint['x-requirements'] |
||
193 | |||
194 | m = self.routes.submapper(_api_path=path, _api_method=method, |
||
195 | conditions=conditions) |
||
196 | for transform in transforms[filter]: |
||
197 | m.connect(None, re.sub(filter, transform, path), **connect_kw) |
||
198 | |||
199 | module_name = endpoint['operationId'].split(':', 1)[0] |
||
200 | __import__(module_name) |
||
201 | |||
202 | for route in sorted(self.routes.matchlist, key=lambda r: r.routepath): |
||
203 | LOG.debug('Route registered: %+6s %s', route.conditions['method'][0], route.routepath) |
||
204 | |||
205 | def match(self, req): |
||
206 | path = url_unquote(req.path) |
||
207 | LOG.debug("Match path: %s", path) |
||
208 | |||
209 | if len(path) > 1 and path.endswith('/'): |
||
210 | path = path[:-1] |
||
211 | |||
212 | match = self.routes.match(path, req.environ) |
||
213 | |||
214 | if match is None: |
||
215 | raise NotFoundException('No route matches "%s" path' % req.path) |
||
216 | |||
217 | # To account for situation when match may return multiple values |
||
218 | try: |
||
219 | path_vars = match[0] |
||
220 | except KeyError: |
||
221 | path_vars = match |
||
222 | |||
223 | path_vars = dict(path_vars) |
||
224 | |||
225 | path = path_vars.pop('_api_path') |
||
226 | method = path_vars.pop('_api_method') |
||
227 | endpoint = self.spec['paths'][path][method] |
||
228 | |||
229 | return endpoint, path_vars |
||
230 | |||
231 | def __call__(self, req): |
||
232 | """ |
||
233 | The method is invoked on every request and shows the lifecycle of the request received from |
||
234 | the middleware. |
||
235 | |||
236 | Although some middleware may use parts of the API spec, it is safe to assume that if you're |
||
237 | looking for the particular spec property handler, it's most likely a part of this method. |
||
238 | |||
239 | At the time of writing, the only property being utilized by middleware was `x-log-result`. |
||
240 | """ |
||
241 | LOG.debug("Received call with WebOb: %s", req) |
||
242 | endpoint, path_vars = self.match(req) |
||
243 | LOG.debug("Parsed endpoint: %s", endpoint) |
||
244 | LOG.debug("Parsed path_vars: %s", path_vars) |
||
245 | |||
246 | context = copy.copy(getattr(self, 'mock_context', {})) |
||
247 | cookie_token = None |
||
248 | |||
249 | # Handle security |
||
250 | if 'security' in endpoint: |
||
251 | security = endpoint.get('security') |
||
252 | else: |
||
253 | security = self.spec.get('security', []) |
||
254 | |||
255 | if self.auth and security: |
||
256 | try: |
||
257 | security_definitions = self.spec.get('securityDefinitions', {}) |
||
258 | for statement in security: |
||
259 | declaration, options = statement.copy().popitem() |
||
260 | definition = security_definitions[declaration] |
||
261 | |||
262 | if definition['type'] == 'apiKey': |
||
263 | if definition['in'] == 'header': |
||
264 | token = req.headers.get(definition['name']) |
||
265 | elif definition['in'] == 'query': |
||
266 | token = req.GET.get(definition['name']) |
||
267 | elif definition['in'] == 'cookie': |
||
268 | token = req.cookies.get(definition['name']) |
||
269 | else: |
||
270 | token = None |
||
271 | |||
272 | if token: |
||
273 | auth_func = op_resolver(definition['x-operationId']) |
||
274 | auth_resp = auth_func(token) |
||
275 | |||
276 | # Include information on how user authenticated inside the context |
||
277 | if 'auth-token' in definition['name'].lower(): |
||
278 | auth_method = 'authentication token' |
||
279 | elif 'api-key' in definition['name'].lower(): |
||
280 | auth_method = 'API key' |
||
281 | |||
282 | context['user'] = User.get_by_name(auth_resp.user) |
||
283 | context['auth_info'] = { |
||
284 | 'method': auth_method, |
||
285 | 'location': definition['in'] |
||
286 | } |
||
287 | |||
288 | # Also include token expiration time when authenticated via auth token |
||
289 | if 'auth-token' in definition['name'].lower(): |
||
290 | context['auth_info']['token_expire'] = auth_resp.expiry |
||
291 | |||
292 | if 'x-set-cookie' in definition: |
||
293 | max_age = auth_resp.expiry - date_utils.get_datetime_utc_now() |
||
294 | cookie_token = cookies.make_cookie(definition['x-set-cookie'], |
||
295 | token, |
||
296 | max_age=max_age, |
||
297 | httponly=True) |
||
298 | |||
299 | break |
||
300 | |||
301 | if 'user' not in context: |
||
302 | raise auth_exc.NoAuthSourceProvidedError('One of Token or API key required.') |
||
303 | except (auth_exc.NoAuthSourceProvidedError, |
||
304 | auth_exc.MultipleAuthSourcesError) as e: |
||
305 | LOG.error(str(e)) |
||
306 | return abort_unauthorized(str(e)) |
||
307 | except auth_exc.TokenNotProvidedError as e: |
||
308 | LOG.exception('Token is not provided.') |
||
309 | return abort_unauthorized(str(e)) |
||
310 | except auth_exc.TokenNotFoundError as e: |
||
311 | LOG.exception('Token is not found.') |
||
312 | return abort_unauthorized(str(e)) |
||
313 | except auth_exc.TokenExpiredError as e: |
||
314 | LOG.exception('Token has expired.') |
||
315 | return abort_unauthorized(str(e)) |
||
316 | except auth_exc.ApiKeyNotProvidedError as e: |
||
317 | LOG.exception('API key is not provided.') |
||
318 | return abort_unauthorized(str(e)) |
||
319 | except auth_exc.ApiKeyNotFoundError as e: |
||
320 | LOG.exception('API key is not found.') |
||
321 | return abort_unauthorized(str(e)) |
||
322 | except auth_exc.ApiKeyDisabledError as e: |
||
323 | LOG.exception('API key is disabled.') |
||
324 | return abort_unauthorized(str(e)) |
||
325 | |||
326 | if cfg.CONF.rbac.enable: |
||
327 | user_db = context['user'] |
||
328 | |||
329 | permission_type = endpoint.get('x-permissions', None) |
||
330 | if permission_type: |
||
331 | resolver = resolvers.get_resolver_for_permission_type(permission_type) |
||
332 | has_permission = resolver.user_has_permission(user_db, permission_type) |
||
333 | |||
334 | if not has_permission: |
||
335 | raise rbac_exc.ResourceTypeAccessDeniedError(user_db, |
||
336 | permission_type) |
||
337 | |||
338 | # Collect parameters |
||
339 | kw = {} |
||
340 | for param in endpoint.get('parameters', []) + endpoint.get('x-parameters', []): |
||
341 | name = param['name'] |
||
342 | argument_name = param.get('x-as', None) or name |
||
343 | source = param['in'] |
||
344 | default = param.get('default', None) |
||
345 | |||
346 | # Collecting params from different sources |
||
347 | if source == 'query': |
||
348 | kw[argument_name] = req.GET.get(name, default) |
||
349 | elif source == 'path': |
||
350 | kw[argument_name] = path_vars[name] |
||
351 | elif source == 'header': |
||
352 | kw[argument_name] = req.headers.get(name, default) |
||
353 | elif source == 'formData': |
||
354 | kw[argument_name] = req.POST.get(name, default) |
||
355 | elif source == 'environ': |
||
356 | kw[argument_name] = req.environ.get(name.upper(), default) |
||
357 | elif source == 'context': |
||
358 | kw[argument_name] = context.get(name, default) |
||
359 | elif source == 'request': |
||
360 | kw[argument_name] = getattr(req, name) |
||
361 | elif source == 'body': |
||
362 | content_type = req.headers.get('Content-Type', 'application/json') |
||
363 | content_type = parse_content_type_header(content_type=content_type)[0] |
||
364 | schema = param['schema'] |
||
365 | |||
366 | # NOTE: HACK: Workaround for eventlet wsgi server which sets Content-Type to |
||
367 | # text/plain if Content-Type is not provided in the request. |
||
368 | # All ouf our API endpoints except /exp/validation/mistral expect application/json |
||
369 | # so we explicitly set it to that if not provided (set to text/plain by the base |
||
370 | # http server) and if it's not /exp/validation/mistral API endpoint |
||
371 | if not self.is_gunicorn and content_type == 'text/plain': |
||
372 | operation_id = endpoint['operationId'] |
||
373 | |||
374 | if 'mistral_validation_controller' not in operation_id: |
||
375 | content_type = 'application/json' |
||
376 | |||
377 | # Note: We also want to perform validation if no body is explicitly provided - in a |
||
378 | # lot of POST, PUT scenarios, body is mandatory |
||
379 | if not req.body and content_type == 'application/json': |
||
380 | req.body = b'{}' |
||
381 | |||
382 | try: |
||
383 | if content_type == 'application/json': |
||
384 | data = req.json |
||
385 | elif content_type == 'text/plain': |
||
386 | data = req.body |
||
387 | elif content_type in ['application/x-www-form-urlencoded', |
||
388 | 'multipart/form-data']: |
||
389 | data = urlparse.parse_qs(req.body) |
||
390 | else: |
||
391 | raise ValueError('Unsupported Content-Type: "%s"' % (content_type)) |
||
392 | except Exception as e: |
||
393 | detail = 'Failed to parse request body: %s' % str(e) |
||
394 | raise exc.HTTPBadRequest(detail=detail) |
||
395 | |||
396 | # Special case for Python 3 |
||
397 | if six.PY3 and content_type == 'text/plain' and isinstance(data, six.binary_type): |
||
398 | # Convert bytes to text type (string / unicode) |
||
399 | data = data.decode('utf-8') |
||
400 | |||
401 | try: |
||
402 | CustomValidator(schema, resolver=self.spec_resolver).validate(data) |
||
403 | except (jsonschema.ValidationError, ValueError) as e: |
||
404 | raise exc.HTTPBadRequest(detail=e.message, |
||
405 | comment=traceback.format_exc()) |
||
406 | |||
407 | if content_type == 'text/plain': |
||
408 | kw[argument_name] = data |
||
409 | else: |
||
410 | class Body(object): |
||
411 | def __init__(self, **entries): |
||
412 | self.__dict__.update(entries) |
||
413 | |||
414 | ref = schema.get('$ref', None) |
||
415 | if ref: |
||
416 | with self.spec_resolver.resolving(ref) as resolved: |
||
417 | schema = resolved |
||
418 | |||
419 | if 'x-api-model' in schema: |
||
420 | input_type = schema.get('type', []) |
||
421 | Model = op_resolver(schema['x-api-model']) |
||
422 | |||
423 | if input_type and not isinstance(input_type, (list, tuple)): |
||
424 | input_type = [input_type] |
||
425 | |||
426 | # root attribute is not an object, we need to use wrapper attribute to |
||
427 | # make it work with **kwarg expansion |
||
428 | if input_type and 'array' in input_type: |
||
429 | data = {'data': data} |
||
430 | |||
431 | instance = self._get_model_instance(model_cls=Model, data=data) |
||
432 | |||
433 | # Call validate on the API model - note we should eventually move all |
||
434 | # those model schema definitions into openapi.yaml |
||
435 | try: |
||
436 | instance = instance.validate() |
||
437 | except (jsonschema.ValidationError, ValueError) as e: |
||
438 | raise exc.HTTPBadRequest(detail=e.message, |
||
439 | comment=traceback.format_exc()) |
||
440 | else: |
||
441 | LOG.debug('Missing x-api-model definition for %s, using generic Body ' |
||
442 | 'model.' % (endpoint['operationId'])) |
||
443 | model = Body |
||
444 | instance = self._get_model_instance(model_cls=model, data=data) |
||
445 | |||
446 | kw[argument_name] = instance |
||
447 | |||
448 | # Making sure all required params are present |
||
449 | required = param.get('required', False) |
||
450 | if required and kw[argument_name] is None: |
||
451 | detail = 'Required parameter "%s" is missing' % name |
||
452 | raise exc.HTTPBadRequest(detail=detail) |
||
453 | |||
454 | # Validating and casting param types |
||
455 | param_type = param.get('type', None) |
||
456 | if kw[argument_name] is not None: |
||
457 | if param_type == 'boolean': |
||
458 | positive = ('true', '1', 'yes', 'y') |
||
459 | negative = ('false', '0', 'no', 'n') |
||
460 | |||
461 | if str(kw[argument_name]).lower() not in positive + negative: |
||
462 | detail = 'Parameter "%s" is not of type boolean' % argument_name |
||
463 | raise exc.HTTPBadRequest(detail=detail) |
||
464 | |||
465 | kw[argument_name] = str(kw[argument_name]).lower() in positive |
||
466 | elif param_type == 'integer': |
||
467 | regex = r'^-?[0-9]+$' |
||
468 | |||
469 | if not re.search(regex, str(kw[argument_name])): |
||
470 | detail = 'Parameter "%s" is not of type integer' % argument_name |
||
471 | raise exc.HTTPBadRequest(detail=detail) |
||
472 | |||
473 | kw[argument_name] = int(kw[argument_name]) |
||
474 | elif param_type == 'number': |
||
475 | regex = r'^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$' |
||
476 | |||
477 | if not re.search(regex, str(kw[argument_name])): |
||
478 | detail = 'Parameter "%s" is not of type float' % argument_name |
||
479 | raise exc.HTTPBadRequest(detail=detail) |
||
480 | |||
481 | kw[argument_name] = float(kw[argument_name]) |
||
482 | elif param_type == 'array' and param.get('items', {}).get('type', None) == 'string': |
||
483 | if kw[argument_name] is None: |
||
484 | kw[argument_name] = [] |
||
485 | elif isinstance(kw[argument_name], (list, tuple)): |
||
486 | # argument is already an array |
||
487 | pass |
||
488 | else: |
||
489 | kw[argument_name] = kw[argument_name].split(',') |
||
490 | |||
491 | # Call the controller |
||
492 | try: |
||
493 | func = op_resolver(endpoint['operationId']) |
||
494 | except Exception as e: |
||
495 | LOG.exception('Failed to load controller for operation "%s": %s' % |
||
496 | (endpoint['operationId'], str(e))) |
||
497 | raise e |
||
498 | |||
499 | try: |
||
500 | resp = func(**kw) |
||
501 | except Exception as e: |
||
502 | LOG.exception('Failed to call controller function "%s" for operation "%s": %s' % |
||
503 | (func.__name__, endpoint['operationId'], str(e))) |
||
504 | raise e |
||
505 | |||
506 | # Handle response |
||
507 | if resp is None: |
||
508 | resp = Response() |
||
509 | |||
510 | if not hasattr(resp, '__call__'): |
||
511 | resp = Response(json=resp) |
||
512 | |||
513 | responses = endpoint.get('responses', {}) |
||
514 | response_spec = responses.get(str(resp.status_code), None) |
||
515 | default_response_spec = responses.get('default', None) |
||
516 | |||
517 | if not response_spec and default_response_spec: |
||
518 | LOG.debug('No custom response spec found for endpoint "%s", using a default one' % |
||
519 | (endpoint['operationId'])) |
||
520 | response_spec_name = 'default' |
||
521 | else: |
||
522 | response_spec_name = str(resp.status_code) |
||
523 | |||
524 | response_spec = response_spec or default_response_spec |
||
525 | |||
526 | if response_spec and 'schema' in response_spec: |
||
527 | LOG.debug('Using response spec "%s" for endpoint %s and status code %s' % |
||
528 | (response_spec_name, endpoint['operationId'], resp.status_code)) |
||
529 | |||
530 | try: |
||
531 | validator = CustomValidator(response_spec['schema'], resolver=self.spec_resolver) |
||
532 | validator.validate(resp.json) |
||
533 | except (jsonschema.ValidationError, ValueError): |
||
534 | LOG.exception('Response validation failed.') |
||
535 | resp.headers.add('Warning', '199 OpenAPI "Response validation failed"') |
||
536 | else: |
||
537 | LOG.debug('No response spec found for endpoint "%s"' % (endpoint['operationId'])) |
||
538 | |||
539 | if cookie_token: |
||
540 | resp.headerlist.append(('Set-Cookie', cookie_token)) |
||
541 | |||
542 | return resp |
||
543 | |||
544 | def as_wsgi(self, environ, start_response): |
||
545 | """ |
||
546 | Converts WSGI request to webob.Request and initiates the response returned by controller. |
||
547 | """ |
||
548 | req = Request(environ) |
||
549 | resp = self(req) |
||
550 | return resp(environ, start_response) |
||
551 | |||
552 | def _get_model_instance(self, model_cls, data): |
||
553 | try: |
||
554 | instance = model_cls(**data) |
||
555 | except TypeError as e: |
||
556 | # Throw a more user-friendly exception when input data is not an object |
||
557 | if 'type object argument after ** must be a mapping, not' in str(e): |
||
558 | type_string = get_json_type_for_python_value(data) |
||
559 | msg = ('Input body needs to be an object, got: %s' % (type_string)) |
||
560 | raise ValueError(msg) |
||
561 | |||
562 | raise e |
||
563 | |||
564 | return instance |
||
565 |
It is generally discouraged to redefine built-ins as this makes code very hard to read.