Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
ponytailer committed Sep 29, 2021
1 parent d6bf7f2 commit 3d19aed
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 33 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
0.1.7 2021-09-28
----------------
0.1.12 2021-09-29
-----------------
* the default response schema
* support show the swagger which include the {tag}
* support export the swagger which include the {tag}

0.1.11 2021-09-28
-----------------

* bugfix
* support tag for swagger

0.1.6 2021-09-27
Expand Down
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ schema-validator
# balabala
return dict(id=1, name="2")
@app.put("/")
@app.get("/")
@validate(
body=Todo,
query=Todo,
responses={200: TodoResponse, 400: TodoResponse}
)
def update_todo():
Expand All @@ -61,18 +61,37 @@ schema-validator
# balabala
return jsonify(id=1)
@tags("SOME-TAG", "OTHER-TAG")
@tags("SOME-TAG", "OTHER-TAG") # only for swagger
class View(MethodView):
@validate(...)
def get(self):
return {}
app.cli.add_command(generate_schema_command)
virtualenv: flask schema swagger.json -> generate json swagger
```

### How to show the swagger
```
app.config["SWAGGER_ROUTE"] = True
http://yourhost/docs -> show the all swagger
http://yourhost/docs/{tag} -> show the swagger which include tag
```

### How to export the swagger
```
add command in flask:
app.cli.add_command(generate_schema_command)
Export all swagger to json file:
##### FEATURES
- direct package/api/view_class name to export json-swagger
- direct tag to swagger html
- flask schema -o swagger.json
Export the swagger which include the ACCOUNT tag:
- flask schema -o swagger.json -t ACCOUNT
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "schema_validator"
version = "0.1.10"
version = "0.1.12"
description = "A flask extension to provide schema validation with pydantic."
authors = ["hs <[email protected]>"]
classifiers = [
Expand Down
17 changes: 15 additions & 2 deletions schema_validator/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,27 @@
type=click.Path(),
help="Output the spec to a file given by a path.",
)
@click.option(
"--tag",
"-t",
type=str,
required=False,
default="",
help="Export swagger include tag"
)
@with_appcontext
def generate_schema_command(output: Optional[str]) -> None:
def generate_schema_command(
output: Optional[str],
tag: Optional[str]
) -> None:
"""
The command which can dump json-swagger
app.cli.add_command(generate_schema_command)
virtualenv: flask schema
"""
schema = _build_openapi_schema(app, app.extensions["FLASK_SCHEMA"])
schema = _build_openapi_schema(
app, app.extensions["FLASK_SCHEMA"], tag if tag else None)

formatted_spec = json.dumps(schema, indent=2)
if output is not None:
with open(output, "w") as file_:
Expand Down
43 changes: 33 additions & 10 deletions schema_validator/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

from .constants import (
IGNORE_METHODS, REF_PREFIX, SCHEMA_QUERYSTRING_ATTRIBUTE,
SCHEMA_REQUEST_ATTRIBUTE, SCHEMA_RESPONSE_ATTRIBUTE, SWAGGER_CSS_URL,
SWAGGER_JS_URL, SWAGGER_TEMPLATE, SCHEMA_TAG_ATTRIBUTE
SCHEMA_REQUEST_ATTRIBUTE, SCHEMA_RESPONSE_ATTRIBUTE, SCHEMA_TAG_ATTRIBUTE,
SWAGGER_CSS_URL, SWAGGER_JS_URL, SWAGGER_TEMPLATE
)
from .typing import ServerObject
from .validation import DataSource
Expand Down Expand Up @@ -67,14 +67,14 @@ def __init__(
self,
app: Optional[Flask] = None,
*,
openapi_path: Optional[str] = "/openapi.json",
swagger_ui_path: Optional[str] = "/docs",
title: Optional[str] = None,
version: str = "0.1.0",
convert_casing: bool = False,
servers: Optional[List[ServerObject]] = None
) -> None:
self.openapi_path = openapi_path
self.openapi_path = "/openapi.json"
self.openapi_tag_path = "/openapi-<tag>.json"
self.swagger_ui_path = swagger_ui_path
self.title = title
self.version = version
Expand Down Expand Up @@ -102,21 +102,33 @@ def init_app(self, app: Flask) -> None:
)

