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, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) with self.assertRaisesRegex(Exception, "Expecting only one type"): spec.call_on_each_endpoint(self.foo)
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_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, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) with self.assertRaisesRegex(Exception, "Only 'application/json' or 'text/html' are supported."): spec.call_on_each_endpoint(self.foo)
def generate_server_app(self, yaml_str, callback=default_error_callback): swagger_dict = yaml.load(yaml_str, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) spec.load_models() app = Flask('test') spawn_server_api('somename', app, spec, callback, None) return app, spec
def generate_client_and_spec(self, yaml_str, callback=default_error_callback, local=False): swagger_dict = yaml.load(yaml_str, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) spec.load_models() callers_dict = generate_client_callers(spec, 10, callback, local, None) assert len(list(callers_dict.keys())) == 1 assert 'do_test' in callers_dict handler = callers_dict['do_test'] assert type(handler).__name__ == 'function' return handler, spec
def test_json_to_model(self): swagger_dict = yaml.load(Tests.yaml_complex_model, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) spec.load_models() j = { 'token': 'abcd', 'bar': { 'a': 1, 'b': date.today().isoformat() } } m = spec.json_to_model('Foo', j) 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()))
def test_apispec_constructor__scheme_https(self): yaml_str = """ swagger: '2.0' info: title: test version: '1.2.3' description: Just a test host: pnt-login.elasticbeanstalk.com schemes: - https basePath: /v1 produces: - application/json """ swagger_dict = yaml.load(yaml_str, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) self.assertEqual(spec.host, 'pnt-login.elasticbeanstalk.com') self.assertEqual(spec.port, 443) self.assertEqual(spec.protocol, 'https') self.assertEqual(spec.version, '1.2.3')
def test_validate(self): swagger_dict = yaml.load(Tests.yaml_complex_model, Loader=yaml.FullLoader) spec = ApiSpec(swagger_dict) spec.load_models() f = { 'token': 'abcd', 'bar': { 'a': 1, 'b': date.today().isoformat() } } # validate ok! spec.validate('Foo', f) # missing required property f = {'bar': {}} try: 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: 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: spec.validate('Foo', f) except Exception as e: self.assertTrue("'123' is not of type 'object'" in str(e)) else: assert 0
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, Loader=yaml.FullLoader) 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)
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()
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 """ 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 _generate_client_callers(self, app=None): # If app is defined, we are doing local calls if app: callers_dict = generate_client_callers(self.api_spec, self.client_timeout, self.error_callback, True, app) else: callers_dict = generate_client_callers(self.api_spec, self.client_timeout, self.error_callback, False, None) for method, caller in list(callers_dict.items()): setattr(self.client, method, caller) 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 self.app = app if self.local: # Re-generate client callers, this time as local and passing them the app self._generate_client_callers(app) 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 object.to_json() def json_to_model(self, model_name, j, validate=False, keep_datetime=False): """Take a json strust and a model name, and return a model instance""" if validate: self.api_spec.validate(model_name, j) o = getattr(self.model, model_name) return o.from_json(j, keep_datetime=keep_datetime)