Allow overriding of spec as a dictionary

This commit is contained in:
Correl Roush 2021-03-05 15:23:23 -05:00
parent 3a622e0306
commit 7a8f195712
7 changed files with 120 additions and 85 deletions

View file

@ -29,10 +29,15 @@ author = "Correl Roush"
# ones. # ones.
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx_rtd_theme", "sphinx_rtd_theme",
] ]
autodoc_member_order = "groupwise" autodoc_member_order = "bysource"
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"tornado": ("https://tornado.readthedocs.org/", None),
}
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]

View file

@ -1,9 +1,5 @@
Handling Incoming Requests Handling Incoming Requests
========================== ==========================
OpenAPIRequestHandler extends Tornado's RequestHandler class, providing
validation of incoming requests and translating errors into appropriate HTTP
responses.
.. automodule:: tornado_openapi3.handler .. automodule:: tornado_openapi3.handler
:members: :members:

View file

@ -1,8 +1,5 @@
Testing API Responses Testing API Responses
===================== =====================
AsyncOpenAPITestCase extends Tornado's AsyncHTTPTestCase class, providing
validation of the responses from your application and raising errors in tests.
.. automodule:: tornado_openapi3.testing .. automodule:: tornado_openapi3.testing
:members: :members:

View file

@ -1,7 +1,6 @@
import json import json
import unittest.mock import unittest.mock
from openapi_core import create_spec # type: ignore
from openapi_core.exceptions import OpenAPIError # type: ignore from openapi_core.exceptions import OpenAPIError # type: ignore
import tornado.httpclient # type: ignore import tornado.httpclient # type: ignore
import tornado.web # type: ignore import tornado.web # type: ignore
@ -11,60 +10,56 @@ from tornado_openapi3.handler import OpenAPIRequestHandler
class ResourceHandler(OpenAPIRequestHandler): class ResourceHandler(OpenAPIRequestHandler):
spec = create_spec( spec_dict = {
{ "openapi": "3.0.0",
"openapi": "3.0.0", "info": {
"info": { "title": "Test API",
"title": "Test API", "version": "1.0.0",
"version": "1.0.0", },
"components": {
"schemas": {
"resource": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
}, },
"components": { "securitySchemes": {
"schemas": { "basicAuth": {
"resource": { "type": "http",
"type": "object", "scheme": "bearer",
"properties": {"name": {"type": "string"}}, }
"required": ["name"], },
},
"security": [{"basicAuth": []}],
"paths": {
"/resource": {
"post": {
"requestBody": {
"required": True,
"content": {
"application/vnd.example.resource+json": {
"schema": {"$ref": "#/components/schemas/resource"},
}
},
}, },
}, "responses": {
"securitySchemes": { "200": {
"basicAuth": { "description": "Success",
"type": "http",
"scheme": "bearer",
}
},
},
"security": [{"basicAuth": []}],
"paths": {
"/resource": {
"post": {
"requestBody": {
"required": True,
"content": { "content": {
"application/vnd.example.resource+json": { "application/vnd.example.resource+json": {
"schema": {"$ref": "#/components/schemas/resource"}, "schema": {"$ref": "#/components/schemas/resource"},
} }
}, },
}, },
"responses": { "401": {
"200": { "description": "Missing or invalid credentials",
"description": "Success",
"content": {
"application/vnd.example.resource+json": {
"schema": {
"$ref": "#/components/schemas/resource"
},
}
},
},
"401": {
"description": "Missing or invalid credentials",
},
}, },
} },
} }
}, }
} },
) }
custom_media_type_deserializers = { custom_media_type_deserializers = {
"application/vnd.example.resource+json": json.loads, "application/vnd.example.resource+json": json.loads,
} }

View file