if self.openapi_path is not None and app.config.get("SWAGGER_ROUTE"):
app.add_url_rule(self.openapi_path, "openapi", self.openapi)
app.add_url_rule(
self.openapi_path, "openapi",
self.openapi
)
app.add_url_rule(
self.openapi_tag_path, "openapi_tag",
lambda tag: self.openapi(tag)
)
if self.swagger_ui_path is not None:
app.add_url_rule(
self.swagger_ui_path, "swagger_ui",
self.swagger_ui
)
app.add_url_rule(
f"{self.swagger_ui_path}/<tag>", "swagger_ui_tag",
lambda tag: self.swagger_ui(tag)
)

def openapi(self) -> dict:
return _build_openapi_schema(current_app, self)
def openapi(self, tag: Optional[str] = None) -> dict:
return _build_openapi_schema(current_app, self, tag)

def swagger_ui(self) -> str:
def swagger_ui(self, tag: Optional[str] = None) -> str:
path = f"/openapi-{tag}.json" if tag else self.openapi_path
return render_template_string(
SWAGGER_TEMPLATE,
title=self.title,
openapi_path=self.openapi_path,
openapi_path=path,
swagger_js_url=current_app.config["FLASK_SCHEMA_SWAGGER_JS_URL"],
swagger_css_url=current_app.config["FLASK_SCHEMA_SWAGGER_CSS_URL"],
)
Expand All @@ -128,7 +140,15 @@ def _split_definitions(schema: dict) -> Tuple[dict, dict]:
return definitions, new_schema


def _build_openapi_schema(app: Flask, extension: FlaskSchema) -> dict:
def _build_openapi_schema(
app: Flask,
extension: FlaskSchema,
expected_tag: str = None
) -> dict:
"""
params:
expected_tag: str
"""
paths: Dict[str, dict] = {}
components = {"schemas": {}}

Expand Down Expand Up @@ -165,6 +185,9 @@ def _build_openapi_schema(app: Flask, extension: FlaskSchema) -> dict:
if tags:
path_object["tags"] = tags

if expected_tag and expected_tag not in tags:
continue

response_models = getattr(function, SCHEMA_RESPONSE_ATTRIBUTE, {})

for status_code, model_class in response_models.items():
Expand Down
23 changes: 22 additions & 1 deletion schema_validator/typing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Dict, List, Tuple, Type, TypedDict, Union
from typing import Dict, List, Tuple, Type, TypedDict, Union, Optional
from dataclasses import dataclass, field

from pydantic import BaseModel
from flask.typing import (
Expand Down Expand Up @@ -31,3 +32,23 @@ class ServerObject(TypedDict, total=False):
url: str
description: str
variables: Dict[str, VariableObject]


class ResponseSchema(BaseModel):
"""
base response to inherit
class TodoResponse(ResponseSchema):
name: str
age: int
"""
success: Optional[bool] = True
error_no: Optional[int] = 0
error_message: Optional[str] = ""


@dataclass
class ResponseClass:
success: Optional[bool] = True
error_no: Optional[int] = 0
error_message: Optional[str] = ""
2 changes: 1 addition & 1 deletion schema_validator/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def check_response(result, response_model: Dict[int, PydanticModel]):
try:
model_value = model_cls(**value)
except (TypeError, ValidationError) as ve:
return jsonify(validation_error=str(ve.errors())), bad_status
return jsonify(validation_error=str(ve)), bad_status
elif type(value) == model_cls:
model_value = value
elif is_builtin_dataclass(value):
Expand Down
18 changes: 10 additions & 8 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,23 @@ def item() -> ResponseReturnValue:
"model, return_value, status",
[
(Item, VALID_DICT, 200),
(Item, INVALID_DICT, 500),
(Item, INVALID_DICT, 400),
(Item, VALID, 200),
(Item, INVALID, 500),
(Item, INVALID, 400),
(DCItem, VALID_DICT, 200),
(DCItem, INVALID_DICT, 500),
(DCItem, INVALID_DICT, 400),
(DCItem, VALID_DC, 200),
(DCItem, INVALID_DC, 500),
(DCItem, INVALID_DC, 400),
(PyDCItem, VALID_DICT, 200),
(PyDCItem, INVALID_DICT, 500),
(PyDCItem, INVALID_DICT, 400),
(PyDCItem, VALID_PyDC, 200),
(PyDCItem, INVALID_PyDC, 500),
(PyDCItem, INVALID_PyDC, 400),
],
)
def test_response_validation(model: Any, return_value: Any,
status: int) -> None:
def test_response_validation(
model: Any, return_value: Any,
status: int
) -> None:
app = Flask(__name__)
FlaskSchema(app)

Expand Down

0 comments on commit 3d19aed

Please sign in to comment.