Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBV method support and OAS3 autodoc #234

Merged
merged 5 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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