@ -1,6 +1,5 @@
import json import json
from openapi_core import create_spec # type: ignore
from openapi_core.schema.responses.exceptions import InvalidResponse # type: ignore from openapi_core.schema.responses.exceptions import InvalidResponse # type: ignore
import tornado.web # type: ignore import tornado.web # type: ignore
@ -11,31 +10,29 @@ from tornado_openapi3.testing import AsyncOpenAPITestCase
def spec(responses: dict = dict()) -> dict: def spec(responses: dict = dict()) -> dict:
if not responses: if not responses:
responses = {"200": {"description": "Success"}} responses = {"200": {"description": "Success"}}
return create_spec( return {
{ "openapi": "3.0.0",
"openapi": "3.0.0", "info": {
"info": { "title": "Test API",
"title": "Test API", "version": "1.0.0",
"version": "1.0.0", },
}, "components": {
"components": { "schemas": {
"schemas": { "resource": {
"resource": { "type": "object",
"type": "object", "properties": {"name": {"type": "string"}},
"properties": {"name": {"type": "string"}}, "required": ["name"],
"required": ["name"],
},
}, },
}, },
"paths": { },
"/resource": { "paths": {
"get": { "/resource": {
"responses": responses, "get": {
} "responses": responses,
} }
}, }
} },
) }
class TestTestCase(AsyncOpenAPITestCase): class TestTestCase(AsyncOpenAPITestCase):
@ -54,7 +51,7 @@ class TestTestCase(AsyncOpenAPITestCase):
class BaseTestCase(AsyncOpenAPITestCase): class BaseTestCase(AsyncOpenAPITestCase):
spec = spec() spec_dict = spec()
custom_media_type_deserializers = { custom_media_type_deserializers = {
"application/vnd.example.resource+json": json.loads, "application/vnd.example.resource+json": json.loads,
} }
@ -76,7 +73,7 @@ class BaseTestCase(AsyncOpenAPITestCase):
class SuccessTests(BaseTestCase): class SuccessTests(BaseTestCase):
spec = spec( spec_dict = spec(
responses={ responses={
"200": { "200": {
"description": "Success", "description": "Success",
@ -99,7 +96,7 @@ class SuccessTests(BaseTestCase):
class IncorrectResponseTests(BaseTestCase): class IncorrectResponseTests(BaseTestCase):
spec = spec(responses={"200": {"description": "Success"}}) spec_dict = spec(responses={"200": {"description": "Success"}})
async def get(self, handler: tornado.web.RequestHandler) -> None: async def get(self, handler: tornado.web.RequestHandler) -> None:
handler.set_status(400) handler.set_status(400)
@ -111,7 +108,7 @@ class IncorrectResponseTests(BaseTestCase):
class RaiseErrorTests(BaseTestCase): class RaiseErrorTests(BaseTestCase):
spec = spec( spec_dict = spec(
responses={ responses={
"500": { "500": {
"description": "An error has occurred.", "description": "An error has occurred.",

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
import logging import logging
from openapi_core import create_spec # type: ignore
from openapi_core.exceptions import OpenAPIError # type: ignore from openapi_core.exceptions import OpenAPIError # type: ignore
from openapi_core.deserializing.exceptions import DeserializeError # type: ignore from openapi_core.deserializing.exceptions import DeserializeError # type: ignore
from openapi_core.schema.specs.models import Spec # type: ignore from openapi_core.schema.specs.models import Spec # type: ignore
@ -21,16 +22,38 @@ logger = logging.getLogger(__name__)
class OpenAPIRequestHandler(tornado.web.RequestHandler): class OpenAPIRequestHandler(tornado.web.RequestHandler):
"""Base class for HTTP request handlers.
A request handler extending :py:class:`tornado.web.RequestHandler` providing
OpenAPI spec validation on incoming requests and translating errors into
appropriate HTTP responses.
"""
@property @property
def spec(self) -> Spec: def spec_dict(self) -> dict:
"""The OpenAPI 3 specification. """The OpenAPI 3 specification
Override this in your request handlers to load or define your OpenAPI 3 Override this in your request handlers to load or define your OpenAPI 3
spec. spec.
:rtype: dict
""" """
raise NotImplementedError() raise NotImplementedError()
@property
def spec(self) -> Spec:
"""The OpenAPI 3 specification.
Override this in your request handlers to customize how your OpenAPI 3
spec is loaded and validated.
:rtype: :py:class:`openapi_core.schema.specs.model.Spec`
"""
return create_spec(self.spec_dict, validate_spec=False)
@property @property
def custom_media_type_deserializers(self) -> dict: def custom_media_type_deserializers(self) -> dict:
"""A dictionary mapping media types to deserializing functions. """A dictionary mapping media types to deserializing functions.

View file

@ -3,20 +3,42 @@ from typing import Any
import tornado.httpclient # type: ignore import tornado.httpclient # type: ignore
import tornado.testing # type: ignore import tornado.testing # type: ignore
from openapi_core import create_spec # type: ignore
from openapi_core.schema.specs.models import Spec # type: ignore from openapi_core.schema.specs.models import Spec # type: ignore
from tornado_openapi3.responses import ResponseValidator from tornado_openapi3.responses import ResponseValidator
class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase): class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase):
"""A test case that starts up an HTTP server.
An async test case extending :py:class:`tornado.testing.AsyncHTTPTestCase`,
providing OpenAPI spec validation on the responses from your application and
raising errors in tests.
"""
@property
def spec_dict(self) -> dict:
"""The OpenAPI 3 specification
Override this in your test cases to load or define your OpenAPI 3 spec.
:rtype: dict
"""
raise NotImplementedError()
@property @property
def spec(self) -> Spec: def spec(self) -> Spec:
"""The OpenAPI 3 specification. """The OpenAPI 3 specification.
Override this in your request handlers to load or define your OpenAPI 3 Override this in your test cases to customize how your OpenAPI 3 spec is
spec. loaded and validated.
:rtype: :py:class:`openapi_core.schema.specs.model.Spec`
""" """
raise NotImplementedError() return create_spec(self.spec_dict)
@property @property
def custom_media_type_deserializers(self) -> dict: def custom_media_type_deserializers(self) -> dict: