From e44eccb563362b3a1384d9e99fb5968e3d12ae37 Mon Sep 17 00:00:00 2001 From: Tristan Sweeney <76963169+tsweeney-dust@users.noreply.github.com> Date: Thu, 20 May 2021 18:06:07 -0400 Subject: [PATCH 01/21] Fix error aggregation and body=None Two bugs noticed (may not be real bugs). When no body is present (i.e. `request.get_json()` returns `None`) the schema is presented `None` to load from, whereas an empty JSON body could better be interpreted as `{}`. For a body schema with no required fields this leads to better behavior. Error aggregation doesn't seem to have been working correctly, as `error.data['errors']` never existed, and comment "If any parsing produced an error, combine them and reraise" hints this was the intended behavior. --- flask_accepts/decorators/decorators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index a434584..64adc17 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -114,7 +114,7 @@ def inner(*args, **kwargs): # Handle Marshmallow schema for request body if schema: try: - obj = schema.load(request.get_json(force=True)) + obj = schema.load(request.get_json(force=True) or {}) request.parsed_obj = obj except ValidationError as ex: schema_error = ex.messages @@ -123,7 +123,7 @@ def inner(*args, **kwargs): f"Error parsing request body: {schema_error}" ) if hasattr(error, "data"): - error.data["errors"].update({"schema_errors": schema_error}) + error.data["schema_errors"].update(schema_error) else: error.data = {"schema_errors": schema_error} @@ -143,7 +143,7 @@ def inner(*args, **kwargs): f"Error parsing query params: {schema_error}" ) if hasattr(error, "data"): - error.data["errors"].update({"schema_errors": schema_error}) + error.data["schema_errors"].update(schema_error) else: error.data = {"schema_errors": schema_error} @@ -163,7 +163,7 @@ def inner(*args, **kwargs): f"Error parsing headers: {schema_error}" ) if hasattr(error, "data"): - error.data["errors"].update({"schema_errors": schema_error}) + error.data["schema_errors"].update(schema_error) else: error.data = {"schema_errors": schema_error} From 1790d3957880162fcc1cedbe31d8de2cca3d0b3c Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Fri, 14 Jan 2022 18:38:47 +0100 Subject: [PATCH 02/21] Resolve marshmallow deprecation warning and support marshmallow 4.0 --- dev-requirements.txt | 2 +- examples/default_values.py | 17 ++++++++--------- examples/marshmallow_example.py | 4 ++-- examples/nested_schemas.py | 16 ++++++++-------- flask_accepts/utils.py | 20 ++++++++++++++++---- flask_accepts/utils_test.py | 8 ++++---- 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 9da31ba..d1d6729 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,7 +8,7 @@ itsdangerous==1.1.0 Jinja2==2.11.3 jsonschema==3.2.0 MarkupSafe==1.1.1 -marshmallow==3.2.2 +marshmallow==3.14.1 more-itertools==8.0.0 packaging==19.2 pluggy==0.13.1 diff --git a/examples/default_values.py b/examples/default_values.py index d209db5..8a4980d 100644 --- a/examples/default_values.py +++ b/examples/default_values.py @@ -1,4 +1,3 @@ -import datetime from dataclasses import dataclass from marshmallow import fields, Schema, post_load from flask import Flask, jsonify, request @@ -6,17 +5,17 @@ class CogSchema(Schema): - cog_foo = fields.String(default="cog") - cog_baz = fields.Integer(default=999) + cog_foo = fields.String(dump_default="cog") + cog_baz = fields.Integer(dump_default=999) class WidgetSchema(Schema): - foo = fields.String(default="test string") - baz = fields.Integer(default=42) - flag = fields.Bool(default=False) - date = fields.Date(default="01-01-1900") - dec = fields.Decimal(default=42.42) - dct = fields.Dict(default={"key": "value"}) + foo = fields.String(dump_default="test string") + baz = fields.Integer(dump_default=42) + flag = fields.Bool(dump_default=False) + date = fields.Date(dump_default="01-01-1900") + dec = fields.Decimal(dump_default=42.42) + dct = fields.Dict(dump_default={"key": "value"}) cog = fields.Nested(CogSchema) diff --git a/examples/marshmallow_example.py b/examples/marshmallow_example.py index 8ef0821..ceec07d 100644 --- a/examples/marshmallow_example.py +++ b/examples/marshmallow_example.py @@ -11,8 +11,8 @@ class Widget: class WidgetSchema(Schema): - foo = fields.String(default="test value") - baz = fields.Integer(default=422) + foo = fields.String(dump_default="test value") + baz = fields.Integer(dump_default=422) @post_load def make(self, data, **kwargs): diff --git a/examples/nested_schemas.py b/examples/nested_schemas.py index 4363a9e..80bba0e 100644 --- a/examples/nested_schemas.py +++ b/examples/nested_schemas.py @@ -10,17 +10,17 @@ class CogSchema(Schema): - cog_foo = fields.String(default="cog") - cog_baz = fields.Integer(default=999) + cog_foo = fields.String(dump_default="cog") + cog_baz = fields.Integer(dump_default=999) class WidgetSchema(Schema): - foo = fields.String(default="test string") - baz = fields.Integer(default=42) - flag = fields.Bool(default=False) - date = fields.Date(default="01-01-1900") - dec = fields.Decimal(default=42.42) - dct = fields.Dict(default={"key": "value"}) + foo = fields.String(dump_default="test string") + baz = fields.Integer(dump_default=42) + flag = fields.Bool(dump_default=False) + date = fields.Date(dump_default="01-01-1900") + dec = fields.Decimal(dump_default=42.42) + dct = fields.Dict(dump_default={"key": "value"}) cog = fields.Nested(CogSchema) diff --git a/flask_accepts/utils.py b/flask_accepts/utils.py index d8e997d..7fcca5d 100644 --- a/flask_accepts/utils.py +++ b/flask_accepts/utils.py @@ -1,10 +1,20 @@ from typing import Optional, Type, Union + from flask_restx import fields as fr, inputs from marshmallow import fields as ma +from marshmallow import __version_info__ as marshmallow_version from marshmallow.schema import Schema, SchemaMeta import uuid +if marshmallow_version >= (3, 13, 0): + _ma_key_for_fr_example_key = "dump_default" + _ma_key_for_fr_default_key = "load_default" +else: + _ma_key_for_fr_example_key = "default" + _ma_key_for_fr_default_key = "missing" + + def unpack_list(val, api, model_name: str = None, operation: str = "dump"): model_name = model_name or get_default_model_name() return fr.List( @@ -168,8 +178,9 @@ def get_default_model_name(schema: Optional[Union[Schema, Type[Schema]]] = None) def _ma_field_to_fr_field(value: ma.Field) -> dict: fr_field_parameters = {} - if hasattr(value, "default") and type(value.default) != ma.utils._Missing: - fr_field_parameters["example"] = value.default + if hasattr(value, _ma_key_for_fr_example_key) \ + and type(getattr(value, _ma_key_for_fr_example_key)) != ma.utils._Missing: + fr_field_parameters["example"] = getattr(value, _ma_key_for_fr_example_key) if hasattr(value, "required"): fr_field_parameters["required"] = value.required @@ -177,8 +188,9 @@ def _ma_field_to_fr_field(value: ma.Field) -> dict: if hasattr(value, "metadata") and "description" in value.metadata: fr_field_parameters["description"] = value.metadata["description"] - if hasattr(value, "missing") and type(value.missing) != ma.utils._Missing: - fr_field_parameters["default"] = value.missing + if hasattr(value, _ma_key_for_fr_default_key) \ + and type(getattr(value, _ma_key_for_fr_default_key)) != ma.utils._Missing: + fr_field_parameters["default"] = getattr(value, _ma_key_for_fr_default_key) return fr_field_parameters diff --git a/flask_accepts/utils_test.py b/flask_accepts/utils_test.py index 4aa9cde..8c3894a 100644 --- a/flask_accepts/utils_test.py +++ b/flask_accepts/utils_test.py @@ -199,9 +199,9 @@ class FakeFieldNoRequired(ma.Field): def test__ma_field_to_fr_field_converts_missing_param_to_default_if_present(): @dataclass class FakeFieldWithMissing(ma.Field): - missing: bool + load_default: bool - fr_field_dict = utils._ma_field_to_fr_field(FakeFieldWithMissing(missing=True)) + fr_field_dict = utils._ma_field_to_fr_field(FakeFieldWithMissing(load_default=True)) assert fr_field_dict["default"] is True @dataclass @@ -242,12 +242,12 @@ class FakeFieldNoDescription(ma.Field): def test__ma_field_to_fr_field_converts_default_to_example_if_present(): @dataclass class FakeFieldWithDefault(ma.Field): - default: str + dump_default: str expected_example_value = "test" fr_field_dict = utils._ma_field_to_fr_field( - FakeFieldWithDefault(default=expected_example_value) + FakeFieldWithDefault(dump_default=expected_example_value) ) assert fr_field_dict["example"] == expected_example_value From be1f91282533a509e8f7f9238609d239a079745c Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Thu, 28 Jul 2022 10:00:45 +0800 Subject: [PATCH 03/21] Bumped versions --- dev-requirements.txt | 42 +++++++++++++++++++++--------------------- requirements.txt | 2 +- setup.py | 6 +++--- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 9da31ba..cabf4d5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,22 +1,22 @@ -aniso8601==8.0.0 -attrs==19.3.0 -Click==7.0 -Flask==1.0.2 -flask-marshmallow==0.10.1 +aniso8601==9.0.1 +attrs==21.4.0 +click==8.1.3 +flask==2.1.3 +flask-marshmallow==0.14.0 flask-restx==0.5.1 -itsdangerous==1.1.0 -Jinja2==2.11.3 -jsonschema==3.2.0 -MarkupSafe==1.1.1 -marshmallow==3.2.2 -more-itertools==8.0.0 -packaging==19.2 -pluggy==0.13.1 -py==1.10.0 -pyparsing==2.4.5 -pyrsistent==0.15.6 -pytest==5.3.1 -pytz==2019.3 -six==1.13.0 -wcwidth==0.1.7 -Werkzeug==0.16.0 +itsdangerous==2.1.2 +jinja2==3.1.2 +jsonschema==4.7.2 +MarkupSafe==2.1.1 +marshmallow==3.17.0 +more-itertools==8.13.0 +packaging==21.3 +pluggy==1.0.0 +py==1.11.0 +pyparsing==3.0.9 +pyrsistent==0.18.1 +pytest==7.1.2 +pytz==2022.1 +six==1.16.0 +wcwidth==0.2.5 +werkzeug==2.1.2 diff --git a/requirements.txt b/requirements.txt index 1ebb6b3..1120480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Flask==1.0.2 +flask==2.1.3 flask-restx==0.5.1 diff --git a/setup.py b/setup.py index 3119e1d..0090146 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,13 @@ name="flask_accepts", author='Alan "AJ" Pryor, Jr.', author_email="apryor6@gmail.com", - version="0.18.4", + version="0.19.0", description="Easy, opinionated Flask input/output handling with Flask-restx and Marshmallow", ext_modules=[], packages=find_packages(), install_requires=[ "marshmallow>=3.0.1", - "flask-restx>=0.2.0", - "Werkzeug" + "flask-restx>=0.5.0", + "Werkzeug==2.1.2" ], ) From b319abec79fb84911fc0b68982a055e3a72777d0 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Thu, 28 Jul 2022 13:01:50 +0800 Subject: [PATCH 04/21] Fixed code coverage Exclude lines used specifically for marshmallow compatibility --- .coveragerc | 4 ++++ flask_accepts/utils.py | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c6c915d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +exclude_lines = + # pragma: no cover + if marshmallow_version < \(3, 13, 0\): diff --git a/flask_accepts/utils.py b/flask_accepts/utils.py index 7fcca5d..2342144 100644 --- a/flask_accepts/utils.py +++ b/flask_accepts/utils.py @@ -7,10 +7,9 @@ import uuid -if marshmallow_version >= (3, 13, 0): - _ma_key_for_fr_example_key = "dump_default" - _ma_key_for_fr_default_key = "load_default" -else: +_ma_key_for_fr_example_key = "dump_default" +_ma_key_for_fr_default_key = "load_default" +if marshmallow_version < (3, 13, 0): _ma_key_for_fr_example_key = "default" _ma_key_for_fr_default_key = "missing" @@ -27,7 +26,7 @@ def unpack_nested(val, api, model_name: str = None, operation: str = "dump"): return unpack_nested_self(val, api, model_name, operation) model_name = get_default_model_name(val.nested) - + if val.many: return fr.List( fr.Nested( From d101f6e2947b11c0543d243ab3baea5f124f816d Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Mon, 15 Aug 2022 11:50:10 +0800 Subject: [PATCH 05/21] Updated dependencies --- requirements.txt | 2 +- setup.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1120480..649ca33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -flask==2.1.3 +flask==2.2.2 flask-restx==0.5.1 diff --git a/setup.py b/setup.py index 0090146..95dc54c 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,13 @@ name="flask_accepts", author='Alan "AJ" Pryor, Jr.', author_email="apryor6@gmail.com", - version="0.19.0", + version="0.19.1", description="Easy, opinionated Flask input/output handling with Flask-restx and Marshmallow", ext_modules=[], packages=find_packages(), install_requires=[ "marshmallow>=3.0.1", - "flask-restx>=0.5.0", - "Werkzeug==2.1.2" + "flask-restx>=0.5.1", + "Werkzeug==2.2.2" ], ) From 0dd9a65e5406b89ace1206df7500a4428a2dc5cb Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Mon, 15 Aug 2022 15:13:23 +0800 Subject: [PATCH 06/21] Adjusted dependencies --- dev-requirements.txt | 4 ++-- requirements.txt | 2 +- setup.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index cabf4d5..d21ee14 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ aniso8601==9.0.1 attrs==21.4.0 click==8.1.3 -flask==2.1.3 +flask==2.1.2 flask-marshmallow==0.14.0 flask-restx==0.5.1 itsdangerous==2.1.2 @@ -19,4 +19,4 @@ pytest==7.1.2 pytz==2022.1 six==1.16.0 wcwidth==0.2.5 -werkzeug==2.1.2 +werkzeug<2.1 diff --git a/requirements.txt b/requirements.txt index 649ca33..9593a42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -flask==2.2.2 +flask==2.1.2 flask-restx==0.5.1 diff --git a/setup.py b/setup.py index 95dc54c..fd1630b 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,8 @@ ext_modules=[], packages=find_packages(), install_requires=[ - "marshmallow>=3.0.1", + "marshmallow>=3.17.0", "flask-restx>=0.5.1", - "Werkzeug==2.2.2" + "werkzeug<2.1" ], ) From 4efe4fe01681557335d09c46dcc08def98c410bd Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Mon, 15 Aug 2022 15:14:20 +0800 Subject: [PATCH 07/21] fixed schema error aggregation and empty body exception --- flask_accepts/decorators/decorators.py | 16 ++++++++-------- flask_accepts/decorators/decorators_test.py | 7 +++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index e25d988..18fedcc 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -135,9 +135,9 @@ def inner(*args, **kwargs): f"Error parsing request body: {schema_error}" ) if hasattr(error, "data"): - error.data["schema_errors"].update(schema_error) + error.data["errors"].update(schema_error) else: - error.data = {"schema_errors": schema_error} + error.data = {"errors": schema_error} # Handle Marshmallow schema for query params if query_params_schema: @@ -155,9 +155,9 @@ def inner(*args, **kwargs): f"Error parsing query params: {schema_error}" ) if hasattr(error, "data"): - error.data["schema_errors"].update(schema_error) + error.data["errors"].update(schema_error) else: - error.data = {"schema_errors": schema_error} + error.data = {"errors": schema_error} # Handle Marshmallow schema for headers if headers_schema: @@ -175,9 +175,9 @@ def inner(*args, **kwargs): f"Error parsing headers: {schema_error}" ) if hasattr(error, "data"): - error.data["schema_errors"].update(schema_error) + error.data["errors"].update(schema_error) else: - error.data = {"schema_errors": schema_error} + error.data = {"errors": schema_error} # Handle Marshmallow schema for form data if form_schema: @@ -195,9 +195,9 @@ def inner(*args, **kwargs): f"Error parsing form data: {schema_error}" ) if hasattr(error, "data"): - error.data["errors"].update({"schema_errors": schema_error}) + error.data["errors"].update(schema_error) else: - error.data = {"schema_errors": schema_error} + error.data = {"errors": schema_error} # If any parsing produced an error, combine them and re-raise if error: diff --git a/flask_accepts/decorators/decorators_test.py b/flask_accepts/decorators/decorators_test.py index bbdfb0a..abd1be0 100644 --- a/flask_accepts/decorators/decorators_test.py +++ b/flask_accepts/decorators/decorators_test.py @@ -115,7 +115,7 @@ def post(self): json={"_id": "this is not an integer and will error", "name": "test name"}, ) assert resp.status_code == 400 - assert "Not a valid integer." in resp.json["schema_errors"]["_id"] + assert "Not a valid integer." in resp.json["errors"]["_id"] def test_validation_errors_from_all_added_to_request_with_Resource_and_schema( @@ -146,7 +146,7 @@ def post(self): ) assert resp.status_code == 400 - assert "Not a valid integer." in resp.json["errors"]["schema_errors"]["_id"] + assert "Not a valid integer." in resp.json["errors"]["_id"] def test_dict_arguments_are_correctly_added(app, client): # noqa @@ -404,7 +404,6 @@ class TestSchema(Schema): class TestResource(Resource): @accepts("TestSchema", form_schema=TestSchema, api=api) def post(self): - assert request.parsed_form["foo"] == 3 assert request.parsed_args["foo"] == 3 return "success" @@ -513,7 +512,7 @@ class QueryParamsSchema(Schema): class HeadersSchema(Schema): Header = fields.Integer(required=True) - + class FormSchema(Schema): form = fields.String(required=True) From c6df3510b0d24c6cce6e3904834258f7484df261 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Wed, 2 Nov 2022 08:43:15 +0800 Subject: [PATCH 08/21] Updated dependencies for compatibility Now works with Flask 2.2 Weukzeug 2.2 flask-restx 1.0.1 --- dev-requirements.txt | 6 +++--- requirements.txt | 4 ++-- setup.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index d21ee14..46b07d2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,9 @@ aniso8601==9.0.1 attrs==21.4.0 click==8.1.3 -flask==2.1.2 +flask==2.2.2 flask-marshmallow==0.14.0 -flask-restx==0.5.1 +flask-restx==1.0.1 itsdangerous==2.1.2 jinja2==3.1.2 jsonschema==4.7.2 @@ -19,4 +19,4 @@ pytest==7.1.2 pytz==2022.1 six==1.16.0 wcwidth==0.2.5 -werkzeug<2.1 +werkzeug==2.2.2 diff --git a/requirements.txt b/requirements.txt index 9593a42..143ba18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -flask==2.1.2 -flask-restx==0.5.1 +flask==2.2.2 +flask-restx==1.0.1 diff --git a/setup.py b/setup.py index fd1630b..158c85d 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,13 @@ name="flask_accepts", author='Alan "AJ" Pryor, Jr.', author_email="apryor6@gmail.com", - version="0.19.1", + version="1.0.0", description="Easy, opinionated Flask input/output handling with Flask-restx and Marshmallow", ext_modules=[], packages=find_packages(), install_requires=[ "marshmallow>=3.17.0", - "flask-restx>=0.5.1", - "werkzeug<2.1" + "flask-restx>=1.0.1", + "werkzeug>=2.2.0" ], ) From eb1e096b6bd4b605706eb326861e0a3a02943501 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 07:26:39 +0800 Subject: [PATCH 09/21] updated dependencies werkzeug 3.x requires python 3.8+ --- requirements.txt | 4 +++- setup.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 143ba18..a04f9ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ -flask==2.2.2 +flask>=2,<3 flask-restx==1.0.1 +werkzeug>=2,<3; python_version < '3.8' +werkzeug>=3,<4; python_version >= '3.8' diff --git a/setup.py b/setup.py index 158c85d..79570da 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ install_requires=[ "marshmallow>=3.17.0", "flask-restx>=1.0.1", - "werkzeug>=2.2.0" + "werkzeug>=2,<3; python_version < '3.8'", + "werkzeug>=3,<4; python_version >= '3.8'", ], ) From 61a34ac457f0b62f86850c4acbb55b182960cdf1 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 07:33:58 +0800 Subject: [PATCH 10/21] handle status_code overrides --- flask_accepts/decorators/decorators.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index 18fedcc..f544cee 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -297,6 +297,11 @@ def inner(*args, **kwargs): # If a Flask response has been made already, it is passed through unchanged if isinstance(rv, Response): return rv + + # allow overriding the stsus code passed to Flask + if isinstance(rv, tuple): + rv, status_code = rv + if schema: serialized = schema.dump(rv) From ca8fb59e3d11bf5b91b55ac21b24d14f926be464 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:50:56 +0800 Subject: [PATCH 11/21] updated requirements --- dev-requirements.txt | 7 ++++--- requirements.txt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 46b07d2..eb219b4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,9 @@ aniso8601==9.0.1 attrs==21.4.0 click==8.1.3 -flask==2.2.2 +flask>=2,<3 +flask-restx>=1,<2 flask-marshmallow==0.14.0 -flask-restx==1.0.1 itsdangerous==2.1.2 jinja2==3.1.2 jsonschema==4.7.2 @@ -19,4 +19,5 @@ pytest==7.1.2 pytz==2022.1 six==1.16.0 wcwidth==0.2.5 -werkzeug==2.2.2 +werkzeug>=2,<3; python_version < '3.8' +werkzeug>=3,<4; python_version >= '3.8' diff --git a/requirements.txt b/requirements.txt index a04f9ed..b6b6634 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flask>=2,<3 -flask-restx==1.0.1 +flask-restx>=1,<2 werkzeug>=2,<3; python_version < '3.8' werkzeug>=3,<4; python_version >= '3.8' From ab34f1d87c4a70bc3c96903a879bbe1fb494078d Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:52:20 +0800 Subject: [PATCH 12/21] Added ability to set schema based on status_code --- flask_accepts/decorators/decorators.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index f544cee..d68ea19 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from typing import Type, Union +from typing import Type, Union, Dict from flask import jsonify from werkzeug.wrappers import Response from werkzeug.exceptions import BadRequest, InternalServerError @@ -232,6 +232,7 @@ def responds( *args, model_name: str = None, schema=None, + alt_schemas: Dict[int, Union[Schema, Type[Schema]]] = None, many: bool = False, api=None, envelope=None, @@ -250,6 +251,7 @@ def responds( Args: schema (bool, optional): Marshmallow schema with which to serialize the output of the wrapped function. + alt_schemas (dict, optional): Dict of alternate schemas to use based on the status_code many (bool, optional): (DEPRECATED) The Marshmallow schema `many` parameter, which will return a list of the corresponding schema objects when set to True. @@ -290,8 +292,10 @@ def decorator(func): # Check if we are decorating a class method _IS_METHOD = _is_method(func) + resp_schema = schema @wraps(func) def inner(*args, **kwargs): + global resp_schema rv = func(*args, **kwargs) # If a Flask response has been made already, it is passed through unchanged @@ -301,13 +305,16 @@ def inner(*args, **kwargs): # allow overriding the stsus code passed to Flask if isinstance(rv, tuple): rv, status_code = rv + if alt_schemas and status_code in alt_schemas: + # override the response schema + resp_schema = alt_schemas[status_code] - if schema: - serialized = schema.dump(rv) + if resp_schema: + serialized = resp_schema.dump(rv) # Validate data if asked to (throws) if validate: - errs = schema.validate(serialized) + errs = resp_schema.validate(serialized) if errs: raise InternalServerError( description="Server attempted to return invalid data" @@ -345,11 +352,11 @@ def remove_none(obj): # Add Swagger if api and use_swagger and _IS_METHOD: - if schema: + if resp_schema: api_model = for_swagger( - schema=schema, model_name=model_name, api=api, operation="dump" + schema=resp_schema, model_name=model_name, api=api, operation="dump" ) - if schema.many is True: + if resp_schema.many is True: api_model = [api_model] inner = _document_like_marshal_with( From aaecde1634ef9d3f34eafde35de0c9676f271e01 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:18:10 +0800 Subject: [PATCH 13/21] consolidated tests --- flask_accepts/{test => tests}/__init__.py | 0 flask_accepts/{test => tests}/fixtures.py | 0 .../test_decorators.py} | 38 +++++++++---------- .../{utils_test.py => tests/test_utils.py} | 10 ++--- 4 files changed, 24 insertions(+), 24 deletions(-) rename flask_accepts/{test => tests}/__init__.py (100%) rename flask_accepts/{test => tests}/fixtures.py (100%) rename flask_accepts/{decorators/decorators_test.py => tests/test_decorators.py} (97%) rename flask_accepts/{utils_test.py => tests/test_utils.py} (97%) diff --git a/flask_accepts/test/__init__.py b/flask_accepts/tests/__init__.py similarity index 100% rename from flask_accepts/test/__init__.py rename to flask_accepts/tests/__init__.py diff --git a/flask_accepts/test/fixtures.py b/flask_accepts/tests/fixtures.py similarity index 100% rename from flask_accepts/test/fixtures.py rename to flask_accepts/tests/fixtures.py diff --git a/flask_accepts/decorators/decorators_test.py b/flask_accepts/tests/test_decorators.py similarity index 97% rename from flask_accepts/decorators/decorators_test.py rename to flask_accepts/tests/test_decorators.py index abd1be0..bd39113 100644 --- a/flask_accepts/decorators/decorators_test.py +++ b/flask_accepts/tests/test_decorators.py @@ -5,7 +5,7 @@ from flask_accepts.decorators import accepts, responds from flask_accepts.decorators.decorators import _convert_multidict_values_to_schema -from flask_accepts.test.fixtures import app, client # noqa +from flask_accepts.tests.fixtures import app, client # noqa def test_arguments_are_added_to_request(app, client): # noqa @@ -53,11 +53,11 @@ class TestResource(Resource): def post(self): assert request.parsed_obj assert request.parsed_obj["_id"] == 42 - assert request.parsed_obj["name"] == "test name" + assert request.parsed_obj["name"] == "tests name" return "success" with client as cl: - resp = cl.post("/test?foo=3", json={"_id": 42, "name": "test name"}) + resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) assert resp.status_code == 200 @@ -81,11 +81,11 @@ class TestResource(Resource): def post(self): assert request.parsed_obj assert request.parsed_obj["_id"] == 42 - assert request.parsed_obj["name"] == "test name" + assert request.parsed_obj["name"] == "tests name" return "success" with client as cl: - resp = cl.post("/test?foo=3", json={"_id": 42, "name": "test name"}) + resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) assert resp.status_code == 200 @@ -112,7 +112,7 @@ def post(self): with client as cl: resp = cl.post( "/test?foo=3", - json={"_id": "this is not an integer and will error", "name": "test name"}, + json={"_id": "this is not an integer and will error", "name": "tests name"}, ) assert resp.status_code == 400 assert "Not a valid integer." in resp.json["errors"]["_id"] @@ -142,7 +142,7 @@ def post(self): with client as cl: resp = cl.post( "/test?foo=not_int", - json={"_id": "this is not an integer and will error", "name": "test name"}, + json={"_id": "this is not an integer and will error", "name": "tests name"}, ) assert resp.status_code == 400 @@ -770,7 +770,7 @@ class TestSchema(Schema): @api.route("/test") class TestResource(Resource): - @responds(schema=TestSchema, api=api, envelope='test-data') + @responds(schema=TestSchema, api=api, envelope='tests-data') def get(self): from flask import make_response, Response @@ -780,7 +780,7 @@ def get(self): with client as cl: resp = cl.get("/test") assert resp.status_code == 200 - assert resp.json == {'test-data': {'_id': 42, 'name': 'Jon Snow'}} + assert resp.json == {'tests-data': {'_id': 42, 'name': 'Jon Snow'}} def test_responds_skips_none_false(app, client): @@ -846,14 +846,14 @@ class TestResource(Resource): ) def post(self): assert request.parsed_obj - assert request.parsed_obj["child"] == {"_id": 42, "name": "test name"} - assert request.parsed_obj["name"] == "test host" + assert request.parsed_obj["child"] == {"_id": 42, "name": "tests name"} + assert request.parsed_obj["name"] == "tests host" return "success" with client as cl: resp = cl.post( "/test?foo=3", - json={"name": "test host", "child": {"_id": 42, "name": "test name"}}, + json={"name": "tests host", "child": {"_id": 42, "name": "tests name"}}, ) assert resp.status_code == 200 @@ -885,23 +885,23 @@ def post(self): assert request.parsed_obj assert request.parsed_obj["child"]["child"] == { "_id": 42, - "name": "test name", + "name": "tests name", } assert request.parsed_obj["child"] == { - "name": "test host", - "child": {"_id": 42, "name": "test name"}, + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, } - assert request.parsed_obj["name"] == "test host host" + assert request.parsed_obj["name"] == "tests host host" return "success" with client as cl: resp = cl.post( "/test?foo=3", json={ - "name": "test host host", + "name": "tests host host", "child": { - "name": "test host", - "child": {"_id": 42, "name": "test name"}, + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, }, }, ) diff --git a/flask_accepts/utils_test.py b/flask_accepts/tests/test_utils.py similarity index 97% rename from flask_accepts/utils_test.py rename to flask_accepts/tests/test_utils.py index 8c3894a..bcd1b66 100644 --- a/flask_accepts/utils_test.py +++ b/flask_accepts/tests/test_utils.py @@ -8,7 +8,7 @@ from flask import Flask from flask_restx import Api, fields as fr, namespace -# from .utils import unpack_list, unpack_nested +# from flask_accepts.utils import unpack_list, unpack_nested import flask_accepts.utils as utils @@ -96,7 +96,7 @@ class IntegerSchema(Schema): def test_get_default_model_name(): - from .utils import get_default_model_name + from flask_accepts.utils import get_default_model_name class TestSchema(Schema): pass @@ -108,7 +108,7 @@ class TestSchema(Schema): def test_get_default_model_name_works_with_multiple_schema_in_name(): - from .utils import get_default_model_name + from flask_accepts.utils import get_default_model_name class TestSchemaSchema(Schema): pass @@ -120,7 +120,7 @@ class TestSchemaSchema(Schema): def test_get_default_model_name_that_does_not_end_in_schema(): - from .utils import get_default_model_name + from flask_accepts.utils import get_default_model_name class SomeOtherName(Schema): pass @@ -132,7 +132,7 @@ class SomeOtherName(Schema): def test_get_default_model_name_default_names(): - from .utils import get_default_model_name, num_default_models + from flask_accepts.utils import get_default_model_name, num_default_models for model_num in range(5): result = get_default_model_name() From 9de8d4081f7c6b571dd26fbbab5290414538b3ac Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:18:53 +0800 Subject: [PATCH 14/21] fixed response --- flask_accepts/decorators/decorators.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index d68ea19..c90d035 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -231,7 +231,7 @@ def inner(*args, **kwargs): def responds( *args, model_name: str = None, - schema=None, + schema: Union[Schema, Type[Schema]] = None, alt_schemas: Dict[int, Union[Schema, Type[Schema]]] = None, many: bool = False, api=None, @@ -292,29 +292,30 @@ def decorator(func): # Check if we are decorating a class method _IS_METHOD = _is_method(func) - resp_schema = schema @wraps(func) def inner(*args, **kwargs): - global resp_schema + nonlocal schema + nonlocal status_code + rv = func(*args, **kwargs) # If a Flask response has been made already, it is passed through unchanged if isinstance(rv, Response): return rv - # allow overriding the stsus code passed to Flask + # allow overriding the status code passed to Flask if isinstance(rv, tuple): rv, status_code = rv if alt_schemas and status_code in alt_schemas: # override the response schema - resp_schema = alt_schemas[status_code] + schema = alt_schemas[status_code] - if resp_schema: - serialized = resp_schema.dump(rv) + if schema: + serialized = schema.dump(rv) # Validate data if asked to (throws) if validate: - errs = resp_schema.validate(serialized) + errs = schema.validate(serialized) if errs: raise InternalServerError( description="Server attempted to return invalid data" @@ -350,13 +351,14 @@ def remove_none(obj): return jsonify(serialized), status_code return serialized, status_code + nonlocal schema # Add Swagger if api and use_swagger and _IS_METHOD: - if resp_schema: + if schema: api_model = for_swagger( - schema=resp_schema, model_name=model_name, api=api, operation="dump" + schema=schema, model_name=model_name, api=api, operation="dump" ) - if resp_schema.many is True: + if schema.many is True: api_model = [api_model] inner = _document_like_marshal_with( From 0f081f7ac353fc9d913deeb08b4939a381bdcf1c Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:13:19 +0800 Subject: [PATCH 15/21] separated test groups separated tests into logical grouping consolidated imorts --- flask_accepts/tests/test_accepts.py | 691 ++++++++++++++++ flask_accepts/tests/test_decorators.py | 1051 +----------------------- flask_accepts/tests/test_responds.py | 462 +++++++++++ flask_accepts/tests/test_utils.py | 41 +- 4 files changed, 1185 insertions(+), 1060 deletions(-) create mode 100644 flask_accepts/tests/test_accepts.py create mode 100644 flask_accepts/tests/test_responds.py diff --git a/flask_accepts/tests/test_accepts.py b/flask_accepts/tests/test_accepts.py new file mode 100644 index 0000000..c93490d --- /dev/null +++ b/flask_accepts/tests/test_accepts.py @@ -0,0 +1,691 @@ +from flask import jsonify, request +from flask_restx import Resource, Api +from marshmallow import Schema, fields +from werkzeug.exceptions import InternalServerError + +from flask_accepts.decorators import accepts, responds +from flask_accepts.tests.fixtures import app, client # noqa + + +def test_arguments_are_added_to_request(app, client): # noqa + @app.route("/test") + @accepts("Foo", dict(name="foo", type=int, help="An important foo")) + def test(): + assert request.parsed_args + return "success" + + with client as cl: + resp = cl.get("/test?foo=3") + assert resp.status_code == 200 + + +def test_arguments_are_added_to_request_with_Resource(app, client): # noqa + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("Foo", dict(name="foo", type=int, help="An important foo"), api=api) + def get(self): + assert request.parsed_args + return "success" + + with client as cl: + resp = cl.get("/test?foo=3") + assert resp.status_code == 200 + + +def test_arguments_are_added_to_request_with_Resource_and_schema(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=TestSchema, + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["_id"] == 42 + assert request.parsed_obj["name"] == "tests name" + return "success" + + with client as cl: + resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) + assert resp.status_code == 200 + + +def test_arguments_are_added_to_request_with_Resource_and_schema_instance( + app, client +): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=TestSchema(), + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["_id"] == 42 + assert request.parsed_obj["name"] == "tests name" + return "success" + + with client as cl: + resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) + assert resp.status_code == 200 + + +def test_validation_errors_added_to_request_with_Resource_and_schema( + app, client +): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=TestSchema, + api=api, + ) + def post(self): + pass # pragma: no cover + + with client as cl: + resp = cl.post( + "/test?foo=3", + json={"_id": "this is not an integer and will error", "name": "tests name"}, + ) + assert resp.status_code == 400 + assert "Not a valid integer." in resp.json["errors"]["_id"] + + +def test_validation_errors_from_all_added_to_request_with_Resource_and_schema( + app, client +): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + dict(name="foo2", type=int, help="An important foo2"), + schema=TestSchema, + api=api, + ) + def post(self): + pass # pragma: no cover + + with client as cl: + resp = cl.post( + "/test?foo=not_int", + json={"_id": "this is not an integer and will error", "name": "tests name"}, + ) + + assert resp.status_code == 400 + assert "Not a valid integer." in resp.json["errors"]["_id"] + + +def test_dict_arguments_are_correctly_added(app, client): # noqa + @app.route("/test") + @accepts( + {"name": "an_int", "type": int, "help": "An important int"}, + {"name": "a_bool", "type": bool, "help": "An important bool"}, + {"name": "a_str", "type": str, "help": "An important str"}, + ) + def test(): + assert request.parsed_args.get("an_int") == 1 + assert request.parsed_args.get("a_bool") + assert request.parsed_args.get("a_str") == "faraday" + return "success" + + with client as cl: + resp = cl.get("/test?an_int=1&a_bool=1&a_str=faraday") + assert resp.status_code == 200 + + +def test_bool_argument_have_correct_input(app, client): + @app.route("/test") + @accepts(dict(name="foo", type=bool, help="An important bool")) + def test(): + assert request.parsed_args["foo"] == False + return "success" + + with client as cl: + resp = cl.get("/test?foo=false") + assert resp.status_code == 200 + + +def test_failure_when_bool_argument_is_incorrect(app, client): + @app.route("/test") + @accepts(dict(name="foo", type=bool, help="An important bool")) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test?foo=falsee") + assert resp.status_code == 400 + + +def test_failure_when_required_arg_is_missing(app, client): # noqa + @app.route("/test") + @accepts(dict(name="foo", type=int, required=True, help="A required foo")) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 400 + + +def test_failure_when_arg_is_wrong_type(app, client): # noqa + @app.route("/test") + @accepts(dict(name="foo", type=int, required=True, help="A required foo")) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test?foo=baz") + assert resp.status_code == 400 + + +def test_accepts_with_query_params_schema_single_value(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + assert request.parsed_query_params["foo"] == 3 + return "success" + + with client as cl: + resp = cl.get("/test?foo=3") + assert resp.status_code == 200 + + +def test_accepts_with_query_params_schema_list_value(app, client): # noqa + class TestSchema(Schema): + foo = fields.List(fields.String(), required=True) + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + assert request.parsed_query_params["foo"] == ["3"] + return "success" + + with client as cl: + resp = cl.get("/test?foo=3") + assert resp.status_code == 200 + + +def test_accepts_with_query_params_schema_unknown_arguments(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + # Extra query params should be excluded. + assert "bar" not in request.parsed_query_params + assert request.parsed_query_params["foo"] == 3 + return "success" + + with client as cl: + resp = cl.get("/test?foo=3&bar=4") + assert resp.status_code == 200 + + +def test_accepts_with_query_params_schema_data_key(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=False, data_key="fooExternal") + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + assert request.parsed_args["fooExternal"] == 3 + assert request.parsed_query_params["foo"] == 3 + return "success" + + with client as cl: + resp = cl.get("/test?fooExternal=3") + assert resp.status_code == 200 + + +def test_failure_when_query_params_schema_arg_is_missing(app, client): # noqa + class TestSchema(Schema): + foo = fields.String(required=True) + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 400 + + +def test_failure_when_query_params_schema_arg_is_wrong_type(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + @app.route("/test") + @accepts("TestSchema", query_params_schema=TestSchema) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test?foo=baz") + assert resp.status_code == 400 + + +def test_accepts_with_header_schema_single_value(app, client): # noqa + class TestSchema(Schema): + Foo = fields.Integer(required=True) + + @app.route("/test") + @accepts(headers_schema=TestSchema) + def test(): + assert request.parsed_headers["Foo"] == 3 + return "success" + + with client as cl: + resp = cl.get("/test", headers={"Foo": "3"}) + assert resp.status_code == 200 + + +def test_accepts_with_header_schema_list_value(app, client): # noqa + class TestSchema(Schema): + Foo = fields.List(fields.String(), required=True) + + @app.route("/test") + @accepts(headers_schema=TestSchema) + def test(): + assert request.parsed_headers["Foo"] == ["3"] + return "success" + + with client as cl: + resp = cl.get("/test", headers={"Foo": "3"}) + assert resp.status_code == 200 + + +def test_accepts_with_header_schema_unknown_arguments(app, client): # noqa + class TestSchema(Schema): + Foo = fields.List(fields.String(), required=True) + + @app.route("/test") + @accepts(headers_schema=TestSchema) + def test(): + # Extra header values should be excluded. + assert "Bar" not in request.parsed_headers + assert request.parsed_headers["Foo"] == ["3"] + return "success" + + with client as cl: + resp = cl.get("/test", headers={"Foo": "3", "Bar": "4"}) + assert resp.status_code == 200 + + +def test_accepts_with_header_schema_data_key(app, client): # noqa + class TestSchema(Schema): + Foo = fields.Integer(required=False, data_key="Foo-External") + + @app.route("/test") + @accepts("TestSchema", headers_schema=TestSchema) + def test(): + assert request.parsed_headers["Foo"] == 3 + assert request.parsed_args["Foo-External"] == 3 + return "success" + + with client as cl: + resp = cl.get("/test", headers={"Foo-External": "3"}) + assert resp.status_code == 200 + + +def test_failure_when_header_schema_arg_is_missing(app, client): # noqa + class TestSchema(Schema): + Foo = fields.String(required=True) + + @app.route("/test") + @accepts("TestSchema", headers_schema=TestSchema) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 400 + + +def test_failure_when_header_schema_arg_is_wrong_type(app, client): # noqa + class TestSchema(Schema): + Foo = fields.Integer(required=True) + + @app.route("/test") + @accepts("TestSchema", headers_schema=TestSchema) + def test(): + pass # pragma: no cover + + with client as cl: + resp = cl.get("/test", headers={"Foo": "baz"}) + assert resp.status_code == 400 + + +def test_accepts_with_form_schema_single_value(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + assert request.parsed_args["foo"] == 3 + return "success" + + with client as cl: + resp = cl.post("/test", data={"foo": 3}) + assert resp.status_code == 200 + + +def test_accepts_with_form_schema_list_value(app, client): # noqa + class TestSchema(Schema): + foo = fields.List(fields.String(), required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + assert request.parsed_form["foo"] == ["3"] + assert request.parsed_args["foo"] == ["3"] + return "success" + + with client as cl: + resp = cl.post("/test", data={"foo": 3}) + assert resp.status_code == 200 + + +def test_accepts_with_form_schema_unknown_arguments(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + # Extra query params should be excluded. + assert "bar" not in request.parsed_form + assert request.parsed_form["foo"] == 3 + assert "bar" not in request.parsed_args + assert request.parsed_args["foo"] == 3 + return "success" + + with client as cl: + resp = cl.post("/test", data={"foo": 3, "bar": 4}) + assert resp.status_code == 200 + + +def test_accepts_with_form_schema_data_key(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=False, data_key="fooExternal") + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + assert request.parsed_args["fooExternal"] == 3 + assert request.parsed_form["foo"] == 3 + return "success" + + with client as cl: + resp = cl.post("/test", data={"fooExternal": 3}) + assert resp.status_code == 200 + + +def test_failure_when_form_schema_arg_is_missing(app, client): # noqa + class TestSchema(Schema): + foo = fields.String(required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + pass # pragma: no cover + + with client as cl: + resp = cl.post("/test") + assert resp.status_code == 400 + + +def test_failure_when_form_schema_arg_is_wrong_type(app, client): # noqa + class TestSchema(Schema): + foo = fields.Integer(required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts("TestSchema", form_schema=TestSchema, api=api) + def post(self): + pass # pragma: no cover + + with client as cl: + resp = cl.post("/test", data={"foo": "baz"}) + assert resp.status_code == 400 + + +def test_accepts_with_postional_args_query_params_schema_and_header_schema_and_form_schema(app, client): # noqa + class QueryParamsSchema(Schema): + query_param = fields.List(fields.String(), required=True) + + class HeadersSchema(Schema): + Header = fields.Integer(required=True) + + class FormSchema(Schema): + form = fields.String(required=True) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + dict(name="foo", type=int, help="An important foo"), + query_params_schema=QueryParamsSchema, + headers_schema=HeadersSchema, + form_schema=FormSchema, + api=api) + def post(self): + assert request.parsed_args["foo"] == 3 + assert request.parsed_query_params["query_param"] == ["baz", "qux"] + assert request.parsed_headers["Header"] == 3 + assert request.parsed_form["form"] == "value" + return "success" + + with client as cl: + resp = cl.post( + "/test?foo=3&query_param=baz&query_param=qux", + headers={"Header": "3"}, + data={"form": "value"}) + assert resp.status_code == 200 + + +def test_accept_schema_instance_respects_many(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts(schema=TestSchema(many=True), api=api) + def post(self): + return request.parsed_obj + + with client as cl: + resp = cl.post("/test", data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') + obj = resp.json + assert obj == [{"_id": 42, "name": "Jon Snow"}] + +def test_accepts_with_nested_schema(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + class HostSchema(Schema): + name = fields.String() + child = fields.Nested(TestSchema) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=HostSchema, + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["child"] == {"_id": 42, "name": "tests name"} + assert request.parsed_obj["name"] == "tests host" + return "success" + + with client as cl: + resp = cl.post( + "/test?foo=3", + json={"name": "tests host", "child": {"_id": 42, "name": "tests name"}}, + ) + assert resp.status_code == 200 + + +def test_accepts_with_twice_nested_schema(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + class HostSchema(Schema): + name = fields.String() + child = fields.Nested(TestSchema) + + class HostHostSchema(Schema): + name = fields.String() + child = fields.Nested(HostSchema) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=HostHostSchema, + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["child"]["child"] == { + "_id": 42, + "name": "tests name", + } + assert request.parsed_obj["child"] == { + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, + } + assert request.parsed_obj["name"] == "tests host host" + return "success" + + with client as cl: + resp = cl.post( + "/test?foo=3", + json={ + "name": "tests host host", + "child": { + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, + }, + }, + ) + assert resp.status_code == 200 + + +def test_responds_with_validate(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer(required=True) + name = fields.String(required=True) + + @app.errorhandler(InternalServerError) + def payload_validation_failure(err): + return jsonify({"message": "Server attempted to return invalid data"}), 500 + + @app.route("/test") + @responds(schema=TestSchema, validate=True) + def get(): + obj = {"wrong_field": 42, "name": "Jon Snow"} + return obj + + with app.test_client() as cl: + resp = cl.get("/test") + obj = resp.json + assert resp.status_code == 500 + assert resp.json == {"message": "Server attempted to return invalid data"} + + +def test_responds_with_validate(app, client): # noqa + class TestDataObj: + def __init__(self, wrong_field, name): + self.wrong_field = wrong_field + self.name = name + + class TestSchema(Schema): + _id = fields.Integer(required=True) + name = fields.String(required=True) + + @app.errorhandler(InternalServerError) + def payload_validation_failure(err): + return jsonify({"message": "Server attempted to return invalid data"}), 500 + + @app.route("/test") + @responds(schema=TestSchema, validate=True) + def get(): + obj = {"wrong_field": 42, "name": "Jon Snow"} + data = TestDataObj(**obj) + return data + + with app.test_client() as cl: + resp = cl.get("/test") + obj = resp.json + assert resp.status_code == 500 + assert resp.json == {"message": "Server attempted to return invalid data"} diff --git a/flask_accepts/tests/test_decorators.py b/flask_accepts/tests/test_decorators.py index bd39113..241b769 100644 --- a/flask_accepts/tests/test_decorators.py +++ b/flask_accepts/tests/test_decorators.py @@ -1,4 +1,3 @@ -from flask import request from flask_restx import Resource, Api from marshmallow import Schema, fields from werkzeug.datastructures import MultiDict @@ -8,963 +7,52 @@ from flask_accepts.tests.fixtures import app, client # noqa -def test_arguments_are_added_to_request(app, client): # noqa - @app.route("/test") - @accepts("Foo", dict(name="foo", type=int, help="An important foo")) - def test(): - assert request.parsed_args - return "success" - - with client as cl: - resp = cl.get("/test?foo=3") - assert resp.status_code == 200 - - -def test_arguments_are_added_to_request_with_Resource(app, client): # noqa - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("Foo", dict(name="foo", type=int, help="An important foo"), api=api) - def get(self): - assert request.parsed_args - return "success" - - with client as cl: - resp = cl.get("/test?foo=3") - assert resp.status_code == 200 - - -def test_arguments_are_added_to_request_with_Resource_and_schema(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - schema=TestSchema, - api=api, - ) - def post(self): - assert request.parsed_obj - assert request.parsed_obj["_id"] == 42 - assert request.parsed_obj["name"] == "tests name" - return "success" - - with client as cl: - resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) - assert resp.status_code == 200 - - -def test_arguments_are_added_to_request_with_Resource_and_schema_instance( - app, client -): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - schema=TestSchema(), - api=api, - ) - def post(self): - assert request.parsed_obj - assert request.parsed_obj["_id"] == 42 - assert request.parsed_obj["name"] == "tests name" - return "success" - - with client as cl: - resp = cl.post("/test?foo=3", json={"_id": 42, "name": "tests name"}) - assert resp.status_code == 200 - - -def test_validation_errors_added_to_request_with_Resource_and_schema( - app, client -): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - schema=TestSchema, - api=api, - ) - def post(self): - pass # pragma: no cover - - with client as cl: - resp = cl.post( - "/test?foo=3", - json={"_id": "this is not an integer and will error", "name": "tests name"}, - ) - assert resp.status_code == 400 - assert "Not a valid integer." in resp.json["errors"]["_id"] - - -def test_validation_errors_from_all_added_to_request_with_Resource_and_schema( - app, client -): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - dict(name="foo2", type=int, help="An important foo2"), - schema=TestSchema, - api=api, - ) - def post(self): - pass # pragma: no cover - - with client as cl: - resp = cl.post( - "/test?foo=not_int", - json={"_id": "this is not an integer and will error", "name": "tests name"}, - ) - - assert resp.status_code == 400 - assert "Not a valid integer." in resp.json["errors"]["_id"] - - -def test_dict_arguments_are_correctly_added(app, client): # noqa - @app.route("/test") - @accepts( - {"name": "an_int", "type": int, "help": "An important int"}, - {"name": "a_bool", "type": bool, "help": "An important bool"}, - {"name": "a_str", "type": str, "help": "An important str"}, - ) - def test(): - assert request.parsed_args.get("an_int") == 1 - assert request.parsed_args.get("a_bool") - assert request.parsed_args.get("a_str") == "faraday" - return "success" - - with client as cl: - resp = cl.get("/test?an_int=1&a_bool=1&a_str=faraday") - assert resp.status_code == 200 - - -def test_bool_argument_have_correct_input(app, client): - @app.route("/test") - @accepts(dict(name="foo", type=bool, help="An important bool")) - def test(): - assert request.parsed_args["foo"] == False - return "success" - - with client as cl: - resp = cl.get("/test?foo=false") - assert resp.status_code == 200 - - -def test_failure_when_bool_argument_is_incorrect(app, client): - @app.route("/test") - @accepts(dict(name="foo", type=bool, help="An important bool")) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test?foo=falsee") - assert resp.status_code == 400 - - -def test_failure_when_required_arg_is_missing(app, client): # noqa - @app.route("/test") - @accepts(dict(name="foo", type=int, required=True, help="A required foo")) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 400 - - -def test_failure_when_arg_is_wrong_type(app, client): # noqa - @app.route("/test") - @accepts(dict(name="foo", type=int, required=True, help="A required foo")) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test?foo=baz") - assert resp.status_code == 400 - - -def test_accepts_with_query_params_schema_single_value(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - assert request.parsed_query_params["foo"] == 3 - return "success" - - with client as cl: - resp = cl.get("/test?foo=3") - assert resp.status_code == 200 - - -def test_accepts_with_query_params_schema_list_value(app, client): # noqa - class TestSchema(Schema): - foo = fields.List(fields.String(), required=True) - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - assert request.parsed_query_params["foo"] == ["3"] - return "success" - - with client as cl: - resp = cl.get("/test?foo=3") - assert resp.status_code == 200 - - -def test_accepts_with_query_params_schema_unknown_arguments(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - # Extra query params should be excluded. - assert "bar" not in request.parsed_query_params - assert request.parsed_query_params["foo"] == 3 - return "success" - - with client as cl: - resp = cl.get("/test?foo=3&bar=4") - assert resp.status_code == 200 - - -def test_accepts_with_query_params_schema_data_key(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=False, data_key="fooExternal") - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - assert request.parsed_args["fooExternal"] == 3 - assert request.parsed_query_params["foo"] == 3 - return "success" - - with client as cl: - resp = cl.get("/test?fooExternal=3") - assert resp.status_code == 200 - - -def test_failure_when_query_params_schema_arg_is_missing(app, client): # noqa - class TestSchema(Schema): - foo = fields.String(required=True) - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 400 - - -def test_failure_when_query_params_schema_arg_is_wrong_type(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - @app.route("/test") - @accepts("TestSchema", query_params_schema=TestSchema) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test?foo=baz") - assert resp.status_code == 400 - - -def test_accepts_with_header_schema_single_value(app, client): # noqa - class TestSchema(Schema): - Foo = fields.Integer(required=True) - - @app.route("/test") - @accepts(headers_schema=TestSchema) - def test(): - assert request.parsed_headers["Foo"] == 3 - return "success" - - with client as cl: - resp = cl.get("/test", headers={"Foo": "3"}) - assert resp.status_code == 200 - - -def test_accepts_with_header_schema_list_value(app, client): # noqa - class TestSchema(Schema): - Foo = fields.List(fields.String(), required=True) - - @app.route("/test") - @accepts(headers_schema=TestSchema) - def test(): - assert request.parsed_headers["Foo"] == ["3"] - return "success" - - with client as cl: - resp = cl.get("/test", headers={"Foo": "3"}) - assert resp.status_code == 200 - - -def test_accepts_with_header_schema_unknown_arguments(app, client): # noqa - class TestSchema(Schema): - Foo = fields.List(fields.String(), required=True) - - @app.route("/test") - @accepts(headers_schema=TestSchema) - def test(): - # Extra header values should be excluded. - assert "Bar" not in request.parsed_headers - assert request.parsed_headers["Foo"] == ["3"] - return "success" - - with client as cl: - resp = cl.get("/test", headers={"Foo": "3", "Bar": "4"}) - assert resp.status_code == 200 - - -def test_accepts_with_header_schema_data_key(app, client): # noqa - class TestSchema(Schema): - Foo = fields.Integer(required=False, data_key="Foo-External") - - @app.route("/test") - @accepts("TestSchema", headers_schema=TestSchema) - def test(): - assert request.parsed_headers["Foo"] == 3 - assert request.parsed_args["Foo-External"] == 3 - return "success" - - with client as cl: - resp = cl.get("/test", headers={"Foo-External": "3"}) - assert resp.status_code == 200 - - -def test_failure_when_header_schema_arg_is_missing(app, client): # noqa - class TestSchema(Schema): - Foo = fields.String(required=True) - - @app.route("/test") - @accepts("TestSchema", headers_schema=TestSchema) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 400 - - -def test_failure_when_header_schema_arg_is_wrong_type(app, client): # noqa - class TestSchema(Schema): - Foo = fields.Integer(required=True) - - @app.route("/test") - @accepts("TestSchema", headers_schema=TestSchema) - def test(): - pass # pragma: no cover - - with client as cl: - resp = cl.get("/test", headers={"Foo": "baz"}) - assert resp.status_code == 400 - - -def test_accepts_with_form_schema_single_value(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - assert request.parsed_args["foo"] == 3 - return "success" - - with client as cl: - resp = cl.post("/test", data={"foo": 3}) - assert resp.status_code == 200 - - -def test_accepts_with_form_schema_list_value(app, client): # noqa - class TestSchema(Schema): - foo = fields.List(fields.String(), required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - assert request.parsed_form["foo"] == ["3"] - assert request.parsed_args["foo"] == ["3"] - return "success" - - with client as cl: - resp = cl.post("/test", data={"foo": 3}) - assert resp.status_code == 200 - - -def test_accepts_with_form_schema_unknown_arguments(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - # Extra query params should be excluded. - assert "bar" not in request.parsed_form - assert request.parsed_form["foo"] == 3 - assert "bar" not in request.parsed_args - assert request.parsed_args["foo"] == 3 - return "success" - - with client as cl: - resp = cl.post("/test", data={"foo": 3, "bar": 4}) - assert resp.status_code == 200 - - -def test_accepts_with_form_schema_data_key(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=False, data_key="fooExternal") - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - assert request.parsed_args["fooExternal"] == 3 - assert request.parsed_form["foo"] == 3 - return "success" - - with client as cl: - resp = cl.post("/test", data={"fooExternal": 3}) - assert resp.status_code == 200 - - -def test_failure_when_form_schema_arg_is_missing(app, client): # noqa - class TestSchema(Schema): - foo = fields.String(required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - pass # pragma: no cover - - with client as cl: - resp = cl.post("/test") - assert resp.status_code == 400 - - -def test_failure_when_form_schema_arg_is_wrong_type(app, client): # noqa - class TestSchema(Schema): - foo = fields.Integer(required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts("TestSchema", form_schema=TestSchema, api=api) - def post(self): - pass # pragma: no cover - - with client as cl: - resp = cl.post("/test", data={"foo": "baz"}) - assert resp.status_code == 400 - - -def test_accepts_with_postional_args_query_params_schema_and_header_schema_and_form_schema(app, client): # noqa - class QueryParamsSchema(Schema): - query_param = fields.List(fields.String(), required=True) - - class HeadersSchema(Schema): - Header = fields.Integer(required=True) - - class FormSchema(Schema): - form = fields.String(required=True) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - dict(name="foo", type=int, help="An important foo"), - query_params_schema=QueryParamsSchema, - headers_schema=HeadersSchema, - form_schema=FormSchema, - api=api) - def post(self): - assert request.parsed_args["foo"] == 3 - assert request.parsed_query_params["query_param"] == ["baz", "qux"] - assert request.parsed_headers["Header"] == 3 - assert request.parsed_form["form"] == "value" - return "success" - - with client as cl: - resp = cl.post( - "/test?foo=3&query_param=baz&query_param=qux", - headers={"Header": "3"}, - data={"form": "value"}) - assert resp.status_code == 200 - - -def test_accept_schema_instance_respects_many(app, client): # noqa +def test_schema_generates_correct_swagger(app, client): # noqa class TestSchema(Schema): _id = fields.Integer() name = fields.String() api = Api(app) + route = "/test" - @api.route("/test") + @api.route(route) class TestResource(Resource): - @accepts(schema=TestSchema(many=True), api=api) + @accepts(model_name="MyRequest", schema=TestSchema(many=False), api=api) + @responds(model_name="MyResponse", schema=TestSchema(many=False), api=api, description="My description") def post(self): - return request.parsed_obj - - with client as cl: - resp = cl.post("/test", data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') - obj = resp.json - assert obj == [{"_id": 42, "name": "Jon Snow"}] - - -def test_responds(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api) - def get(self): obj = {"_id": 42, "name": "Jon Snow"} return obj with client as cl: - resp = cl.get("/test") - obj = resp.json - assert obj["_id"] == 42 - assert obj["name"] == "Jon Snow" - - -def test_respond_schema_instance(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema(), api=api) - def get(self): - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - resp = cl.get("/test") - obj = resp.json - assert obj["_id"] == 42 - assert obj["name"] == "Jon Snow" - - -def test_respond_schema_instance_respects_exclude(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema(exclude=("_id",)), api=api) - def get(self): - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - resp = cl.get("/test") - obj = resp.json - assert "_id" not in obj - assert obj["name"] == "Jon Snow" - - -def test_respond_schema_respects_many(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, many=True, api=api) - def get(self): - obj = [{"_id": 42, "name": "Jon Snow"}] - return obj - - with client as cl: - resp = cl.get("/test") - obj = resp.json - assert obj == [{"_id": 42, "name": "Jon Snow"}] + cl.post(route, data='{"_id": 42, "name": "Jon Snow"}', content_type='application/json') + route_docs = api.__schema__["paths"][route]["post"] + responses_docs = route_docs['responses']['200'] + assert responses_docs['description'] == "My description" + assert responses_docs['schema'] == {'$ref': '#/definitions/MyResponse'} + assert route_docs['parameters'][0]['schema'] == {'$ref': '#/definitions/MyRequest'} -def test_respond_schema_instance_respects_many(app, client): # noqa +def test_schema_generates_correct_swagger_for_many(app, client): # noqa class TestSchema(Schema): _id = fields.Integer() name = fields.String() api = Api(app) + route = "/test" - @api.route("/test") + @api.route(route) class TestResource(Resource): - @responds(schema=TestSchema(many=True), api=api) - def get(self): + @accepts(schema=TestSchema(many=True), api=api) + @responds(schema=TestSchema(many=True), api=api, description="My description") + def post(self): obj = [{"_id": 42, "name": "Jon Snow"}] return obj with client as cl: - resp = cl.get("/test") - obj = resp.json - assert obj == [{"_id": 42, "name": "Jon Snow"}] - - -def test_responds_regular_route(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - @app.route("/test", methods=["GET"]) - @responds(schema=TestSchema) - def get(): - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - resp = cl.get("/test") - obj = resp.json - assert obj["_id"] == 42 - assert obj["name"] == "Jon Snow" - - -def test_responds_passes_raw_responses_through_untouched(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api) - def get(self): - from flask import make_response, Response - - obj = {"_id": 42, "name": "Jon Snow"} - return Response("A prebuild response that won't be serialised", 201) - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 201 - - -def test_responds_with_parser(app, client): # noqa - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds( - "King", - dict(name="_id", type=int), - dict(name="name", type=str), - dict(name="value", type=float), - dict(name="status", choices=("alive", "dead")), - dict(name="todos", action="append"), - api=api, - ) - def get(self): - from flask import make_response, Response - - return { - "_id": 42, - "name": "Jon Snow", - "value": 100.0, - "status": "alive", - "todos": ["one", "two"], - } - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 200 - assert resp.json == { - "_id": 42, - "name": "Jon Snow", - "value": 100.0, - "status": "alive", - "todos": ["one", "two"], - } - - -def test_responds_respects_status_code(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api, status_code=999) - def get(self): - from flask import make_response, Response - - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 999 - - -def test_responds_respects_envelope(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api, envelope='tests-data') - def get(self): - from flask import make_response, Response - - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 200 - assert resp.json == {'tests-data': {'_id': 42, 'name': 'Jon Snow'}} - - -def test_responds_skips_none_false(app, client): - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api) - def get(self): - return {"_id": 42, "name": None} - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 200 - assert resp.json == {'_id': 42, 'name': None} - - -def test_responds_with_nested_skips_none_true(app, client): - class NestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - class TestSchema(Schema): - name = fields.String() - child = fields.Nested(NestSchema) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @responds(schema=TestSchema, api=api, skip_none=True, many=True) - def get(self): - return [{"name": None, "child": {"_id": 42, "name": None}}] - - with client as cl: - resp = cl.get("/test") - assert resp.status_code == 200 - assert resp.json == [{"child": {'_id': 42}}] - - -def test_accepts_with_nested_schema(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - class HostSchema(Schema): - name = fields.String() - child = fields.Nested(TestSchema) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - schema=HostSchema, - api=api, - ) - def post(self): - assert request.parsed_obj - assert request.parsed_obj["child"] == {"_id": 42, "name": "tests name"} - assert request.parsed_obj["name"] == "tests host" - return "success" - - with client as cl: - resp = cl.post( - "/test?foo=3", - json={"name": "tests host", "child": {"_id": 42, "name": "tests name"}}, - ) - assert resp.status_code == 200 - - -def test_accepts_with_twice_nested_schema(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - class HostSchema(Schema): - name = fields.String() - child = fields.Nested(TestSchema) - - class HostHostSchema(Schema): - name = fields.String() - child = fields.Nested(HostSchema) - - api = Api(app) - - @api.route("/test") - class TestResource(Resource): - @accepts( - "Foo", - dict(name="foo", type=int, help="An important foo"), - schema=HostHostSchema, - api=api, - ) - def post(self): - assert request.parsed_obj - assert request.parsed_obj["child"]["child"] == { - "_id": 42, - "name": "tests name", - } - assert request.parsed_obj["child"] == { - "name": "tests host", - "child": {"_id": 42, "name": "tests name"}, - } - assert request.parsed_obj["name"] == "tests host host" - return "success" - - with client as cl: - resp = cl.post( - "/test?foo=3", - json={ - "name": "tests host host", - "child": { - "name": "tests host", - "child": {"_id": 42, "name": "tests name"}, - }, - }, - ) - assert resp.status_code == 200 - - -def test_responds_with_validate(app, client): # noqa - import pytest - from flask import jsonify - from werkzeug.exceptions import InternalServerError - - class TestSchema(Schema): - _id = fields.Integer(required=True) - name = fields.String(required=True) - - @app.errorhandler(InternalServerError) - def payload_validation_failure(err): - return jsonify({"message": "Server attempted to return invalid data"}), 500 - - @app.route("/test") - @responds(schema=TestSchema, validate=True) - def get(): - obj = {"wrong_field": 42, "name": "Jon Snow"} - return obj - - with app.test_client() as cl: - resp = cl.get("/test") - obj = resp.json - assert resp.status_code == 500 - assert resp.json == {"message": "Server attempted to return invalid data"} - - -def test_responds_with_validate(app, client): # noqa - import pytest - from flask import jsonify - from werkzeug.exceptions import InternalServerError - - class TestDataObj: - def __init__(self, wrong_field, name): - self.wrong_field = wrong_field - self.name = name - - class TestSchema(Schema): - _id = fields.Integer(required=True) - name = fields.String(required=True) - - @app.errorhandler(InternalServerError) - def payload_validation_failure(err): - return jsonify({"message": "Server attempted to return invalid data"}), 500 - - @app.route("/test") - @responds(schema=TestSchema, validate=True) - def get(): - obj = {"wrong_field": 42, "name": "Jon Snow"} - data = TestDataObj(**obj) - return data - - with app.test_client() as cl: - resp = cl.get("/test") - obj = resp.json - assert resp.status_code == 500 - assert resp.json == {"message": "Server attempted to return invalid data"} - + resp = cl.post(route, data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') + route_docs = api.__schema__["paths"][route]["post"] + assert route_docs['responses']['200']['schema'] == {"type": "array", "items": {"$ref": "#/definitions/Test"}} + assert route_docs['parameters'][0]['schema'] == {"type": "array", "items": {"$ref": "#/definitions/Test"}} def test_multidict_single_values_interpreted_correctly(app, client): # noqa class TestSchema(Schema): @@ -989,7 +77,6 @@ class TestSchema(Schema): result = _convert_multidict_values_to_schema(multidict, TestSchema()) assert result["name"] == "value" - def test_multidict_list_values_interpreted_correctly(app, client): # noqa class TestSchema(Schema): name = fields.List(fields.String(), required=True) @@ -1013,99 +100,3 @@ class TestSchema(Schema): ]) result = _convert_multidict_values_to_schema(multidict, TestSchema()) assert result["name"] == ["value", "value2"] - - -def test_no_schema_generates_correct_swagger(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - route = "/test" - - @api.route(route) - class TestResource(Resource): - @responds(api=api, status_code=201, description="My description") - def post(self): - obj = [{"_id": 42, "name": "Jon Snow"}] - return obj - - with client as cl: - cl.post(route, data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') - route_docs = api.__schema__["paths"][route]["post"] - - responses_docs = route_docs['responses']['201'] - - assert responses_docs['description'] == "My description" - - -def test_schema_generates_correct_swagger(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - route = "/test" - - @api.route(route) - class TestResource(Resource): - @accepts(model_name="MyRequest", schema=TestSchema(many=False), api=api) - @responds(model_name="MyResponse", schema=TestSchema(many=False), api=api, description="My description") - def post(self): - obj = {"_id": 42, "name": "Jon Snow"} - return obj - - with client as cl: - cl.post(route, data='{"_id": 42, "name": "Jon Snow"}', content_type='application/json') - route_docs = api.__schema__["paths"][route]["post"] - responses_docs = route_docs['responses']['200'] - - assert responses_docs['description'] == "My description" - assert responses_docs['schema'] == {'$ref': '#/definitions/MyResponse'} - assert route_docs['parameters'][0]['schema'] == {'$ref': '#/definitions/MyRequest'} - - -def test_schema_generates_correct_swagger_for_many(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - route = "/test" - - @api.route(route) - class TestResource(Resource): - @accepts(schema=TestSchema(many=True), api=api) - @responds(schema=TestSchema(many=True), api=api, description="My description") - def post(self): - obj = [{"_id": 42, "name": "Jon Snow"}] - return obj - - with client as cl: - resp = cl.post(route, data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') - route_docs = api.__schema__["paths"][route]["post"] - assert route_docs['responses']['200']['schema'] == {"type": "array", "items": {"$ref": "#/definitions/Test"}} - assert route_docs['parameters'][0]['schema'] == {"type": "array", "items": {"$ref": "#/definitions/Test"}} - - -def test_swagger_respects_existing_response_docs(app, client): # noqa - class TestSchema(Schema): - _id = fields.Integer() - name = fields.String() - - api = Api(app) - route = "/test" - - @api.route(route) - class TestResource(Resource): - @responds(schema=TestSchema(many=True), api=api, description="My description") - @api.doc(responses={401: "Not Authorized", 404: "Not Found"}) - def get(self): - return [{"_id": 42, "name": "Jon Snow"}] - - with client as cl: - cl.get(route) - route_docs = api.__schema__["paths"][route]["get"] - assert route_docs["responses"]["200"]["description"] == "My description" - assert route_docs["responses"]["401"]["description"] == "Not Authorized" - assert route_docs["responses"]["404"]["description"] == "Not Found" diff --git a/flask_accepts/tests/test_responds.py b/flask_accepts/tests/test_responds.py new file mode 100644 index 0000000..b5c1422 --- /dev/null +++ b/flask_accepts/tests/test_responds.py @@ -0,0 +1,462 @@ +from flask import request, Response, jsonify +from flask_restx import Resource, Api +from marshmallow import Schema, fields +from werkzeug.exceptions import InternalServerError + +from flask_accepts.decorators import accepts, responds +from flask_accepts.tests.fixtures import app, client # noqa + +def test_responds(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api) + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert obj["_id"] == 42 + assert obj["name"] == "Jon Snow" + + +def test_respond_schema_instance(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema(), api=api) + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert obj["_id"] == 42 + assert obj["name"] == "Jon Snow" + + +def test_respond_schema_instance_respects_exclude(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema(exclude=("_id",)), api=api) + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert "_id" not in obj + assert obj["name"] == "Jon Snow" + + +def test_respond_schema_respects_many(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, many=True, api=api) + def get(self): + obj = [{"_id": 42, "name": "Jon Snow"}] + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert obj == [{"_id": 42, "name": "Jon Snow"}] + + +def test_respond_schema_instance_respects_many(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema(many=True), api=api) + def get(self): + obj = [{"_id": 42, "name": "Jon Snow"}] + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert obj == [{"_id": 42, "name": "Jon Snow"}] + + +def test_responds_regular_route(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + @app.route("/test", methods=["GET"]) + @responds(schema=TestSchema) + def get(): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + obj = resp.json + assert obj["_id"] == 42 + assert obj["name"] == "Jon Snow" + + +def test_responds_passes_raw_responses_through_untouched(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api) + def get(self): + + + obj = {"_id": 42, "name": "Jon Snow"} + return Response("A prebuild response that won't be serialised", 201) + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 201 + + +def test_responds_with_parser(app, client): # noqa + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds( + "King", + dict(name="_id", type=int), + dict(name="name", type=str), + dict(name="value", type=float), + dict(name="status", choices=("alive", "dead")), + dict(name="todos", action="append"), + api=api, + ) + def get(self): + return { + "_id": 42, + "name": "Jon Snow", + "value": 100.0, + "status": "alive", + "todos": ["one", "two"], + } + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 200 + assert resp.json == { + "_id": 42, + "name": "Jon Snow", + "value": 100.0, + "status": "alive", + "todos": ["one", "two"], + } + + +def test_responds_respects_status_code(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api, status_code=999) + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 999 + +def test_responds_respects_custom_status_code(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api, status_code=999) + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj, 888 + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 888 + +def test_responds_respects_envelope(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api, envelope='tests-data') + def get(self): + obj = {"_id": 42, "name": "Jon Snow"} + return obj + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 200 + assert resp.json == {'tests-data': {'_id': 42, 'name': 'Jon Snow'}} + + +def test_responds_skips_none_false(app, client): + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api) + def get(self): + return {"_id": 42, "name": None} + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 200 + assert resp.json == {'_id': 42, 'name': None} + + +def test_responds_with_nested_skips_none_true(app, client): + class NestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + class TestSchema(Schema): + name = fields.String() + child = fields.Nested(NestSchema) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @responds(schema=TestSchema, api=api, skip_none=True, many=True) + def get(self): + return [{"name": None, "child": {"_id": 42, "name": None}}] + + with client as cl: + resp = cl.get("/test") + assert resp.status_code == 200 + assert resp.json == [{"child": {'_id': 42}}] + + +def test_accepts_with_nested_schema(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + class HostSchema(Schema): + name = fields.String() + child = fields.Nested(TestSchema) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=HostSchema, + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["child"] == {"_id": 42, "name": "tests name"} + assert request.parsed_obj["name"] == "tests host" + return "success" + + with client as cl: + resp = cl.post( + "/test?foo=3", + json={"name": "tests host", "child": {"_id": 42, "name": "tests name"}}, + ) + assert resp.status_code == 200 + + +def test_accepts_with_twice_nested_schema(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + class HostSchema(Schema): + name = fields.String() + child = fields.Nested(TestSchema) + + class HostHostSchema(Schema): + name = fields.String() + child = fields.Nested(HostSchema) + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + @accepts( + "Foo", + dict(name="foo", type=int, help="An important foo"), + schema=HostHostSchema, + api=api, + ) + def post(self): + assert request.parsed_obj + assert request.parsed_obj["child"]["child"] == { + "_id": 42, + "name": "tests name", + } + assert request.parsed_obj["child"] == { + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, + } + assert request.parsed_obj["name"] == "tests host host" + return "success" + + with client as cl: + resp = cl.post( + "/test?foo=3", + json={ + "name": "tests host host", + "child": { + "name": "tests host", + "child": {"_id": 42, "name": "tests name"}, + }, + }, + ) + assert resp.status_code == 200 + + +def test_responds_with_validate(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer(required=True) + name = fields.String(required=True) + + @app.errorhandler(InternalServerError) + def payload_validation_failure(err): + return jsonify({"message": "Server attempted to return invalid data"}), 500 + + @app.route("/test") + @responds(schema=TestSchema, validate=True) + def get(): + obj = {"wrong_field": 42, "name": "Jon Snow"} + return obj + + with app.test_client() as cl: + resp = cl.get("/test") + obj = resp.json + assert resp.status_code == 500 + assert resp.json == {"message": "Server attempted to return invalid data"} + + +def test_responds_with_validate(app, client): # noqa + class TestDataObj: + def __init__(self, wrong_field, name): + self.wrong_field = wrong_field + self.name = name + + class TestSchema(Schema): + _id = fields.Integer(required=True) + name = fields.String(required=True) + + @app.errorhandler(InternalServerError) + def payload_validation_failure(err): + return jsonify({"message": "Server attempted to return invalid data"}), 500 + + @app.route("/test") + @responds(schema=TestSchema, validate=True) + def get(): + obj = {"wrong_field": 42, "name": "Jon Snow"} + data = TestDataObj(**obj) + return data + + with app.test_client() as cl: + resp = cl.get("/test") + obj = resp.json + assert resp.status_code == 500 + assert resp.json == {"message": "Server attempted to return invalid data"} + + +def test_no_schema_generates_correct_swagger(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + route = "/test" + + @api.route(route) + class TestResource(Resource): + @responds(api=api, status_code=201, description="My description") + def post(self): + obj = [{"_id": 42, "name": "Jon Snow"}] + return obj + + with client as cl: + cl.post(route, data='[{"_id": 42, "name": "Jon Snow"}]', content_type='application/json') + route_docs = api.__schema__["paths"][route]["post"] + + responses_docs = route_docs['responses']['201'] + + assert responses_docs['description'] == "My description" + + +def test_swagger_respects_existing_response_docs(app, client): # noqa + class TestSchema(Schema): + _id = fields.Integer() + name = fields.String() + + api = Api(app) + route = "/test" + + @api.route(route) + class TestResource(Resource): + @responds(schema=TestSchema(many=True), api=api, description="My description") + @api.doc(responses={401: "Not Authorized", 404: "Not Found"}) + def get(self): + return [{"_id": 42, "name": "Jon Snow"}] + + with client as cl: + cl.get(route) + route_docs = api.__schema__["paths"][route]["get"] + assert route_docs["responses"]["200"]["description"] == "My description" + assert route_docs["responses"]["401"]["description"] == "Not Authorized" + assert route_docs["responses"]["404"]["description"] == "Not Found" diff --git a/flask_accepts/tests/test_utils.py b/flask_accepts/tests/test_utils.py index bcd1b66..e78d4ac 100644 --- a/flask_accepts/tests/test_utils.py +++ b/flask_accepts/tests/test_utils.py @@ -8,7 +8,6 @@ from flask import Flask from flask_restx import Api, fields as fr, namespace -# from flask_accepts.utils import unpack_list, unpack_nested import flask_accepts.utils as utils @@ -96,46 +95,40 @@ class IntegerSchema(Schema): def test_get_default_model_name(): - from flask_accepts.utils import get_default_model_name - class TestSchema(Schema): pass - result = get_default_model_name(TestSchema) + result = utils.get_default_model_name(TestSchema) expected = "Test" assert result == expected def test_get_default_model_name_works_with_multiple_schema_in_name(): - from flask_accepts.utils import get_default_model_name - class TestSchemaSchema(Schema): pass - result = get_default_model_name(TestSchemaSchema) + result = utils.get_default_model_name(TestSchemaSchema) expected = "TestSchema" assert result == expected def test_get_default_model_name_that_does_not_end_in_schema(): - from flask_accepts.utils import get_default_model_name - class SomeOtherName(Schema): pass - result = get_default_model_name(SomeOtherName) + result = utils.get_default_model_name(SomeOtherName) expected = "SomeOtherName" assert result == expected def test_get_default_model_name_default_names(): - from flask_accepts.utils import get_default_model_name, num_default_models + from flask_accepts.utils import num_default_models for model_num in range(5): - result = get_default_model_name() + result = utils.get_default_model_name() expected = f"DefaultResponseModel_{model_num + num_default_models}" assert result == expected @@ -269,82 +262,70 @@ class FakeFieldWithNoParams(ma.Field): def test_make_type_mapper_works_with_required(): - from flask_accepts.utils import make_type_mapper - app = Flask(__name__) api = Api(app) - mapper = make_type_mapper(fr.Raw) + mapper = utils.make_type_mapper(fr.Raw) result = mapper(ma.Raw(required=True), api=api, model_name="test_model_name", operation="load") assert result.required def test_make_type_mapper_produces_nonrequired_param_by_default(): - from flask_accepts.utils import make_type_mapper - app = Flask(__name__) api = Api(app) - mapper = make_type_mapper(fr.Raw) + mapper = utils.make_type_mapper(fr.Raw) result = mapper(ma.Raw(), api=api, model_name="test_model_name", operation="load") assert not result.required def test__maybe_add_operation_passes_through_if_no_load_only(): - from flask_accepts.utils import _maybe_add_operation - class TestSchema(Schema): _id = ma.Integer() model_name = "TestSchema" operation = "load" - result = _maybe_add_operation(TestSchema(), model_name, operation) + result = utils._maybe_add_operation(TestSchema(), model_name, operation) expected = model_name assert result == expected def test__maybe_add_operation_append_if_load_only(): - from flask_accepts.utils import _maybe_add_operation - class TestSchema(Schema): _id = ma.Integer(load_only=True) model_name = "TestSchema" operation = "load" - result = _maybe_add_operation(TestSchema(), model_name, operation) + result = utils._maybe_add_operation(TestSchema(), model_name, operation) expected = f"{model_name}-load" assert result == expected def test__maybe_add_operation_passes_through_if_no_dump_only(): - from flask_accepts.utils import _maybe_add_operation - class TestSchema(Schema): _id = ma.Integer() model_name = "TestSchema" operation = "dump" - result = _maybe_add_operation(TestSchema(), model_name, operation) + result = utils._maybe_add_operation(TestSchema(), model_name, operation) expected = model_name assert result == expected def test__maybe_add_operation_append_if_dump_only(): - from flask_accepts.utils import _maybe_add_operation - class TestSchema(Schema): _id = ma.Integer(dump_only=True) model_name = "TestSchema" operation = "dump" - result = _maybe_add_operation(TestSchema(), model_name, operation) + result = utils._maybe_add_operation(TestSchema(), model_name, operation) expected = f"{model_name}-dump" assert result == expected From 8a57fb89c556989a4d1641c3463faf2cd61f7109 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:13:56 +0800 Subject: [PATCH 16/21] removed unused import --- flask_accepts/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask_accepts/utils.py b/flask_accepts/utils.py index 2342144..c8d6e47 100644 --- a/flask_accepts/utils.py +++ b/flask_accepts/utils.py @@ -4,7 +4,6 @@ from marshmallow import fields as ma from marshmallow import __version_info__ as marshmallow_version from marshmallow.schema import Schema, SchemaMeta -import uuid _ma_key_for_fr_example_key = "dump_default" From 1f6749f52dc8efe19759df58b78c4ecafbc33345 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:50:56 +0800 Subject: [PATCH 17/21] use local vars for clarity --- flask_accepts/decorators/decorators.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flask_accepts/decorators/decorators.py b/flask_accepts/decorators/decorators.py index c90d035..6d5d799 100644 --- a/flask_accepts/decorators/decorators.py +++ b/flask_accepts/decorators/decorators.py @@ -303,19 +303,20 @@ def inner(*args, **kwargs): if isinstance(rv, Response): return rv + resp_schema = schema # allow overriding the status code passed to Flask if isinstance(rv, tuple): rv, status_code = rv if alt_schemas and status_code in alt_schemas: - # override the response schema - schema = alt_schemas[status_code] + # override the default response schema + resp_schema = _get_or_create_schema(alt_schemas[status_code]) - if schema: - serialized = schema.dump(rv) + if resp_schema: + serialized = resp_schema.dump(rv) # Validate data if asked to (throws) if validate: - errs = schema.validate(serialized) + errs = resp_schema.validate(serialized) if errs: raise InternalServerError( description="Server attempted to return invalid data" From ff03d23d7756e673fbf38b2b70e6f0df9e9390cc Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:51:36 +0800 Subject: [PATCH 18/21] added test for alternative schema responses --- flask_accepts/tests/test_responds.py | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/flask_accepts/tests/test_responds.py b/flask_accepts/tests/test_responds.py index b5c1422..6bc7ce5 100644 --- a/flask_accepts/tests/test_responds.py +++ b/flask_accepts/tests/test_responds.py @@ -1,3 +1,6 @@ +import json + +from attr import dataclass from flask import request, Response, jsonify from flask_restx import Resource, Api from marshmallow import Schema, fields @@ -6,6 +9,7 @@ from flask_accepts.decorators import accepts, responds from flask_accepts.tests.fixtures import app, client # noqa + def test_responds(app, client): # noqa class TestSchema(Schema): _id = fields.Integer() @@ -460,3 +464,53 @@ def get(self): assert route_docs["responses"]["200"]["description"] == "My description" assert route_docs["responses"]["401"]["description"] == "Not Authorized" assert route_docs["responses"]["404"]["description"] == "Not Found" + +def test_responds_can_use_alt_schema(app, client): # noqa + class DefaultSchema(Schema): + id = fields.Integer() + name = fields.String() + + class ErrorSchema(Schema): + code = fields.String() + error = fields.String() + + class TokenSchema(Schema): + access_token = fields.String() + refresh_token = fields.String() + + api = Api(app) + + @api.route("/test") + class TestResource(Resource): + alt_schemas = { + 888: TokenSchema, + 666: ErrorSchema, + } + @responds(schema=DefaultSchema, api=api, alt_schemas=alt_schemas) + def get(self): + resp_code = int(request.args.get("code")) + + if resp_code == 888: + resp = {"access_token": "test_access_token", "refresh_token": "test_refresh_token"} + elif resp_code == 666: + resp = {"code": "UNKNOWN", "error": "Unhandled Exception"} + else: + resp = {"id": 1234, "name": "Fred Smith"} + + return resp, resp_code + + with client as cl: + # test alternate schema + resp = cl.get("/test?code=666") + assert resp.status_code == 666 + assert resp.json == {"code": "UNKNOWN", "error": "Unhandled Exception"} + + # test different alternate schema + resp = cl.get("/test?code=888") + assert resp.status_code == 888 + assert resp.json == {"access_token": "test_access_token", "refresh_token": "test_refresh_token"} + + # test fallback to default schema with status code passthrough + resp = cl.get("/test?code=401") + assert resp.status_code == 401 + assert resp.json == {"id": 1234, "name": "Fred Smith"} From 3f2e569ed6f4e86a49bd75295aa6b42b3ab313a7 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:57:42 +0800 Subject: [PATCH 19/21] fixed broken links in TOC --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index efbad89..92ff75e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ --- -- [flask_accepts](#flask-accepts) +- [flask_accepts](#flask_accepts) - [Installation](#installation) - [Basic usage](#basic-usage) - - [Usage with "vanilla Flask"](#usage-with--vanilla-flask-) + - [Usage with "vanilla Flask"](#usage-with-vanilla-flask) * [Usage with Marshmallow schemas](#usage-with-marshmallow-schemas) - [Marshmallow validators](#marshmallow-validators) - [Default values](#default-values) From 4d791513403d09df76bea17889ddf3a5a811d908 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:24:54 +0800 Subject: [PATCH 20/21] Updated docs --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 92ff75e..a2c7e5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![codecov](https://codecov.io/gh/apryor6/flask_accepts/branch/master/graph/badge.svg)](https://codecov.io/gh/apryor6/flask_accepts) +from lib2to3.btm_utils import tokens[![codecov](https://codecov.io/gh/apryor6/flask_accepts/branch/master/graph/badge.svg)](https://codecov.io/gh/apryor6/flask_accepts) [![license](https://img.shields.io/github/license/apryor6/flask_accepts)](https://img.shields.io/github/license/apryor6/flask_accepts) [![code_style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://img.shields.io/badge/code%20style-black-000000.svg) @@ -11,6 +11,8 @@ * [Usage with Marshmallow schemas](#usage-with-marshmallow-schemas) - [Marshmallow validators](#marshmallow-validators) - [Default values](#default-values) + * [ Returning Different Response Schemas](#returning-different-response-schemas) + - [Pass-through of arbitrary status codes](#pass-through-of-arbitrary-status-codes) * [Automatic Swagger documentation](#automatic-swagger-documentation) - [Defining the model name](#defining-the-model-name) - [Error handling](#error-handling) @@ -187,6 +189,71 @@ You can provide any of the built-in validators to Marshmallow schemas. See [here Default values provided to Marshmallow schemas will be internally mapped and displayed in the Swagger documentation. See [this example](https://github.com/apryor6/flask_accepts/blob/master/examples/default_values.py) for a usage of `flask_accepts` with nested Marshmallow schemas and default values that will display correctly in Swagger. +## Returning Different Response Schemas + +In real world scenarios things dont always go to plan and you may need to return an error code with your response data +or a different schema entirely (eg Internal Serrver Errors) + +the `responds` decorator accepts a dictionary of response codes and their associated schema. +when returning response data simply provied the status code as you would for a standard Flask response. +The response will then be loaded into the correct schema. + +**Example:** +```python + +class LoginSchema(Schema): + username = fields.String() + password = fields.String() + +class TokenSchema(Schema): + access_token = fields.String() + id_token = fields.String() + refresh_token = fields.String() + +class ErrorSchema(Schema): + error_code = fields.Integer() + errors = fields.List() + +@api.route("/restx/update_user") +class LoginResource(Resource): + alt_schemas = {401: ErrorSchema} + @accepts(schema=LoginSchema, api=api) + @responds(schema=TokenSchema, alt_schemas=alt_schemas, api=api) + def post(self): + payload = request.parsed_obj + username = payload.username + password = payload.password + + tokens = self.attempt_login(username, password) + if tokens is None: + return {"error_code": 8001, "errors": ["invalid username or password"]}, 401 + + return tokens +``` + +### Pass-through of arbitrary status codes +You can also provide a status code that does not have an associated schema +Also works if there are no alternate schemas set. + +The response data will be loaded into the default schema and the provided status code passed through. +An example of this usage might be returning a 422 for bad/invalid data. +```python + +class ResponseSchema(Schema): + response = fields.String() + errors = fields.List() + +@api.route("/restx/update_user") +class UserResource(Resource): + @responds(schema=ResponseSchema, api=api) + def post(self): + # check data + if not data_valid: + return {"response": None, "errors": ["invalid user id"]}, 422 + + return {"response": "user updated", "errors": []} +``` + ## Automatic Swagger documentation The `accepts` decorator will automatically enable Swagger by internally adding the `@api.expects` decorator. If you have provided positional arguments to `accepts`, this involves generating the corresponding `api.parser()` (which is a `reqparse.RequestParser` that includes the Swagger context). If you provide a Marshmallow Schema, an equivalent `api.model` is generated and passed to `@api.expect`. These two can be mixed-and-matched, and the documentation will update accordingly. From eeb3a6270d896efe4a202cafd34c2267bd16cda7 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:13:45 +0800 Subject: [PATCH 21/21] fixewd dependency issues for python 3.7 --- requirements.txt | 6 ++++-- setup.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b6b6634..8ad3f9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -flask>=2,<3 -flask-restx>=1,<2 +flask>=2,<3; python_version < '3.8' +flask>=3.0; python_version >= '3.8' +flask-restx==1.1; python_version < '3.8' +flask-restx>=1.2; python_version >= '3.8' werkzeug>=2,<3; python_version < '3.8' werkzeug>=3,<4; python_version >= '3.8' diff --git a/setup.py b/setup.py index 79570da..9ed1760 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ packages=find_packages(), install_requires=[ "marshmallow>=3.17.0", - "flask-restx>=1.0.1", + "flask-restx==1.1.0; python_version < '3.8'", + "flask-restx>=1.2.0; python_version >= '3.8'", "werkzeug>=2,<3; python_version < '3.8'", "werkzeug>=3,<4; python_version >= '3.8'", ],