Merge pull request #5 from Maillol/fix_bug_oas_generation
Fix bug query string appear as required in generated Open API specifi…
This commit is contained in:
commit
8aee135f95
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Utility to write Open Api Specifications using the Python language.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +11,7 @@ class Info:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
return self._spec["title"]
|
return self._spec.get("title")
|
||||||
|
|
||||||
@title.setter
|
@title.setter
|
||||||
def title(self, title):
|
def title(self, title):
|
||||||
@ -15,7 +19,7 @@ class Info:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
return self._spec["description"]
|
return self._spec.get("description")
|
||||||
|
|
||||||
@description.setter
|
@description.setter
|
||||||
def description(self, description):
|
def description(self, description):
|
||||||
@ -23,12 +27,20 @@ class Info:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
return self._spec["version"]
|
return self._spec.get("version")
|
||||||
|
|
||||||
@version.setter
|
@version.setter
|
||||||
def version(self, version):
|
def version(self, version):
|
||||||
self._spec["version"] = 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:
|
class RequestBody:
|
||||||
def __init__(self, spec: dict):
|
def __init__(self, spec: dict):
|
||||||
@ -43,8 +55,8 @@ class RequestBody:
|
|||||||
self._spec["description"] = description
|
self._spec["description"] = description
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required(self):
|
def required(self) -> bool:
|
||||||
return self._spec["required"]
|
return self._spec.get("required", False)
|
||||||
|
|
||||||
@required.setter
|
@required.setter
|
||||||
def required(self, required: bool):
|
def required(self, required: bool):
|
||||||
@ -220,6 +232,22 @@ class PathItem:
|
|||||||
def trace(self) -> OperationObject:
|
def trace(self) -> OperationObject:
|
||||||
return OperationObject(self._spec.setdefault("trace", {}))
|
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:
|
class Paths:
|
||||||
def __init__(self, spec: dict):
|
def __init__(self, spec: dict):
|
||||||
@ -244,7 +272,7 @@ class Server:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
return self._spec["url"]
|
return self._spec["description"]
|
||||||
|
|
||||||
@description.setter
|
@description.setter
|
||||||
def description(self, description: str):
|
def description(self, description: str):
|
||||||
|
@ -13,7 +13,7 @@ Example:
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from types import new_class
|
from types import new_class
|
||||||
from typing import Protocol, TypeVar
|
from typing import Protocol, TypeVar, Optional, Type
|
||||||
|
|
||||||
RespContents = TypeVar("RespContents", covariant=True)
|
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):
|
def _make_status_code_type(status_code):
|
||||||
if status_code in _status_code:
|
if status_code in _status_code:
|
||||||
return new_class(status_code, (Protocol[RespContents],))
|
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.
|
Return True if obj is a status code type such as _200 or _404.
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
from inspect import getdoc
|
|
||||||
import typing
|
import typing
|
||||||
|
from datetime import date, datetime
|
||||||
|
from inspect import getdoc
|
||||||
|
from itertools import count
|
||||||
from typing import List, Type
|
from typing import List, Type
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
from aiohttp.web_app import Application
|
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 ..view import PydanticView, is_pydantic_view
|
||||||
from .typing import is_status_code_type
|
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:
|
class _OASResponseBuilder:
|
||||||
@ -77,24 +103,23 @@ def _add_http_method_to_oas(
|
|||||||
"application/json": {"schema": next(iter(body_args.values())).schema()}
|
"application/json": {"schema": next(iter(body_args.values())).schema()}
|
||||||
}
|
}
|
||||||
|
|
||||||
i = 0
|
indexes = count()
|
||||||
for i, (name, type_) in enumerate(path_args.items()):
|
for args_location, args in (
|
||||||
oas_operation.parameters[i].required = True
|
("path", path_args.items()),
|
||||||
oas_operation.parameters[i].in_ = "path"
|
("query", qs_args.items()),
|
||||||
oas_operation.parameters[i].name = name
|
("header", header_args.items()),
|
||||||
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
):
|
||||||
|
for name, type_ in args:
|
||||||
for i, (name, type_) in enumerate(qs_args.items(), i + 1):
|
i = next(indexes)
|
||||||
oas_operation.parameters[i].required = False
|
oas_operation.parameters[i].in_ = args_location
|
||||||
oas_operation.parameters[i].in_ = "query"
|
oas_operation.parameters[i].name = name
|
||||||
oas_operation.parameters[i].name = name
|
optional_type = _handle_optional(type_)
|
||||||
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
if optional_type is None:
|
||||||
|
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[type_]
|
||||||
for i, (name, type_) in enumerate(header_args.items(), i + 1):
|
oas_operation.parameters[i].required = True
|
||||||
oas_operation.parameters[i].required = False
|
else:
|
||||||
oas_operation.parameters[i].in_ = "header"
|
oas_operation.parameters[i].schema = JSON_SCHEMA_TYPES[optional_type]
|
||||||
oas_operation.parameters[i].name = name
|
oas_operation.parameters[i].required = False
|
||||||
oas_operation.parameters[i].schema = {"type": JSON_SCHEMA_TYPES[type_]}
|
|
||||||
|
|
||||||
return_type = handler.__annotations__.get("return")
|
return_type = handler.__annotations__.get("return")
|
||||||
if return_type is not None:
|
if return_type is not None:
|
||||||
|
0
tests/test_oas/test_struct/__init__.py
Normal file
0
tests/test_oas/test_struct/__init__.py
Normal file
54
tests/test_oas/test_struct/test_info.py
Normal file
54
tests/test_oas/test_struct/test_info.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp_pydantic.oas.struct import OpenApiSpec3
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_title():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
assert oas.info.title is None
|
||||||
|
oas.info.title = "Info Title"
|
||||||
|
assert oas.info.title == "Info Title"
|
||||||
|
assert oas.spec == {"info": {"title": "Info Title"}, "openapi": "3.0.0"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_description():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
assert oas.info.description is None
|
||||||
|
oas.info.description = "info description"
|
||||||
|
assert oas.info.description == "info description"
|
||||||
|
assert oas.spec == {"info": {"description": "info description"}, "openapi": "3.0.0"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_version():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
assert oas.info.version is None
|
||||||
|
oas.info.version = "3.14"
|
||||||
|
assert oas.info.version == "3.14"
|
||||||
|
assert oas.spec == {"info": {"version": "3.14"}, "openapi": "3.0.0"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_info_terms_of_service():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
assert oas.info.terms_of_service is None
|
||||||
|
oas.info.terms_of_service = "http://example.com/terms/"
|
||||||
|
assert oas.info.terms_of_service == "http://example.com/terms/"
|
||||||
|
assert oas.spec == {
|
||||||
|
"info": {"termsOfService": "http://example.com/terms/"},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip("Not yet implemented")
|
||||||
|
def test_info_license():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
oas.info.license.name = "Apache 2.0"
|
||||||
|
oas.info.license.url = "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
assert oas.spec == {
|
||||||
|
"info": {
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
124
tests/test_oas/test_struct/test_paths.py
Normal file
124
tests/test_oas/test_struct/test_paths.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
from aiohttp_pydantic.oas.struct import OpenApiSpec3
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_description():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
oas.paths["/users/{id}"].description = "This route ..."
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {"/users/{id}": {"description": "This route ..."}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_get():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
oas.paths["/users/{id}"].get
|
||||||
|
assert oas.spec == {"openapi": "3.0.0", "paths": {"/users/{id}": {"get": {}}}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_description():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
operation = oas.paths["/users/{id}"].get
|
||||||
|
operation.description = "Long descriptions ..."
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {"/users/{id}": {"get": {"description": "Long descriptions ..."}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_summary():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
operation = oas.paths["/users/{id}"].get
|
||||||
|
operation.summary = "Updates a pet in the store with form data"
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/users/{id}": {
|
||||||
|
"get": {"summary": "Updates a pet in the store with form data"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_parameters():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
operation = oas.paths["/users/{petId}"].get
|
||||||
|
parameter = operation.parameters[0]
|
||||||
|
parameter.name = "petId"
|
||||||
|
parameter.description = "ID of pet that needs to be updated"
|
||||||
|
parameter.in_ = "path"
|
||||||
|
parameter.required = True
|
||||||
|
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/users/{petId}": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "ID of pet that needs to be updated",
|
||||||
|
"in": "path",
|
||||||
|
"name": "petId",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_requestBody():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
request_body = oas.paths["/users/{petId}"].get.request_body
|
||||||
|
request_body.description = "user to add to the system"
|
||||||
|
request_body.content = {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/User"},
|
||||||
|
"examples": {
|
||||||
|
"user": {
|
||||||
|
"summary": "User Example",
|
||||||
|
"externalValue": "http://foo.bar/examples/user-example.json",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request_body.required = True
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/users/{petId}": {
|
||||||
|
"get": {
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"user": {
|
||||||
|
"externalValue": "http://foo.bar/examples/user-example.json",
|
||||||
|
"summary": "User Example",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {"$ref": "#/components/schemas/User"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "user to add to the system",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_responses():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
response = oas.paths["/users/{petId}"].get.responses[200]
|
||||||
|
response.description = "A complex object array response"
|
||||||
|
response.content = {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/VeryComplexType"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
tests/test_oas/test_struct/test_servers.py
Normal file
36
tests/test_oas/test_struct/test_servers.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp_pydantic.oas.struct import OpenApiSpec3
|
||||||
|
|
||||||
|
|
||||||
|
def test_sever_url():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
oas.servers[0].url = "https://development.gigantic-server.com/v1"
|
||||||
|
oas.servers[1].url = "https://development.gigantic-server.com/v2"
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"servers": [
|
||||||
|
{"url": "https://development.gigantic-server.com/v1"},
|
||||||
|
{"url": "https://development.gigantic-server.com/v2"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_sever_description():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
oas.servers[0].url = "https://development.gigantic-server.com/v1"
|
||||||
|
oas.servers[0].description = "Development server"
|
||||||
|
assert oas.spec == {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "https://development.gigantic-server.com/v1",
|
||||||
|
"description": "Development server",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip("Not yet implemented")
|
||||||
|
def test_sever_variables():
|
||||||
|
oas = OpenApiSpec3()
|
@ -1,4 +1,5 @@
|
|||||||
from typing import List, Union
|
from typing import List, Optional, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@ -14,7 +15,9 @@ class Pet(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PetCollectionView(PydanticView):
|
class PetCollectionView(PydanticView):
|
||||||
async def get(self) -> r200[List[Pet]]:
|
async def get(
|
||||||
|
self, format: str, name: Optional[str] = None, *, promo: Optional[UUID] = None
|
||||||
|
) -> r200[List[Pet]]:
|
||||||
"""
|
"""
|
||||||
Get a list of pets
|
Get a list of pets
|
||||||
"""
|
"""
|
||||||
@ -57,21 +60,41 @@ async def test_generated_oas_should_have_pets_paths(generated_oas):
|
|||||||
async def test_pets_route_should_have_get_method(generated_oas):
|
async def test_pets_route_should_have_get_method(generated_oas):
|
||||||
assert generated_oas["paths"]["/pets"]["get"] == {
|
assert generated_oas["paths"]["/pets"]["get"] == {
|
||||||
"description": "Get a list of pets",
|
"description": "Get a list of pets",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "format",
|
||||||
|
"required": True,
|
||||||
|
"schema": {"type": "string"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "name",
|
||||||
|
"required": False,
|
||||||
|
"schema": {"type": "string"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "header",
|
||||||
|
"name": "promo",
|
||||||
|
"required": False,
|
||||||
|
"schema": {"format": "uuid", "type": "string"},
|
||||||
|
},
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Pet",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"title": "Id", "type": "integer"},
|
"id": {"title": "Id", "type": "integer"},
|
||||||
"name": {"title": "Name", "type": "string"},
|
"name": {"title": "Name", "type": "string"},
|
||||||
},
|
},
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name"],
|
||||||
|
"title": "Pet",
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"type": "array",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user