Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
flask-version: [ "Flask>=2.0,<3.0", "Flask>=3.0" ]
env:
PYTHONPATH: .
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The key features are:

## Requirements

Python 3.9+
Python 3.10+

flask-openapi3 is dependent on the following libraries:

Expand Down
39 changes: 38 additions & 1 deletion docs/Usage/Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,46 @@ def hello(path: HelloPath):
return response
```


![image-20210526104627124](../assets/image-20210526104627124.png)

## Validate responses

By default, responses are not validated. If you need to validate responses, set validate_responses to True. Here are
several ways to achieve this:

```python
# 1. APP level
app = OpenAPI(__name__, validate_response=True)

# 2. APIBlueprint level
api = APIBlueprint(__name__, validate_response=True)

# 3. APIView level
@api_view.route("/test")
class TestAPI:
@api_view.doc(responses={201: Response}, validate_response=True)
def post(self):
...

# 4. api level
@app.post("/test", responses={201: Response}, validate_response=True)
def endpoint_test(body: BaseRequest):
...
```

You can also customize the default behavior of response validation by using a custom `validate_response_callback`.

```python

def validate_response_callback(response: Any, responses: Optional[ResponseDict] = None) -> Any:

# do something

return response

app = OpenAPI(__name__, validate_response=True, validate_response_callback=validate_response_callback)
```

## More information about OpenAPI responses

- [OpenAPI Responses Object](https://spec.openapis.org/oas/v3.1.0#responses-object), it includes the Response Object.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

## 依赖

Python 3.9+
Python 3.10+

flask-openapi3 依赖以下库:

Expand Down
3 changes: 1 addition & 2 deletions examples/api_blueprint_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# @Author : llc
# @Time : 2021/6/6 14:05

from typing import Optional

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -37,7 +36,7 @@ class Unauthorized(BaseModel):


class BookBody(BaseModel):
age: Optional[int] = Field(..., ge=2, le=4, description="Age")
age: int | None = Field(..., ge=2, le=4, description="Age")
author: str = Field(None, min_length=2, max_length=4, description="Author")


Expand Down
5 changes: 2 additions & 3 deletions examples/api_view_demo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2022/10/18 9:00
from typing import Optional

from pydantic import BaseModel, Field

Expand All @@ -22,11 +21,11 @@ class BookPath(BaseModel):


class BookQuery(BaseModel):
age: Optional[int] = Field(None, description="Age")
age: int | None = Field(None, description="Age")


class BookBody(BaseModel):
age: Optional[int] = Field(..., ge=2, le=4, description="Age")
age: int | None = Field(..., ge=2, le=4, description="Age")
author: str = Field(None, min_length=2, max_length=4, description="Author")


Expand Down
5 changes: 2 additions & 3 deletions examples/async_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# @Author : llc
# @Time : 2022/11/30 14:55

from typing import Optional

from pydantic import BaseModel, Field

Expand All @@ -17,11 +16,11 @@ class Query(BaseModel):


class BookQuery(BaseModel):
age: Optional[int] = Field(None, description="Age")
age: int | None = Field(None, description="Age")


class BookBody(BaseModel):
age: Optional[int] = Field(..., ge=2, le=4, description="Age")
age: int | None = Field(..., ge=2, le=4, description="Age")
author: str = Field(None, min_length=2, max_length=4, description="Author")


Expand Down
9 changes: 4 additions & 5 deletions examples/rest_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# @Author : llc
# @Time : 2021/4/28 11:24
from http import HTTPStatus
from typing import Optional

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -45,25 +44,25 @@ class BookPath(BaseModel):


class BookQuery(BaseModel):
age: Optional[int] = Field(None, description="Age")
age: int | None = Field(None, description="Age")
s_list: list[str] = Field(None, alias="s_list[]", description="some array")


class BookBody(BaseModel):
age: Optional[int] = Field(..., ge=2, le=4, description="Age")
age: int | None = Field(..., ge=2, le=4, description="Age")
author: str = Field(None, min_length=2, max_length=4, description="Author")


class BookBodyWithID(BaseModel):
bid: int = Field(..., description="book id")
age: Optional[int] = Field(None, ge=2, le=4, description="Age")
age: int | None = Field(None, ge=2, le=4, description="Age")
author: str = Field(None, min_length=2, max_length=4, description="Author")


class BookResponse(BaseModel):
code: int = Field(0, description="Status Code")
message: str = Field("ok", description="Exception Information")
data: Optional[BookBodyWithID]
data: BookBodyWithID | None


@app.get(
Expand Down
33 changes: 19 additions & 14 deletions flask_openapi3/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# @Author : llc
# @Time : 2022/4/1 16:54
import inspect
from typing import Any, Callable, Optional
from typing import Any, Callable

from flask import Blueprint

Expand All @@ -28,11 +28,12 @@ def __init__(
name: str,
import_name: str,
*,
abp_tags: Optional[list[Tag]] = None,
abp_security: Optional[list[dict[str, list[str]]]] = None,
abp_responses: Optional[ResponseDict] = None,
abp_tags: list[Tag] | None = None,
abp_security: list[dict[str, list[str]]] | None = None,
abp_responses: ResponseDict | None = None,
doc_ui: bool = True,
operation_id_callback: Callable = get_operation_id_for_path,
validate_response: bool | None = None,
**kwargs: Any,
) -> None:
"""
Expand All @@ -49,6 +50,7 @@ def __init__(
operation_id_callback: Callback function for custom operation_id generation.
Receives name (str), path (str) and method (str) parameters.
Defaults to `get_operation_id_for_path` from utils
validate_response: Verify the response body.
**kwargs: Flask Blueprint kwargs
"""
super(APIBlueprint, self).__init__(name, import_name, **kwargs)
Expand All @@ -71,6 +73,9 @@ def __init__(
# Set the operation ID callback function
self.operation_id_callback: Callable = operation_id_callback

# Verify the response body
self.validate_response = validate_response

def register_api(self, api: "APIBlueprint") -> None:
"""Register a nested APIBlueprint"""

Expand Down Expand Up @@ -111,16 +116,16 @@ def _collect_openapi_info(
rule: str,
func: Callable,
*,
tags: Optional[list[Tag]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
external_docs: Optional[ExternalDocumentation] = None,
operation_id: Optional[str] = None,
responses: Optional[ResponseDict] = None,
deprecated: Optional[bool] = None,
security: Optional[list[dict[str, list[Any]]]] = None,
servers: Optional[list[Server]] = None,
openapi_extensions: Optional[dict[str, Any]] = None,
tags: list[Tag] | None = None,
summary: str | None = None,
description: str | None = None,
external_docs: ExternalDocumentation | None = None,
operation_id: str | None = None,
responses: ResponseDict | None = None,
deprecated: bool | None = None,
security: list[dict[str, list[Any]]] | None = None,
servers: list[Server] | None = None,
openapi_extensions: dict[str, Any] | None = None,
doc_ui: bool = True,
method: str = HTTPMethod.GET,
) -> ParametersTuple:
Expand Down
30 changes: 15 additions & 15 deletions flask_openapi3/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ class APISpec(BaseModel):

openapi: str
info: Info
servers: Optional[list[Server]] = None
servers: list[Server] | None = None
paths: Paths
components: Optional[Components] = None
security: Optional[list[SecurityRequirement]] = None
tags: Optional[list[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
webhooks: Optional[dict[str, Union[PathItem, Reference]]] = None
components: Components | None = None
security: list[SecurityRequirement] | None = None
tags: list[Tag] | None = None
externalDocs: ExternalDocumentation | None = None
webhooks: dict[str, PathItem | Reference] | None = None

model_config = {"extra": "allow"}

Expand All @@ -73,15 +73,15 @@ class OAuthConfig(BaseModel):
https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md#oauth-20-configuration
"""

clientId: Optional[str] = None
clientSecret: Optional[str] = None
realm: Optional[str] = None
appName: Optional[str] = None
scopeSeparator: Optional[str] = None
scopes: Optional[str] = None
additionalQueryStringParams: Optional[dict[str, str]] = None
useBasicAuthenticationWithAccessCodeGrant: Optional[bool] = False
usePkceWithAuthorizationCodeGrant: Optional[bool] = False
clientId: str | None = None
clientSecret: str | None = None
realm: str | None = None
appName: str | None = None
scopeSeparator: str | None = None
scopes: str | None = None
additionalQueryStringParams: dict[str, str] | None = None
useBasicAuthenticationWithAccessCodeGrant: bool | None = False
usePkceWithAuthorizationCodeGrant: bool | None = False


class RawModel(Request):
Expand Down
22 changes: 11 additions & 11 deletions flask_openapi3/models/components.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2023/7/4 9:36
from typing import Any, Optional, Union
from typing import Any

from pydantic import BaseModel, Field

Expand All @@ -23,15 +23,15 @@ class Components(BaseModel):
https://spec.openapis.org/oas/v3.1.0#components-object
"""

schemas: Optional[dict[str, Union[Reference, Schema]]] = Field(None)
responses: Optional[dict[str, Union[Response, Reference]]] = None
parameters: Optional[dict[str, Union[Parameter, Reference]]] = None
examples: Optional[dict[str, Union[Example, Reference]]] = None
requestBodies: Optional[dict[str, Union[RequestBody, Reference]]] = None
headers: Optional[dict[str, Union[Header, Reference]]] = None
securitySchemes: Optional[dict[str, Union[SecurityScheme, dict[str, Any]]]] = None
links: Optional[dict[str, Union[Link, Reference]]] = None
callbacks: Optional[dict[str, Union[Callback, Reference]]] = None
pathItems: Optional[dict[str, Union[PathItem, Reference]]] = None
schemas: dict[str, Reference | Schema] | None = Field(None)
responses: dict[str, Response | Reference] | None = None
parameters: dict[str, Parameter | Reference] | None = None
examples: dict[str, Example | Reference] | None = None
requestBodies: dict[str, RequestBody | Reference] | None = None
headers: dict[str, Header | Reference] | None = None
securitySchemes: dict[str, SecurityScheme | dict[str, Any]] | None = None
links: dict[str, Link | Reference] | None = None
callbacks: dict[str, Callback | Reference] | None = None
pathItems: dict[str, PathItem | Reference] | None = None

model_config = {"extra": "allow"}
7 changes: 3 additions & 4 deletions flask_openapi3/models/contact.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2023/7/4 9:37
from typing import Optional

from pydantic import BaseModel

Expand All @@ -11,8 +10,8 @@ class Contact(BaseModel):
https://spec.openapis.org/oas/v3.1.0#contact-object
"""

name: Optional[str] = None
url: Optional[str] = None
email: Optional[str] = None
name: str | None = None
url: str | None = None
email: str | None = None

model_config = {"extra": "allow"}
3 changes: 1 addition & 2 deletions flask_openapi3/models/discriminator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2023/7/4 9:41
from typing import Optional

from pydantic import BaseModel

Expand All @@ -12,6 +11,6 @@ class Discriminator(BaseModel):
"""

propertyName: str
mapping: Optional[dict[str, str]] = None
mapping: dict[str, str] | None = None

model_config = {"extra": "allow"}
10 changes: 5 additions & 5 deletions flask_openapi3/models/encoding.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2023/7/4 9:41
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Union

from pydantic import BaseModel

Expand All @@ -18,10 +18,10 @@ class Encoding(BaseModel):
https://spec.openapis.org/oas/v3.1.0#encoding-object
"""

contentType: Optional[str] = None
headers: Optional[dict[str, Union[Header, Reference]]] = None
style: Optional[str] = None
explode: Optional[bool] = None
contentType: str | None = None
headers: dict[str, Union[Header, Reference]] | None = None
style: str | None = None
explode: bool | None = None
allowReserved: bool = False

model_config = {"extra": "allow"}
Loading
Loading