From e23e7bc8eb1ba4c2e22170de1dd0220f8f18aaaa Mon Sep 17 00:00:00 2001 From: Correl Date: Wed, 24 Feb 2021 16:41:03 -0500 Subject: [PATCH] Create project --- .gitignore | 10 ++ README.rst | 0 pyproject.toml | 20 +++ tests/__init__.py | 0 tests/test_tornado_openapi3_example.py | 5 + tornado_openapi3_example/__init__.py | 1 + tornado_openapi3_example/__main__.py | 105 ++++++++++++ tornado_openapi3_example/app.py | 0 tornado_openapi3_example/static/index.html | 19 +++ .../templates/openapi.yaml | 149 ++++++++++++++++++ 10 files changed, 309 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_tornado_openapi3_example.py create mode 100644 tornado_openapi3_example/__init__.py create mode 100644 tornado_openapi3_example/__main__.py create mode 100644 tornado_openapi3_example/app.py create mode 100644 tornado_openapi3_example/static/index.html create mode 100644 tornado_openapi3_example/templates/openapi.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5632d3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.idea +*.log +tmp/ + +*.py[cod] +*.egg +build +htmlcov +poetry.lock diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6d20193 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "tornado-openapi3-example" +version = "0.1.0" +description = "" +authors = ["Correl "] + +[tool.poetry.dependencies] +python = "^3.7" +tornado = "^6" +tornado-openapi3 = "^0.2.4" + +[tool.poetry.dev-dependencies] +black = { version = "*", allow-prereleases = true } +pytest = "^5.2" +pytest-black = "*" + + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tornado_openapi3_example.py b/tests/test_tornado_openapi3_example.py new file mode 100644 index 0000000..0f13f5d --- /dev/null +++ b/tests/test_tornado_openapi3_example.py @@ -0,0 +1,5 @@ +from tornado_openapi3_example import __version__ + + +def test_version(): + assert __version__ == '0.1.0' diff --git a/tornado_openapi3_example/__init__.py b/tornado_openapi3_example/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/tornado_openapi3_example/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/tornado_openapi3_example/__main__.py b/tornado_openapi3_example/__main__.py new file mode 100644 index 0000000..3cb999a --- /dev/null +++ b/tornado_openapi3_example/__main__.py @@ -0,0 +1,105 @@ +import json +import os +import logging +import pathlib +import pkg_resources + +import msgpack +from openapi_core import create_spec # type: ignore +from openapi_core.exceptions import OpenAPIError # type: ignore +from openapi_core.deserializing.exceptions import DeserializeError # type: ignore +from openapi_core.schema.media_types.exceptions import ( # type: ignore + InvalidContentType, +) +from openapi_core.templating.paths.exceptions import OperationNotFound # type: ignore +from openapi_core.unmarshalling.schemas.exceptions import ValidateError # type: ignore +from openapi_core.validation.exceptions import InvalidSecurity # type: ignore +import tornado.ioloop +import tornado.web +from tornado_openapi3 import RequestValidator +import yaml + + +class OpenAPISpecHandler(tornado.web.RequestHandler): + async def get(self) -> None: + self.set_header("Content-Type", "application/x-yaml") + return self.render("openapi.yaml") + + +class OpenAPIRequestHandler(tornado.web.RequestHandler): + async def prepare(self) -> None: + maybe_coro = super().prepare() + if maybe_coro and asyncio.iscoroutine(maybe_coro): # pragma: no cover + await maybe_coro + + spec = create_spec(yaml.safe_load(self.render_string("openapi.yaml"))) + validator = RequestValidator(spec) + result = validator.validate(self.request) + try: + result.raise_for_errors() + except OperationNotFound: + self.set_status(405) + self.finish() + except InvalidContentType: + self.set_status(415) + self.finish() + except (DeserializeError, ValidateError): + self.set_status(400) + self.finish() + except InvalidSecurity: + self.set_status(401) + self.finish() + except OpenAPIError: + raise + self.validated = result + + +class LoginHandler(OpenAPIRequestHandler): + async def post(self) -> None: + self.set_header("Content-Type", "application/json") + self.finish( + json.dumps( + { + "username": self.validated.body["username"], + } + ) + ) + + +class NoteHandler(OpenAPIRequestHandler): + async def get(self, identifier: str) -> None: + self.set_header("Content-Type", "application/json") + self.finish( + json.dumps( + { + "subject": "Shopping list", + "body": "\n".join(["- Dish soap", "- Potatoes", "- Milk"]), + } + ) + ) + + +def make_app(): + pkg_root = pathlib.Path(pkg_resources.resource_filename(__package__, "")) + return tornado.web.Application( + [ + (r"/", tornado.web.RedirectHandler, {"url": "/static/index.html"}), + ( + r"/static/(.*)", + tornado.web.StaticFileHandler, + {"path": pkg_root / "static", "default_filename": "index.html"}, + ), + (f"/openapi.yaml", OpenAPISpecHandler), + (r"/login", LoginHandler), + (r"/notes/(?P.+)", NoteHandler), + ], + debug=os.environ.get("DEBUG"), + template_path=str(pkg_root / "templates"), + ) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + app = make_app() + app.listen(8888) + tornado.ioloop.IOLoop.current().start() diff --git a/tornado_openapi3_example/app.py b/tornado_openapi3_example/app.py new file mode 100644 index 0000000..e69de29 diff --git a/tornado_openapi3_example/static/index.html b/tornado_openapi3_example/static/index.html new file mode 100644 index 0000000..fbc8d13 --- /dev/null +++ b/tornado_openapi3_example/static/index.html @@ -0,0 +1,19 @@ + + + Tornado OpenAPI3 Example + + + + + + + + + + + diff --git a/tornado_openapi3_example/templates/openapi.yaml b/tornado_openapi3_example/templates/openapi.yaml new file mode 100644 index 0000000..52f6143 --- /dev/null +++ b/tornado_openapi3_example/templates/openapi.yaml @@ -0,0 +1,149 @@ +--- +openapi: "3.0.0" +info: + title: Tornado OpenAPI3 Example + version: "1.0.0" + description: | + An example application using tornado-openapi3 to validate requests and + responses. +paths: + /: + get: + summary: Service Root + tags: + - Documentation + responses: + '301': + description: Redirect to HTML documentation + headers: + Location: + schema: + type: string + enum: + - /static/index.html + /static/index.html: + get: + summary: HTML Rendered OpenAPI Specification + tags: + - Documentation + responses: + '200': + description: HTML documentation + content: + text/html: + schema: + type: string + /openapi.yaml: + get: + summary: YAML OpenAPI specification + tags: + - Documentation + responses: + '200': + description: OpenAPI specification + content: + application/x-yaml: + schema: + type: string + /login: + post: + summary: Log In + tags: + - Examples + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/login' + responses: + '200': + description: Login successful + '403': + description: Forbidden + /notes: + post: + summary: Create a note + tags: + - Examples + security: + - token: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/note' + responses: + '204': + description: Note created + /notes/{id}: + get: + summary: Retrieve a note + tags: + - Examples + security: + - token: [] + parameters: + - name: id + in: path + schema: + type: integer + required: true + responses: + '200': + description: Note + content: + application/json: + schema: + $ref: '#/components/schemas/note' + text/html: + schema: + type: string + delete: + summary: Remove a note + tags: + - Examples + security: + - token: [] + parameters: + - name: id + in: path + schema: + type: integer + required: true + responses: + '204': + description: Note deleted +components: + schemas: + login: + type: object + properties: + username: + type: string + example: admin + password: + type: string + example: correct-horse-battery-staple + additionalProperties: false + required: + - username + - password + note: + type: object + properties: + subject: + type: string + maxLength: 72 + body: + type: string + tags: + type: array + items: + type: string + securitySchemes: + token: + type: http + scheme: bearer + bearerFormat: Access token