Skip to content

Commit

Permalink
CBV method support and OAS3 autodoc (#234)
Browse files Browse the repository at this point in the history
* Add autodoc and CBV support

* Add CBV support to oas2

* Version and changelod
  • Loading branch information
ahopkins authored May 19, 2021
1 parent c4ba3d6 commit 7c8593f
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 37 deletions.
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# Changelog


## 21.3.2 (2021-05-19)

### Features

* [#234](https://github.com/sanic-org/sanic-openapi/pull/234) - CBV method support; OAS3 autodoc

## 21.3 (2021-05-05)

### Features

* [#189](https://github.com/sanic-org/sanic-openapi/pull/189) - Path parameter description
* [#198](https://github.com/sanic-org/sanic-openapi/pull/198) - Update With Raw Dictionary
* [#207](https://github.com/sanic-org/sanic-openapi/pull/207) - Using both produces and response
* [#208](https://github.com/sanic-org/sanic-openapi/pull/208) - Docstring parsing
* [#210](https://github.com/sanic-org/sanic-openapi/pull/210) - OAS3 support for sanic-openapi3
* [#218](https://github.com/sanic-org/sanic-openapi/pull/218) - Sanic v21.3 Support

### Bug fixes

* [#192](https://github.com/sanic-org/sanic-openapi/pull/192) - Fix getattr default
* [#202](https://github.com/sanic-org/sanic-openapi/pull/202) - Fix consumes_content_type multiple times

### Build system

* [#204](https://github.com/sanic-org/sanic-openapi/pull/204) - Fix the broken build
* [#214](https://github.com/sanic-org/sanic-openapi/pull/214) - add OS related section to .gitignore

### Documentation

* [#183](https://github.com/sanic-org/sanic-openapi/pull/183) - Fix README
* [#197](https://github.com/sanic-org/sanic-openapi/pull/197) - Fix README
* [#206](https://github.com/sanic-org/sanic-openapi/pull/206) - Fix badges in README

### 0.6.2 (2020-06-01)

### Features
Expand All @@ -11,7 +45,7 @@
* TypeError when Spec obj is not JSON serializable ([fe29d0](https://github.com/sanic-org/sanic-openapi/commit/fe29d07ccb0e02ec0be6496e971946269b2d7907))
* Attribute name "name" conflict in consumes body ([67aaf3](https://github.com/sanic-org/sanic-openapi/commit/67aaf34eca5e339c349ef65bd0392cb8a97f184e))

### 0.6.1 (2020-01-03)
## 0.6.1 (2020-01-03)


### Features
Expand Down
2 changes: 1 addition & 1 deletion sanic_openapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

swagger_blueprint = openapi2_blueprint

__version__ = "21.3.1"
__version__ = "21.3.2"
__all__ = [
"openapi2_blueprint",
"swagger_blueprint",
Expand Down
50 changes: 41 additions & 9 deletions sanic_openapi/openapi2/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ def blueprint_factory():
dir_path = dirname(dirname(realpath(__file__)))
dir_path = abspath(dir_path + "/ui")

swagger_blueprint.static("/", dir_path + "/index.html", strict_slashes=True)
swagger_blueprint.static(
"/", dir_path + "/index.html", strict_slashes=True
)
swagger_blueprint.static("/", dir_path)

# Redirect "/swagger" to "/swagger/"
Expand All @@ -39,7 +41,9 @@ def spec(request):

@swagger_blueprint.route("/swagger-config")
def config(request):
return json(getattr(request.app.config, "SWAGGER_UI_CONFIGURATION", {}))
return json(
getattr(request.app.config, "SWAGGER_UI_CONFIGURATION", {})
)

@swagger_blueprint.listener("after_server_start")
def build_spec(app, loop):
Expand All @@ -55,7 +59,12 @@ def build_spec(app, loop):

paths = {}

for (uri, route_name, route_parameters, method_handlers) in get_all_routes(app, swagger_blueprint.url_prefix):
for (
uri,
route_name,
route_parameters,
method_handlers,
) in get_all_routes(app, swagger_blueprint.url_prefix):

# --------------------------------------------------------------- #
# Methods
Expand All @@ -64,16 +73,33 @@ def build_spec(app, loop):
methods = {}
for _method, _handler in method_handlers:

if hasattr(_handler, "view_class"):
_handler = getattr(_handler.view_class, _method.lower())

route_spec = route_specs.get(_handler) or RouteSpec()

if route_spec.exclude:
continue

api_consumes_content_types = getattr(app.config, "API_CONSUMES_CONTENT_TYPES", ["application/json"])
consumes_content_types = route_spec.consumes_content_type or api_consumes_content_types
api_consumes_content_types = getattr(
app.config,
"API_CONSUMES_CONTENT_TYPES",
["application/json"],
)
consumes_content_types = (
route_spec.consumes_content_type
or api_consumes_content_types
)

api_produces_content_types = getattr(app.config, "API_PRODUCES_CONTENT_TYPES", ["application/json"])
produces_content_types = route_spec.produces_content_type or api_produces_content_types
api_produces_content_types = getattr(
app.config,
"API_PRODUCES_CONTENT_TYPES",
["application/json"],
)
produces_content_types = (
route_spec.produces_content_type
or api_produces_content_types
)

# Parameters - Path & Query String
route_parameters = []
Expand Down Expand Up @@ -103,7 +129,8 @@ def build_spec(app, loop):
"required": consumer.required,
"in": consumer.location,
"name": consumer.field.name
if not isinstance(consumer.field, type) and hasattr(consumer.field, "name")
if not isinstance(consumer.field, type)
and hasattr(consumer.field, "name")
else "body",
}

Expand Down Expand Up @@ -176,7 +203,12 @@ def build_spec(app, loop):

_spec = Swagger2Spec(app=app)

_spec.add_definitions(definitions={obj.object_name: definition for obj, definition in definitions.values()})
_spec.add_definitions(
definitions={
obj.object_name: definition
for obj, definition in definitions.values()
}
)

# --------------------------------------------------------------- #
# Tags
Expand Down
24 changes: 21 additions & 3 deletions sanic_openapi/openapi3/blueprint.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import inspect
from os.path import abspath, dirname, realpath

from sanic.blueprints import Blueprint
from sanic.response import json, redirect

from ..autodoc import YamlStyleParametersParser
from ..utils import get_all_routes, get_blueprinted_routes
from . import operations, specification

DEFAULT_SWAGGER_UI_CONFIG = {"apisSorter": "alpha", "operationsSorter": "alpha"}
DEFAULT_SWAGGER_UI_CONFIG = {
"apisSorter": "alpha",
"operationsSorter": "alpha",
}


def blueprint_factory():
Expand Down Expand Up @@ -51,7 +56,12 @@ def build_spec(app, loop):
# --------------------------------------------------------------- #
# Operations
# --------------------------------------------------------------- #
for uri, route_name, route_parameters, method_handlers in get_all_routes(app, oas3_blueprint.url_prefix):
for (
uri,
route_name,
route_parameters,
method_handlers,
) in get_all_routes(app, oas3_blueprint.url_prefix):

# --------------------------------------------------------------- #
# Methods
Expand All @@ -64,15 +74,23 @@ def build_spec(app, loop):
if method == "OPTIONS":
continue

if hasattr(_handler, "view_class"):
_handler = getattr(_handler.view_class, method.lower())
operation = operations[_handler]
docstring = inspect.getdoc(_handler)

if docstring:
operation.autodoc(docstring)

# operation ID must be unique, and it isnt currently used for
# anything in UI, so dont add something meaningless
# if not hasattr(operation, "operationId"):
# operation.operationId = "%s_%s" % (method.lower(), route.name)

for _parameter in route_parameters:
operation.parameter(_parameter.name, _parameter.cast, "path")
operation.parameter(
_parameter.name, _parameter.cast, "path"
)

specification.operation(uri, method, operation)

Expand Down
33 changes: 28 additions & 5 deletions sanic_openapi/openapi3/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
from collections import defaultdict

from ..autodoc import YamlStyleParametersParser
from ..utils import remove_nulls, remove_nulls_from_kwargs
from .definitions import (
Any,
Expand Down Expand Up @@ -44,6 +45,7 @@ def __init__(self):
self.security = []
self.parameters = []
self.responses = {}
self._autodoc = None

def name(self, value: str):
self.operationId = value
Expand All @@ -68,10 +70,16 @@ def deprecate(self):
def body(self, content: Any, **kwargs):
self.requestBody = RequestBody.make(content, **kwargs)

def parameter(self, name: str, schema: Any, location: str = "query", **kwargs):
self.parameters.append(Parameter.make(name, schema, location, **kwargs))
def parameter(
self, name: str, schema: Any, location: str = "query", **kwargs
):
self.parameters.append(
Parameter.make(name, schema, location, **kwargs)
)

def response(self, status, content: Any = None, description: str = None, **kwargs):
def response(
self, status, content: Any = None, description: str = None, **kwargs
):
self.responses[status] = Response.make(content, description, **kwargs)

def secured(self, *args, **kwargs):
Expand All @@ -90,8 +98,15 @@ def build(self):
# todo -- look into more consistent default response format
operation_dict["responses"]["default"] = {"description": "OK"}

if self._autodoc:
operation_dict.update(self._autodoc)

return Operation(**operation_dict)

def autodoc(self, docstring: str):
y = YamlStyleParametersParser(docstring)
self._autodoc = y.to_openAPI_3()


class SpecificationBuilder:
_urls: List[str]
Expand All @@ -115,7 +130,13 @@ def __init__(self):
def url(self, value: str):
self._urls.append(value)

def describe(self, title: str, version: str, description: str = None, terms: str = None):
def describe(
self,
title: str,
version: str,
description: str = None,
terms: str = None,
):
self._title = title
self._version = version
self._description = description
Expand Down Expand Up @@ -174,6 +195,8 @@ def _build_paths(self) -> Dict:
paths = {}

for path, operations in self._paths.items():
paths[path] = PathItem(**{k: v.build() for k, v in operations.items()})
paths[path] = PathItem(
**{k: v.build() for k, v in operations.items()}
)

return paths
22 changes: 18 additions & 4 deletions sanic_openapi/openapi3/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ def fields(self):
return self.__fields

def guard(self, fields):
return {k: v for k, v in fields.items() if k in _properties(self).keys() or k.startswith("x-")}
return {
k: v
for k, v in fields.items()
if k in _properties(self).keys() or k.startswith("x-")
}

def serialize(self):
return _serialize(self.fields)
Expand Down Expand Up @@ -98,9 +102,14 @@ def make(value, **kwargs):

return Array(schema, **kwargs)
elif _type == dict:
return Object({k: Schema.make(v) for k, v in value.items()}, **kwargs)
return Object(
{k: Schema.make(v) for k, v in value.items()}, **kwargs
)
else:
return Object({k: Schema.make(v) for k, v in _properties(value).items()}, **kwargs)
return Object(
{k: Schema.make(v) for k, v in _properties(value).items()},
**kwargs,
)


class Boolean(Schema):
Expand Down Expand Up @@ -201,6 +210,11 @@ def _serialize(value) -> Any:


def _properties(value: object) -> Dict:
fields = {x: v for x, v in value.__dict__.items() if not x.startswith("_")}
try:
fields = {
x: v for x, v in value.__dict__.items() if not x.startswith("_")
}
except AttributeError:
return {}

return {**get_type_hints(value.__class__), **fields}
Loading

0 comments on commit 7c8593f

Please sign in to comment.