Skip to content

Commit

Permalink
v0.9.4 better responses
Browse files Browse the repository at this point in the history
  • Loading branch information
endafarrell-herecom committed Jan 22, 2021
1 parent fed8003 commit e39d662
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 26 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ pretty:
.PHONY: good
good: pretty
pylint sanic_openapi3e
$(MAKE) mypy


.PHONY: mypy
mypy:
mypy -p sanic_openapi3e
mypy examples/*.py

.PHONY: pytest
pytest:
Expand Down
27 changes: 10 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@ including sanic path params. python 3.6+
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Downloads](https://pepy.tech/badge/sanic-openapi3e)](https://pepy.tech/project/sanic-openapi3e)

[comment]: <> (## Table of Contents)

[comment]: <> (1. [Installation]&#40;#installation&#41;)

[comment]: <> (2. [Usage]&#40;#usage&#41;)

[comment]: <> (3. [Control spec generation]&#40;#Control-spec-generation&#41;)

[comment]: <> (4. [OAS Object maturity]&#40;#oas-object-maturity&#41;)

## Installation

Expand Down Expand Up @@ -440,14 +431,10 @@ production use, but some of the spec's objects are marked here as "beta" due to

| Class | sanic-openapi3e maturity | notes |
|---|---|---|
Callback | beta | no known usage
Components | production/stable | |
Contact | production/stable | |
Discriminator | beta | no known usage
Encoding | stable | no known usage |
Example | production/stable | |
ExternalDocumentation | production/stable | |
Header | beta | no known usage
Info | production/stable | |
License | production/stable | |
Link | beta | no known usage
Expand All @@ -461,23 +448,29 @@ PathItem | production/stable | |
Paths | production/stable | |
Reference | production/stable | |
RequestBody | production/stable | |
Response | production/stable | |
Responses | production/stable | |
Schema | production/stable | |
SecurityRequirement | beta | no known usage
SecurityScheme | production/stable | |
Server | production/stable | |
ServerVariable | beta | no known usage
Tag | production/stable | |
Response | production/stable | The "default" value is not implemented. |
SecurityRequirement | beta | only the `[]` empty-list override known
Discriminator | beta | no known usage
Encoding | stable | no known usage |
Header | beta | no known usage
ServerVariable | beta | no known usage
XML | beta | no known usage
Callback | none | not implemented

`sanic-openapi3e` is built to create [OpenAPI 3.0.2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md)
specs.

## Changelog
* v0.9.4
* Adds a `@doc.responses()` to give an easier way of documenting a route.
* v0.9.3
* Adds a `@doc.security()` to override security requirements on a route.
* Removes entries with `false` values from the spec if `false` is the default value. This makes the specs smaller in
in size and are more idomatic.
in size and are more idiomatic.
* v0.9.2
* Fixes an issue of rendering SecurityRequirement when there were no entries in the list.
4 changes: 3 additions & 1 deletion examples/simple_04_post_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ async def get_user(request, user_id):
@app.post("/user")
@doc.summary("Creates a new user")
@doc.request_body(
description="A (JSON) user object", required=True, content={"application/json": {"schema": {"type": "object"}}},
description="A (JSON) user object",
required=True,
content={"application/json": doc.MediaType(schema=doc.Schema.Object)},
)
@doc.response(201, "User created")
async def post_user(request):
Expand Down
82 changes: 82 additions & 0 deletions examples/simple_07_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pathlib
from typing import Dict, List, Optional, Union

import sanic.request
import sanic.response
import sanic.router
from sanic import Sanic

# isort: off
# These two lines are to ensure that the version of `sanic_openapi3e` your app uses is from this checkout.
import sys

sys.path.insert(0, str(pathlib.Path(__file__).absolute().parent.parent))
from sanic_openapi3e import doc, openapi_blueprint, swagger_blueprint

# isort: on


schemas: Optional[Dict[str, Union[doc.Schema, doc.Reference]]] = {
"str.min4": doc.Schema(title="str.min4", _type="string", minimum=4, description="A string of len >= 4",),
"int.min4": doc.Schema(title="int.min4", _type="integer", _format="int32", minimum=4, description="Minimum: 4",),
}
components = doc.Components(schemas=schemas)
security: List[doc.SecurityRequirement] = [doc.SecurityRequirement({"bearerAuth": []})]
responses_200only = doc.Responses({"200": doc.Reference("#/components/responses/200")}, no_defaults=True)


app = Sanic(name=__file__, strict_slashes=True)
app.blueprint(openapi_blueprint)
app.blueprint(swagger_blueprint)

app.config.API_TITLE = __file__
app.config.API_DESCRIPTION = "This has an example adding multiple responses in a single `@doc.responses` call"
app.config.OPENAPI_COMPONENTS = components
app.config.OPENAPI_SECURITY = security


@app.get("/object/<an_id:int>")
@doc.parameter(
name="an_id", description="An ID", required=True, _in="path", schema=doc.Schema.Integer,
)
@doc.tag("Tag 1", description="A tag desc")
@doc.responses(
{
200: {"r": doc.Reference("#/components/responses/200")},
404: {"d": "Not there", "h": None, "c": None, "l": None},
401: None,
403: None,
405: None,
410: None,
500: None,
}
)
def get_id(request, an_id: int):
d = locals()
del d["request"] # not JSON serializable
return sanic.response.json(d)


@app.get("/object2/<an_id:int>")
@doc.parameter(
name="an_id", description="An ID", required=True, _in="path", schema=doc.Schema.Integer,
)
@doc.tag("Tag 1", description="A tag desc")
@doc.responses(responses_200only)
def get_id2(request, an_id: int):
d = locals()
del d["request"] # not JSON serializable
return sanic.response.json(d)


example_port = 8002


@app.listener("after_server_start")
async def notify_server_started(_, __):
print("\n\n************* sanic-openapi3e ********************************")
print(f"* See your openapi swagger on http://127.0.0.1:{example_port}/swagger/ *")
print("************* sanic-openapi3e ********************************\n\n")


app.go_fast(port=example_port)
2 changes: 1 addition & 1 deletion sanic_openapi3e/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
from .openapi import blueprint as openapi_blueprint
from .swagger import blueprint as swagger_blueprint

__version__ = "0.9.3"
__version__ = "0.9.4"
__all__ = ["openapi_blueprint", "swagger_blueprint", "doc"]
58 changes: 58 additions & 0 deletions sanic_openapi3e/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,64 @@ def inner(func):
return inner


def responses(
container: Union[
Dict[
Union[int, str],
Optional[
Dict[
str,
Union[
str,
Optional[Dict[str, Union[Header, Reference]]],
Optional[Dict[str, MediaType]],
Optional[Dict[str, Union[Link, Reference]]],
Optional[Reference],
],
]
],
],
Responses,
]
):
"""
A container for the expected responses of an operation. The container maps a HTTP response code to the expected
response.
The documentation is not necessarily expected to cover all possible HTTP response codes because they may not be
known in advance. However, documentation is expected to cover a successful operation response and any known errors.
"""

def inner(func):
if isinstance(container, Responses):
endpoints[func].x_responses_holder = container
else:
for status_code, opt_desc in container.items():
if opt_desc:
d = opt_desc.get("d") # pylint: disable=invalid-name
h = opt_desc.get("c") # pylint: disable=invalid-name
c = opt_desc.get("c") # pylint: disable=invalid-name
l = opt_desc.get("l") # pylint: disable=invalid-name
r = opt_desc.get("r") # pylint: disable=invalid-name
if any((d, h, c, l)):
assert not r, "You cannot combine `Reference`s in this `Response`."
if r:
endpoints[func].x_responses_holder[str(status_code)] = r
elif any((d, h, c, l)):
endpoints[func].x_responses_holder[str(status_code)] = Response(
description=d, headers=h, content=c, links=l
)
else:
endpoints[func].x_responses_holder[str(status_code)] = None
else:
endpoints[func].x_responses_holder[str(status_code)] = None
return func

return inner


def security(requirements: List[SecurityRequirement]):
"""
Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a
Expand Down
27 changes: 20 additions & 7 deletions sanic_openapi3e/oas_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import traceback
import warnings
from collections import OrderedDict
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Type, Union

import sanic.router

Expand Down Expand Up @@ -1266,6 +1266,9 @@ class Schema(OObject): # pylint: disable=too-many-instance-attributes
Strings = None # type: Schema
"""A pre-defined String Schema. An array of (simple) String elements."""

Object = None # type: Schema
"""A pre-defined Object Schema."""

def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-statements
self,
_type: str,
Expand Down Expand Up @@ -1297,7 +1300,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
_not: Optional[Union["Schema", Reference]] = None,
items: Optional[Dict[str, Union["Schema", Reference]]] = None,
properties: Optional[Union["Schema", Reference]] = None,
additional_properties: Optional[Union["Schema", Reference]] = None,
additional_properties: Optional[Union[bool, "Schema", Reference]] = None,
description: Optional[str] = None,
_format: Optional[str] = None,
default: Optional[Union[str, int, float, bool]] = None,
Expand Down Expand Up @@ -1495,7 +1498,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
# TODO - finish the definition of "items", it is incomplete. Is it supposed to be a dict of Schemae?
_assert_type(properties, (Schema, Reference), "properties", self.__class__)
_assert_type(
additional_properties, (Schema, Reference), "additional_properties", self.__class__,
additional_properties, (bool, Schema, Reference), "additional_properties", self.__class__,
)
_assert_type(description, (str,), "description", self.__class__)
_assert_type(_format, (str,), "_format", self.__class__)
Expand Down Expand Up @@ -1534,6 +1537,10 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
if _type == "array":
_assert_required(items, "items", self.__class__, " as `type=array`.")

if additional_properties is None:
if _type == "object":
additional_properties = True

# Assignment and docs
self.title = title
"""
Expand Down Expand Up @@ -1856,6 +1863,7 @@ def get_enum_type(cls, enum: List) -> str:
Schema.Integers = Schema(_type="array", items=Schema.Integer.serialize(), x_frozen=True)
Schema.Numbers = Schema(_type="array", items=Schema.Number.serialize(), x_frozen=True)
Schema.Strings = Schema(_type="array", items=Schema.String.serialize(), x_frozen=True)
Schema.Object = Schema(_type="object", additional_properties=True, x_frozen=True)


class Parameter(OObject): # pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -2666,7 +2674,7 @@ class Responses(OObject):
}

# TODO - may need to reimplement the ``serialise`` and ``schema``.
def __init__(self, responses: Optional[Dict[str, Union[Response, Reference]]] = None):
def __init__(self, responses: Optional[Dict[str, Union[Response, Reference]]] = None, no_defaults: bool = False):
"""
A container for the expected responses of an operation. The container maps a HTTP response code to the expected
response. The documentation is not necessarily expected to cover all possible HTTP response codes because they
Expand Down Expand Up @@ -2696,15 +2704,20 @@ def __init__(self, responses: Optional[Dict[str, Union[Response, Reference]]] =
explicit code definition takes precedence over the range definition for that code.
:param responses: A mapping of response code (as str) to a Response.
:param no_defaults: If True, the defaults are not used, thus only the entries of the `responses` dict will be
used.
"""
# TODO - types
# TODO - validations
_init_responses: Dict[str, Union[Response, Reference]] = {
key: Reference("#/components/responses/{}".format(key)) for key in Responses.DEFAULT_RESPONSES
}
if responses:
for status_code, response in responses.items():
_init_responses[str(status_code)] = response
if no_defaults:
_init_responses = responses
else:
for status_code, response in responses.items():
_init_responses[str(status_code)] = response
self.__dict__ = _init_responses

@property
Expand Down Expand Up @@ -2769,7 +2782,7 @@ class Components(OObject): # pylint: disable=too-many-instance-attributes

def __init__( # pylint: disable=too-many-arguments
self,
schemas: Optional[Dict[str, Union[Schema, Reference]]] = None,
schemas: Optional[Mapping[str, Union[Schema, Reference]]] = None,
responses: Optional[Dict[str, Union[Response, Reference]]] = None,
parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None,
examples: Optional[Dict[str, Union[Example, Reference]]] = None,
Expand Down
Loading

0 comments on commit e39d662

Please sign in to comment.