def test_model_to_json(self): swagger_dict = yaml.load(Tests.yaml_complex_model) spec = ApiSpec(swagger_dict) Foo = spec.definitions['Foo'] Bar = spec.definitions['Bar'] f = Foo( token='abcd', bar=Bar( a=1, b=date.today() ) ) print("foo: " + pprint.pformat(f)) j = spec.model_to_json(f) self.assertDictEqual(j, { 'token': 'abcd', 'bar': { 'a': 1, 'b': date.today().isoformat() } })
def test_call_on_each_endpoint__too_many_produces(self): yaml_str = """ swagger: '2.0' host: pnt-login.elasticbeanstalk.com schemes: - http - https paths: /v1/auth/login: post: parameters: - in: query name: whatever description: User login credentials. required: true type: string produces: - foo/bar - bar/baz x-bind-server: pnt_login.handlers.do_login """ swagger_dict = yaml.load(yaml_str) spec = ApiSpec(swagger_dict) with self.assertRaisesRegexp(Exception, "Expecting only one type"): spec.call_on_each_endpoint(self.foo)
def test_call_on_each_endpoint__invalid_produces(self): yaml_str = """ swagger: '2.0' host: pnt-login.elasticbeanstalk.com schemes: - http - https paths: /v1/auth/login: post: parameters: - in: query name: whatever description: User login credentials. required: true type: string produces: - foo/bar x-bind-server: pnt_login.handlers.do_login """ swagger_dict = yaml.load(yaml_str) spec = ApiSpec(swagger_dict) with self.assertRaisesRegexp(Exception, "Only 'application/json' or 'text/html' are supported."): spec.call_on_each_endpoint(self.foo)
def test_validate(self): swagger_dict = yaml.load(Tests.yaml_complex_model) spec = ApiSpec(swagger_dict) f = { 'token': 'abcd', 'bar': { 'a': 1, 'b': date.today().isoformat() } } # validate ok! m = spec.validate('Foo', f) # missing required property f = {'bar': {}} try: m = spec.validate('Foo', f) except Exception as e: self.assertTrue("'a' is a required property" in str(e)) else: assert 0 # invalid type f = {'a': 'oaeuaoue'} try: m = spec.validate('Bar', f) except Exception as e: self.assertTrue("'oaeuaoue' is not of type 'number'" in str(e)) else: assert 0 # invalid object reference type f = {'bar': '123'} try: m = spec.validate('Foo', f) except Exception as e: self.assertTrue("'123' is not of type 'object'" in str(e)) else: assert 0
def test_json_to_model(self): swagger_dict = yaml.load(Tests.yaml_complex_model) spec = ApiSpec(swagger_dict) j = { 'token': 'abcd', 'bar': { 'a': 1, 'b': date.today().isoformat() } } m = spec.json_to_model('Foo', j) Foo = spec.definitions['Foo'] Bar = spec.definitions['Bar'] self.assertEqual(m.token, 'abcd') b = m.bar self.assertEqual(b.__class__.__name__, 'Bar') self.assertEqual(b.a, 1) self.assertEqual(str(b.b), str(date.today()) + " 00:00:00")
def __init__(self, name, yaml_str=None, yaml_path=None, timeout=None, error_callback=None, formats=None, do_persist=True, host=None, port=None): """An API Specification""" self.name = name if yaml_path: log.info("Loading swagger file at %s" % yaml_path) swagger_dict = yaml.load(open(yaml_path)) elif yaml_str: swagger_dict = yaml.load(yaml_str) else: raise Exception("No swagger file specified") self.api_spec = ApiSpec(swagger_dict, formats, host, port) if timeout: self.client_timeout = timeout if error_callback: self.error_callback = error_callback # Auto-generate class methods for every object model defined # in the swagger spec, calling that model's constructor # Ex: # klue_api.Version(version='1.2.3') => return a Version object for model_name in self.api_spec.definitions: model_generator = generate_model_instantiator(model_name, self.api_spec.definitions) # Associate model generator to ApiPool().<api_name>.model.<model_name> setattr(self.model, model_name, model_generator) # Make this bravado-core model persistent? if do_persist: spec = swagger_dict['definitions'][model_name] if 'x-persist' in spec: self._make_persistent(model_name, spec['x-persist']) # Auto-generate client callers # so we can write # api.call.login(param) => call /v1/login/ on server with param as json parameter callers_dict = generate_client_callers(self.api_spec, self.client_timeout, self.error_callback) for method, caller in callers_dict.items(): setattr(self.client, method, caller)
class API(): """Describes a REST client/server API, with sugar coating: - easily instantiating the objects defined in the API - auto-generation of client code usage: See apipool.py """ # The API's swagger representation api_spec = None # Object holding the client side code to call the API client = APIClient() # Object holding constructors for the API's objects model = APIModels() # Default timeout when calling server endpoint, in sec client_timeout = 10 # Callback to handle exceptions error_callback = default_error_callback # Flag: true if this api has spawned_api is_server = False # The api's name name = None def __init__(self, name, yaml_str=None, yaml_path=None, timeout=None, error_callback=None, formats=None, do_persist=True, host=None, port=None): """An API Specification""" self.name = name if yaml_path: log.info("Loading swagger file at %s" % yaml_path) swagger_dict = yaml.load(open(yaml_path)) elif yaml_str: swagger_dict = yaml.load(yaml_str) else: raise Exception("No swagger file specified") self.api_spec = ApiSpec(swagger_dict, formats, host, port) if timeout: self.client_timeout = timeout if error_callback: self.error_callback = error_callback # Auto-generate class methods for every object model defined # in the swagger spec, calling that model's constructor # Ex: # klue_api.Version(version='1.2.3') => return a Version object for model_name in self.api_spec.definitions: model_generator = generate_model_instantiator(model_name, self.api_spec.definitions) # Associate model generator to ApiPool().<api_name>.model.<model_name> setattr(self.model, model_name, model_generator) # Make this bravado-core model persistent? if do_persist: spec = swagger_dict['definitions'][model_name] if 'x-persist' in spec: self._make_persistent(model_name, spec['x-persist']) # Auto-generate client callers # so we can write # api.call.login(param) => call /v1/login/ on server with param as json parameter callers_dict = generate_client_callers(self.api_spec, self.client_timeout, self.error_callback) for method, caller in callers_dict.items(): setattr(self.client, method, caller) # # WARNING: ugly piece of monkey-patching below. Hopefully will replace # with native bravado-core code in the future... # def _make_persistent(self, model_name, pkg_name): """Monkey-patch object persistence (ex: to/from database) into a bravado-core model class""" # Load class at path pkg_name c = get_function(pkg_name) for name in ('load_from_db', 'save_to_db'): if not hasattr(c, name): raise KlueException("Class %s has no static method '%s'" % (pkg_name, name)) log.info("Making %s persistent via %s" % (model_name, pkg_name)) # Replace model generator with one that adds 'save_to_db' to every instance model = getattr(self.model, model_name) n = self._wrap_bravado_model_generator(model, c.save_to_db) setattr(self.model, model_name, n) # Add class method load_from_db to model generator model = getattr(self.model, model_name) setattr(model, 'load_from_db', c.load_from_db) def _wrap_bravado_model_generator(self, model, method): def new_creator(*args, **kwargs): r = model(*args, **kwargs) r.save_to_db = types.MethodType(method, r) return r return new_creator # # End of ugly-monkey-patching # def spawn_api(self, app, decorator=None): """Auto-generate server endpoints implementing the API into this Flask app""" if decorator: assert type(decorator).__name__ == 'function' self.is_server = True return spawn_server_api(self.name, app, self.api_spec, self.error_callback, decorator) def get_version(self): """Return the version of the API (as defined in the swagger file)""" return self.api_spec.version def model_to_json(self, object): """Take a model instance and return it as a json struct""" return self.api_spec.model_to_json(object) def json_to_model(self, model_name, j, validate=False): """Take a json strust and a model name, and return a model instance""" if validate: self.api_spec.validate(model_name, j) return self.api_spec.json_to_model(model_name, j)
def test_call_on_each_endpoint(self): yaml_str = """ swagger: '2.0' info: title: test version: '0.0.1' description: Just a test host: pnt-login.elasticbeanstalk.com schemes: - http basePath: /v1 produces: - application/json paths: /v1/auth/login: post: summary: blabla description: blabla parameters: - in: body name: whatever description: User login credentials. required: true schema: $ref: '#/definitions/Credentials' produces: - application/json x-bind-server: pnt_login.handlers.do_login x-bind-client: login x-auth-required: false responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' /v1/auth/logout: get: summary: blabla description: blabla parameters: - in: query name: baboom description: foooo required: true type: string produces: - application/json x-bind-server: pnt_login.babar x-decorate-request: foo.bar.baz responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' /v1/version: get: summary: blabla description: blabla produces: - application/json x-bind-server: do_version x-decorate-server: foo.bar.baz responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' /v1/versionhtml: get: summary: blabla description: blabla produces: - text/html x-bind-server: do_version_html x-decorate-server: foo.bar.baz responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' /v1/hybrid/{foo}: get: summary: blabla description: blabla produces: - application/json parameters: - in: body name: whatever description: User login credentials. required: true schema: $ref: '#/definitions/Credentials' - in: path name: foo description: foooo required: true type: string x-bind-server: do_version x-decorate-server: foo.bar.baz responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' /v1/ignoreme: get: summary: blabla description: blabla produces: - application/json x-no-bind-server: true responses: 200: description: A session token schema: $ref: '#/definitions/SessionToken' definitions: SessionToken: type: object description: An authenticated user''s session token properties: token: type: string description: Session token. Credentials: type: object description: A user''s login credentials. properties: email: type: string description: User''s email. password_hash: type: string description: MD5 of user''s password, truncated to 16 first hexadecimal characters. """ Tests.call_count = 0 swagger_dict = yaml.load(yaml_str) spec = ApiSpec(swagger_dict) def test_callback(data): print("Looking at %s %s" % (data.method, data.path)) Tests.call_count = Tests.call_count + 1 self.assertEqual(type(data.operation).__name__, 'Operation') if data.path == '/v1/auth/logout/': self.assertEqual(data.method, 'GET') self.assertEqual(data.handler_server, 'pnt_login.babar') self.assertIsNone(data.handler_client) self.assertIsNone(data.decorate_server) self.assertEqual(data.decorate_request, 'foo.bar.baz') self.assertFalse(data.param_in_body) self.assertTrue(data.param_in_query) self.assertFalse(data.no_params) self.assertTrue(data.produces_json) self.assertFalse(data.produces_html) elif data.path == '/v1/auth/login': self.assertEqual(data.method, 'POST') self.assertEqual(data.handler_server, 'pnt_login.handlers.do_login') self.assertEqual(data.handler_client, 'login') self.assertIsNone(data.decorate_server) self.assertIsNone(data.decorate_request) self.assertTrue(data.param_in_body) self.assertFalse(data.param_in_query) self.assertFalse(data.param_in_path) self.assertFalse(data.no_params) self.assertTrue(data.produces_json) self.assertFalse(data.produces_html) elif data.path == '/v1/version': self.assertEqual(data.method, 'GET') self.assertEqual(data.handler_server, 'do_version') self.assertIsNone(data.handler_client) self.assertIsNone(data.decorate_request) self.assertEqual(data.decorate_server, 'foo.bar.baz') self.assertFalse(data.param_in_body) self.assertFalse(data.param_in_query) self.assertFalse(data.param_in_path) self.assertTrue(data.no_params) self.assertTrue(data.produces_json) self.assertFalse(data.produces_html) elif data.path == '/v1/versionhtml': self.assertEqual(data.method, 'GET') self.assertEqual(data.handler_server, 'do_version_html') self.assertIsNone(data.handler_client) self.assertIsNone(data.decorate_request) self.assertEqual(data.decorate_server, 'foo.bar.baz') self.assertFalse(data.param_in_body) self.assertFalse(data.param_in_query) self.assertFalse(data.param_in_path) self.assertTrue(data.no_params) self.assertFalse(data.produces_json) self.assertTrue(data.produces_html) elif data.path == '/v1/hybrib': self.assertEqual(data.method, 'GET') self.assertEqual(data.handler_server, 'do_version') self.assertIsNone(data.handler_client) self.assertIsNone(data.decorate_request) self.assertEqual(data.decorate_server, 'foo.bar.baz') self.assertTrue(data.param_in_body) self.assertFalse(data.param_in_query) self.assertTrue(data.param_in_path) self.assertTrue(data.no_params) self.assertTrue(data.produces_json) self.assertFalse(data.produces_html) spec.call_on_each_endpoint(test_callback) self.assertEqual(Tests.call_count, 5)