From 3d19aed130dab9e5a0dad1b527695b80c685f7be Mon Sep 17 00:00:00 2001 From: huangsong Date: Wed, 29 Sep 2021 13:45:52 +0800 Subject: [PATCH] fix --- CHANGELOG.rst | 12 ++++++++-- README.md | 35 ++++++++++++++++++++------- pyproject.toml | 2 +- schema_validator/command.py | 17 ++++++++++++-- schema_validator/extension.py | 43 ++++++++++++++++++++++++++-------- schema_validator/typing.py | 23 +++++++++++++++++- schema_validator/validation.py | 2 +- tests/test_validation.py | 18 +++++++------- 8 files changed, 119 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d1b0c89..8ec4eb4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/README.md b/README.md index 8ef9157..5057d56 100644 --- a/README.md +++ b/README.md @@ -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(): @@ -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 + +``` diff --git a/pyproject.toml b/pyproject.toml index e3b5c4f..a4a8dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] classifiers = [ diff --git a/schema_validator/command.py b/schema_validator/command.py index 5abe4f6..cbfc7d5 100644 --- a/schema_validator/command.py +++ b/schema_validator/command.py @@ -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_: diff --git a/schema_validator/extension.py b/schema_validator/extension.py index a88b752..7103488 100644 --- a/schema_validator/extension.py +++ b/schema_validator/extension.py @@ -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 @@ -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-.json" self.swagger_ui_path = swagger_ui_path self.title = title self.version = version @@ -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}/", "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"], ) @@ -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": {}} @@ -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(): diff --git a/schema_validator/typing.py b/schema_validator/typing.py index d3adbd0..143ea77 100644 --- a/schema_validator/typing.py +++ b/schema_validator/typing.py @@ -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 ( @@ -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] = "" diff --git a/schema_validator/validation.py b/schema_validator/validation.py index 88aae48..ad1258e 100644 --- a/schema_validator/validation.py +++ b/schema_validator/validation.py @@ -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): diff --git a/tests/test_validation.py b/tests/test_validation.py index 26df7ac..3555afc 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -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)