Fix bug query string appear as required in generated Open API specification.

This commit is contained in:
Vincent Maillol
2020-11-15 19:54:53 +01:00
parent 0d3a33c964
commit 462d8d8b98
8 changed files with 324 additions and 33 deletions

View File

@@ -1,3 +1,7 @@
"""
Utility to write Open Api Specifications using the Python language.
"""
from typing import Union
@@ -7,7 +11,7 @@ class Info:
@property
def title(self):
return self._spec["title"]
return self._spec.get("title")
@title.setter
def title(self, title):
@@ -15,7 +19,7 @@ class Info:
@property
def description(self):
return self._spec["description"]
return self._spec.get("description")
@description.setter
def description(self, description):
@@ -23,12 +27,20 @@ class Info:
@property
def version(self):
return self._spec["version"]
return self._spec.get("version")
@version.setter
def version(self, version):
self._spec["version"] = version
@property
def terms_of_service(self):
return self._spec.get("termsOfService")
@terms_of_service.setter
def terms_of_service(self, terms_of_service):
self._spec["termsOfService"] = terms_of_service
class RequestBody:
def __init__(self, spec: dict):
@@ -43,8 +55,8 @@ class RequestBody:
self._spec["description"] = description
@property
def required(self):
return self._spec["required"]
def required(self) -> bool:
return self._spec.get("required", False)
@required.setter
def required(self, required: bool):
@@ -220,6 +232,22 @@ class PathItem:
def trace(self) -> OperationObject:
return OperationObject(self._spec.setdefault("trace", {}))
@property
def description(self) -> str:
return self._spec["description"]
@description.setter
def description(self, description: str):
self._spec["description"] = description
@property
def summary(self) -> str:
return self._spec["summary"]
@summary.setter
def summary(self, summary: str):
self._spec["summary"] = summary
class Paths:
def __init__(self, spec: dict):
@@ -244,7 +272,7 @@ class Server:
@property
def description(self) -> str:
return self._spec["url"]
return self._spec["description"]
@description.setter
def description(self, description: str):

View File

@@ -13,7 +13,7 @@ Example:
from functools import lru_cache
from types import new_class
from typing import Protocol, TypeVar
from typing import Protocol, TypeVar, Optional, Type
RespContents = TypeVar("RespContents", covariant=True)
@@ -24,9 +24,10 @@ _status_code = frozenset(f"r{code}" for code in range(100, 600))
def _make_status_code_type(status_code):
if status_code in _status_code:
return new_class(status_code, (Protocol[RespContents],))
return None
def is_status_code_type(obj):
def is_status_code_type(obj) -> bool:
"""
Return True if obj is a status code type such as _200 or _404.
"""

View File

@@ -1,6 +1,9 @@
from inspect import getdoc
import typing
from datetime import date, datetime
from inspect import getdoc
from itertools import count
from typing import List, Type
from uuid import UUID
from aiohttp.web import Response, json_response
from aiohttp.web_app import Application
@@ -12,7 +15,30 @@ from ..utils import is_pydantic_base_model
from ..view import PydanticView, is_pydantic_view
from .typing import is_status_code_type
JSON_SCHEMA_TYPES = {float: "number", str: "string", int: "integer"}
JSON_SCHEMA_TYPES = {
float: {"type": "number"},
str: {"type": "string"},
int: {"type": "integer"},
UUID: {"type": "string", "format": "uuid"},
bool: {"type": "boolean"},
datetime: {"type": "string", "format": "date-time"},
date: {"type": "string", "format": "date"},
}
def _handle_optional(type_):
"""
Returns the type wrapped in Optional or None.
>>> _handle_optional(int)
>>> _handle_optional(Optional[str])
<class 'str'>
"""
if typing.get_origin(type_) is typing.Union:
args = typing.get_args(type_)
if len(args) == 2 and type(None) in args:
return next(iter(set(args) - {type(None)}))
return None
class _OASResponseBuilder:
@@ -77,24 +103,23 @@ def _add_http_method_to_oas(
"application/json": {"schema": next(iter(body_args.values())).schema()}
}
i = 0
for i, (name, type_) in enumerate(path_args.items()):
oas_operation.parameters[i].required = True
oas_operation.parameters[i].in_ = "path"
oas_operation.parameters[i].name = name
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
for i, (name, type_) in enumerate(qs_args.items(), i + 1):
oas_operation.parameters[i].required = False
oas_operation.parameters[i].in_ = "query"
oas_operation.parameters[i].name = name
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
for i, (name, type_) in enumerate(header_args.items(), i + 1):
oas_operation.parameters[i].required = False
oas_operation.parameters[i].in_ = "header"
oas_operation.parameters[i].name = name
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
indexes = count()
for args_location, args in (
("path", path_args.items()),
("query", qs_args.items()),
("header", header_args.items()),
):
for name, type_ in args:
i = next(indexes)
oas_operation.parameters[i].in_ = args_location
oas_operation.parameters[i].name = name
optional_type = _handle_optional(type_)
if optional_type is None:
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[type_]
oas_operation.parameters[i].required = True
else:
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[optional_type]
oas_operation.parameters[i].required = False
return_type = handler.__annotations__.get("return")
if return_type is not None: