def test_model_to_json(self): # Test model_to_json on a deep structure, with object in object swagger_dict = yaml.load(Tests.yaml_complex_model, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) spec.load_models() Foo = get_model('Foo') Bar = get_model('Bar') f = Foo( token='abcd', bar=Bar( a=1, b=datetime(2016, 8, 26, tzinfo=timezone.utc) ) ) print("foo: " + pprint.pformat(f)) j = spec.model_to_json(f) self.assertDictEqual(j, { 'token': 'abcd', 'bar': { 'a': 1, 'b': datetime(2016, 8, 26, tzinfo=timezone.utc).isoformat() } })
def test__getitem__setitem__delitem(self): o = get_model('Foo')() self.assertEqual(o.s, None) self.assertEqual(o['s'], None) o['s'] = 'bob' self.assertEqual(o.s, 'bob') self.assertEqual(o['s'], 'bob') o['s'] = None self.assertEqual(o.s, None) self.assertEqual(o['s'], None) o['s'] = 'bob' self.assertEqual(o.s, 'bob') del o['s'] self.assertEqual(o.s, None) # But local attributes may not be set this way with self.assertRaises(Exception) as context: o['local'] self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception)) with self.assertRaises(Exception) as context: o['local'] = 123 self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception)) with self.assertRaises(Exception) as context: del o['local'] self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception))
def test_swagger_server_param_in_body(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_body) SessionToken = get_model('SessionToken') Credentials = get_model('Credentials') func.return_value = SessionToken(token='456') with app.test_client() as c: r = c.get('/v1/in/body', data=json.dumps({ 'email': '[email protected]', 'int': '123123', })) self.assertReplyOK(r, '456') func.assert_called_once_with(Credentials(email='[email protected]', int='123123'))
def test_client_with_path_body_param(self): handler, spec = self.generate_client_and_spec( self.yaml_path_body_param) responses.add(responses.GET, "http://some.server.com:80/v1/some/123/path", body=json.dumps({ "foo": "a", "bar": "b" }), status=200, content_type="application/json") # Send a valid parameter object model_class = get_model('Param') param = model_class(arg1='a', arg2='b') res = handler(param, foo=123) self.assertEqual(type(res).__name__, 'Result') self.assertEqual(res.foo, 'a') self.assertEqual(res.bar, 'b') # Only 1 parameter expected with self.assertRaises(ValidationError) as e: res = handler(foo=123) self.assertTrue('expects exactly' in str(e.exception)) with self.assertRaises(ValidationError) as e: res = handler(1, 2, foo=123) self.assertTrue('expects exactly' in str(e.exception)) with self.assertRaises(ValidationError) as e: res = handler(param) self.assertTrue('Missing some arguments' in str(e.exception))
def test_client_with_body_param(self): handler, spec = self.generate_client_and_spec(self.yaml_body_param) responses.add(responses.POST, "http://some.server.com:80/v1/some/path", body=json.dumps({ "foo": "a", "bar": "b" }), status=200, content_type="application/json") # Only 1 parameter expected with self.assertRaises(ValidationError): res = handler() with self.assertRaises(ValidationError): res = handler(1, 2) # Send a valid parameter object model_class = get_model('Param') param = model_class(arg1='a', arg2='b') res = handler(param) self.assertEqual(type(res).__name__, 'Result') self.assertEqual(res.foo, 'a') self.assertEqual(res.bar, 'b')
def test__delattr(self): o = get_model('Foo')() o.s = 'bob' self.assertEqual(o.s, 'bob') del o.s self.assertTrue(hasattr(o, 's')) self.assertEqual(o.s, None) o.s = 'bob' self.assertEqual(o.s, 'bob') delattr(o, 's') self.assertTrue(hasattr(o, 's')) self.assertEqual(o.s, None) o.local = 'bob' self.assertEqual(o.local, 'bob') del o.local self.assertFalse(hasattr(o, 'local')) with self.assertRaises(Exception) as context: o.local self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception)) o.local = 'bob' self.assertEqual(o.local, 'bob') delattr(o, 'local') self.assertFalse(hasattr(o, 'local')) with self.assertRaises(Exception) as context: o.local self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception))
def test_client_error_callback_return_dict(self): def callback(e): return {'error': str(e)} handler, spec = self.generate_client_and_spec( self.yaml_path_body_param, callback=callback, ) responses.add(responses.GET, "http://some.server.com:80/v1/some/123/path", body=json.dumps({ "foo": "a", "bar": "b" }), status=200, content_type="application/json") # Send a valid parameter object model_class = get_model('Param') param = model_class(arg1='a', arg2='b') res = handler(param) print("got: %s" % res) self.assertDictEqual( res, { 'error': 'Missing some arguments to format url: http://some.server.com:80/v1/some/<foo>/path' })
def test__eq(self): Foo = get_model('Foo') Bar = get_model('Bar') a = Foo(s='abc', i=12, o=Bar(s='def')) b = Foo(s='abc', i=12, o=Bar(s='def')) self.assertEqual(a, b) self.assertNotEqual(a, 'bob') # Adding local parameters does not affect eq a.local = 'whatever' self.assertEqual(a, b) # Changing bravado values makes them different a.o.s = '123' self.assertNotEqual(a, b)
def test__hasattr(self): o = get_model('Foo')() self.assertTrue(hasattr(o, 's')) self.assertFalse(hasattr(o, 'local')) o.local = None self.assertTrue(hasattr(o, 'local'))
def test__setattr__getattr(self): o = get_model('Foo')() # set/get a bravado attribute self.assertEqual(o.s, None) self.assertEqual(getattr(o, 's'), None) o.s = 'bob' self.assertEqual(o.s, 'bob') self.assertEqual(getattr(o, 's'), 'bob') # Make sure it's really the Bravado instance's attribute that was updated self.assertTrue('s' not in dir(o)) self.assertEqual(getattr(o, '__bravado_instance').s, 'bob') o.s = None self.assertEqual(o.s, None) self.assertEqual(getattr(o, 's'), None) setattr(o, 's', 'bob') self.assertTrue('s' not in dir(o)) self.assertEqual(getattr(o, '__bravado_instance').s, 'bob') self.assertEqual(o.s, 'bob') self.assertEqual(getattr(o, 's'), 'bob') setattr(o, 's', None) self.assertTrue('s' not in dir(o)) self.assertEqual(getattr(o, '__bravado_instance').s, None) self.assertEqual(o.s, None) self.assertEqual(getattr(o, 's'), None) # set/get a local attribute with self.assertRaises(Exception) as context: o.local self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception)) with self.assertRaises(Exception) as context: getattr(o, 'local') self.assertTrue( "Model 'Foo' has no attribute local" in str(context.exception)) o.local = 'bob' self.assertTrue('local' in dir(o)) self.assertEqual(o.local, 'bob') self.assertEqual(getattr(o, 'local'), 'bob') o.local = None self.assertEqual(o.local, None) self.assertEqual(getattr(o, 'local'), None) setattr(o, 'local', 'bob') self.assertEqual(o.local, 'bob') self.assertEqual(getattr(o, 'local'), 'bob') setattr(o, 'local', None) self.assertEqual(o.local, None) self.assertEqual(getattr(o, 'local'), None)
def test__to_json__from_json(self): Foo = get_model('Foo') Bar = get_model('Bar') Baz = get_model('Baz') a = Foo(s='abc', i=12, lst=['a', 'b', 'c'], o=Bar(s='1', o=Baz(s='2')), lo=[ Baz(s='r'), Baz(s='t'), Baz(s='u'), Baz(), ]) self.assertTrue(isinstance(a, PyMacaronModel)) j = a.to_json() self.assertEqual( j, { 's': 'abc', 'i': 12, 'lst': ['a', 'b', 'c'], 'o': { 'o': { 's': '2' }, 's': '1' }, 'lo': [{ 's': 'r' }, { 's': 't' }, { 's': 'u' }, {}], }) o = Foo.from_json(j) self.assertTrue(isinstance(o, PyMacaronModel)) # TODO: o now has multiple attributes set to None, while a lacks them, # and bravado's __eq__ does not see None and absence as equal... # self.assertEqual(o, a) jj = o.to_json() self.assertEqual(jj, j)
def test_swagger_server_param_in_query__missing_required_param(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_query) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='456') with app.test_client() as c: r = c.get('/v1/in/query?bar=bbbb') self.assertError(r, 400, 'BAD REQUEST') func.assert_not_called()
def test_swagger_server_no_param(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_no_param) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='123') with app.test_client() as c: r = c.get('/v1/no/param') self.assertReplyOK(r, '123') func.assert_called_once_with()
def test_swagger_server_param_in_path(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_path) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='456') with app.test_client() as c: r = c.get('/v1/in/1234/foo/bob234') self.assertReplyOK(r, '456') func.assert_called_once_with(item='1234', path='bob234')
def test_unmarshal_request_error__missing_required_argument(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_body) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='456') with app.test_client() as c: r = c.get('/v1/in/body', data=json.dumps({'bazzoom': 'thiswontwork'})) self.assertError(r, 400, 'BAD REQUEST') func.assert_not_called()
def test_swagger_server_param_in_query(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_query) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='456') with app.test_client() as c: r = c.get('/v1/in/query?foo=aaaa&bar=bbbb') self.assertReplyOK(r, '456') func.assert_called_once_with(bar='bbbb', foo='aaaa')
def test_unmarshal_request_error__wrong_argument_format(self, func): func.__name__ = 'return_token' app, spec = self.generate_server_app(self.yaml_in_body) SessionToken = get_model('SessionToken') func.return_value = SessionToken(token='456') with app.test_client() as c: data = json.dumps({ 'email': '[email protected]', 'int': [1, 2, 3], }) r = c.get('/v1/in/body', data=data)
def test__update_from_dict(self): foo = get_model('Foo')() foo.update_from_dict({'s': 'bob'}) self.assertEqual( foo.to_json(), {'s': 'bob'}, ) foo.update_from_dict({'s': 'abc', 'i': 12}) self.assertEqual( foo.to_json(), { 's': 'abc', 'i': 12 }, ) foo.update_from_dict({}) self.assertEqual( foo.to_json(), { 's': 'abc', 'i': 12 }, ) foo.update_from_dict({'i': None}) self.assertEqual( foo.to_json(), {'s': 'abc'}, ) foo.update_from_dict({'s': None, 'i': 32}, ignore_none=True) self.assertEqual( foo.to_json(), { 's': 'abc', 'i': 32 }, ) foo.update_from_dict({'s': None}) self.assertEqual( foo.to_json(), {'i': 32}, )
def test_requests_parameters_with_body_param(self, requests): handler, spec = self.generate_client_and_spec(self.yaml_body_param) model_class = get_model('Param') param = model_class(arg1='a', arg2='b') with self.assertRaises(PyMacaronCoreException): handler(param) requests.post.assert_called_once_with( 'http://some.server.com:80/v1/some/path', data=json.dumps({ "arg1": "a", "arg2": "b" }), headers={'Content-Type': 'application/json'}, params=None, timeout=(10, 10), verify=True)
def callback(e): return get_model('SessionToken')(token=str(e))
def __init__(self, name, yaml_str=None, yaml_path=None, timeout=10, error_callback=None, formats=None, do_persist=True, host=None, port=None, local=False, proto=None, verify_ssl=True): """An API Specification""" self.name = name # Is the endpoint callable directly as a python method from within the server? # (true is the flask server also serves that api) self.local = local # Callback to handle exceptions self.error_callback = default_error_callback # Flag: true if this api has spawned_api self.is_server = False self.app = None # Object holding the client side code to call the API self.client = APIClient() # Object holding constructors for the API's objects self.model = APIModels() self.client_timeout = timeout # Support versions of PyYAML with and without Loader import pkg_resources v = pkg_resources.get_distribution("PyYAML").version yamlkwargs = {} if v > '3.15': yamlkwargs['Loader'] = yaml.FullLoader if yaml_path: log.info("Loading swagger file at %s" % yaml_path) swagger_dict = yaml.load(open(yaml_path), **yamlkwargs) elif yaml_str: swagger_dict = yaml.load(yaml_str, **yamlkwargs) else: raise Exception("No swagger file specified") self.api_spec = ApiSpec(swagger_dict, formats, host, port, proto, verify_ssl) model_names = self.api_spec.load_models(do_persist=do_persist) # Add aliases to all models into self.model, so a developer may write: # 'ApiPool.<api_name>.model.<model_name>(*args)' to instantiate a model for model_name in model_names: setattr(self.model, model_name, get_model(model_name)) if error_callback: self.error_callback = error_callback # Auto-generate client callers, so a developer may write: # 'ApiPool.<api_name>.call.login(param)' to call the login endpoint self._generate_client_callers()
def handler_wrapper(**path_params): if os.environ.get('PYM_DEBUG', None) == '1': log.debug("PYM_DEBUG: Request headers are: %s" % dict(request.headers)) # Get caller's pym-call-id or generate one call_id = request.headers.get('PymCallID', None) if not call_id: call_id = str(uuid.uuid4()) stack.top.call_id = call_id # Append current server to call path, or start one call_path = request.headers.get('PymCallPath', None) if call_path: call_path = "%s.%s" % (call_path, api_name) else: call_path = api_name stack.top.call_path = call_path if endpoint.param_in_body or endpoint.param_in_query or endpoint.param_in_formdata: # Turn the flask request into something bravado-core can process... has_data = endpoint.param_in_body or endpoint.param_in_formdata try: req = FlaskRequestProxy(request, has_data) except BadRequest: ee = error_callback(ValidationError("Cannot parse json data: have you set 'Content-Type' to 'application/json'?")) return _responsify(api_spec, ee, 400) try: # Note: unmarshall validates parameters but does not fail # if extra unknown parameters are submitted parameters = unmarshal_request(req, endpoint.operation) # Example of parameters: {'body': RegisterCredentials()} except jsonschema.exceptions.ValidationError as e: ee = error_callback(ValidationError(str(e))) return _responsify(api_spec, ee, 400) # Call the endpoint, with proper parameters depending on whether # parameters are in body, query or url args = [] kwargs = {} if endpoint.param_in_path: kwargs = path_params if endpoint.param_in_body: # Remove the parameters already defined in path_params for k in list(path_params.keys()): del parameters[k] lst = list(parameters.values()) assert len(lst) == 1 # Now convert the Bravado body object into a pymacaron model body = lst[0] cls = get_model(body.__class__.__name__) body = cls.from_bravado(body) args.append(body) if endpoint.param_in_query: kwargs.update(parameters) if endpoint.param_in_formdata: for k in list(path_params.keys()): del parameters[k] kwargs.update(parameters) if os.environ.get('PYM_DEBUG', None) == '1': log.debug("PYM_DEBUG: Request args are: [args: %s] [kwargs: %s]" % (args, kwargs)) result = handler_func(*args, **kwargs) if not result: e = error_callback(PyMacaronCoreException("Have nothing to send in response")) return _responsify(api_spec, e, 500) # Did we get the expected response? if endpoint.produces_html: if type(result) is not tuple: e = error_callback(PyMacaronCoreException("Method %s should return %s but returned %s" % (endpoint.handler_server, endpoint.produces, type(result)))) return _responsify(api_spec, e, 500) # Return an html page return result elif endpoint.produces_json: if not hasattr(result, '__module__') or not hasattr(result, '__class__'): e = error_callback(PyMacaronCoreException("Method %s did not return a class instance but a %s" % (endpoint.handler_server, type(result)))) return _responsify(api_spec, e, 500) # If it's already a flask Response, just pass it through. # Errors in particular may be either passed back as flask Responses, or # raised as exceptions to be caught and formatted by the error_callback result_type = result.__module__ + "." + result.__class__.__name__ if result_type == 'flask.wrappers.Response': return result # We may have got a pymacaron Error instance, in which case # it has a http_reply() method... if hasattr(result, 'http_reply'): # Let's transform this Error into a flask Response log.info("Looks like a pymacaron error instance - calling .http_reply()") return result.http_reply() # Otherwise, assume no error occured and make a flask Response out of # the result. # TODO: check that result is an instance of a model expected as response from this endpoint result_json = api_spec.model_to_json(result) # Send a Flask Response with code 200 and result_json r = jsonify(result_json) r.status_code = 200 return r