Adding upstream version 0.5.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
303fa6e9d8
commit
97e6d74bac
110 changed files with 12006 additions and 0 deletions
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
1103
tests/data/swagger_openapi_v3.0.1.json
Normal file
1103
tests/data/swagger_openapi_v3.0.1.json
Normal file
File diff suppressed because it is too large
Load diff
0
tests/schema_classes/__init__.py
Normal file
0
tests/schema_classes/__init__.py
Normal file
64
tests/schema_classes/test_schema.py
Normal file
64
tests/schema_classes/test_schema.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import logging
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openapi_pydantic import Reference, schema_validate
|
||||
from openapi_pydantic.compat import (
|
||||
DEFS_KEY,
|
||||
PYDANTIC_V2,
|
||||
ConfigDict,
|
||||
Extra,
|
||||
models_json_schema,
|
||||
v1_schema,
|
||||
)
|
||||
|
||||
|
||||
def test_schema() -> None:
|
||||
schema = schema_validate(
|
||||
{
|
||||
"title": "reference list",
|
||||
"description": "schema for list of reference type",
|
||||
"allOf": [{"$ref": "#/definitions/TestType"}],
|
||||
}
|
||||
)
|
||||
logging.debug(f"schema.allOf={schema.allOf}")
|
||||
assert schema.allOf
|
||||
assert isinstance(schema.allOf, list)
|
||||
assert isinstance(schema.allOf[0], Reference)
|
||||
assert schema.allOf[0].ref == "#/definitions/TestType"
|
||||
|
||||
|
||||
def test_additional_properties_is_bool() -> None:
|
||||
class TestModel(BaseModel):
|
||||
test_field: str
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
if PYDANTIC_V2:
|
||||
_key_map, schema_definition = models_json_schema([(TestModel, "validation")])
|
||||
else:
|
||||
schema_definition = v1_schema([TestModel])
|
||||
|
||||
assert schema_definition == {
|
||||
DEFS_KEY: {
|
||||
"TestModel": {
|
||||
"title": "TestModel",
|
||||
"type": "object",
|
||||
"properties": {"test_field": {"title": "Test Field", "type": "string"}},
|
||||
"required": ["test_field"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# allow "additionalProperties" to have boolean value
|
||||
result = schema_validate(schema_definition[DEFS_KEY]["TestModel"])
|
||||
assert result.additionalProperties is False
|
46
tests/schema_classes/test_security_scheme.py
Normal file
46
tests/schema_classes/test_security_scheme.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from openapi_pydantic import SecurityScheme
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
|
||||
|
||||
def test_oidc_parsing() -> None:
|
||||
security_scheme_1 = SecurityScheme(
|
||||
type="openIdConnect", openIdConnectUrl="https://example.com/openIdConnect"
|
||||
)
|
||||
assert isinstance(security_scheme_1.openIdConnectUrl, str)
|
||||
dump1 = getattr(security_scheme_1, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
if PYDANTIC_V2:
|
||||
assert dump1(by_alias=True, exclude_none=True) == (
|
||||
'{"type":"openIdConnect","openIdConnectUrl":"https://example.com/openIdConnect"}'
|
||||
)
|
||||
else:
|
||||
assert dump1(by_alias=True, exclude_none=True) == (
|
||||
'{"type": "openIdConnect", "openIdConnectUrl": "https://example.com/openIdConnect"}'
|
||||
)
|
||||
|
||||
security_scheme_2 = SecurityScheme(
|
||||
type="openIdConnect", openIdConnectUrl="/openIdConnect"
|
||||
)
|
||||
assert isinstance(security_scheme_2.openIdConnectUrl, str)
|
||||
dump2 = getattr(security_scheme_2, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
if PYDANTIC_V2:
|
||||
assert dump2(by_alias=True, exclude_none=True) == (
|
||||
'{"type":"openIdConnect","openIdConnectUrl":"/openIdConnect"}'
|
||||
)
|
||||
else:
|
||||
assert dump2(by_alias=True, exclude_none=True) == (
|
||||
'{"type": "openIdConnect", "openIdConnectUrl": "/openIdConnect"}'
|
||||
)
|
||||
|
||||
security_scheme_3 = SecurityScheme(
|
||||
type="openIdConnect", openIdConnectUrl="openIdConnect"
|
||||
)
|
||||
assert isinstance(security_scheme_3.openIdConnectUrl, str)
|
||||
dump3 = getattr(security_scheme_3, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
if PYDANTIC_V2:
|
||||
assert dump3(by_alias=True, exclude_none=True) == (
|
||||
'{"type":"openIdConnect","openIdConnectUrl":"openIdConnect"}'
|
||||
)
|
||||
else:
|
||||
assert dump3(by_alias=True, exclude_none=True) == (
|
||||
'{"type": "openIdConnect", "openIdConnectUrl": "openIdConnect"}'
|
||||
)
|
78
tests/test_alias.py
Normal file
78
tests/test_alias.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from typing import Callable
|
||||
|
||||
from openapi_pydantic import (
|
||||
MediaType,
|
||||
Parameter,
|
||||
PathItem,
|
||||
Reference,
|
||||
Schema,
|
||||
SecurityScheme,
|
||||
)
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
|
||||
validate_func_name = "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
|
||||
|
||||
def test_media_type_alias() -> None:
|
||||
media_type_1 = MediaType(media_type_schema=Schema())
|
||||
media_type_2 = MediaType(schema=Schema())
|
||||
model_validate: Callable[[dict], MediaType] = getattr(MediaType, validate_func_name)
|
||||
media_type_3 = model_validate({"media_type_schema": Schema()})
|
||||
media_type_4 = model_validate({"schema": Schema()})
|
||||
assert media_type_1 == media_type_2 == media_type_3 == media_type_4
|
||||
|
||||
|
||||
def test_parameter_alias() -> None:
|
||||
parameter_1 = Parameter( # type: ignore
|
||||
name="test",
|
||||
param_in="path",
|
||||
param_schema=Schema(),
|
||||
)
|
||||
parameter_2 = Parameter( # type: ignore
|
||||
name="test",
|
||||
param_in="path",
|
||||
schema=Schema(),
|
||||
)
|
||||
model_validate: Callable[[dict], Parameter] = getattr(Parameter, validate_func_name)
|
||||
parameter_3 = model_validate(
|
||||
{"name": "test", "param_in": "path", "param_schema": Schema()}
|
||||
)
|
||||
parameter_4 = model_validate({"name": "test", "in": "path", "schema": Schema()})
|
||||
assert parameter_1 == parameter_2 == parameter_3 == parameter_4
|
||||
|
||||
|
||||
def test_path_item_alias() -> None:
|
||||
path_item_1 = PathItem(ref="#/dummy")
|
||||
model_validate: Callable[[dict], PathItem] = getattr(PathItem, validate_func_name)
|
||||
path_item_2 = model_validate({"ref": "#/dummy"})
|
||||
path_item_3 = model_validate({"$ref": "#/dummy"})
|
||||
assert path_item_1 == path_item_2 == path_item_3
|
||||
|
||||
|
||||
def test_reference_alias() -> None:
|
||||
reference_1 = Reference(ref="#/dummy") # type: ignore
|
||||
reference_2 = Reference(**{"$ref": "#/dummy"})
|
||||
model_validate: Callable[[dict], Reference] = getattr(Reference, validate_func_name)
|
||||
reference_3 = model_validate({"ref": "#/dummy"})
|
||||
reference_4 = model_validate({"$ref": "#/dummy"})
|
||||
assert reference_1 == reference_2 == reference_3 == reference_4
|
||||
|
||||
|
||||
def test_security_scheme() -> None:
|
||||
security_scheme_1 = SecurityScheme(type="apiKey", security_scheme_in="header")
|
||||
model_validate: Callable[[dict], SecurityScheme] = getattr(
|
||||
SecurityScheme, validate_func_name
|
||||
)
|
||||
security_scheme_2 = model_validate(
|
||||
{"type": "apiKey", "security_scheme_in": "header"}
|
||||
)
|
||||
security_scheme_3 = model_validate({"type": "apiKey", "in": "header"})
|
||||
assert security_scheme_1 == security_scheme_2 == security_scheme_3
|
||||
|
||||
|
||||
def test_schema() -> None:
|
||||
schema_1 = Schema(schema_not=Schema(), schema_format="email")
|
||||
model_validate: Callable[[dict], Schema] = getattr(Schema, validate_func_name)
|
||||
schema_2 = model_validate({"schema_not": Schema(), "schema_format": "email"})
|
||||
schema_3 = model_validate({"not": Schema(), "format": "email"})
|
||||
assert schema_1 == schema_2 == schema_3
|
95
tests/test_config_example.py
Normal file
95
tests/test_config_example.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
from typing import Any
|
||||
|
||||
from openapi_pydantic import (
|
||||
XML,
|
||||
Callback,
|
||||
Components,
|
||||
Contact,
|
||||
Discriminator,
|
||||
Encoding,
|
||||
Example,
|
||||
ExternalDocumentation,
|
||||
Header,
|
||||
Info,
|
||||
License,
|
||||
Link,
|
||||
MediaType,
|
||||
OAuthFlow,
|
||||
OAuthFlows,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
Parameter,
|
||||
PathItem,
|
||||
Paths,
|
||||
Reference,
|
||||
RequestBody,
|
||||
Response,
|
||||
Responses,
|
||||
Schema,
|
||||
SecurityRequirement,
|
||||
SecurityScheme,
|
||||
Server,
|
||||
ServerVariable,
|
||||
Tag,
|
||||
)
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
|
||||
|
||||
def test_config_example() -> None:
|
||||
all_types = [
|
||||
OpenAPI,
|
||||
Info,
|
||||
Contact,
|
||||
License,
|
||||
Server,
|
||||
ServerVariable,
|
||||
Components,
|
||||
Paths,
|
||||
PathItem,
|
||||
Operation,
|
||||
ExternalDocumentation,
|
||||
Parameter,
|
||||
RequestBody,
|
||||
MediaType,
|
||||
Encoding,
|
||||
Responses,
|
||||
Response,
|
||||
Callback,
|
||||
Example,
|
||||
Link,
|
||||
Header,
|
||||
Tag,
|
||||
Reference,
|
||||
Schema,
|
||||
Discriminator,
|
||||
XML,
|
||||
SecurityScheme,
|
||||
OAuthFlows,
|
||||
OAuthFlow,
|
||||
SecurityRequirement,
|
||||
]
|
||||
for schema_type in all_types:
|
||||
_assert_config_examples(schema_type)
|
||||
|
||||
|
||||
def _assert_config_examples(schema_type: Any) -> None:
|
||||
if PYDANTIC_V2:
|
||||
if not hasattr(schema_type, "model_config"):
|
||||
return
|
||||
extra = schema_type.model_config.get("json_schema_extra")
|
||||
if extra is not None:
|
||||
examples = extra["examples"]
|
||||
if examples is None:
|
||||
breakpoint()
|
||||
for example_dict in examples:
|
||||
obj = schema_type.model_validate(example_dict)
|
||||
assert obj.model_fields_set
|
||||
|
||||
else:
|
||||
Config = getattr(schema_type, "Config", None)
|
||||
schema_extra = getattr(Config, "schema_extra", None)
|
||||
if schema_extra is not None:
|
||||
examples = schema_extra["examples"]
|
||||
for example_dict in examples:
|
||||
obj = schema_type(**example_dict)
|
||||
assert obj.__fields_set__
|
67
tests/test_example.py
Normal file
67
tests/test_example.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from openapi_pydantic import Info, OpenAPI, Operation, PathItem, Response
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
|
||||
|
||||
def test_readme_example() -> None:
|
||||
open_api_1 = readme_example_1()
|
||||
assert open_api_1
|
||||
dump_json = getattr(open_api_1, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
open_api_json_1 = dump_json(by_alias=True, exclude_none=True, indent=2)
|
||||
logging.debug(open_api_json_1)
|
||||
assert open_api_json_1
|
||||
|
||||
open_api_2 = readme_example_2()
|
||||
assert open_api_1 == open_api_2
|
||||
|
||||
open_api_3 = readme_example_3()
|
||||
assert open_api_1 == open_api_3
|
||||
|
||||
|
||||
def readme_example_1() -> OpenAPI:
|
||||
"""Construct OpenAPI using data class"""
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
get=Operation(responses={"200": Response(description="pong")})
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def readme_example_2() -> OpenAPI:
|
||||
"""Construct OpenAPI from raw data object"""
|
||||
openapi_validate: Callable[[dict], OpenAPI] = getattr(
|
||||
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
return openapi_validate(
|
||||
{
|
||||
"info": {"title": "My own API", "version": "v0.0.1"},
|
||||
"paths": {
|
||||
"/ping": {"get": {"responses": {"200": {"description": "pong"}}}}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def readme_example_3() -> OpenAPI:
|
||||
"""Construct OpenAPI from mixed object"""
|
||||
openapi_validate: Callable[[dict], OpenAPI] = getattr(
|
||||
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
return openapi_validate(
|
||||
{
|
||||
"info": {"title": "My own API", "version": "v0.0.1"},
|
||||
"paths": {
|
||||
"/ping": PathItem(
|
||||
get={"responses": {"200": Response(description="pong")}}
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
81
tests/test_openapi.py
Normal file
81
tests/test_openapi.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.v3 import v3_0, v3_1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["3.0.4", "3.1.1"])
|
||||
def test_parse_with_callback(version: str) -> None:
|
||||
data = {
|
||||
"openapi": version,
|
||||
"info": {"title": "API with Callback", "version": ""},
|
||||
"paths": {
|
||||
"/create": {
|
||||
"post": {
|
||||
"responses": {"200": {"description": "Success"}},
|
||||
"callbacks": {
|
||||
"event": {
|
||||
"callback": {
|
||||
"post": {
|
||||
"responses": {"200": {"description": "Success"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if version == "3.0.4":
|
||||
model_validate_3_0: Callable[[dict], v3_0.OpenAPI] = getattr(
|
||||
v3_0.OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
assert model_validate_3_0(data) == v3_0.OpenAPI(
|
||||
info=v3_0.Info(title="API with Callback", version=""),
|
||||
paths={
|
||||
"/create": v3_0.PathItem(
|
||||
post=v3_0.Operation(
|
||||
responses={"200": v3_0.Response(description="Success")},
|
||||
callbacks={
|
||||
"event": {
|
||||
"callback": v3_0.PathItem(
|
||||
post=v3_0.Operation(
|
||||
responses={
|
||||
"200": v3_0.Response(description="Success")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
else:
|
||||
model_validate_3_1: Callable[[dict], v3_1.OpenAPI] = getattr(
|
||||
v3_1.OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
assert model_validate_3_1(data) == v3_1.OpenAPI(
|
||||
info=v3_1.Info(title="API with Callback", version=""),
|
||||
paths={
|
||||
"/create": v3_1.PathItem(
|
||||
post=v3_1.Operation(
|
||||
responses={"200": v3_1.Response(description="Success")},
|
||||
callbacks={
|
||||
"event": {
|
||||
"callback": v3_1.PathItem(
|
||||
post=v3_1.Operation(
|
||||
responses={
|
||||
"200": v3_1.Response(description="Success")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
42
tests/test_parse.py
Normal file
42
tests/test_parse.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from openapi_pydantic import parse_obj
|
||||
from openapi_pydantic.v3 import v3_0, v3_1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["3.0.4", "3.0.3", "3.0.2", "3.0.1", "3.0.0"])
|
||||
def test_parse_obj_3_0(
|
||||
version: Literal["3.0.4", "3.0.3", "3.0.2", "3.0.1", "3.0.0"]
|
||||
) -> None:
|
||||
result = parse_obj(
|
||||
{
|
||||
"openapi": version,
|
||||
"info": {"title": "foo", "version": "0.1.0"},
|
||||
"paths": {"/": {}},
|
||||
}
|
||||
)
|
||||
|
||||
assert result == v3_0.OpenAPI(
|
||||
openapi=version,
|
||||
info=v3_0.Info(title="foo", version="0.1.0"),
|
||||
paths={"/": v3_0.PathItem()},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["3.1.1", "3.1.0"])
|
||||
def test_parse_obj_3_1(version: Literal["3.1.1", "3.1.0"]) -> None:
|
||||
result = parse_obj(
|
||||
{
|
||||
"openapi": version,
|
||||
"info": {"title": "foo", "version": "0.1.0"},
|
||||
"paths": {"/": {}},
|
||||
}
|
||||
)
|
||||
|
||||
assert result == v3_1.OpenAPI(
|
||||
openapi=version,
|
||||
info=v3_1.Info(title="foo", version="0.1.0"),
|
||||
paths={"/": v3_1.PathItem()},
|
||||
)
|
47
tests/test_swagger_openapi_v3.py
Normal file
47
tests/test_swagger_openapi_v3.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict
|
||||
from openapi_pydantic.v3.v3_0 import OpenAPI, Operation, PathItem
|
||||
|
||||
|
||||
def test_swagger_openapi_v3() -> None:
|
||||
with open("tests/data/swagger_openapi_v3.0.1.json") as f:
|
||||
if PYDANTIC_V2:
|
||||
validate = getattr(ExtendedOpenAPI, "model_validate_json")
|
||||
else:
|
||||
validate = getattr(ExtendedOpenAPI, "parse_raw")
|
||||
open_api = validate(f.read())
|
||||
assert open_api
|
||||
|
||||
|
||||
class ExtendedOperation(Operation):
|
||||
"""Override classes to use "x-codegen-request-body-name" in Operation"""
|
||||
|
||||
xCodegenRequestBodyName: Optional[str] = Field(
|
||||
default=None, alias="x-codegen-request-body-name"
|
||||
)
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class ExtendedPathItem(PathItem):
|
||||
get: Optional[ExtendedOperation] = None
|
||||
put: Optional[ExtendedOperation] = None
|
||||
post: Optional[ExtendedOperation] = None
|
||||
delete: Optional[ExtendedOperation] = None
|
||||
options: Optional[ExtendedOperation] = None
|
||||
head: Optional[ExtendedOperation] = None
|
||||
patch: Optional[ExtendedOperation] = None
|
||||
trace: Optional[ExtendedOperation] = None
|
||||
|
||||
|
||||
class ExtendedOpenAPI(OpenAPI):
|
||||
paths: Dict[str, ExtendedPathItem] # type: ignore[assignment]
|
0
tests/util/__init__.py
Normal file
0
tests/util/__init__.py
Normal file
114
tests/util/test_optional_and_computed.py
Normal file
114
tests/util/test_optional_and_computed.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
# mypy: ignore-errors
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from openapi_pydantic import (
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
|
||||
|
||||
|
||||
@pytest.mark.skipif(not PYDANTIC_V2, reason="computed fields require Pydantic V2")
|
||||
def test_optional_and_computed_fields() -> None:
|
||||
api = construct_sample_api()
|
||||
|
||||
result = construct_open_api_with_schema_class(api)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
|
||||
req_schema = result.components.schemas["SampleRequest"]
|
||||
assert isinstance(req_schema, Schema)
|
||||
assert req_schema.properties is not None
|
||||
assert req_schema.required is not None
|
||||
|
||||
resp_schema = result.components.schemas["SampleResponse"]
|
||||
assert isinstance(resp_schema, Schema)
|
||||
assert resp_schema.properties is not None
|
||||
assert resp_schema.required is not None
|
||||
|
||||
# When validating:
|
||||
# - required fields are still required
|
||||
# - optional fields are still optional
|
||||
# - computed fields don't exist
|
||||
assert "req" in req_schema.properties
|
||||
assert "opt" in req_schema.properties
|
||||
assert "comp" not in req_schema.properties
|
||||
assert set(req_schema.required) == {"req"}
|
||||
|
||||
# When serializing:
|
||||
# - required fields are still required
|
||||
# - optional fields are still optional
|
||||
# (except when json_schema_serialization_defaults_required is enabled)
|
||||
# - computed fields are required
|
||||
assert "req" in resp_schema.properties
|
||||
assert "opt" in resp_schema.properties
|
||||
assert "comp" in resp_schema.properties
|
||||
assert set(resp_schema.required) == {"req", "comp"}
|
||||
|
||||
|
||||
def construct_sample_api() -> OpenAPI:
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def computed_field(x: Callable) -> Callable: ...
|
||||
|
||||
else:
|
||||
from pydantic import computed_field
|
||||
|
||||
class SampleModel(BaseModel):
|
||||
req: bool
|
||||
opt: Optional[bool] = None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def comp(self) -> bool:
|
||||
return True
|
||||
|
||||
class SampleRequest(SampleModel):
|
||||
model_config = {"json_schema_mode": "validation"}
|
||||
|
||||
class SampleResponse(SampleModel):
|
||||
model_config = {"json_schema_mode": "serialization"}
|
||||
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="Sample API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/callme": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleRequest)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="resp",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleResponse)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
163
tests/util/test_pydantic_field.py
Normal file
163
tests/util/test_pydantic_field.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
from typing import Dict, List, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openapi_pydantic import (
|
||||
Discriminator,
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
Reference,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.compat import (
|
||||
DEFS_KEY,
|
||||
PYDANTIC_MINOR_VERSION,
|
||||
PYDANTIC_V2,
|
||||
models_json_schema,
|
||||
v1_schema,
|
||||
)
|
||||
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
|
||||
|
||||
|
||||
class DataAModel(BaseModel):
|
||||
kind: Literal["a"]
|
||||
|
||||
|
||||
class DataBModel(BaseModel):
|
||||
kind: Literal["b"]
|
||||
|
||||
|
||||
class RequestModel(BaseModel):
|
||||
data: Union[DataAModel, DataBModel] = Field(discriminator="kind")
|
||||
|
||||
|
||||
def construct_base_open_api() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=RequestModel
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={"200": Response(description="pong")},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_pydantic_discriminator_schema_generation() -> None:
|
||||
"""https://github.com/kuimono/openapi-schema-pydantic/issues/8"""
|
||||
|
||||
a_kind: Dict[str, Union[str, List[str]]] = {"title": "Kind", "type": "string"}
|
||||
b_kind: Dict[str, Union[str, List[str]]] = {"title": "Kind", "type": "string"}
|
||||
|
||||
if PYDANTIC_V2:
|
||||
_key_map, json_schema = models_json_schema([(RequestModel, "validation")])
|
||||
# In Pydantic v2, string literal types are mapped to consts.
|
||||
a_kind["const"] = "a"
|
||||
b_kind["const"] = "b"
|
||||
if PYDANTIC_MINOR_VERSION < 10:
|
||||
# Prior to 2.10, string literal types are also mapped to enums with a single entry.
|
||||
a_kind["enum"] = ["a"]
|
||||
b_kind["enum"] = ["b"]
|
||||
else:
|
||||
json_schema = v1_schema([RequestModel])
|
||||
# In Pydantic v1, string literal types are mapped to enums with a single entry.
|
||||
a_kind["enum"] = ["a"]
|
||||
b_kind["enum"] = ["b"]
|
||||
|
||||
assert json_schema == {
|
||||
DEFS_KEY: {
|
||||
"DataAModel": {
|
||||
"properties": {
|
||||
"kind": a_kind,
|
||||
},
|
||||
"required": ["kind"],
|
||||
"title": "DataAModel",
|
||||
"type": "object",
|
||||
},
|
||||
"DataBModel": {
|
||||
"properties": {
|
||||
"kind": b_kind,
|
||||
},
|
||||
"required": ["kind"],
|
||||
"title": "DataBModel",
|
||||
"type": "object",
|
||||
},
|
||||
"RequestModel": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"oneOf": [
|
||||
{"$ref": f"#/{DEFS_KEY}/DataAModel"},
|
||||
{"$ref": f"#/{DEFS_KEY}/DataBModel"},
|
||||
],
|
||||
"discriminator": {
|
||||
"mapping": {
|
||||
"a": f"#/{DEFS_KEY}/DataAModel",
|
||||
"b": f"#/{DEFS_KEY}/DataBModel",
|
||||
},
|
||||
"propertyName": "kind",
|
||||
},
|
||||
"title": "Data",
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
"title": "RequestModel",
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_pydantic_discriminator_openapi_generation() -> None:
|
||||
"""https://github.com/kuimono/openapi-schema-pydantic/issues/8"""
|
||||
|
||||
open_api = construct_open_api_with_schema_class(construct_base_open_api())
|
||||
assert open_api.components is not None
|
||||
assert open_api.components.schemas is not None
|
||||
json_schema = open_api.components.schemas["RequestModel"]
|
||||
assert json_schema.properties == {
|
||||
"data": Schema(
|
||||
oneOf=[
|
||||
Reference(
|
||||
**{
|
||||
"$ref": "#/components/schemas/DataAModel",
|
||||
"summary": None,
|
||||
"description": None,
|
||||
}
|
||||
),
|
||||
Reference(
|
||||
**{
|
||||
"$ref": "#/components/schemas/DataBModel",
|
||||
"summary": None,
|
||||
"description": None,
|
||||
}
|
||||
),
|
||||
],
|
||||
title="Data",
|
||||
discriminator=Discriminator(
|
||||
propertyName="kind",
|
||||
mapping={
|
||||
"a": "#/components/schemas/DataAModel",
|
||||
"b": "#/components/schemas/DataBModel",
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
271
tests/util/test_util.py
Normal file
271
tests/util/test_util.py
Normal file
|
@ -0,0 +1,271 @@
|
|||
import logging
|
||||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openapi_pydantic import (
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
Reference,
|
||||
RequestBody,
|
||||
Response,
|
||||
)
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_1() -> None:
|
||||
open_api = construct_base_open_api_1()
|
||||
result_open_api_1 = construct_open_api_with_schema_class(open_api)
|
||||
result_open_api_2 = construct_open_api_with_schema_class(
|
||||
open_api, [PingRequest, PingResponse]
|
||||
)
|
||||
assert result_open_api_1.components == result_open_api_2.components
|
||||
assert result_open_api_1 == result_open_api_2
|
||||
|
||||
dump_json = getattr(result_open_api_1, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
open_api_json = dump_json(by_alias=True, exclude_none=True, indent=2)
|
||||
logging.debug(open_api_json)
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_2() -> None:
|
||||
open_api_1 = construct_base_open_api_1()
|
||||
open_api_2 = construct_base_open_api_2()
|
||||
result_open_api_1 = construct_open_api_with_schema_class(open_api_1)
|
||||
result_open_api_2 = construct_open_api_with_schema_class(
|
||||
open_api_2, [PingRequest, PingResponse]
|
||||
)
|
||||
assert result_open_api_1 == result_open_api_2
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_3() -> None:
|
||||
open_api_3 = construct_base_open_api_3()
|
||||
|
||||
result_with_alias_1 = construct_open_api_with_schema_class(open_api_3)
|
||||
assert result_with_alias_1.components is not None
|
||||
assert result_with_alias_1.components.schemas is not None
|
||||
schema_with_alias = result_with_alias_1.components.schemas["PongResponse"]
|
||||
assert schema_with_alias.properties is not None
|
||||
assert "pong_foo" in schema_with_alias.properties
|
||||
assert "pong_bar" in schema_with_alias.properties
|
||||
|
||||
result_with_alias_2 = construct_open_api_with_schema_class(
|
||||
open_api_3, by_alias=True
|
||||
)
|
||||
assert result_with_alias_1 == result_with_alias_2
|
||||
|
||||
result_without_alias = construct_open_api_with_schema_class(
|
||||
open_api_3, by_alias=False
|
||||
)
|
||||
assert result_without_alias.components is not None
|
||||
assert result_without_alias.components.schemas is not None
|
||||
schema_without_alias = result_without_alias.components.schemas["PongResponse"]
|
||||
assert schema_without_alias.properties is not None
|
||||
assert "resp_foo" in schema_without_alias.properties
|
||||
assert "resp_bar" in schema_without_alias.properties
|
||||
|
||||
|
||||
@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1")
|
||||
def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None:
|
||||
DataT = TypeVar("DataT")
|
||||
from pydantic.v1.generics import GenericModel
|
||||
|
||||
class GenericResponse(GenericModel, Generic[DataT]):
|
||||
msg: str = Field(description="message of the generic response")
|
||||
data: DataT = Field(description="data value of the generic response")
|
||||
|
||||
open_api_4 = construct_base_open_api_4_generic_response(
|
||||
GenericResponse[PongResponse]
|
||||
)
|
||||
|
||||
result = construct_open_api_with_schema_class(open_api_4)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
assert "GenericResponse_PongResponse_" in result.components.schemas
|
||||
|
||||
|
||||
@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2")
|
||||
def test_construct_open_api_with_schema_class_4_generic_response_v2() -> None:
|
||||
DataT = TypeVar("DataT")
|
||||
|
||||
class GenericResponse(BaseModel, Generic[DataT]):
|
||||
msg: str = Field(description="message of the generic response")
|
||||
data: DataT = Field(description="data value of the generic response")
|
||||
|
||||
open_api_4 = construct_base_open_api_4_generic_response(
|
||||
GenericResponse[PongResponse]
|
||||
)
|
||||
|
||||
result = construct_open_api_with_schema_class(open_api_4)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
assert "GenericResponse_PongResponse_" in result.components.schemas
|
||||
|
||||
|
||||
def construct_base_open_api_1() -> OpenAPI:
|
||||
model_validate: Callable[[dict], OpenAPI] = getattr(
|
||||
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
return model_validate(
|
||||
{
|
||||
"info": {"title": "My own API", "version": "v0.0.1"},
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": PydanticSchema(schema_class=PingRequest)
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "pong",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": PydanticSchema(
|
||||
schema_class=PingResponse
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_2() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=Reference(
|
||||
**{"$ref": "#/components/schemas/PingRequest"}
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=Reference(
|
||||
**{"$ref": "#/components/schemas/PingResponse"}
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_3() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PingRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PongResponse
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PingRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=response_schema
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class PingRequest(BaseModel):
|
||||
"""Ping Request"""
|
||||
|
||||
req_foo: str = Field(description="foo value of the request")
|
||||
req_bar: str = Field(description="bar value of the request")
|
||||
|
||||
|
||||
class PingResponse(BaseModel):
|
||||
"""Ping response"""
|
||||
|
||||
resp_foo: str = Field(description="foo value of the response")
|
||||
resp_bar: str = Field(description="bar value of the response")
|
||||
|
||||
|
||||
class PongResponse(BaseModel):
|
||||
"""Pong response"""
|
||||
|
||||
resp_foo: str = Field(alias="pong_foo", description="foo value of the response")
|
||||
resp_bar: str = Field(alias="pong_bar", description="bar value of the response")
|
117
tests/util/test_validated_schema.py
Normal file
117
tests/util/test_validated_schema.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# mypy: ignore-errors
|
||||
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from openapi_spec_validator import validate
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openapi_pydantic import (
|
||||
DataType,
|
||||
Header,
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated, Literal
|
||||
else:
|
||||
from typing import Annotated, Literal
|
||||
|
||||
|
||||
def test_basic_schema() -> None:
|
||||
class SampleModel(BaseModel):
|
||||
required: bool
|
||||
optional: Optional[bool] = None
|
||||
one_literal_choice: Literal["only_choice"]
|
||||
multiple_literal_choices: Literal["choice1", "choice2"]
|
||||
|
||||
part_api = construct_sample_api(SampleModel)
|
||||
|
||||
api = construct_open_api_with_schema_class(part_api)
|
||||
assert api.components is not None
|
||||
assert api.components.schemas is not None
|
||||
|
||||
if PYDANTIC_V2:
|
||||
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
else:
|
||||
json_api: Any = api.dict(by_alias=True, exclude_none=True)
|
||||
validate(json_api)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not PYDANTIC_V2,
|
||||
reason="Field-level JSON examples not supported in Pydantic V1",
|
||||
)
|
||||
def test_field_json_schema_example() -> None:
|
||||
from pydantic import WithJsonSchema
|
||||
|
||||
Thing = Annotated[str, WithJsonSchema({"examples": ["thing1"]})]
|
||||
|
||||
class SampleModel(BaseModel):
|
||||
a: Thing
|
||||
|
||||
part_api = construct_sample_api(SampleModel)
|
||||
|
||||
api = construct_open_api_with_schema_class(part_api)
|
||||
assert api.components is not None
|
||||
assert api.components.schemas is not None
|
||||
|
||||
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
props = json_api["components"]["schemas"]["SampleRequest"]["properties"]
|
||||
assert props["a"]["examples"] == ["thing1"]
|
||||
|
||||
validate(json_api)
|
||||
|
||||
|
||||
def construct_sample_api(SampleModel) -> OpenAPI:
|
||||
class SampleRequest(SampleModel):
|
||||
model_config = {"json_schema_mode": "validation"}
|
||||
|
||||
class SampleResponse(SampleModel):
|
||||
model_config = {"json_schema_mode": "serialization"}
|
||||
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="Sample API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/callme": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleRequest)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="resp",
|
||||
headers={
|
||||
"WWW-Authenticate": Header(
|
||||
description="Indicate how to authenticate",
|
||||
schema=Schema(type=DataType.STRING),
|
||||
)
|
||||
},
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleResponse)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
0
tests/v3_0/__init__.py
Normal file
0
tests/v3_0/__init__.py
Normal file
93
tests/v3_0/test_config_example.py
Normal file
93
tests/v3_0/test_config_example.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
from typing import Any
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.v3.v3_0 import (
|
||||
XML,
|
||||
Callback,
|
||||
Components,
|
||||
Contact,
|
||||
Discriminator,
|
||||
Encoding,
|
||||
Example,
|
||||
ExternalDocumentation,
|
||||
Header,
|
||||
Info,
|
||||
License,
|
||||
Link,
|
||||
MediaType,
|
||||
OAuthFlow,
|
||||
OAuthFlows,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
Parameter,
|
||||
PathItem,
|
||||
Paths,
|
||||
Reference,
|
||||
RequestBody,
|
||||
Response,
|
||||
Responses,
|
||||
Schema,
|
||||
SecurityRequirement,
|
||||
SecurityScheme,
|
||||
Server,
|
||||
ServerVariable,
|
||||
Tag,
|
||||
)
|
||||
|
||||
|
||||
def test_config_example() -> None:
|
||||
all_types = [
|
||||
OpenAPI,
|
||||
Info,
|
||||
Contact,
|
||||
License,
|
||||
Server,
|
||||
ServerVariable,
|
||||
Components,
|
||||
Paths,
|
||||
PathItem,
|
||||
Operation,
|
||||
ExternalDocumentation,
|
||||
Parameter,
|
||||
RequestBody,
|
||||
MediaType,
|
||||
Encoding,
|
||||
Responses,
|
||||
Response,
|
||||
Callback,
|
||||
Example,
|
||||
Link,
|
||||
Header,
|
||||
Tag,
|
||||
Reference,
|
||||
Schema,
|
||||
Discriminator,
|
||||
XML,
|
||||
SecurityScheme,
|
||||
OAuthFlows,
|
||||
OAuthFlow,
|
||||
SecurityRequirement,
|
||||
]
|
||||
for schema_type in all_types:
|
||||
_assert_config_examples(schema_type)
|
||||
|
||||
|
||||
def _assert_config_examples(schema_type: Any) -> None:
|
||||
if PYDANTIC_V2:
|
||||
if not hasattr(schema_type, "model_config"):
|
||||
return
|
||||
extra = schema_type.model_config.get("json_schema_extra")
|
||||
if extra is not None:
|
||||
examples = extra["examples"]
|
||||
for example_dict in examples:
|
||||
obj = schema_type.model_validate(example_dict)
|
||||
assert obj.model_fields_set
|
||||
|
||||
else:
|
||||
Config = getattr(schema_type, "Config", None)
|
||||
schema_extra = getattr(Config, "schema_extra", None)
|
||||
if schema_extra is not None:
|
||||
examples = schema_extra["examples"]
|
||||
for example_dict in examples:
|
||||
obj = schema_type(**example_dict)
|
||||
assert obj.__fields_set__
|
34
tests/v3_0/test_datatype.py
Normal file
34
tests/v3_0/test_datatype.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openapi_pydantic.v3.v3_0 import Schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"datatype",
|
||||
(
|
||||
"string",
|
||||
"number",
|
||||
"integer",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
),
|
||||
)
|
||||
def test_good_types_parse_and_equate(datatype: str) -> None:
|
||||
assert Schema(type=datatype).type == datatype
|
||||
|
||||
|
||||
def test_bad_types_raise_validation_errors() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(type="invalid")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(anyOf=[{"type": "invalid"}])
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(
|
||||
properties={
|
||||
"a": Schema(type="invalid"),
|
||||
},
|
||||
)
|
117
tests/v3_0/test_optional_and_computed.py
Normal file
117
tests/v3_0/test_optional_and_computed.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# mypy: ignore-errors
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.v3.v3_0 import (
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.v3.v3_0.util import (
|
||||
PydanticSchema,
|
||||
construct_open_api_with_schema_class,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not PYDANTIC_V2, reason="computed fields require Pydantic V2")
|
||||
def test_optional_and_computed_fields() -> None:
|
||||
api = construct_sample_api()
|
||||
|
||||
result = construct_open_api_with_schema_class(api)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
|
||||
req_schema = result.components.schemas["SampleRequest"]
|
||||
assert isinstance(req_schema, Schema)
|
||||
assert req_schema.properties is not None
|
||||
assert req_schema.required is not None
|
||||
|
||||
resp_schema = result.components.schemas["SampleResponse"]
|
||||
assert isinstance(resp_schema, Schema)
|
||||
assert resp_schema.properties is not None
|
||||
assert resp_schema.required is not None
|
||||
|
||||
# When validating:
|
||||
# - required fields are still required
|
||||
# - optional fields are still optional
|
||||
# - computed fields don't exist
|
||||
assert "req" in req_schema.properties
|
||||
assert "opt" in req_schema.properties
|
||||
assert "comp" not in req_schema.properties
|
||||
assert set(req_schema.required) == {"req"}
|
||||
|
||||
# When serializing:
|
||||
# - required fields are still required
|
||||
# - optional fields are still optional
|
||||
# (except when json_schema_serialization_defaults_required is enabled)
|
||||
# - computed fields are required
|
||||
assert "req" in resp_schema.properties
|
||||
assert "opt" in resp_schema.properties
|
||||
assert "comp" in resp_schema.properties
|
||||
assert set(resp_schema.required) == {"req", "comp"}
|
||||
|
||||
|
||||
def construct_sample_api() -> OpenAPI:
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def computed_field(x: Callable) -> Callable: ...
|
||||
|
||||
else:
|
||||
from pydantic import computed_field
|
||||
|
||||
class SampleModel(BaseModel):
|
||||
req: bool
|
||||
opt: Optional[bool] = None
|
||||
|
||||
@computed_field # type: ignore
|
||||
@property
|
||||
def comp(self) -> bool:
|
||||
return True
|
||||
|
||||
class SampleRequest(SampleModel):
|
||||
model_config = {"json_schema_mode": "validation"}
|
||||
|
||||
class SampleResponse(SampleModel):
|
||||
model_config = {"json_schema_mode": "serialization"}
|
||||
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="Sample API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/callme": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleRequest)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="resp",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleResponse)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
330
tests/v3_0/test_util.py
Normal file
330
tests/v3_0/test_util.py
Normal file
|
@ -0,0 +1,330 @@
|
|||
import logging
|
||||
from typing import Callable, Generic, Literal, TypeVar
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.v3.v3_0 import (
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
Reference,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.v3.v3_0.util import (
|
||||
PydanticSchema,
|
||||
construct_open_api_with_schema_class,
|
||||
)
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_1() -> None:
|
||||
open_api = construct_base_open_api_1()
|
||||
result_open_api_1 = construct_open_api_with_schema_class(open_api)
|
||||
result_open_api_2 = construct_open_api_with_schema_class(
|
||||
open_api, [PingRequest, PingResponse]
|
||||
)
|
||||
assert result_open_api_1.components == result_open_api_2.components
|
||||
assert result_open_api_1 == result_open_api_2
|
||||
|
||||
dump_json = getattr(result_open_api_1, "model_dump_json" if PYDANTIC_V2 else "json")
|
||||
open_api_json = dump_json(by_alias=True, exclude_none=True, indent=2)
|
||||
logging.debug(open_api_json)
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_2() -> None:
|
||||
open_api_1 = construct_base_open_api_1()
|
||||
open_api_2 = construct_base_open_api_2()
|
||||
result_open_api_1 = construct_open_api_with_schema_class(open_api_1)
|
||||
result_open_api_2 = construct_open_api_with_schema_class(
|
||||
open_api_2, [PingRequest, PingResponse]
|
||||
)
|
||||
assert result_open_api_1 == result_open_api_2
|
||||
|
||||
|
||||
def test_construct_open_api_with_schema_class_3() -> None:
|
||||
open_api_3 = construct_base_open_api_3()
|
||||
|
||||
result_with_alias_1 = construct_open_api_with_schema_class(open_api_3)
|
||||
assert result_with_alias_1.components is not None
|
||||
assert result_with_alias_1.components.schemas is not None
|
||||
schema_with_alias = result_with_alias_1.components.schemas["PongResponse"]
|
||||
assert isinstance(schema_with_alias, Schema)
|
||||
assert schema_with_alias.properties is not None
|
||||
assert "pong_foo" in schema_with_alias.properties
|
||||
assert "pong_bar" in schema_with_alias.properties
|
||||
|
||||
result_with_alias_2 = construct_open_api_with_schema_class(
|
||||
open_api_3, by_alias=True
|
||||
)
|
||||
assert result_with_alias_1 == result_with_alias_2
|
||||
|
||||
result_without_alias = construct_open_api_with_schema_class(
|
||||
open_api_3, by_alias=False
|
||||
)
|
||||
assert result_without_alias.components is not None
|
||||
assert result_without_alias.components.schemas is not None
|
||||
schema_without_alias = result_without_alias.components.schemas["PongResponse"]
|
||||
assert isinstance(schema_without_alias, Schema)
|
||||
assert schema_without_alias.properties is not None
|
||||
assert "resp_foo" in schema_without_alias.properties
|
||||
assert "resp_bar" in schema_without_alias.properties
|
||||
|
||||
|
||||
@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1")
|
||||
def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None:
|
||||
DataT = TypeVar("DataT")
|
||||
from pydantic.v1.generics import GenericModel
|
||||
|
||||
class GenericResponse(GenericModel, Generic[DataT]):
|
||||
msg: str = Field(description="message of the generic response")
|
||||
data: DataT = Field(description="data value of the generic response")
|
||||
|
||||
open_api_4 = construct_base_open_api_4_generic_response(
|
||||
GenericResponse[PongResponse]
|
||||
)
|
||||
|
||||
result = construct_open_api_with_schema_class(open_api_4)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
assert "GenericResponse_PongResponse_" in result.components.schemas
|
||||
|
||||
|
||||
@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2")
|
||||
def test_construct_open_api_with_schema_class_4_generic_response() -> None:
|
||||
DataT = TypeVar("DataT")
|
||||
|
||||
class GenericResponse(BaseModel, Generic[DataT]):
|
||||
msg: str = Field(description="message of the generic response")
|
||||
data: DataT = Field(description="data value of the generic response")
|
||||
|
||||
open_api_4 = construct_base_open_api_4_generic_response(
|
||||
GenericResponse[PongResponse]
|
||||
)
|
||||
|
||||
result = construct_open_api_with_schema_class(open_api_4)
|
||||
assert result.components is not None
|
||||
assert result.components.schemas is not None
|
||||
assert "GenericResponse_PongResponse_" in result.components.schemas
|
||||
|
||||
|
||||
def construct_base_open_api_1() -> OpenAPI:
|
||||
model_validate: Callable[[dict], OpenAPI] = getattr(
|
||||
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
|
||||
)
|
||||
return model_validate(
|
||||
{
|
||||
"info": {"title": "My own API", "version": "v0.0.1"},
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": PydanticSchema(schema_class=PingRequest)
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "pong",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": PydanticSchema(
|
||||
schema_class=PingResponse
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_2() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(title="My own API", version="v0.0.1"),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=Reference(
|
||||
**{"$ref": "#/components/schemas/PingRequest"}
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=Reference(
|
||||
**{"$ref": "#/components/schemas/PingResponse"}
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_3() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PingRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PongResponse
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_3_plus() -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PingPlusRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PongResponse
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI:
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="My own API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/ping": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=PingRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="pong",
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
media_type_schema=PydanticSchema(
|
||||
schema_class=response_schema
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class PingRequest(BaseModel):
|
||||
"""Ping Request"""
|
||||
|
||||
req_foo: str = Field(description="foo value of the request")
|
||||
req_bar: str = Field(description="bar value of the request")
|
||||
|
||||
|
||||
class PingResponse(BaseModel):
|
||||
"""Ping response"""
|
||||
|
||||
resp_foo: str = Field(description="foo value of the response")
|
||||
resp_bar: str = Field(description="bar value of the response")
|
||||
|
||||
|
||||
class PongResponse(BaseModel):
|
||||
"""Pong response"""
|
||||
|
||||
resp_foo: str = Field(alias="pong_foo", description="foo value of the response")
|
||||
resp_bar: str = Field(alias="pong_bar", description="bar value of the response")
|
||||
|
||||
|
||||
class PingPlusRequest(BaseModel):
|
||||
"""Ping Request with extra"""
|
||||
|
||||
req_foo: str
|
||||
req_bar: str
|
||||
req_single_choice: Literal["one"]
|
||||
|
||||
|
||||
def test_enum_with_single_choice() -> None:
|
||||
api_obj = construct_open_api_with_schema_class(construct_base_open_api_3_plus())
|
||||
model_dump = getattr(api_obj, "model_dump" if PYDANTIC_V2 else "dict")
|
||||
api = model_dump(by_alias=True, exclude_none=True)
|
||||
schema = api["components"]["schemas"]["PingPlusRequest"]
|
||||
prop = schema["properties"]["req_single_choice"]
|
||||
# OpenAPI 3.0 does not support "const", so make sure the enum is not
|
||||
# rendered that way.
|
||||
assert not prop.get("const")
|
||||
assert prop["enum"] == ["one"]
|
97
tests/v3_0/test_validated_schema.py
Normal file
97
tests/v3_0/test_validated_schema.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
# mypy: ignore-errors
|
||||
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
from openapi_spec_validator import validate
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openapi_pydantic.compat import PYDANTIC_V2
|
||||
from openapi_pydantic.v3.v3_0 import (
|
||||
Components,
|
||||
DataType,
|
||||
Example,
|
||||
Header,
|
||||
Info,
|
||||
MediaType,
|
||||
OpenAPI,
|
||||
Operation,
|
||||
PathItem,
|
||||
RequestBody,
|
||||
Response,
|
||||
Schema,
|
||||
)
|
||||
from openapi_pydantic.v3.v3_0.util import (
|
||||
PydanticSchema,
|
||||
construct_open_api_with_schema_class,
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Literal
|
||||
else:
|
||||
from typing import Literal
|
||||
|
||||
|
||||
def test_basic_schema() -> None:
|
||||
class SampleModel(BaseModel):
|
||||
required: bool
|
||||
optional: Optional[bool] = None
|
||||
one_literal_choice: Literal["only_choice"]
|
||||
multiple_literal_choices: Literal["choice1", "choice2"]
|
||||
|
||||
part_api = construct_sample_api(SampleModel)
|
||||
|
||||
api = construct_open_api_with_schema_class(part_api)
|
||||
assert api.components is not None
|
||||
assert api.components.schemas is not None
|
||||
|
||||
if PYDANTIC_V2:
|
||||
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
else:
|
||||
json_api: Any = api.dict(by_alias=True, exclude_none=True)
|
||||
validate(json_api)
|
||||
|
||||
|
||||
def construct_sample_api(SampleModel) -> OpenAPI:
|
||||
class SampleRequest(SampleModel):
|
||||
model_config = {"json_schema_mode": "validation"}
|
||||
|
||||
class SampleResponse(SampleModel):
|
||||
model_config = {"json_schema_mode": "serialization"}
|
||||
|
||||
return OpenAPI(
|
||||
info=Info(
|
||||
title="Sample API",
|
||||
version="v0.0.1",
|
||||
),
|
||||
paths={
|
||||
"/callme": PathItem(
|
||||
post=Operation(
|
||||
requestBody=RequestBody(
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleRequest)
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
"200": Response(
|
||||
description="resp",
|
||||
headers={
|
||||
"WWW-Authenticate": Header(
|
||||
description="Indicate how to authenticate",
|
||||
schema=Schema(type=DataType.STRING),
|
||||
)
|
||||
},
|
||||
content={
|
||||
"application/json": MediaType(
|
||||
schema=PydanticSchema(schema_class=SampleResponse)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
components=Components(examples={"thing-example": Example(value="thing1")}),
|
||||
)
|
6
tests/v3_1/__init__.py
Normal file
6
tests/v3_1/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from openapi_pydantic.v3.v3_1.schema import Schema, schema_validate
|
||||
|
||||
|
||||
def test_empty_schema() -> None:
|
||||
schema = schema_validate({})
|
||||
assert schema == Schema()
|
35
tests/v3_1/test_datatype.py
Normal file
35
tests/v3_1/test_datatype.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openapi_pydantic.v3.v3_1 import Schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"datatype",
|
||||
(
|
||||
"string",
|
||||
"number",
|
||||
"integer",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
"null",
|
||||
),
|
||||
)
|
||||
def test_good_types_parse_and_equate(datatype: str) -> None:
|
||||
assert Schema(type=datatype).type == datatype
|
||||
|
||||
|
||||
def test_bad_types_raise_validation_errors() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(type="invalid")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(anyOf=[{"type": "invalid"}])
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Schema(
|
||||
properties={
|
||||
"a": Schema(type="invalid"),
|
||||
},
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue