From b1b3447de498fa9915e5fbd54fdd3976a3847747 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 21 Aug 2022 11:19:01 +0800 Subject: [PATCH] Sync changes for 1.1.2 version (#37) Co-authored-by: Grey Li Co-authored-by: Benno Rice Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andy Zhou --- .github/workflows/tests.yml | 10 +- CHANGES.md | 19 +++ README.md | 20 +-- codecov.yml | 1 + docs/api-docs.md | 23 +-- docs/authentication.md | 4 +- docs/comparison.md | 10 +- docs/configuration.md | 10 +- docs/docs.md | 2 +- docs/error-handling.md | 10 +- docs/migrating.md | 12 +- docs/openapi.md | 18 +-- docs/request.md | 30 ++-- docs/response.md | 16 +-- docs/schema.md | 10 +- docs/usage.md | 136 ++++++++++-------- examples/auth/token_auth/app.py | 4 +- examples/base_response/app.py | 22 +-- examples/basic/app.py | 18 +-- examples/blueprint_tags/app.py | 18 +-- examples/cbv/app.py | 18 +-- examples/dataclass/app.py | 4 +- examples/openapi/app.py | 18 +-- examples/orm/app.py | 18 +-- examples/pagination/app.py | 14 +- mkdocs.yml | 4 +- src/apiflask/__init__.py | 2 +- src/apiflask/app.py | 20 +-- src/apiflask/fields.py | 8 +- src/apiflask/helpers.py | 10 +- src/apiflask/route.py | 4 +- src/apiflask/scaffold.py | 8 +- src/apiflask/schemas.py | 2 +- src/apiflask/settings.py | 10 +- src/apiflask/ui_templates.py | 6 + tests/schemas.py | 24 ++-- tests/test_app.py | 32 ++--- tests/test_async.py | 10 +- tests/test_base_response.py | 32 ++--- tests/test_decorator_doc.py | 24 ++-- tests/test_decorator_input.py | 40 +++--- tests/test_decorator_output.py | 36 ++--- tests/test_fields.py | 8 +- tests/test_openapi_basic.py | 20 +-- tests/test_openapi_paths.py | 56 ++++---- tests/test_route.py | 4 +- tests/test_settings_api_docs.py | 6 + tests/test_settings_auto_behaviour.py | 12 +- tests/test_settings_response_customization.py | 28 ++-- tox.ini | 18 +-- 50 files changed, 480 insertions(+), 409 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdaffbc..69ea2e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,19 @@ -name: build +name: Tests on: push: branches: - main + - 'v*' + paths-ignore: + - 'docs/**' + - '*.md' pull_request: branches: - main + - 'v*' + paths-ignore: + - 'docs/**' + - '*.md' jobs: lint: name: lint diff --git a/CHANGES.md b/CHANGES.md index dd7c6df..ee96755 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,25 @@ Released: - +## Version 1.1.2 + +Released: 2022/8/13 + +- Set default Elements router to `hash` to fix incorrect path updates. + + +## Version 1.1.1 + +Released: 2022/8/3 + +- Improve CI setup and test again Python 3.10 and 3.11. +- Fix the typing of `APIFlask` path parameters ([issue #329][issue_329]). +- Update `MethodViewType` usages for Flask 2.2 ([issue #335][issue_335]). + +[issue_329]: https://github.com/apiflask/apiflask/issues/329 +[issue_335]: https://github.com/apiflask/apiflask/issues/335 + + ## Version 1.1.0 Released: 2022/7/3 diff --git a/README.md b/README.md index 9588e17..c415be5 100644 --- a/README.md +++ b/README.md @@ -127,12 +127,12 @@ pets = [ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -145,7 +145,7 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): if pet_id > len(pets) - 1: abort(404) @@ -155,8 +155,8 @@ def get_pet(pet_id): @app.patch('/pets/') -@app.input(PetInSchema(partial=True)) -@app.output(PetOutSchema) +@app.input(PetIn(partial=True)) +@app.output(PetOut) def update_pet(pet_id, data): # 验证且解析后的请求输入数据会 # 作为一个字典传递给视图函数 @@ -188,12 +188,12 @@ pets = [ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -211,15 +211,15 @@ class Hello(MethodView): @app.route('/pets/') class Pet(MethodView): - @app.output(PetOutSchema) + @app.output(PetOut) def get(self, pet_id): """Get a pet""" if pet_id > len(pets) - 1: abort(404) return pets[pet_id] - @app.input(PetInSchema(partial=True)) - @app.output(PetOutSchema) + @app.input(PetIn(partial=True)) + @app.output(PetOut) def patch(self, pet_id, data): """Update a pet""" if pet_id > len(pets) - 1: diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..69cb760 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/docs/api-docs.md b/docs/api-docs.md index 0a6e2e0..9304b4e 100644 --- a/docs/api-docs.md +++ b/docs/api-docs.md @@ -1,4 +1,4 @@ -# API documentations +# API documentation APIFlask provides support to the following API documentation UIs: @@ -64,7 +64,7 @@ Now the paths to docs and spec will be and . -## Add custom API documentations +## Add custom API documentation You can easily add support to other API docs or serve the supported docs UI by yourself. @@ -129,9 +129,9 @@ def my_redoc(): ``` -## Disable the API documentations globally +## Disable the API documentation globally -You can set the `docs_path` parameter to `None` to disable Swagger UI documentation: +You can set the `docs_path` parameter to `None` to disable the API documentation: ```python from apiflask import APIFlask @@ -156,9 +156,9 @@ See *[Disable the OpenAPI support for specific blueprints](/openapi/#disable-for See *[Disable the OpenAPI support for specific view functions](/openapi/#disable-for-specific-view-functions)* for more details. -## Configure Swagger UI/Redoc +## Configure API documentations -The following configuration variables can be used to config Swagger UI/Redoc: +The following configuration variables can be used to configure API docs: - `DOCS_FAVICON` - `REDOC_USE_GOOGLE_FONT` @@ -166,12 +166,17 @@ The following configuration variables can be used to config Swagger UI/Redoc: - `SWAGGER_UI_LAYOUT` - `SWAGGER_UI_CONFIG` - `SWAGGER_UI_OAUTH_CONFIG` +- `ELEMENTS_LAYOUT` +- `ELEMENTS_CONFIG` +- `RAPIDOC_THEME` +- `RAPIDOC_CONFIG` +- `RAPIPDF_CONFIG` -See *[Configuration](/configuration/#api-documentation)* for the +See *[Configuration](/configuration/#api-documentations)* for the introduction and examples of these configuration variables. -## Use different CDN server for Swagger UI/Redoc resources +## Use different CDN server for API documentation resources Each resource (JavaScript/CSS files) URL has a configuration variable. You can pass the URL from your preferred CDN server to the corresponding configuration variables: @@ -207,7 +212,7 @@ See *[Configuration](/configuration/#api-documentations)* for the introduction and examples of these configuration variables. -## Serve Swagger UI/Redoc from local resources +## Serve API documentation from local resources Like what you need to do in the last section, to use local resources, you can pass the URL of local static files to the corresponding configuration variables: diff --git a/docs/authentication.md b/docs/authentication.md index 8cdd73c..4787975 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -47,8 +47,8 @@ Then you can set the security scheme with the `security` parameter in `app.doc() ```python hl_lines="5" @app.post('/pets') @my_auth_lib.protect # protect the view with the decorator provided by external authentication library -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) @app.doc(security='ApiKeyAuth') def create_pet(data): pet_id = len(pets) diff --git a/docs/comparison.md b/docs/comparison.md index 2b2d21c..11f92b1 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -13,14 +13,16 @@ differences between APIFlask and similar projects. ## APIFlask vs FastAPI - For the web part, FastAPI builds on top of Starlette, while APIFlask builts on top of -Flask. + Flask. - For the data part (serialization/deserialization, OpenAPI support), FastAPI relies -on Pydantic, while APIFlask uses marshmallow-code projects (marshmallow, webargs, apispec). + on Pydantic, while APIFlask uses marshmallow-code projects (marshmallow, webargs, apispec). - APIFlask builds on top of Flask, so it's compatible with Flask extensions. - FastAPI support async. APIFlask will have the basic async support with Flask 2.0. - APIFlask provides more decorators to help organize things better. - FastAPI injects the input data as an object, while APIFlask passes it as a dict. - APIFlask has built-in class-based views support based on Flask's `MethodView`. +- On top of Swagger UI and Redoc, APIFlask supports more API documentation tools: + Elements, RapiDoc, and RapiPDF. ## APIFlask vs APIFairy/flask-smorest @@ -68,8 +70,8 @@ Assume a view like this: ```python @app.get('//articles/') # category, article_id -@app.input(ArticleQuerySchema, location='query') # query -@app.input(ArticleInSchema) # data +@app.input(ArticleQuery, location='query') # query +@app.input(ArticleIn) # data def get_article(category, article_id, query, data): pass ``` diff --git a/docs/configuration.md b/docs/configuration.md index 94941d9..6b01fd1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -99,7 +99,7 @@ Read more about configuration management in from .settings import CATEGORIES # import the configuration variable - class PetInSchema(Schema): + class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(CATEGORIES)) # use it ``` @@ -801,12 +801,12 @@ from apiflask.fields import String, Integer, Field app = APIFlask(__name__) -class BaseResponseSchema(Schema): +class BaseResponse(Schema): message = String() status_code = Integer() data = Field() -app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema +app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse ``` !!! warning "Version >= 0.9.0" @@ -832,9 +832,9 @@ app.config['BASE_RESPONSE_DATA_KEY'] = 'data' This configuration variable was added in the [version 0.9.0](/changelog/#version-090). -## API documentations +## API documentation -The following configuration variables used to customize API documentations. +The following configuration variables used to customize API documentation. ### DOCS_FAVICON diff --git a/docs/docs.md b/docs/docs.md index 12afa53..c305467 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -22,7 +22,7 @@ Go through the following chapters to learn how to use APIFlask: - **[OpenAPI Generating](/openapi)**: Introduce how the OpenAPI generation works and how to customize it with `@app.doc` decorator and configuration variables. - **[Authentication](/authentication)**: Introduce how to implement authentication support for your application. -- **[Swagger UI and Redoc](/api-docs)**: Introduce the usage and configuration of the API +- **[API Documentation](/api-docs)**: Introduce the usage and configuration of the API documentation tools. - **[Configuration](/configuration)**: A list of all the built-in configuration variables - **[Examples](/examples)**: A collection of application examples. diff --git a/docs/error-handling.md b/docs/error-handling.md index 956efb2..3b842b5 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -15,7 +15,7 @@ The error handling in APIFlask is based on the following basic concepts: !!! tip The error handler registered with `app.errorhandler` for specific HTTP errors will be - used over the custom error response processor. + used over the custom error response processor registered with `app.error_processor`. ## Automatic JSON error response @@ -253,9 +253,9 @@ so you can get error information via its attributes: ... ``` - The value of `location` can be `json` (i.e., request body) or `query` - (i.e., query string) depending on the place where the validation error - happened. + The value of `location` can be `json` (i.e., request body), `query` + (i.e., query string) or other values depending on the place where the + validation error happened (it matches the value you passed in `app.input`). - headers: The value will be `{}` unless you pass it in `HTTPError` or `abort`. - extra_data: Additional error information passed with `HTTPError` or `abort`. @@ -275,7 +275,7 @@ def my_error_processor(error): !!! tip - I would recommend keeping the `detail` in the response since it contains + I would recommend keeping the `error.detail` data in the response since it contains the detailed information about the validation error when it happened. After you change the error response, you have to update the corresponding OpenAPI schema diff --git a/docs/migrating.md b/docs/migrating.md index 9d0b16c..d4f9b2e 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -90,21 +90,21 @@ class Pet(MethodView): decorators = [doc(responses=[404])] - @app.output(PetOutSchema) + @app.output(PetOut) def get(self, pet_id): pass - @app.output({}, 204) + @app.output({}, status_code=204) def delete(self, pet_id): pass - @app.input(PetInSchema) - @app.output(PetOutSchema) + @app.input(PetIn) + @app.output(PetOut) def put(self, pet_id, data): pass - @app.input(PetInSchema(partial=True)) - @app.output(PetOutSchema) + @app.input(PetIn(partial=True)) + @app.output(PetOut) def patch(self, pet_id, data): pass ``` diff --git a/docs/openapi.md b/docs/openapi.md index 84b0753..ad6dc95 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -413,7 +413,7 @@ on the view function: ```python hl_lines="2" @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): return pets[pet_id] ``` @@ -423,7 +423,7 @@ corresponding parameters in the `output` decorator: ```python hl_lines="2" @app.get('/pets/') -@app.output(PetOutSchema, status_code=200, description='Output data of a pet') +@app.output(PetOut, status_code=200, description='Output data of a pet') def get_pet(pet_id): return pets[pet_id] ``` @@ -450,7 +450,7 @@ on the view function: ```python hl_lines="2" @app.post('/pets') -@app.input(PetInSchema) +@app.input(PetIn) def create_pet(pet_id): pass ``` @@ -460,7 +460,7 @@ will be generated instead: ```python hl_lines="2" @app.get('/pets') -@app.input(PetQuerySchema, location='query') +@app.input(PetQuery, location='query') def get_pets(): pass ``` @@ -502,7 +502,7 @@ you passed. To set the OpenAPI spec for schema fields, you can pass a dict with the `metadata` keyword: ```python -class PetInSchema(Schema): +class PetIn(Schema): name = String(metadata={'description': 'The name of the pet.'}) ``` @@ -553,7 +553,7 @@ to `validate`: from apiflask import Schema from apiflask.fields import String -class PetInSchema(Schema): +class PetIn(Schema): name = String( required=True, validate=Length(0, 10), @@ -608,7 +608,7 @@ from apiflask import APIFlask, input app = APIFlask(__name__) @app.post('/pets') -@app.input(PetInSchema, example={'name': 'foo', 'category': 'cat'}) +@app.input(PetIn, example={'name': 'foo', 'category': 'cat'}) def create_pet(): pass ``` @@ -633,7 +633,7 @@ examples = { } @app.get('/pets') -@app.output(PetOutSchema, examples=examples) +@app.output(PetOut, examples=examples) def get_pets(): pass ``` @@ -645,7 +645,7 @@ def get_pets(): you can set the field example (property-level example) in the data schema: ```python - class PetQuerySchema(Schema): + class PetQuery(Schema): name = String(metadata={'example': 'Flash'}) ``` diff --git a/docs/request.md b/docs/request.md index 1ef761f..8f3f5f6 100644 --- a/docs/request.md +++ b/docs/request.md @@ -38,9 +38,9 @@ you can only declare one body location for your view function, one of: ```python @app.get('/') -@app.input(FooHeaderSchema, location='headers') # header -@app.input(FooQuerySchema, location='query') # query string -@app.input(FooInSchema, location='json') # JSON data (body location) +@app.input(FooHeader, location='headers') # header +@app.input(FooQuery, location='query') # query string +@app.input(FooIn, location='json') # JSON data (body location) def hello(headers, query, data): pass ``` @@ -56,8 +56,8 @@ declared multiple inputs, the order will be from top to bottom: ```python @app.get('/') -@app.input(FooQuerySchema, location='query') # query -@app.input(FooInSchema, location='json') # data +@app.input(FooQuery, location='query') # query +@app.input(FooIn, location='json') # data def hello(query, data): pass ``` @@ -70,8 +70,8 @@ If you also defined some URL variables, the order will be from left to right (pl ```python @app.get('//articles/') # category, article_id -@app.input(ArticleQuerySchema, location='query') # query -@app.input(ArticleInSchema, location='json') # data +@app.input(ArticleQuery, location='query') # query +@app.input(ArticleIn, location='json') # data def get_article(category, article_id, query, data): pass ``` @@ -125,7 +125,7 @@ from apiflask.fields import Integer {'page': Integer(load_default=1), 'per_page': Integer(load_default=10)}, location='query' ) -@app.input(FooInSchema, location='json') +@app.input(FooIn, location='json') def hello(query, data): pass ``` @@ -141,9 +141,9 @@ from apiflask.fields import Integer @app.input( {'page': Integer(load_default=1), 'per_page': Integer(load_default=10)}, location='query', - schema_name='PaginationQuerySchema' + schema_name='PaginationQuery' ) -@app.input(FooInSchema, location='json') +@app.input(FooIn, location='json') def hello(query, data): pass ``` @@ -168,12 +168,12 @@ from werkzeug.utils import secure_filename from apiflask.fields import File -class ImageSchema(Schema): +class Image(Schema): image = File() @app.post('/images') -@app.input(ImageSchema, location='files') +@app.input(Image, location='files') def upload_image(data): f = data['image'] @@ -204,12 +204,12 @@ from werkzeug.utils import secure_filename from apiflask.fields import String, File -class ProfileInSchema(Schema): +class ProfileIn(Schema): name = String() avatar = File() @app.post('/profiles') -@app.input(ProfileInSchema, location='form_and_files') +@app.input(ProfileIn, location='form_and_files') def create_profile(data): avatar_file = data['avatar'] name = data['name'] @@ -232,7 +232,7 @@ the form data (equals to `form_and_files`). you can manually validate the file in the view function or the schema: ```python - class ImageSchema(Schema): + class Image(Schema): image = File(validate=lambda f: f.mimetype in ['image/jpeg', 'image/png']) ``` diff --git a/docs/response.md b/docs/response.md index cfa310d..b0a3e8b 100644 --- a/docs/response.md +++ b/docs/response.md @@ -37,14 +37,14 @@ from apiflask.fields import Integer from apiflask.validators import Range -class PetQuerySchema(Schema): +class PetQuery(Schema): page = Integer(load_default=1) # set default page to 1 # set default value to 20, and make sure the value is less than 30 per_page = Integer(load_default=20, validate=Range(max=30)) ``` Then we create a pet output schema, and a pets schema that contains -a list of nested `PetOutSchema` schema, and a nested `PaginationSchema` +a list of nested `PetOut` schema, and a nested `PaginationSchema` schema. ```python @@ -52,14 +52,14 @@ from apiflask import Schema, PaginationSchema from apiflask.fields import Integer, String, List, Nested -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() -class PetsOutSchema(Schema): - pets = List(Nested(PetOutSchema)) +class PetsOut(Schema): + pets = List(Nested(PetOut)) pagination = Nested(PaginationSchema) ``` @@ -70,8 +70,8 @@ from apiflask import APIFlask, pagination_builder @app.get('/pets') -@app.input(PetQuerySchema, 'query') -@app.output(PetsOutSchema) +@app.input(PetQuery, location='query') +@app.output(PetsOut) def get_pets(query): pagination = PetModel.query.paginate( page=query['page'], @@ -137,7 +137,7 @@ from apiflask.fields import Integer @app.get('/') -@app.output({'answer': Integer(dump_default=42)}, schema_name='AnswerSchema') +@app.output({'answer': Integer(dump_default=42)}, schema_name='Answer') def get_answer(): return {'answer': 'Nothing'} ``` diff --git a/docs/schema.md b/docs/schema.md index 7bfd303..db8c003 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -73,7 +73,7 @@ Or you prefer to keep a reference: ```python from apiflask import Schema, fields -class FooBarSchema(Schema): +class FooBar(Schema): foo = fields.String() bar = fields.Integer() ``` @@ -194,7 +194,7 @@ from apiflask.fields import String from apiflask.validators import OneOf -class PetInSchema(Schema): +class PetIn(Schema): category = String(required=True, validate=OneOf(['dog', 'cat'])) ``` @@ -206,7 +206,7 @@ from apiflask.fields import String from apiflask.validators import Length, OneOf -class PetInSchema(Schema): +class PetIn(Schema): category = String(required=True, validate=[OneOf(['dog', 'cat']), Length(0, 10)]) ``` @@ -301,12 +301,12 @@ from apiflask.fields import String, Integer, Field app = APIFlask(__name__) -class BaseResponseSchema(Schema): +class BaseResponse(Schema): data = Field() # the data key message = String() code = Integer() -app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema +app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse ``` The default data key is "data", you can change it to match your data field name in your schema diff --git a/docs/usage.md b/docs/usage.md index fbe0efb..ab8d93c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -77,15 +77,21 @@ $ flask run If your script's name isn't `app.py`, you will need to declare which application should be started before execute `flask run`. See the note below for more details. -??? note "Assign the specific application to run with `FLASK_APP`" +??? note "Assign the specific application to run" In default, Flask will look for an application instance called `app` or `application` or application factory function called `create_app` or `make_app` in module/package called `app` or `wsgi`. That's why I recommend naming the file as `app.py`. If you use a different name, then you need to tell Flask the application module path via the - environment variable `FLASK_APP`. For example, if your application instance stored in - a file called `hello.py`, then you will need to set `FLASK_APP` to the module name - `hello`: + `--app` (Flask 2.2+) option or the environment variable `FLASK_APP`. For example, if + your application instance stored in a file called `hello.py`, then you will need to + set `--app` or `FLASK_APP` to the module name `hello`: + + ``` + $ flask --app hello run + ``` + + or: === "Bash" @@ -106,7 +112,11 @@ should be started before execute `flask run`. See the note below for more detail ``` Similarly, If your application instance or application factory function stored in - `mypkg/__init__.py`, you can set `FLASK_APP` to the package name: + `mypkg/__init__.py`, you can pass the package name: + + ``` + $ flask --app mypkg run + ``` === "Bash" @@ -127,7 +137,13 @@ should be started before execute `flask run`. See the note below for more detail ``` However, if the application instance or application factory function store in - `mypkg/myapp.py`, you will need to set `FLASK_APP` to: + `mypkg/myapp.py`, you will need to use: + + ``` + $ flask --app mypkg.myapp run + ``` + + or: === "Bash" @@ -177,29 +193,35 @@ $ flask run --reload We highly recommend enabling "debug mode" when developing Flask application. See the note below for the details. -??? note "Enable the debug mode with `FLASK_ENV`" +??? note "Enable the debug mode" Flask can automatically restart and reload the application when code changes and display useful debug information for errors. To enable these features - in your Flask application, we will need to set the environment variable - `FLASK_ENV` to `development`: + in your Flask application, we will need to use the `--debug` option: + + ``` + $ flask --debug run + ``` + + If you are not using the latest Flask version (>2.2), you will need to set + the environment variable `FLASK_DEBUG` to `True` instead: === "Bash" ```bash - $ export FLASK_ENV=development + $ export FLASK_DEBUG=True ``` === "Windows CMD" ``` - > set FLASK_ENV=development + > set FLASK_DEBUG=True ``` === "Powershell" ``` - > $env:FLASK_APP="development" + > $env:FLASK_DEBUG="True" ``` See *[Debug Mode][_debug_mode]{target=_blank}* for more details. @@ -421,8 +443,8 @@ from apiflask import APIFlask app = APIFlask(__name__) @app.get('/') -@app.input(FooSchema) -@app.output(BarSchema) +@app.input(Foo) +@app.output(Bar) def hello(): return {'message': 'Hello'} ``` @@ -435,8 +457,8 @@ from apiflask import APIFlask, input, output app = APIFlask(__name__) @app.get('/') -@input(FooSchema) -@output(BarSchema) +@input(Foo) +@output(Bar) def hello(): return {'message': 'Hello'} ``` @@ -461,7 +483,7 @@ from apiflask.fields import Integer, String from apiflask.validators import Length, OneOf -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) ``` @@ -479,7 +501,7 @@ from apiflask.fields import Integer, String from apiflask.validators import Length, OneOf -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) ``` @@ -492,7 +514,7 @@ from apiflask.fields import Integer, String from apiflask.validators import Length, OneOf -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) ``` @@ -507,7 +529,7 @@ from apiflask.fields import Integer, String from apiflask.validators import Length, OneOf -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) ``` @@ -545,13 +567,13 @@ from apiflask.validators import Length, OneOf app = APIFlask(__name__) -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) @app.post('/pets') -@app.input(PetInSchema) +@app.input(PetIn) def create_pet(data): print(data) return {'message': 'created'}, 201 @@ -570,8 +592,8 @@ you can do something like this to create an ORM model instance: ```python hl_lines="5" @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema) +@app.input(PetIn) +@app.output(PetOut) def create_pet(pet_id, data): pet = Pet(**data) return pet @@ -581,8 +603,8 @@ or update an ORM model class instance like this: ```python hl_lines="6 7" @app.patch('/pets/') -@app.input(PetInSchema) -@app.output(PetOutSchema) +@app.input(PetIn) +@app.output(PetOut) def update_pet(pet_id, data): pet = Pet.query.get(pet_id) for attr, value in data.items(): @@ -619,7 +641,7 @@ Similarly, we can define a schema for output data with `@app.output` decorator. from apiflask.fields import String, Integer -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -645,14 +667,14 @@ from apiflask.fields import String, Integer app = APIFlask(__name__) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): return { 'name': 'Coco', @@ -665,8 +687,8 @@ status code with the `status_code` argument: ```python hl_lines="3" @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, status_code=201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): data['id'] = 2 return data @@ -675,7 +697,7 @@ def create_pet(data): Or just: ```python -@app.output(PetOutSchema, 201) +@app.output(PetOut, status_code=201) ``` If you want to return a 204 response, you can use the `EmptySchema` from `apiflask.schemas`: @@ -685,7 +707,7 @@ from apiflask.schemas import EmptySchema @app.delete('/pets/') -@app.output(EmptySchema, 204) +@app.output(EmptySchema, status_code=204) def delete_pet(pet_id): return '' ``` @@ -694,7 +716,7 @@ From version 0.4.0, you can use a empty dict to represent empty schema: ```python hl_lines="2" @app.delete('/pets/') -@app.output({}, 204) +@app.output({}, status_code=204) def delete_pet(pet_id): return '' ``` @@ -709,8 +731,8 @@ def delete_pet(pet_id): ```python hl_lines="4" @app.put('/pets/') - @app.input(PetInSchema) - @app.output(PetOutSchema) # 200 + @app.input(PetIn) + @app.output(PetOut) # 200 @app.doc(responses=[204, 404]) def update_pet(pet_id, data): pass @@ -735,7 +757,7 @@ from apiflask import Schema from apiflask.fields import String, Integer -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -745,7 +767,7 @@ Now you can return a dict: ```python @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): return { 'id': 1, @@ -758,7 +780,7 @@ or you can return an ORM model instance directly: ```python hl_lines="5" @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): pet = Pet.query.get(pet_id) return pet @@ -787,7 +809,7 @@ class Pet(Model): `data_key` to declare the actual key name to dump to: ```python - class UserOutSchema(Schema): + class UserOut(Schema): phone = String(data_key='phone_number') ``` @@ -796,7 +818,7 @@ class Pet(Model): Similarly, you can tell APIFlask to load from different key in input schema: ```python - class UserInSchema(Schema): + class UserIn(Schema): phone = String(data_key='phone_number') ``` @@ -807,8 +829,8 @@ you can pass a `status_code` argument in the `@app.output` decorator: ```python hl_lines="3" @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): # ... return pet @@ -819,8 +841,8 @@ You don't need to return the same status code in the end of the view function ```python hl_lines="8" @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): # ... # equals to: @@ -833,8 +855,8 @@ of the return tuple: ```python hl_lines="8" @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): # ... # equals to: @@ -1097,19 +1119,19 @@ instead of class: @app.route('/pets/', endpoint='pet') class Pet(MethodView): - @app.output(PetOutSchema) + @app.output(PetOut) @app.doc(summary='Get a Pet') def get(self, pet_id): # ... @app.auth_required(auth) - @app.input(PetInSchema) - @app.output(PetOutSchema) + @app.input(PetIn) + @app.output(PetOut) def put(self, pet_id, data): # ... - @app.input(PetInSchema(partial=True)) - @app.output(PetOutSchema) + @app.input(PetIn(partial=True)) + @app.output(PetOut) def patch(self, pet_id, data): # ... ``` @@ -1124,19 +1146,19 @@ class Pet(MethodView): decorators = [auth_required(auth), doc(responses=[404])] - @app.output(PetOutSchema) + @app.output(PetOut) @app.doc(summary='Get a Pet') def get(self, pet_id): # ... @app.auth_required(auth) - @app.input(PetInSchema) - @app.output(PetOutSchema) + @app.input(PetIn) + @app.output(PetOut) def put(self, pet_id, data): # ... - @app.input(PetInSchema(partial=True)) - @app.output(PetOutSchema) + @app.input(PetIn(partial=True)) + @app.output(PetOut) def patch(self, pet_id, data): # ... ``` diff --git a/examples/auth/token_auth/app.py b/examples/auth/token_auth/app.py index cfabb36..f73694b 100644 --- a/examples/auth/token_auth/app.py +++ b/examples/auth/token_auth/app.py @@ -51,12 +51,12 @@ def verify_token(token: str) -> t.Union[User, None]: return user -class TokenSchema(Schema): +class Token(Schema): token = String() @app.post('/token/') -@app.output(TokenSchema) +@app.output(Token) def get_token(id: int): if get_user_by_id(id) is None: abort(404) diff --git a/examples/base_response/app.py b/examples/base_response/app.py index 5345e74..4576f37 100644 --- a/examples/base_response/app.py +++ b/examples/base_response/app.py @@ -5,13 +5,13 @@ app = APIFlask(__name__) -class BaseResponseSchema(Schema): +class BaseResponse(Schema): data = Field() # the data key message = String() code = Integer() -app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema +app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse # the data key should match the data field name in the base response schema # defaults to "data" app.config['BASE_RESPONSE_DATA_KEY '] = 'data' @@ -23,12 +23,12 @@ class BaseResponseSchema(Schema): ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -45,7 +45,7 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): if pet_id > len(pets) - 1 or pets[pet_id].get('deleted'): abort(404) @@ -57,7 +57,7 @@ def get_pet(pet_id): @app.get('/pets') -@app.output(PetOutSchema(many=True)) +@app.output(PetOut(many=True)) def get_pets(): return { 'data': pets, @@ -67,8 +67,8 @@ def get_pets(): @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): pet_id = len(pets) data['id'] = pet_id @@ -81,8 +81,8 @@ def create_pet(data): @app.patch('/pets/') -@app.input(PetInSchema(partial=True)) -@app.output(PetOutSchema) +@app.input(PetIn(partial=True)) +@app.output(PetOut) def update_pet(pet_id, data): if pet_id > len(pets) - 1: abort(404) @@ -96,7 +96,7 @@ def update_pet(pet_id, data): @app.delete('/pets/') -@app.output({}, 204) +@app.output({}, status_code=204) def delete_pet(pet_id): if pet_id > len(pets) - 1: abort(404) diff --git a/examples/basic/app.py b/examples/basic/app.py index 7f6ce7f..3fa5fb9 100644 --- a/examples/basic/app.py +++ b/examples/basic/app.py @@ -11,12 +11,12 @@ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -28,7 +28,7 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): if pet_id > len(pets) - 1 or pets[pet_id].get('deleted'): abort(404) @@ -36,14 +36,14 @@ def get_pet(pet_id): @app.get('/pets') -@app.output(PetOutSchema(many=True)) +@app.output(PetOut(many=True)) def get_pets(): return pets @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): pet_id = len(pets) data['id'] = pet_id @@ -52,8 +52,8 @@ def create_pet(data): @app.patch('/pets/') -@app.input(PetInSchema(partial=True)) -@app.output(PetOutSchema) +@app.input(PetIn(partial=True)) +@app.output(PetOut) def update_pet(pet_id, data): if pet_id > len(pets) - 1: abort(404) @@ -63,7 +63,7 @@ def update_pet(pet_id, data): @app.delete('/pets/') -@app.output({}, 204) +@app.output({}, status_code=204) def delete_pet(pet_id): if pet_id > len(pets) - 1: abort(404) diff --git a/examples/blueprint_tags/app.py b/examples/blueprint_tags/app.py index 509a9c6..e69bfc8 100755 --- a/examples/blueprint_tags/app.py +++ b/examples/blueprint_tags/app.py @@ -20,12 +20,12 @@ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -37,7 +37,7 @@ def say_hello(): @pet_bp.get('/pets/') -@pet_bp.output(PetOutSchema) +@pet_bp.output(PetOut) def get_pet(pet_id): if pet_id > len(pets) - 1 or pets[pet_id].get('deleted'): abort(404) @@ -45,14 +45,14 @@ def get_pet(pet_id): @pet_bp.get('/pets') -@pet_bp.output(PetOutSchema(many=True)) +@pet_bp.output(PetOut(many=True)) def get_pets(): return pets @pet_bp.post('/pets') -@pet_bp.input(PetInSchema) -@pet_bp.output(PetOutSchema, 201) +@pet_bp.input(PetIn) +@pet_bp.output(PetOut, status_code=201) def create_pet(data): pet_id = len(pets) data['id'] = pet_id @@ -61,8 +61,8 @@ def create_pet(data): @pet_bp.patch('/pets/') -@pet_bp.input(PetInSchema(partial=True)) -@pet_bp.output(PetOutSchema) +@pet_bp.input(PetIn(partial=True)) +@pet_bp.output(PetOut) def update_pet(pet_id, data): if pet_id > len(pets) - 1: abort(404) @@ -72,7 +72,7 @@ def update_pet(pet_id, data): @pet_bp.delete('/pets/') -@pet_bp.output({}, 204) +@pet_bp.output({}, status_code=204) def delete_pet(pet_id): if pet_id > len(pets) - 1: abort(404) diff --git a/examples/cbv/app.py b/examples/cbv/app.py index 51a2839..5379a38 100644 --- a/examples/cbv/app.py +++ b/examples/cbv/app.py @@ -12,12 +12,12 @@ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -35,15 +35,15 @@ def get(self): @app.route('/pets/') class Pet(MethodView): - @app.output(PetOutSchema) + @app.output(PetOut) def get(self, pet_id): """Get a pet""" if pet_id > len(pets) - 1 or pets[pet_id].get('deleted'): abort(404) return pets[pet_id] - @app.input(PetInSchema(partial=True)) - @app.output(PetOutSchema) + @app.input(PetIn(partial=True)) + @app.output(PetOut) def patch(self, pet_id, data): """Update a pet""" if pet_id > len(pets) - 1: @@ -52,7 +52,7 @@ def patch(self, pet_id, data): pets[pet_id][attr] = value return pets[pet_id] - @app.output({}, 204) + @app.output({}, status_code=204) def delete(self, pet_id): """Delete a pet""" if pet_id > len(pets) - 1: @@ -65,13 +65,13 @@ def delete(self, pet_id): @app.route('/pets') class Pets(MethodView): - @app.output(PetOutSchema(many=True)) + @app.output(PetOut(many=True)) def get(self): """Get all pets""" return pets - @app.input(PetInSchema) - @app.output(PetOutSchema, 201) + @app.input(PetIn) + @app.output(PetOut, status_code=201) def post(self, data): """Create a pet""" pet_id = len(pets) diff --git a/examples/dataclass/app.py b/examples/dataclass/app.py index 31106b0..28ca907 100644 --- a/examples/dataclass/app.py +++ b/examples/dataclass/app.py @@ -52,7 +52,7 @@ def get_pets(): @app.post('/pets') @app.input(PetIn.Schema) -@app.output(PetOut.Schema, 201) +@app.output(PetOut.Schema, status_code=201) def create_pet(pet: PetIn): pet_id = len(pets) pets.append({ @@ -77,7 +77,7 @@ def update_pet(pet_id, pet: PetIn): @app.delete('/pets/') -@app.output({}, 204) +@app.output({}, status_code=204) def delete_pet(pet_id): if pet_id > len(pets) - 1: abort(404) diff --git a/examples/openapi/app.py b/examples/openapi/app.py index 482479a..1c5e760 100755 --- a/examples/openapi/app.py +++ b/examples/openapi/app.py @@ -96,7 +96,7 @@ ] -class PetInSchema(Schema): +class PetIn(Schema): name = String( required=True, validate=Length(0, 10), @@ -109,7 +109,7 @@ class PetInSchema(Schema): ) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer(metadata={'title': 'Pet ID', 'description': 'The ID of the pet.'}) name = String(metadata={'title': 'Pet Name', 'description': 'The name of the pet.'}) category = String(metadata={'title': 'Pet Category', 'description': 'The category of the pet.'}) @@ -129,7 +129,7 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema, description='The pet with given ID') +@app.output(PetOut, description='The pet with given ID') @app.doc(tags=['Pet'], operation_id='getPet') def get_pet(pet_id): """Get a Pet @@ -142,7 +142,7 @@ def get_pet(pet_id): @app.get('/pets') -@app.output(PetOutSchema(many=True), description='A list of pets') +@app.output(PetOut(many=True), description='A list of pets') @app.doc(tags=['Pet']) def get_pets(): """Get All Pet @@ -153,9 +153,9 @@ def get_pets(): @app.post('/pets') -@app.input(PetInSchema) +@app.input(PetIn) @app.output( - PetOutSchema, + PetOut, 201, description='The pet you just created', links={'getPetById': { @@ -178,8 +178,8 @@ def create_pet(data): @app.patch('/pets/') -@app.input(PetInSchema(partial=True)) -@app.output(PetOutSchema, description='The updated pet') +@app.input(PetIn(partial=True)) +@app.output(PetOut, description='The updated pet') @app.doc(tags=['Pet']) def update_pet(pet_id, data): """Update a Pet @@ -194,7 +194,7 @@ def update_pet(pet_id, data): @app.delete('/pets/') -@app.output({}, 204, description='Empty') +@app.output({}, status_code=204, description='Empty') @app.doc(tags=['Pet']) def delete_pet(pet_id): """Delete a Pet diff --git a/examples/orm/app.py b/examples/orm/app.py index ff41d0a..7809fd5 100644 --- a/examples/orm/app.py +++ b/examples/orm/app.py @@ -31,12 +31,12 @@ def init_database(): db.session.commit() -class PetInSchema(Schema): +class PetIn(Schema): name = String(required=True, validate=Length(0, 10)) category = String(required=True, validate=OneOf(['dog', 'cat'])) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() @@ -48,20 +48,20 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): return PetModel.query.get_or_404(pet_id) @app.get('/pets') -@app.output(PetOutSchema(many=True)) +@app.output(PetOut(many=True)) def get_pets(): return PetModel.query.all() @app.post('/pets') -@app.input(PetInSchema) -@app.output(PetOutSchema, 201) +@app.input(PetIn) +@app.output(PetOut, status_code=201) def create_pet(data): pet = PetModel(**data) db.session.add(pet) @@ -70,8 +70,8 @@ def create_pet(data): @app.patch('/pets/') -@app.input(PetInSchema(partial=True)) -@app.output(PetOutSchema) +@app.input(PetIn(partial=True)) +@app.output(PetOut) def update_pet(pet_id, data): pet = PetModel.query.get_or_404(pet_id) for attr, value in data.items(): @@ -81,7 +81,7 @@ def update_pet(pet_id, data): @app.delete('/pets/') -@app.output({}, 204) +@app.output({}, status_code=204) def delete_pet(pet_id): pet = PetModel.query.get_or_404(pet_id) db.session.delete(pet) diff --git a/examples/pagination/app.py b/examples/pagination/app.py index c70de35..214cf51 100644 --- a/examples/pagination/app.py +++ b/examples/pagination/app.py @@ -29,19 +29,19 @@ def init_database(): db.session.commit() -class PetQuerySchema(Schema): +class PetQuery(Schema): page = Integer(load_default=1) per_page = Integer(load_default=20, validate=Range(max=30)) -class PetOutSchema(Schema): +class PetOut(Schema): id = Integer() name = String() category = String() -class PetsOutSchema(Schema): - pets = List(Nested(PetOutSchema)) +class PetsOut(Schema): + pets = List(Nested(PetOut)) pagination = Nested(PaginationSchema) @@ -51,14 +51,14 @@ def say_hello(): @app.get('/pets/') -@app.output(PetOutSchema) +@app.output(PetOut) def get_pet(pet_id): return PetModel.query.get_or_404(pet_id) @app.get('/pets') -@app.input(PetQuerySchema, 'query') -@app.output(PetsOutSchema) +@app.input(PetQuery, location='query') +@app.output(PetsOut) def get_pets(query): pagination = PetModel.query.paginate( page=query['page'], diff --git a/mkdocs.yml b/mkdocs.yml index 66ca0c4..81e1a06 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,7 +27,9 @@ extra: - name: 简体中文 link: https://zh.apiflask.com lang: zh -copyright: Copyright © 2021 Grey Li +copyright: + Copyright © 2021 Grey Li
+ Hosted with Netlify markdown_extensions: - admonition - pymdownx.details diff --git a/src/apiflask/__init__.py b/src/apiflask/__init__.py index ac18fca..1ffb900 100644 --- a/src/apiflask/__init__.py +++ b/src/apiflask/__init__.py @@ -11,4 +11,4 @@ from .security import HTTPBasicAuth as HTTPBasicAuth from .security import HTTPTokenAuth as HTTPTokenAuth -__version__ = '1.2.0dev' +__version__ = '1.2.0.dev' diff --git a/src/apiflask/app.py b/src/apiflask/app.py index 29a7818..ca8514c 100644 --- a/src/apiflask/app.py +++ b/src/apiflask/app.py @@ -265,11 +265,11 @@ def __init__( import_name: str, title: str = 'APIFlask', version: str = '0.1.0', - spec_path: str = '/openapi.json', - docs_path: str = '/docs', - docs_oauth2_redirect_path: str = '/docs/oauth2-redirect', + spec_path: t.Optional[str] = '/openapi.json', + docs_path: t.Optional[str] = '/docs', + docs_oauth2_redirect_path: t.Optional[str] = '/docs/oauth2-redirect', docs_ui: str = 'swagger-ui', - redoc_path: str = '/redoc', + redoc_path: t.Optional[str] = '/redoc', openapi_blueprint_url_prefix: t.Optional[str] = None, json_errors: bool = True, enable_openapi: bool = True, @@ -396,8 +396,8 @@ def dispatch_request(self) -> ResponseReturnValueType: # type: ignore ```python @app.get('/pets///') # -> name, pet_id, age - @app.input(QuerySchema) # -> query - @app.output(PetSchema) # -> pet + @app.input(Query) # -> query + @app.output(Pet) # -> pet def get_pet(name, pet_id, age, query, pet): pass ``` @@ -560,7 +560,7 @@ def _register_openapi_blueprint(self) -> None: """Register a blueprint for OpenAPI support. The name of the blueprint is "openapi". This blueprint will hold the view - functions for spec file, Swagger UI and Redoc. + functions for spec file and API docs. *Version changed: 1.1.0* @@ -578,7 +578,7 @@ def _register_openapi_blueprint(self) -> None: if self.spec_path: @bp.route(self.spec_path) - def spec() -> ResponseReturnValueType: + def spec(): if self.config['SPEC_FORMAT'] == 'json': response = jsonify(self._get_spec('json')) response.mimetype = self.config['JSON_SPEC_MIMETYPE'] @@ -594,7 +594,7 @@ def spec() -> ResponseReturnValueType: ) @bp.route(self.docs_path) - def docs() -> str: + def docs(): return render_template_string( ui_templates[self.docs_ui], title=self.title, @@ -610,7 +610,7 @@ def swagger_ui_oauth_redirect() -> str: if self.redoc_path: @bp.route(self.redoc_path) - def redoc() -> str: + def redoc(): warnings.warn( 'The `/redoc` path and `redoc_path` parameter is deprecated ' 'and will be removed in 2.0, Set `APIFlask(docs_ui="redoc")` ' diff --git a/src/apiflask/fields.py b/src/apiflask/fields.py index 3adc3ac..08d1939 100644 --- a/src/apiflask/fields.py +++ b/src/apiflask/fields.py @@ -51,12 +51,12 @@ class File(Field): from apiflask.fields import File - class ImageSchema(Schema): + class Image(Schema): image = File() @app.post('/images') - @app.input(ImageSchema, location='files') + @app.input(Image, location='files') def upload_image(data): f = data['image'] # use `secure_filename` to clean the filename, notice it will only keep ascii characters @@ -78,12 +78,12 @@ def upload_image(data): from apiflask.fields import String, File - class ProfileInSchema(Schema): + class ProfileIn(Schema): name = String() avatar = File() @app.post('/profiles') - @app.input(ProfileInSchema, location='form_and_files') + @app.input(ProfileIn, location='form_and_files') def create_profile(data): avatar_file = data['avatar'] name = data['name'] diff --git a/src/apiflask/helpers.py b/src/apiflask/helpers.py index a46b4cd..8e8491d 100644 --- a/src/apiflask/helpers.py +++ b/src/apiflask/helpers.py @@ -49,19 +49,19 @@ def pagination_builder(pagination: PaginationType, **kwargs: t.Any) -> dict: ... - class PetQuerySchema(Schema): + class PetQuery(Schema): page = Integer(load_default=1) per_page = Integer(load_default=20, validate=Range(max=30)) - class PetsOutSchema(Schema): - pets = List(Nested(PetOutSchema)) + class PetsOut(Schema): + pets = List(Nested(PetOut)) pagination = Nested(PaginationSchema) @app.get('/pets') - @app.input(PetQuerySchema, 'query') - @app.output(PetsOutSchema) + @app.input(PetQuery, location='query') + @app.output(PetsOut) def get_pets(query): pagination = PetModel.query.paginate( page=query['page'], diff --git a/src/apiflask/route.py b/src/apiflask/route.py index 2138f6d..c365ff8 100644 --- a/src/apiflask/route.py +++ b/src/apiflask/route.py @@ -1,6 +1,6 @@ import typing as t -from flask.views import MethodViewType +from flask.views import MethodView from .openapi import get_path_description from .openapi import get_path_summary @@ -79,7 +79,7 @@ def add_url_rule( # a function returned by MethodViewClass.as_view() is_view_class = True view_class = view_func.view_class # type: ignore - elif isinstance(view_func, MethodViewType): + elif isinstance(view_func, type(MethodView)): # a MethodView class passed with the route decorator is_view_class = True view_class = view_func # type: ignore diff --git a/src/apiflask/scaffold.py b/src/apiflask/scaffold.py index 099932e..27ac754 100644 --- a/src/apiflask/scaffold.py +++ b/src/apiflask/scaffold.py @@ -7,7 +7,7 @@ from flask import current_app from flask import jsonify from flask import Response -from flask.views import MethodViewType +from flask.views import MethodView from marshmallow import ValidationError as MarshmallowValidationError from webargs.flaskparser import FlaskParser as BaseFlaskParser from webargs.multidictproxy import MultiDictProxy @@ -116,7 +116,7 @@ def _method_route(self, method: str, rule: str, options: t.Any): raise RuntimeError('Use the "route" decorator to use the "methods" argument.') def decorator(f): - if isinstance(f, MethodViewType): + if isinstance(f, type(MethodView)): raise RuntimeError( 'The route shortcuts cannot be used with "MethodView" classes, ' 'use the "route" decorator instead.' @@ -238,7 +238,7 @@ def input( app = APIFlask(__name__) @app.get('/') - @app.input(PetInSchema) + @app.input(PetIn) def hello(parsed_and_validated_input_data): print(parsed_and_validated_input_data) return 'Hello'! @@ -362,7 +362,7 @@ def output( app = APIFlask(__name__) @app.get('/') - @app.output(PetOutSchema) + @app.output(PetOut) def hello(): return the_dict_or_object_match_petout_schema ``` diff --git a/src/apiflask/schemas.py b/src/apiflask/schemas.py index 3056b45..861ed05 100644 --- a/src/apiflask/schemas.py +++ b/src/apiflask/schemas.py @@ -66,7 +66,7 @@ def delete_foo(): ```python @app.delete('/foo') - @app.output({}, 204) + @app.output({}, status_code=204) def delete_foo(): return '' ``` diff --git a/src/apiflask/settings.py b/src/apiflask/settings.py index e99579c..2cab8a4 100644 --- a/src/apiflask/settings.py +++ b/src/apiflask/settings.py @@ -44,16 +44,16 @@ HTTP_ERROR_SCHEMA: OpenAPISchemaType = http_error_schema BASE_RESPONSE_SCHEMA: t.Optional[OpenAPISchemaType] = None BASE_RESPONSE_DATA_KEY: str = 'data' -# Swagger UI and Redoc +# API docs DOCS_FAVICON: str = 'https://apiflask.com/_assets/favicon.png' REDOC_USE_GOOGLE_FONT: bool = True -REDOC_STANDALONE_JS: str = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/\ +REDOC_STANDALONE_JS: str = 'https://cdn.redoc.ly/redoc/latest/bundles/\ redoc.standalone.js' # TODO: rename to REDOC_JS REDOC_CONFIG: t.Optional[dict] = None -SWAGGER_UI_CSS: str = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css' -SWAGGER_UI_BUNDLE_JS: str = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/\ +SWAGGER_UI_CSS: str = 'https://unpkg.com/swagger-ui-dist/swagger-ui.css' +SWAGGER_UI_BUNDLE_JS: str = 'https://unpkg.com/swagger-ui-dist/\ swagger-ui-bundle.js' # TODO: rename to SWAGGER_UI_JS -SWAGGER_UI_STANDALONE_PRESET_JS: str = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/\ +SWAGGER_UI_STANDALONE_PRESET_JS: str = 'https://unpkg.com/swagger-ui-dist/\ swagger-ui-standalone-preset.js' # TODO: rename to SWAGGER_UI_STANDALONE_JS SWAGGER_UI_LAYOUT: str = 'BaseLayout' SWAGGER_UI_CONFIG: t.Optional[dict] = None diff --git a/src/apiflask/ui_templates.py b/src/apiflask/ui_templates.py index 8ec6821..da254c1 100644 --- a/src/apiflask/ui_templates.py +++ b/src/apiflask/ui_templates.py @@ -207,6 +207,12 @@ /toys/') - @app.input(QuerySchema, 'query') - @app.input(PaginationSchema, 'query') - @app.input(PetSchema) + @app.input(Query, location='query') + @app.input(Pagination, location='query') + @app.input(Pet) def pets(pet_id, toy_id, query, pagination, body): return {'pet_id': pet_id, 'toy_id': toy_id, 'foo': query['foo'], 'bar': query['bar'], 'pagination': pagination, **body} @app.route('/animals//toys/') class Animals(MethodView): - @app.input(QuerySchema, 'query') - @app.input(PaginationSchema, 'query') - @app.input(PetSchema) + @app.input(Query, location='query') + @app.input(Pagination, location='query') + @app.input(Pet) def post(self, pet_id, toy_id, query, pagination, body): return {'pet_id': pet_id, 'toy_id': toy_id, 'foo': query['foo'], 'bar': query['bar'], 'pagination': pagination, **body} @@ -195,7 +195,7 @@ def test_dispatch_static_request(app, client): # positional arguments @app.get('/mystatic/') - @app.input(FooSchema) + @app.input(Foo) def mystatic(pet_id, foo): # endpoint: mystatic return {'pet_id': pet_id, 'foo': foo} @@ -237,19 +237,19 @@ def test_schema_name_resolver(app, client, resolver): app.schema_name_resolver = resolver @app.route('/foo') - @app.output(FooSchema) + @app.output(Foo) def foo(): pass @app.route('/bar') - @app.output(BarSchema(partial=True)) + @app.output(Bar(partial=True)) def bar(): pass spec = app.spec if resolver == schema_name_resolver1: - assert 'FooSchema' in spec['components']['schemas'] - assert 'BarSchema_' in spec['components']['schemas'] + assert 'Foo' in spec['components']['schemas'] + assert 'Bar_' in spec['components']['schemas'] elif resolver == schema_name_resolver2: assert 'Foo' in spec['components']['schemas'] assert 'BarPartial' in spec['components']['schemas'] diff --git a/tests/test_async.py b/tests/test_async.py index 7586c65..cfd4e7b 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,7 +1,7 @@ import pytest from openapi_spec_validator import validate_spec -from .schemas import FooSchema +from .schemas import Foo from apiflask import HTTPTokenAuth @@ -73,7 +73,7 @@ async def index(): def test_input_on_async_view(app, client): @app.post('/') - @app.input(FooSchema) + @app.input(Foo) async def index(data): return data @@ -86,7 +86,7 @@ async def index(data): def test_output_on_async_view(app, client): @app.get('/foo') - @app.output(FooSchema) + @app.output(Foo) async def foo(): return {'id': 1, 'name': 'foo'} @@ -107,8 +107,8 @@ def test_async_doc_input_and_output_decorator(app, client): @app.post('/') @app.doc(summary='Test Root Endpoint') - @app.input(FooSchema) - @app.output(FooSchema) + @app.input(Foo) + @app.output(Foo) async def index(data): return data diff --git a/tests/test_base_response.py b/tests/test_base_response.py index d0e79f9..1adb4ec 100644 --- a/tests/test_base_response.py +++ b/tests/test_base_response.py @@ -1,20 +1,20 @@ import pytest from openapi_spec_validator import validate_spec -from .schemas import FooSchema +from .schemas import Foo from apiflask import Schema from apiflask.fields import Field from apiflask.fields import Integer from apiflask.fields import String -class BaseResponseSchema(Schema): +class BaseResponse(Schema): message = String() status_code = Integer() data = Field() -class BadBaseResponseSchema(Schema): +class BadBaseResponse(Schema): message = String() status_code = Integer() some_data = Field() @@ -52,10 +52,10 @@ class BadBaseResponseSchema(Schema): def test_output_base_response(app, client): - app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema + app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse @app.get('/') - @app.output(FooSchema) + @app.output(Foo) def foo(): data = {'id': '123', 'name': 'test'} return {'message': 'Success.', 'status_code': '200', 'data': data} @@ -73,9 +73,9 @@ def foo(): @pytest.mark.parametrize( 'base_schema', [ - BaseResponseSchema, + BaseResponse, base_response_schema_dict, - BadBaseResponseSchema, + BadBaseResponse, bad_base_response_schema_dict, '', None @@ -85,7 +85,7 @@ def test_base_response_spec(app, client, base_schema): app.config['BASE_RESPONSE_SCHEMA'] = base_schema @app.get('/') - @app.output(FooSchema) + @app.output(Foo) def foo(): data = {'id': '123', 'name': 'test'} return {'message': 'Success.', 'status_code': '200', 'data': data} @@ -94,7 +94,7 @@ def foo(): with pytest.raises(TypeError) as e: app.spec assert 'marshmallow' in str(e.value) - elif base_schema in [BadBaseResponseSchema, bad_base_response_schema_dict]: + elif base_schema in [BadBaseResponse, bad_base_response_schema_dict]: with pytest.raises(RuntimeError) as e: app.spec assert 'data key' in str(e.value) @@ -108,7 +108,7 @@ def foo(): # TODO the output schema ref contains unused `'x-scope': ['']` field # it seems related to openapi-spec-validator: # https://github.com/p1c2u/openapi-spec-validator/issues/53 - if base_schema in [BaseResponseSchema, base_response_schema_dict]: + if base_schema in [BaseResponse, base_response_schema_dict]: properties = schema['properties'] assert properties['data']['$ref'] == schema_ref assert properties['status_code'] == {'type': 'integer'} @@ -121,11 +121,11 @@ def foo(): def test_base_response_data_key(app, client): pytest.skip() - app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema + app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse app.config['BASE_RESPONSE_DATA_KEY '] = 'data' @app.get('/') - @app.output(FooSchema) + @app.output(Foo) def foo(): data = {'id': '123', 'name': 'test'} return {'message': 'Success.', 'status_code': '200', 'info': data} @@ -135,10 +135,10 @@ def foo(): def test_base_response_204(app, client): - app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema + app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse @app.get('/') - @app.output({}, 204) + @app.output({}, status_code=204) def foo(): return '' @@ -149,10 +149,10 @@ def foo(): def test_base_response_many(app, client): - app.config['BASE_RESPONSE_SCHEMA'] = BaseResponseSchema + app.config['BASE_RESPONSE_SCHEMA'] = BaseResponse @app.get('/') - @app.output(FooSchema(many=True)) + @app.output(Foo(many=True)) def foo(): data = {'id': '123', 'name': 'test'} return {'message': 'Success.', 'status_code': '200', 'data': data} diff --git a/tests/test_decorator_doc.py b/tests/test_decorator_doc.py index 1417105..9fb435e 100644 --- a/tests/test_decorator_doc.py +++ b/tests/test_decorator_doc.py @@ -2,7 +2,7 @@ from flask.views import MethodView from openapi_spec_validator import validate_spec -from .schemas import FooSchema +from .schemas import Foo def test_doc_summary_and_description(app, client): @@ -146,7 +146,7 @@ def foo(): def test_doc_deprecated_with_methodview(app, client): @app.route('/foo') - class Foo(MethodView): + class FooAPI(MethodView): @app.doc(deprecated=True) def get(self): pass @@ -159,15 +159,15 @@ def get(self): def test_doc_responses(app, client): @app.route('/foo') - @app.input(FooSchema) - @app.output(FooSchema) + @app.input(Foo) + @app.output(Foo) @app.doc(responses={200: 'success', 400: 'bad', 404: 'not found', 500: 'server error'}) def foo(): pass @app.route('/bar') - @app.input(FooSchema) - @app.output(FooSchema) + @app.input(Foo) + @app.output(Foo) @app.doc(responses=[200, 400, 404, 500]) def bar(): pass @@ -205,17 +205,17 @@ def bar(): def test_doc_responses_with_methodview(app, client): @app.route('/foo') - class Foo(MethodView): - @app.input(FooSchema) - @app.output(FooSchema) + class FooAPI(MethodView): + @app.input(Foo) + @app.output(Foo) @app.doc(responses={200: 'success', 400: 'bad', 404: 'not found', 500: 'server error'}) def get(self): pass @app.route('/bar') - class Bar(MethodView): - @app.input(FooSchema) - @app.output(FooSchema) + class BarAPI(MethodView): + @app.input(Foo) + @app.output(Foo) @app.doc(responses=[200, 400, 404, 500]) def get(self): pass diff --git a/tests/test_decorator_input.py b/tests/test_decorator_input.py index aa14727..64cbb3e 100644 --- a/tests/test_decorator_input.py +++ b/tests/test_decorator_input.py @@ -5,24 +5,24 @@ from openapi_spec_validator import validate_spec from werkzeug.datastructures import FileStorage -from .schemas import BarSchema -from .schemas import FilesSchema -from .schemas import FooSchema -from .schemas import FormAndFilesSchema -from .schemas import FormSchema -from .schemas import QuerySchema +from .schemas import Bar +from .schemas import Files +from .schemas import Foo +from .schemas import Form +from .schemas import FormAndFiles +from .schemas import Query from apiflask.fields import String def test_input(app, client): @app.route('/foo', methods=['POST']) - @app.input(FooSchema) + @app.input(Foo) def foo(schema): return schema @app.route('/bar') class Bar(MethodView): - @app.input(FooSchema) + @app.input(Foo) def post(self, data): return data @@ -62,8 +62,8 @@ def post(self, data): def test_input_with_query_location(app, client): @app.route('/foo', methods=['POST']) - @app.input(FooSchema, location='query') - @app.input(BarSchema, location='query') + @app.input(Foo, location='query') + @app.input(Bar, location='query') def foo(schema, schema2): return {'name': schema['name'], 'name2': schema2['name2']} @@ -96,7 +96,7 @@ def foo(schema, schema2): def test_input_with_form_location(app, client): @app.post('/') - @app.input(FormSchema, location='form') + @app.input(Form, location='form') def index(form_data): return form_data @@ -116,7 +116,7 @@ def index(form_data): def test_input_with_files_location(app, client): @app.post('/') - @app.input(FilesSchema, location='files') + @app.input(Files, location='files') def index(files_data): data = {} if 'image' in files_data and isinstance(files_data['image'], FileStorage): @@ -148,7 +148,7 @@ def index(files_data): def test_input_with_form_and_files_location(app, client): @app.post('/') - @app.input(FormAndFilesSchema, location='form_and_files') + @app.input(FormAndFiles, location='form_and_files') def index(form_data): data = {} if 'name' in form_data: @@ -196,8 +196,8 @@ def index(form_data): def test_multiple_input_body_location(app, locations): with pytest.raises(RuntimeError): @app.route('/foo') - @app.input(FooSchema, locations[0]) - @app.input(BarSchema, locations[1]) + @app.input(Foo, locations[0]) + @app.input(Bar, locations[1]) def foo(query): pass @@ -205,7 +205,7 @@ def foo(query): def test_bad_input_location(app): with pytest.raises(ValueError): @app.route('/foo') - @app.input(QuerySchema, 'bad') + @app.input(Query, location='bad') def foo(query): pass @@ -306,22 +306,22 @@ def test_input_body_example(app, client): } @app.post('/foo') - @app.input(FooSchema, example=example) + @app.input(Foo, example=example) def foo(): pass @app.post('/bar') - @app.input(FooSchema, examples=examples) + @app.input(Foo, examples=examples) def bar(): pass @app.route('/baz') class Baz(MethodView): - @app.input(FooSchema, example=example) + @app.input(Foo, example=example) def get(self): pass - @app.input(FooSchema, examples=examples) + @app.input(Foo, examples=examples) def post(self): pass diff --git a/tests/test_decorator_output.py b/tests/test_decorator_output.py index 8fd65d0..178a731 100644 --- a/tests/test_decorator_output.py +++ b/tests/test_decorator_output.py @@ -2,25 +2,25 @@ from flask.views import MethodView from openapi_spec_validator import validate_spec -from .schemas import FooSchema -from .schemas import QuerySchema +from .schemas import Foo +from .schemas import Query from apiflask.fields import String def test_output(app, client): @app.route('/foo') - @app.output(FooSchema) + @app.output(Foo) def foo(): return {'name': 'bar'} @app.route('/bar') - @app.output(FooSchema, status_code=201) + @app.output(Foo, status_code=201) def bar(): return {'name': 'foo'} @app.route('/baz') - @app.input(QuerySchema, 'query') - @app.output(FooSchema, status_code=201) + @app.input(Query, location='query') + @app.output(Foo, status_code=201) def baz(query): if query['id'] == 1: return {'name': 'baz'}, 202 @@ -61,17 +61,17 @@ def baz(query): def test_output_with_methodview(app, client): @app.route('/') - class Foo(MethodView): - @app.output(FooSchema) + class FooAPI(MethodView): + @app.output(Foo) def get(self): return {'name': 'bar'} - @app.output(FooSchema, status_code=201) + @app.output(Foo, status_code=201) def post(self): return {'name': 'foo'} - @app.input(QuerySchema, 'query') - @app.output(FooSchema, status_code=201) + @app.input(Query, location='query') + @app.output(Foo, status_code=201) def delete(self, query): if query['id'] == 1: return {'name': 'baz'}, 202 @@ -184,12 +184,12 @@ def test_output_body_example(app, client): } @app.get('/foo') - @app.output(FooSchema, example=example) + @app.output(Foo, example=example) def foo(): pass @app.get('/bar') - @app.output(FooSchema, examples=examples) + @app.output(Foo, examples=examples) def bar(): pass @@ -204,13 +204,13 @@ def bar(): def test_output_with_empty_dict_as_schema(app, client): @app.delete('/foo') - @app.output({}, 204) + @app.output({}, status_code=204) def delete_foo(): return '' @app.route('/bar') class Bar(MethodView): - @app.output({}, 204) + @app.output({}, status_code=204) def delete(self): return '' @@ -228,7 +228,7 @@ def delete(self): def test_output_response_object_directly(app, client): @app.get('/foo') - @app.output(FooSchema) + @app.output(Foo) def foo(): return make_response({'message': 'hello'}) @@ -250,7 +250,7 @@ def test_response_links(app, client): } @app.get('/foo') - @app.output(FooSchema, links=links) + @app.output(Foo, links=links) def foo(): pass @@ -274,7 +274,7 @@ def add_links(spec): return spec @app.get('/foo') - @app.output(FooSchema, links=links) + @app.output(Foo, links=links) def foo(): pass diff --git a/tests/test_fields.py b/tests/test_fields.py index ab2ca3b..50e3712 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -2,13 +2,13 @@ from werkzeug.datastructures import FileStorage -from .schemas import FilesListSchema -from .schemas import FilesSchema +from .schemas import Files +from .schemas import FilesList def test_file_field(app, client): @app.post('/') - @app.input(FilesSchema, location='files') + @app.input(Files, location='files') def index(files_data): data = {} if 'image' in files_data and isinstance(files_data['image'], FileStorage): @@ -44,7 +44,7 @@ def index(files_data): def test_multiple_file_field(app, client): @app.post('/') - @app.input(FilesListSchema, location='files') + @app.input(FilesList, location='files') def index(files_list_data): data = {'images': True} for f in files_list_data['images']: diff --git a/tests/test_openapi_basic.py b/tests/test_openapi_basic.py index 703745c..34e7e11 100644 --- a/tests/test_openapi_basic.py +++ b/tests/test_openapi_basic.py @@ -3,9 +3,9 @@ import pytest from openapi_spec_validator import validate_spec -from .schemas import BarSchema -from .schemas import BazSchema -from .schemas import FooSchema +from .schemas import Bar +from .schemas import Baz +from .schemas import Foo from apiflask import Schema as BaseSchema from apiflask.fields import Integer @@ -46,7 +46,7 @@ def test_get_spec_force_update(app): app._get_spec() @app.route('/foo') - @app.output(FooSchema) + @app.output(Foo) def foo(): pass @@ -61,7 +61,7 @@ def test_spec_attribute(app): spec = app._get_spec() @app.route('/foo') - @app.output(FooSchema) + @app.output(Foo) def foo(): pass @@ -71,17 +71,17 @@ def foo(): def test_spec_schemas(app): @app.route('/foo') - @app.output(FooSchema(partial=True)) + @app.output(Foo(partial=True)) def foo(): pass @app.route('/bar') - @app.output(BarSchema(many=True)) + @app.output(Bar(many=True)) def bar(): pass @app.route('/baz') - @app.output(BazSchema) + @app.output(Baz) def baz(): pass @@ -154,7 +154,7 @@ def bare(): pass @app.get('/bar') - @app.input(FooSchema) + @app.input(Foo) def only_input(): pass @@ -164,7 +164,7 @@ def only_doc(): pass @app.get('/eggs') - @app.output(FooSchema, 204) + @app.output(Foo, status_code=204) def output_204(): pass diff --git a/tests/test_openapi_paths.py b/tests/test_openapi_paths.py index bf4e891..5095f33 100644 --- a/tests/test_openapi_paths.py +++ b/tests/test_openapi_paths.py @@ -1,20 +1,20 @@ from openapi_spec_validator import validate_spec -from .schemas import FooSchema -from .schemas import HeaderSchema -from .schemas import PaginationSchema -from .schemas import QuerySchema +from .schemas import Foo +from .schemas import Header +from .schemas import Pagination +from .schemas import Query def test_spec_path_summary_description_from_docs(app, client): @app.route('/users') - @app.output(FooSchema) + @app.output(Foo) def get_users(): """Get Users""" pass @app.route('/users/', methods=['PUT']) - @app.output(FooSchema) + @app.output(Foo) def update_user(id): """Update User @@ -34,22 +34,22 @@ def update_user(id): def test_spec_path_parameters_registration(app, client): @app.route('/strings/') - @app.output(FooSchema) + @app.output(Foo) def get_string(some_string): pass @app.route('/floats/', methods=['POST']) - @app.output(FooSchema) + @app.output(Foo) def get_float(some_float): pass @app.route('/integers/', methods=['PUT']) - @app.output(FooSchema) + @app.output(Foo) def get_integer(some_integer): pass @app.route('/users//articles/') - @app.output(FooSchema) + @app.output(Foo) def get_article(user_id, article_id): pass @@ -74,17 +74,17 @@ def get_article(user_id, article_id): def test_spec_path_summary_auto_generation(app, client): @app.route('/users') - @app.output(FooSchema) + @app.output(Foo) def get_users(): pass @app.route('/users/', methods=['PUT']) - @app.output(FooSchema) + @app.output(Foo) def update_user(id): pass @app.route('/users/', methods=['DELETE']) - @app.output(FooSchema) + @app.output(Foo) def delete_user(id): """Summary from Docs @@ -106,27 +106,27 @@ def delete_user(id): def test_path_arguments_detection(app, client): @app.route('/foo/') - @app.output(FooSchema) + @app.output(Foo) def pattern1(bar): pass @app.route('//bar') - @app.output(FooSchema) + @app.output(Foo) def pattern2(foo): pass @app.route('///baz') - @app.output(FooSchema) + @app.output(Foo) def pattern3(foo, bar): pass @app.route('/foo//') - @app.output(FooSchema) + @app.output(Foo) def pattern4(bar, baz): pass @app.route('///') - @app.output(FooSchema) + @app.output(Foo) def pattern5(foo, bar, baz): pass @@ -148,13 +148,13 @@ def pattern5(foo, bar, baz): def test_path_arguments_order(app, client): @app.route('//bar') - @app.input(QuerySchema, 'query') - @app.output(FooSchema) + @app.input(Query, location='query') + @app.output(Foo) def path_and_query(foo, query): pass @app.route('//') - @app.output(FooSchema) + @app.output(Foo) def two_path_variables(foo, bar): pass @@ -175,15 +175,15 @@ def two_path_variables(foo, bar): def test_parameters_registration(app, client): @app.route('/foo') - @app.input(QuerySchema, 'query') - @app.output(FooSchema) + @app.input(Query, location='query') + @app.output(Foo) def foo(query): pass @app.route('/bar') - @app.input(QuerySchema, 'query') - @app.input(PaginationSchema, 'query') - @app.input(HeaderSchema, 'headers') + @app.input(Query, location='query') + @app.input(Pagination, location='query') + @app.input(Header, location='headers') def bar(query, pagination, header): return { 'query': query['id'], @@ -212,12 +212,12 @@ def test_register_validation_error_response(app, client): error_code = str(app.config['VALIDATION_ERROR_STATUS_CODE']) @app.post('/foo') - @app.input(FooSchema) + @app.input(Foo) def foo(): pass @app.get('/bar') - @app.input(FooSchema, 'query') + @app.input(Foo, location='query') def bar(): pass diff --git a/tests/test_route.py b/tests/test_route.py index 6053858..fb2ee4f 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -2,7 +2,7 @@ from flask.views import MethodView from openapi_spec_validator import validate_spec -from .schemas import FooSchema +from .schemas import Foo from apiflask import APIBlueprint from apiflask import HTTPTokenAuth @@ -13,7 +13,7 @@ def test_route_shortcuts(app, client, method): client_method = getattr(client, method) @route_method('/pet') - @app.output(FooSchema) + @app.output(Foo) def test_shortcuts(): return {'name': method} diff --git a/tests/test_settings_api_docs.py b/tests/test_settings_api_docs.py index aaa3ce0..258d854 100644 --- a/tests/test_settings_api_docs.py +++ b/tests/test_settings_api_docs.py @@ -92,6 +92,12 @@ def test_swagger_ui_oauth_config(app, client): def test_elements_config(): app = APIFlask(__name__, docs_ui='elements') + + rv = app.test_client().get('/docs') + assert rv.status_code == 200 + # test default router + assert b'router="hash"' in rv.data + app.config['ELEMENTS_CONFIG'] = { 'hideTryIt': False, 'router': 'memory' diff --git a/tests/test_settings_auto_behaviour.py b/tests/test_settings_auto_behaviour.py index 1b9b391..90e6f73 100644 --- a/tests/test_settings_auto_behaviour.py +++ b/tests/test_settings_auto_behaviour.py @@ -2,8 +2,8 @@ from flask.views import MethodView from openapi_spec_validator import validate_spec -from .schemas import FooSchema -from .schemas import QuerySchema +from .schemas import Foo +from .schemas import Query from apiflask import APIBlueprint from apiflask.security import HTTPBasicAuth @@ -188,7 +188,7 @@ class Baz(MethodView): def get(self): pass - @app.input(FooSchema) + @app.input(Foo) def post(self): pass @@ -207,13 +207,13 @@ def test_auto_200_response_for_no_output_views(app, client, config_value): app.config['AUTO_200_RESPONSE'] = config_value @app.get('/foo') - @app.input(QuerySchema, 'query') + @app.input(Query, location='query') def foo(): pass @app.route('/bar') class Bar(MethodView): - @app.input(QuerySchema, 'query') + @app.input(Query, location='query') def get(self): pass @@ -231,7 +231,7 @@ def test_auto_validation_error_response(app, client, config_value): app.config['AUTO_VALIDATION_ERROR_RESPONSE'] = config_value @app.post('/foo') - @app.input(FooSchema) + @app.input(Foo) def foo(): pass diff --git a/tests/test_settings_response_customization.py b/tests/test_settings_response_customization.py index 05ad785..c085dce 100644 --- a/tests/test_settings_response_customization.py +++ b/tests/test_settings_response_customization.py @@ -1,9 +1,9 @@ import pytest from openapi_spec_validator import validate_spec -from .schemas import FooSchema -from .schemas import HTTPErrorSchema -from .schemas import ValidationErrorSchema +from .schemas import Foo +from .schemas import HTTPError +from .schemas import ValidationError from apiflask.schemas import EmptySchema from apiflask.schemas import http_error_schema from apiflask.security import HTTPBasicAuth @@ -14,12 +14,12 @@ def test_response_description_config(app, client): app.config['NOT_FOUND_DESCRIPTION'] = 'Egg not found' @app.get('/foo') - @app.input(FooSchema) # 200 + @app.input(Foo) # 200 def only_body_schema(foo): pass @app.get('/bar') - @app.output(FooSchema, 201) + @app.output(Foo, status_code=201) def create(): pass @@ -29,7 +29,7 @@ def no_schema(): pass @app.get('/spam') - @app.output(FooSchema, 206) + @app.output(Foo, status_code=206) def spam(): pass @@ -57,7 +57,7 @@ def test_validation_error_status_code_and_description(app, client): app.config['VALIDATION_ERROR_DESCRIPTION'] = 'Bad' @app.post('/foo') - @app.input(FooSchema) + @app.input(Foo) def foo(): pass @@ -71,13 +71,13 @@ def foo(): @pytest.mark.parametrize('schema', [ http_error_schema, - ValidationErrorSchema + ValidationError ]) def test_validation_error_schema(app, client, schema): app.config['VALIDATION_ERROR_SCHEMA'] = schema @app.post('/foo') - @app.input(FooSchema) + @app.input(Foo) def foo(): pass @@ -94,7 +94,7 @@ def test_validation_error_schema_bad_type(app): app.config['VALIDATION_ERROR_SCHEMA'] = 'schema' @app.post('/foo') - @app.input(FooSchema) + @app.input(Foo) def foo(): pass @@ -137,7 +137,7 @@ def foo(): def test_http_auth_error_response(app, client): @app.get('/foo') - @app.output(FooSchema) + @app.output(Foo) @app.doc(responses={204: 'empty', 400: 'bad', 404: 'not found', 500: 'server error'}) def foo(): pass @@ -157,13 +157,13 @@ def foo(): @pytest.mark.parametrize('schema', [ http_error_schema, - HTTPErrorSchema + HTTPError ]) def test_http_error_schema(app, client, schema): app.config['HTTP_ERROR_SCHEMA'] = schema @app.get('/foo') - @app.output(FooSchema) + @app.output(Foo) @app.doc(responses={400: 'bad', 404: 'not found', 500: 'server error'}) def foo(): pass @@ -179,7 +179,7 @@ def test_http_error_schema_bad_type(app): app.config['HTTP_ERROR_SCHEMA'] = 'schema' @app.get('/foo') - @app.output(FooSchema) + @app.output(Foo) @app.doc(responses={400: 'bad', 404: 'not found', 500: 'server error'}) def foo(): pass diff --git a/tox.ini b/tox.ini index d8ed93b..9e146a8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ [tox] -envlist = style,py37,py38,py39,pypy38,docs,mypy,flask1 +envlist = + py3{11,10,9,8,7},pypy3{8,7} + py310-min + py37-dev + style + typing + docs + flask1 skip_missing_interpreters = True -[gh-actions] -python = - 3.7: py37 - 3.8: py38, mypy, flask1 - 3.9: py39 - pypy3.8: pypy38 - [testenv] deps = -r requirements/tests.txt @@ -27,7 +27,7 @@ deps = -r requirements/docs.txt whitelist_externals = mkdocs commands = mkdocs build -[testenv:mypy] +[testenv:typing] deps = -r requirements/typing.txt commands = mypy