diff --git a/README.md b/README.md index b9cc89c..17bfa09 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,11 @@ The main idea for this project is to automatically extract API specs from the us Swirl uses the ```@restapi``` decorator to get both the routing info AND the swagger spec which is derived from the method module docs. While ```@schema``` decorator is used to mark classes to include them into the ```components/schemas``` section of the resulting OpenAPI spec. +## Recent News: +* Tornado-Swirl has now moved to my personal Github account. + ## Releases: +* V. 0.1.20 -- Fix on basic auth security scheme spec, minor edits. * V. 0.1.19 -- Modified property name regex to accomodate JSON:API query strings with square brackets. * V. 0.1.18 -- Added support for ```example```/```examples``` for schema model types, bug fix on enum values with dash, updated swagger UI resources, code cleanup. * V. 0.1.17 -- Added support for ```object``` (freeform object) type, added support for schema inheritance, and ```json_mime_type``` option in ```swirl.describe(...)``` to enable JSON:API authors to specify ```application/vnd.api+json```. diff --git a/TUTORIAL.md b/TUTORIAL.md index 9ea2bf1..eb8d909 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -47,9 +47,12 @@ swirl.describe(title="My REST API", description="Example API that does wonders") #setup security scheme scheme1 = security.APIKey('X-CUSTOM-KEY', location="header") scheme2 = security.HTTP('bearer', bearerFormat='JWT') +scheme3 = security.HTTP('basic') swirl.add_security_scheme('my_custom_key', scheme1) swirl.add_security_scheme('my_http_key' , scheme2) +swirl.add_security_scheme('my_http_key2' , scheme3) + ``` diff --git a/examples/app_sec.py b/examples/app_sec.py new file mode 100644 index 0000000..227ee39 --- /dev/null +++ b/examples/app_sec.py @@ -0,0 +1,60 @@ +from tornado_swirl.swagger import restapi, schema, Application, describe +from tornado_swirl import api_routes, add_security_scheme +import tornado.web +import tornado.ioloop +from tornado_swirl.openapi import security + +describe(title='Test API', description='Just things to test') + +#setup security scheme +scheme1 = security.HTTP('basic') + +add_security_scheme('basic_scheme', scheme1) + +@restapi('/path/to/api') +class MyHandler(tornado.web.RequestHandler): + async def get(self): + """This will be the API path summary. + + While the long description will be the API description. + + Query Parameters: + date (date) -- Required. The target date. + sort (enum[asc, desc]) -- Optional. Sort order. + items (int) -- Optional. Number of items to display. + minimum: 100 maximum: 200 + + Returns: + items ([string]) -- List of random strings. + + Error Responses: + 400 (ErrorResponse) -- Bad Request. + 500 (ErrorResponse) -- Internal Server Error. + + Tags: + internal + + Security: + basic_scheme + """ + self.finish() + + +@schema +class ErrorResponse(object): + """Error response object. + + Properties: + code (int) -- Required. Error code. + message (string) -- Error description. + """ + pass + +def make_app(): + return Application(api_routes(), autoreload=True) + +if __name__ == "__main__": + app = make_app() + app.debug = True + app.listen(8888) + tornado.ioloop.IOLoop.current().start() \ No newline at end of file diff --git a/setup.py b/setup.py index 3d3a109..b10f43b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup(name='tornado-swirl', python_requires='>=3.6.0', - version='0.1.19', + version='0.1.20', url='https://github.com/rduldulao/tornado-swirl', zip_safe=False, packages=['tornado_swirl'], @@ -33,5 +33,5 @@ 'Programming Language :: Python :: 3.6' ], keywords=['SWAGGER', 'OPENAPI', 'TORNADO'], - download_url='https://github.com/rduldulao/tornado-swirl/archive/v_0.1.19.tar.gz', + download_url='https://github.com/rduldulao/tornado-swirl/archive/v_0.1.20.tar.gz', ) diff --git a/tests/test_swirl.py b/tests/test_swirl.py index 82a6ae8..ddc85a6 100644 --- a/tests/test_swirl.py +++ b/tests/test_swirl.py @@ -1,6 +1,7 @@ #pylint: disable=all import tornado_swirl.swagger as swirl import tornado_swirl.settings as settings +import tornado_swirl.openapi.security as security from tornado_swirl import api_routes from tornado.testing import AsyncHTTPTestCase, gen_test from tornado.web import RequestHandler @@ -606,4 +607,91 @@ def post(self): assert obj['paths'] assert obj['paths']['/test'] assert obj['paths']['/test']['head'] - assert obj['paths']['/test'].get('post') == None \ No newline at end of file + assert obj['paths']['/test'].get('post') == None + + @gen_test + def test_security_scheme1(self): + self.reset_settings() + swirl.describe(title='My API', description='My description') + swirl.add_security_scheme("test_basic", security.HTTP("basic")) + @swirl.restapi("/test") + class Handler(RequestHandler): + + def post(self): + """This is the simple description. + With a second line. + + Long description. + With a second line. + + Security: + test_basic + + Request Body: + user (User) -- sample user. + """ + pass + + + + self.get_app().add_handlers(r".*", api_routes()) + response = yield self.http_client.fetch(self.get_url('/swagger/spec')) + obj = json.loads(response.body.decode('utf-8')) + + assert obj['paths'] + assert obj['paths']['/test'] + assert obj['paths']['/test']['post'] + assert obj['paths']['/test']['post']['security'] + assert obj['paths']['/test']['post']['security'][0] + assert 'test_basic' in obj['paths']['/test']['post']['security'][0] + + + assert obj['components'] + assert obj['components']['securitySchemes'] + assert obj['components']['securitySchemes']['test_basic'] + assert obj['components']['securitySchemes']['test_basic']['type'] == 'http' + assert obj['components']['securitySchemes']['test_basic']['scheme'] == 'basic' + + @gen_test + def test_security_scheme2(self): + self.reset_settings() + swirl.describe(title='My API', description='My description') + swirl.add_security_scheme("test_basic", security.HTTP("bearer", bearerFormat="JWT")) + @swirl.restapi("/test") + class Handler(RequestHandler): + + def post(self): + """This is the simple description. + With a second line. + + Long description. + With a second line. + + Security: + test_basic + + Request Body: + user (User) -- sample user. + """ + pass + + + + self.get_app().add_handlers(r".*", api_routes()) + response = yield self.http_client.fetch(self.get_url('/swagger/spec')) + obj = json.loads(response.body.decode('utf-8')) + + assert obj['paths'] + assert obj['paths']['/test'] + assert obj['paths']['/test']['post'] + assert obj['paths']['/test']['post']['security'] + assert obj['paths']['/test']['post']['security'][0] + assert 'test_basic' in obj['paths']['/test']['post']['security'][0] + + + assert obj['components'] + assert obj['components']['securitySchemes'] + assert obj['components']['securitySchemes']['test_basic'] + assert obj['components']['securitySchemes']['test_basic']['type'] == 'http' + assert obj['components']['securitySchemes']['test_basic']['scheme'] == 'bearer' + assert obj['components']['securitySchemes']['test_basic']['bearerFormat'] == 'JWT' \ No newline at end of file diff --git a/tornado_swirl/docparser.py b/tornado_swirl/docparser.py index 10aa86f..55a792c 100644 --- a/tornado_swirl/docparser.py +++ b/tornado_swirl/docparser.py @@ -1,4 +1,4 @@ -# pylint: disable=W0611 +# pylint: disable=W0611, line-too-long, unused-argument """Docstring line parser implementation. Returns: @@ -77,7 +77,7 @@ def _process_security_params(fsm_obj, ptype): cleaned_lines = _clean_lines(lines) params = {} # parse the lines - for i, line in enumerate(cleaned_lines): + for _, line in enumerate(cleaned_lines): matcher = PARAM_MATCHER.match(line.lstrip()) if not matcher: continue diff --git a/tornado_swirl/openapi/security.py b/tornado_swirl/openapi/security.py index 1d31201..ff056ad 100644 --- a/tornado_swirl/openapi/security.py +++ b/tornado_swirl/openapi/security.py @@ -3,6 +3,7 @@ """ class SecurityScheme(object): + """Represents security scheme""" pass class APIKey(SecurityScheme): @@ -24,8 +25,8 @@ def spec(self): class HTTP(SecurityScheme): Schemes = ['basic', 'bearer', 'digest', 'hoba', 'mutual', 'negotiate', 'oauth', 'scram-sha-1', 'scram-sha-256', 'vapid'] #from http://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml - - + + def __init__(self, scheme, bearerFormat=None): self.scheme = scheme self.bearerFormat = bearerFormat @@ -33,7 +34,7 @@ def __init__(self, scheme, bearerFormat=None): @property def type(self): return "http" - + def spec(self): sp = { "type": "http", @@ -42,7 +43,3 @@ def spec(self): if self.bearerFormat: sp["bearerFormat"] = self.bearerFormat return sp - - - - diff --git a/tornado_swirl/views.py b/tornado_swirl/views.py index cd1e6f3..1df5032 100644 --- a/tornado_swirl/views.py +++ b/tornado_swirl/views.py @@ -85,7 +85,7 @@ def get(self): proto = proto or self.request.headers.get( "X-Forwarded-Proto", None) or self.request.protocol servers = [{ - 'url': proto + "://" + server_host + "/", + 'url': proto + "://" + server_host, 'description': 'Default server' }] @@ -124,8 +124,8 @@ def get(self): for name, scheme in security_schemes.items(): components['securitySchemes'][name] = scheme.spec() - specs.update(components) - + #specs.update(components) + self.finish(json_dumps(specs, self.get_arguments('pretty'))) def __get_schema_spec(self, cls):