Skip to content

Commit

Permalink
v0.9.7 Extends the ability to define complex Schema objects.
Browse files Browse the repository at this point in the history
  • Loading branch information
endafarrell-herecom committed Jan 26, 2021
1 parent 687e276 commit d005a18
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 167 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Sanic OpenAPI v3e

OpenAPI v3 support for Sanic. Document and describe all parameters,
including sanic path params. python 3.6+
including sanic path params. python 3.6+; sanic 18.12.0+ but sanic 20.12.0 and later are recommended.

[![Pythons](https://img.shields.io/pypi/pyversions/sanic-openapi3e.svg)](https://img.shields.io/pypi/pyversions/sanic-openapi3e.svg)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
Expand Down Expand Up @@ -453,11 +453,11 @@ Schema | production/stable | |
SecurityScheme | production/stable | |
Server | production/stable | |
Tag | production/stable | |
Response | production/stable | The "default" value is not implemented. |
Response | production/stable | no known use of the "default" value |
SecurityRequirement | beta | only the `[]` empty-list override known
Discriminator | beta | no known usage
Encoding | stable | no known usage |
Header | beta | no known usage
Header | production/stable | |
ServerVariable | beta | no known usage
XML | beta | no known usage
Callback | none | not implemented
Expand All @@ -466,8 +466,10 @@ Callback | none | not implemented
specs.

## Changelog
* v0.9.7
* Extends the ability to define complex Schema objects.
* v0.9.6
* Fixes a bug where None values in examples would cause a runtime error.
* Fixes a bug where None values in examples would cause a runtime error.
* v0.9.5
* Improves how the `.components.responses` are rendered.
* Fixes issues with the "static"/"frozen" predefined `Schema` objects.
Expand Down
95 changes: 95 additions & 0 deletions examples/simple_08_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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",),
"cra.properties": doc.Schema(
title="cra.properties",
_type="object",
additional_properties=False,
required=["name", "placeId", "cats", "chains"],
external_docs=doc.ExternalDocumentation("https://tools.ietf.org/html/rfc7946#section-3.2"),
properties={
"name": doc.Schema.String,
"placeId": doc.Schema.String,
"cats": doc.Schema(_type="array", items=doc.Schema.String, unique_items=True),
"chains": doc.Schema(_type="array", items=doc.Schema.String, nullable=True, unique_items=True),
},
),
}
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 with a non-trivial schema -see `cra.properties`"
app.config.OPENAPI_COMPONENTS = components
app.config.OPENAPI_SECURITY = security


@app.route("/object/<an_id:int>", methods=frozenset(("GET", "HEAD", "OPTIONS")))
@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-conda-env.py36.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ dependencies:
- pyyaml >= 5.3.1
# - requests-async >=0.5.0
- pip:
- sanic >=19.6.0
- sanic >= 20.12.0

167 changes: 13 additions & 154 deletions sanic_openapi3e/oas_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ def default_operation_id_fn(method: str, uri: str, route: sanic.router.Route) ->

def camel_case_operation_id_fn(method: str, uri: str, route: sanic.router.Route) -> str:
if hasattr(route.handler, "__class__") and hasattr(route.handler, "handlers"):
# These are `sanic.view.CompositeView`s
# These are `sanic.view.CompositeView`s and while seen in the wild, getting a test case for this is
# surprisingly difficult
_method_handler = route.handler.handlers.get(method.upper())
if _method_handler:
handler_name = method + "_" + _method_handler.__name__
Expand Down Expand Up @@ -162,10 +163,10 @@ class OObject:
@staticmethod
def _as_yamlable_object(
value: Any, sort=False, opt_key: Optional[str] = None
) -> Union[Dict, str, bytes, int, float, List]:
) -> Union[Dict, str, bytes, int, float, List, NoneType]:
if isinstance(value, OObject):
return value.as_yamlable_object(sort=sort, opt_key=opt_key)
if isinstance(value, (str, bytes, int, float, NoneType)):
if isinstance(value, (str, bytes, int, float, type(None))):
return value
if isinstance(value, list):
if sort:
Expand Down Expand Up @@ -223,22 +224,15 @@ def as_yamlable_object( # pylint: disable=too-many-branches
# By default, items in specs are `deprecated: false` - these are not desirable in the specs
continue

value2: Union[Dict, List, str, bytes, int, float, bool]
value2: Union[Dict, List, str, bytes, int, float, bool, NoneType]
if value is False or value is True:
value2 = value

############################################################################################################
# List of yamlable objects for element in value
elif key2 == "parameters" and self.__class__ in (PathItem, Operation):
value2 = [OObject.as_yamlable_object(e, sort=sort, opt_key=f"{opt_key}.{key2}") for e in value]
# elif key2 == "security":
# value2 = [
# SecurityRequirement._as_yamlable_object( # pylint: disable=protected-access
# sr, opt_key=f"{opt_key}.{key2}"
# )
# for sr in value
# ]
# print(209, key, key2, value, value2)

############################################################################################################
# dicts of yamlable objects for items() value
elif key2 == "responses" and self.__class__ == Components:
Expand Down Expand Up @@ -280,67 +274,6 @@ def as_yamlable_object( # pylint: disable=too-many-branches

return _repr

def serialize(self, sort=False) -> OrderedDict:
"""
Serialisation to a dict.
:return: A dict serialisation of self.
:rtype: OrderedDict
"""
# _repr = OrderedDict() # type: OrderedDict[str, Union[Dict, List]]
# for key, value in self.__dict__.items():
#
# if callable(value):
# continue
# if not value and not isinstance(value, bool):
# continue
# if key.startswith("x_") and not for_repr:
# continue
# key2 = openapi_keyname(key)
# value2: Union[Dict, List]
# if key2 == "parameters" and self.__class__.__qualname__ in ("PathItem", "Operation",):
# value2 = list(OObject._serialize(e, for_repr=for_repr, sort=sort) for e in value)
# elif key2 == "security":
# value2 = list(
# SecurityRequirement._serialize(sr, for_repr=for_repr) # pylint: disable=protected-access
# for sr in value
# )
# elif key2 == "schemas":
# # Everyone wants sorted schema entries!
# value2 = OObject._serialize(value, sort=True)
# else:
# value2 = OObject._serialize(value)
# if not value2 or callable(value2):
#
# continue
#
# _repr[key2] = value2
# if sort:
# _sorted_repr = OrderedDict()
# for key in sorted(_repr.keys()):
# _sorted_repr[key] = _repr[key]
# return _repr
yamable = self.as_yamlable_object(sort=sort)
# if self.__class__.__qualname__ == "OpenAPIv3":
# print(278)
# # There is special ordering for this
# _repr = OrderedDict()
# _repr["openapi"] = getattr(self, "version")
# for key in ("info", "servers", "paths", "components", "security", "tags" "externalDocs"):
# _value = yamable.get(key)
# if key == "tags":
# print(284, _value)
# if _value:
# _repr[key] = _value
#
# else:
# _repr = OrderedDict(yamable)
# return _repr
return OrderedDict(yamable)

def __str__(self):
return json.dumps(self.as_yamlable_object(sort=True), sort_keys=True)

def __repr__(self):
return "{}({})".format(
self.__class__.__qualname__, json.dumps(self.as_yamlable_object(sort=True), sort_keys=True),
Expand All @@ -359,81 +292,6 @@ class OType(OObject):
formats: List[str] = []


# class OInteger(OType):
# """A sanic_openapi3e class to hold OpenAPI integer types of formats `int32` and/or `int64`."""
#
# name = "integer"
# formats = ["int32", "int64"]
#
# def __init__(self, value: int, _format: Optional[str] = None):
# if _format:
# assert _format in self.formats
# self.format = _format
# self.value = value
#
# def serialise(self) -> int:
# return self.value


# class ONumber(OType):
# """A sanic_openapi3e class to hold OpenAPI non-integer numeric types of formats `float` and `double`."""
#
# name = "number"
# formats = ["float", "double"]
#
# def __init__(self, value: float, _format: Optional[str] = None):
# if _format:
# assert _format in self.formats
# self.format = _format
# self.value = value
#
# def serialise(self) -> float:
# return self.value


# class OString(OType):
# """
# A sanic_openapi3e class to hold OpenAPI string types of formats `byte`, `binary`, `date`, `date-time` and
# `password`.
# """
#
# name = "string"
# formats = ["byte", "binary", "date", "date-time", "password"]
#
# def __init__(self, value: str, _format: Optional[str] = None):
# if _format:
# assert _format in self.formats
# self.format = _format
# self.value = value
#
# def serialise(self) -> str:
# return self.value


# class OBoolean(OType):
# """
# A sanic_openapi3e class to hold OpenAPI string types of formats `byte`, `binary`, `date`, `date-time` and
# `password`.
# """
#
# name = "boolean"
# formats: List[str] = []
#
# def __init__(self, value: bool, _format: Optional[str] = None):
# if _format:
# assert _format in self.formats
# self.format = _format
# self.value = value
#
# def serialise(self) -> bool:
# return self.value


# OTypeFormats: Dict[str, List[str]] = {
# _type.name: _type.formats for _type in (OInteger, ONumber, OString, OBoolean)
# }


# --------------------------------------------------------------- #
# Info Object
# --------------------------------------------------------------- #
Expand Down Expand Up @@ -1312,7 +1170,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
any_of: Optional[Union["Schema", Reference]] = None,
_not: Optional[Union["Schema", Reference]] = None,
items: Optional[Union["Schema", Reference]] = None,
properties: Optional[Union["Schema", Reference]] = None,
properties: Optional[Dict[str, Union["Schema", Reference]]] = None,
additional_properties: Optional[Union[bool, "Schema", Reference]] = None,
description: Optional[str] = None,
_format: Optional[str] = None,
Expand Down Expand Up @@ -1508,7 +1366,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
_assert_type(any_of, (Schema, Reference), "any_of", self.__class__)
_assert_type(_not, (Schema, Reference), "_not", self.__class__)
_assert_type(items, (Schema, Reference,), "items", self.__class__)
_assert_type(properties, (Schema, Reference), "properties", self.__class__)
_assert_type(properties, (dict,), "properties", self.__class__)
_assert_type(
additional_properties, (bool, Schema, Reference), "additional_properties", self.__class__,
)
Expand Down Expand Up @@ -1553,6 +1411,11 @@ def __init__( # pylint: disable=too-many-arguments, too-many-locals, too-many-s
if _type == "object":
additional_properties = True

if properties is not None:
for property_name, property_value in properties.items():
_assert_type(property_name, (str,), "properties.{} name".format(property_name), self.__class__)
_assert_type(property_value, (Schema, Reference), "properties.{}".format(property_name), self.__class__)

# Assignment and docs
self.title = title
"""
Expand Down Expand Up @@ -2908,10 +2771,6 @@ def __getitem__(self, key):
def __repr__(self):
return repr(self.__dict__)

def serialize(self, sort=False):
raise NotImplementedError(self)
# return self.__dict__

def __len__(self):
return len(self.__dict__)

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def run_asserts(
spec = response # pragma: no cover
elif isinstance(response, sanic_openapi3e.oas_types.OpenAPIv3):
# Allows for test writing flexibility
spec = response.serialize() # pragma: no cover
spec = response.as_yamlable_object()
else:
# Most tests use this
assert response.status == 200
Expand Down
Loading

0 comments on commit d005a18

Please sign in to comment.