Skip to content

Commit 56f163e

Browse files
committed
Add v3.v3_0_3.util for users who still wants to use v3.0.3 OpenAPI with PydanticSchema
1 parent 0c11579 commit 56f163e

File tree

5 files changed

+279
-0
lines changed

5 files changed

+279
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## v1.2.1 - 2021-09-10
4+
5+
### Added
6+
- Add `v3.v3_0_3.util` for users who still wants to use v3.0.3 OpenAPI with `PydanticSchema`
7+
38
## v1.2.0 - 2021-06-28
49

510
### Added

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,16 @@ Please refer to the following for more info:
277277
| 3.1.0 | [here](https://github.com/kuimono/openapi-schema-pydantic/blob/master/openapi_schema_pydantic/v3/v3_1_0/README.md#non-pydantic-schema-types) |
278278
| 3.0.3 | [here](https://github.com/kuimono/openapi-schema-pydantic/blob/master/openapi_schema_pydantic/v3/v3_0_3/README.md#non-pydantic-schema-types) |
279279

280+
### Use OpenAPI 3.0.3 instead of 3.1.0
281+
282+
Some UI renderings (e.g. Swagger) still does not support OpenAPI 3.1.0,
283+
so user may still use the 3.0.3 version by importing from different paths:
284+
285+
```python
286+
from openapi_schema_pydantic.v3.v3_0_3 import OpenAPI, ...
287+
from openapi_schema_pydantic.v3.v3_0_3.util import PydanticSchema, construct_open_api_with_schema_class
288+
```
289+
280290
## License
281291

282292
[MIT License](https://github.com/kuimono/openapi-schema-pydantic/blob/master/LICENSE)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import logging
2+
from typing import Any, List, Set, Type, TypeVar
3+
4+
from pydantic import BaseModel
5+
from pydantic.schema import schema
6+
7+
from . import Components, OpenAPI, Reference, Schema
8+
9+
PydanticType = TypeVar("PydanticType", bound=BaseModel)
10+
ref_prefix = "#/components/schemas/"
11+
12+
13+
class PydanticSchema(Schema):
14+
"""Special `Schema` class to indicate a reference from pydantic class"""
15+
16+
schema_class: Type[PydanticType] = ...
17+
"""the class that is used for generate the schema"""
18+
19+
20+
def construct_open_api_with_schema_class(
21+
open_api: OpenAPI,
22+
schema_classes: List[Type[PydanticType]] = None,
23+
scan_for_pydantic_schema_reference: bool = True,
24+
by_alias: bool = True,
25+
) -> OpenAPI:
26+
"""
27+
Construct a new OpenAPI object, with the use of pydantic classes to produce JSON schemas
28+
29+
:param open_api: the base `OpenAPI` object
30+
:param schema_classes: pydanitic classes that their schema will be used "#/components/schemas" values
31+
:param scan_for_pydantic_schema_reference: flag to indicate if scanning for `PydanticSchemaReference` class
32+
is needed for "#/components/schemas" value updates
33+
:param by_alias: construct schema by alias (default is True)
34+
:return: new OpenAPI object with "#/components/schemas" values updated.
35+
If there is no update in "#/components/schemas" values, the original `open_api` will be returned.
36+
"""
37+
new_open_api: OpenAPI = open_api.copy(deep=True)
38+
if scan_for_pydantic_schema_reference:
39+
extracted_schema_classes = _handle_pydantic_schema(new_open_api)
40+
if schema_classes:
41+
schema_classes = list({*schema_classes, *_handle_pydantic_schema(new_open_api)})
42+
else:
43+
schema_classes = extracted_schema_classes
44+
45+
if not schema_classes:
46+
return open_api
47+
48+
schema_classes.sort(key=lambda x: x.__name__)
49+
logging.debug(f"schema_classes{schema_classes}")
50+
51+
# update new_open_api with new #/components/schemas
52+
schema_definitions = schema(schema_classes, by_alias=by_alias, ref_prefix=ref_prefix)
53+
if not new_open_api.components:
54+
new_open_api.components = Components()
55+
if new_open_api.components.schemas:
56+
for existing_key in new_open_api.components.schemas:
57+
if existing_key in schema_definitions.get("definitions"):
58+
logging.warning(
59+
f'"{existing_key}" already exists in {ref_prefix}. '
60+
f'The value of "{ref_prefix}{existing_key}" will be overwritten.'
61+
)
62+
new_open_api.components.schemas.update(
63+
{key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items()}
64+
)
65+
else:
66+
new_open_api.components.schemas = {
67+
key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items()
68+
}
69+
return new_open_api
70+
71+
72+
def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[PydanticType]]:
73+
"""
74+
This function traverses the `OpenAPI` object and
75+
76+
1. Replaces the `PydanticSchema` object with `Reference` object, with correct ref value;
77+
2. Extracts the involved schema class from `PydanticSchema` object.
78+
79+
**This function will mutate the input `OpenAPI` object.**
80+
81+
:param open_api: the `OpenAPI` object to be traversed and mutated
82+
:return: a list of schema classes extracted from `PydanticSchema` objects
83+
"""
84+
85+
pydantic_types: Set[Type[PydanticType]] = set()
86+
87+
def _traverse(obj: Any):
88+
if isinstance(obj, BaseModel):
89+
fields = obj.__fields_set__
90+
for field in fields:
91+
child_obj = obj.__getattribute__(field)
92+
if isinstance(child_obj, PydanticSchema):
93+
logging.debug(f"PydanticSchema found in {obj.__repr_name__()}: {child_obj}")
94+
obj.__setattr__(field, _construct_ref_obj(child_obj))
95+
pydantic_types.add(child_obj.schema_class)
96+
else:
97+
_traverse(child_obj)
98+
elif isinstance(obj, list):
99+
for index, elem in enumerate(obj):
100+
if isinstance(elem, PydanticSchema):
101+
logging.debug(f"PydanticSchema found in list: {elem}")
102+
obj[index] = _construct_ref_obj(elem)
103+
pydantic_types.add(elem.schema_class)
104+
else:
105+
_traverse(elem)
106+
elif isinstance(obj, dict):
107+
for key, value in obj.items():
108+
if isinstance(value, PydanticSchema):
109+
logging.debug(f"PydanticSchema found in dict: {value}")
110+
obj[key] = _construct_ref_obj(value)
111+
pydantic_types.add(value.schema_class)
112+
else:
113+
_traverse(value)
114+
115+
_traverse(open_api)
116+
return list(pydantic_types)
117+
118+
119+
def _construct_ref_obj(pydantic_schema: PydanticSchema):
120+
ref_obj = Reference(ref=ref_prefix + pydantic_schema.schema_class.__name__)
121+
logging.debug(f"ref_obj={ref_obj}")
122+
return ref_obj

tests/v3_0_3/__init__.py

Whitespace-only changes.

tests/v3_0_3/test_util.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import logging
2+
3+
from pydantic import BaseModel, Field
4+
5+
from openapi_schema_pydantic.v3.v3_0_3 import Info, MediaType, OpenAPI, Operation, PathItem, Reference, RequestBody, Response
6+
from openapi_schema_pydantic.v3.v3_0_3.util import PydanticSchema, construct_open_api_with_schema_class
7+
8+
9+
def test_construct_open_api_with_schema_class_1():
10+
open_api = construct_base_open_api_1()
11+
result_open_api_1 = construct_open_api_with_schema_class(open_api)
12+
result_open_api_2 = construct_open_api_with_schema_class(open_api, [PingRequest, PingResponse])
13+
assert result_open_api_1.components == result_open_api_2.components
14+
assert result_open_api_1 == result_open_api_2
15+
16+
open_api_json = result_open_api_1.json(by_alias=True, exclude_none=True, indent=2)
17+
logging.debug(open_api_json)
18+
19+
20+
def test_construct_open_api_with_schema_class_2():
21+
open_api_1 = construct_base_open_api_1()
22+
open_api_2 = construct_base_open_api_2()
23+
result_open_api_1 = construct_open_api_with_schema_class(open_api_1)
24+
result_open_api_2 = construct_open_api_with_schema_class(open_api_2, [PingRequest, PingResponse])
25+
assert result_open_api_1 == result_open_api_2
26+
27+
28+
def test_construct_open_api_with_schema_class_3():
29+
open_api_3 = construct_base_open_api_3()
30+
31+
result_with_alias_1 = construct_open_api_with_schema_class(open_api_3)
32+
schema_with_alias = result_with_alias_1.components.schemas["PongResponse"]
33+
assert "pong_foo" in schema_with_alias.properties
34+
assert "pong_bar" in schema_with_alias.properties
35+
36+
result_with_alias_2 = construct_open_api_with_schema_class(open_api_3, by_alias=True)
37+
assert result_with_alias_1 == result_with_alias_2
38+
39+
result_without_alias = construct_open_api_with_schema_class(open_api_3, by_alias=False)
40+
schema_without_alias = result_without_alias.components.schemas["PongResponse"]
41+
assert "resp_foo" in schema_without_alias.properties
42+
assert "resp_bar" in schema_without_alias.properties
43+
44+
45+
def construct_base_open_api_1() -> OpenAPI:
46+
return OpenAPI.parse_obj(
47+
{
48+
"info": {"title": "My own API", "version": "v0.0.1"},
49+
"paths": {
50+
"/ping": {
51+
"post": {
52+
"requestBody": {
53+
"content": {"application/json": {"schema": PydanticSchema(schema_class=PingRequest)}}
54+
},
55+
"responses": {
56+
"200": {
57+
"description": "pong",
58+
"content": {"application/json": {"schema": PydanticSchema(schema_class=PingResponse)}},
59+
}
60+
},
61+
}
62+
}
63+
},
64+
}
65+
)
66+
67+
68+
def construct_base_open_api_2() -> OpenAPI:
69+
return OpenAPI(
70+
info=Info(title="My own API", version="v0.0.1"),
71+
paths={
72+
"/ping": PathItem(
73+
post=Operation(
74+
requestBody=RequestBody(
75+
content={
76+
"application/json": MediaType(
77+
media_type_schema=Reference(ref="#/components/schemas/PingRequest")
78+
)
79+
}
80+
),
81+
responses={
82+
"200": Response(
83+
description="pong",
84+
content={
85+
"application/json": MediaType(
86+
media_type_schema=Reference(ref="#/components/schemas/PingResponse")
87+
)
88+
},
89+
)
90+
},
91+
)
92+
)
93+
},
94+
)
95+
96+
97+
def construct_base_open_api_3() -> OpenAPI:
98+
return OpenAPI(
99+
info=Info(title="My own API", version="v0.0.1",),
100+
paths={
101+
"/ping": PathItem(
102+
post=Operation(
103+
requestBody=RequestBody(
104+
content={
105+
"application/json": MediaType(media_type_schema=PydanticSchema(schema_class=PingRequest))
106+
}
107+
),
108+
responses={
109+
"200": Response(
110+
description="pong",
111+
content={
112+
"application/json": MediaType(
113+
media_type_schema=PydanticSchema(schema_class=PongResponse)
114+
)
115+
},
116+
)
117+
},
118+
)
119+
)
120+
},
121+
)
122+
123+
124+
class PingRequest(BaseModel):
125+
"""Ping Request"""
126+
127+
req_foo: str = Field(description="foo value of the request")
128+
req_bar: str = Field(description="bar value of the request")
129+
130+
131+
class PingResponse(BaseModel):
132+
"""Ping response"""
133+
134+
resp_foo: str = Field(description="foo value of the response")
135+
resp_bar: str = Field(description="bar value of the response")
136+
137+
138+
class PongResponse(BaseModel):
139+
"""Pong response"""
140+
141+
resp_foo: str = Field(alias="pong_foo", description="foo value of the response")
142+
resp_bar: str = Field(alias="pong_bar", description="bar value of the response")

0 commit comments

Comments
 (0)