Skip to content

Commit

Permalink
Merge branch 'BP-778/Issue-125/improve-swagger-support-for-authentica…
Browse files Browse the repository at this point in the history
…tors' of github.com:plangrid/flask-rebar into BP-778/Issue-125/improve-swagger-support-for-authenticators
  • Loading branch information
airstandley committed Sep 25, 2019
2 parents 6e99be8 + a0b07b9 commit 69d378c
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.10.0
current_version = 1.10.2
commit = True
tag = True

Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,5 @@ man/
.pytest_cache

#vscode
.vscode/
.vscode/
pip-wheel-metadata
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ jobs:
password:
secure: GoFaAo3a08PoPklGQ68q7EMYE59hb1Z1+PEMuSSXKelL2amyG6/kq5t7r8p6trjFabco1u94IwZFFlmUdfr4PqDQ/eaM9MVF1VvI8dxp/fg3WgSxjtxy4io/ELUWTGXTGs3V8Lp61haRdHjrqk5DwG6SHDVVzjuJBZ4Kydr6fuo0RWu9C4qAkU1MWmoIPeyeB4ZydXimD15uOvMp13UA7SqLUdXlAhwyL7c2h6pihu2gnbLXEnfkh8rlvRT8fSp7PUkFsg3FOvgMZg8dn8/J0I/pTmG7c71v0hacOVTKLsfAtSSHAjgcc8KffvMBOcE/YXgUr+lgR4R181lMqXv2LBNMuE5Gyc8GBjTb8Liiowpvg8XXH86vC/k+0/3toKWPobjsXJCAgR21+B7FFzE8hmZdP43HcSfCqbzS8ELUWF9AvUBlFtdakr0a0wLjcKsJyBNiKwwmw50kKsb7agpIXgS/I0Y6EyGwyXi8/q7IPmR7wffg3bAGnvvg5yz7vOZTGYJnFM+Ky1LdQ7fJ44D+Ze/SraB1syK2pmednX6YVMgIlPU5QpaprmQ9AkeoWmPZBShWl07Js73BnbQKWklkV8yAXq9qy51cjRA9SWQIFzeiHScOwhUalb1CpggOyG1sx4/GMFLg5nNE0ldKDfqUGJ4kbcNh25jiiBEJ7mEQkB0=
on:
branch: master
tags: true
29 changes: 29 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@ Changelog
=========


v1.10.2 (2019-09-19)
--------------------

Fix
~~~
- Update authenticators to catch Forbidden exception (#133) [Marc-Éric]


v1.10.1 (2019-09-19)
--------------------

Changes
~~~~~~~
- Tweaking build rules, updating docs, and prepping for bumpversion do-
over. [Rick Riensche]

Fix
~~~
- Treat "description" key the same way as "explode" key for query and h…
(#129) [Artem Revenko]

Other
~~~~~
- Accept bare class for schema arguments (#126) [Rick Riensche]
- Fix marshmallow test helpers so that they work will all unittest
compatible frameworks and not just pytest. 'python setup.py test'
works again. (#127) [Andrew Standley]


v1.10.0 (2019-09-11)
--------------------
- BP-763: Add support for multiple authenticators (#122) [Andrew
Expand Down
22 changes: 15 additions & 7 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,30 @@ Pull Requests
Releasing to PyPI
-----------------

Travis CI handles releasing package versions to PyPI.
We use Travis CI to automate releasing package versions to PyPI.

.. warning:: These steps must be completed by an administrator. We generally do at least patch releases fairly frequently, but if you have a feature that urgently requires release, feel free to reach out and request one and we'll do our best to accommodate.


Flask-Rebar uses `semantic versions <https://semver.org/>`_. Once you know the appropriate version part to bump, use the ``bumpversion`` tool which will bump the package version, add a commit, and tag the commit appropriately. Note, it's not a bad idea to do a manual inspection and any cleanup you deem necessary after running ``gitchangelog`` to ensure it looks good before then committing a "@cosmetic" update.

.. note:: Before completing the following steps, you will need to temporarily change settings on GitHub under branch protection rules to NOT include administrators. This is required to allow you to push the changelog update.

.. code-block:: bash
git checkout -b your-release-branch
bumpversion minor
git checkout master
git pull # just to play it safe and make sure you're up to date
bumpversion patch # or major or minor if applicable
gitchangelog
# STOP HERE: inspect CHANGELOG.rst and clean up as needed before continuing
git commit -a -m "@cosmetic - changelog"
Then push the new commit and tags:
Then push the new commits and tags:

.. code-block:: bash
git push -u origin your-release-branch --tags
git push && git push --tags
Finally, while you're waiting for Travis CI to pick up the tagged version, build it, and deploy it to PyPi, don't forget to reset branch protection settings (for normal operation, administrators should be subject to these restrictions to enforce PR code review requirements).


Create a Pull Request and merge back into master. Voila.
23 changes: 20 additions & 3 deletions flask_rebar/rebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@
from flask_rebar.utils.request_utils import get_header_params_or_400
from flask_rebar.utils.request_utils import get_json_body_params_or_400
from flask_rebar.utils.request_utils import get_query_string_params_or_400
from flask_rebar.utils.request_utils import normalize_schema
from flask_rebar.utils.deprecation import deprecated, deprecated_parameters
from flask_rebar.swagger_generation import SwaggerV2Generator
from flask_rebar.swagger_ui import create_swagger_ui_blueprint

# Deal with maintaining (for now at least) support for 2.7+:
try:
from collections.abc import Mapping # 3.3+
except ImportError:
from collections import Mapping # 2.7+

# To catch redirection exceptions, app.errorhandler expects 301 in versions
# below 0.11.0 but the exception itself in versions greater than 0.11.0.
Expand Down Expand Up @@ -119,7 +125,7 @@ def wrapped(*args, **kwargs):
try:
authenticator.authenticate()
break # Short-circuit on first successful authentication
except errors.Unauthorized as e:
except (errors.Unauthorized, errors.Forbidden) as e:
first_error = first_error or e
else:
raise first_error or errors.Unauthorized
Expand Down Expand Up @@ -459,8 +465,19 @@ def add_handler(
:param Type[USE_DEFAULT]|None|str mimetype:
Content-Type header to add to the response schema
"""
if isinstance(response_body_schema, marshmallow.Schema):
response_body_schema = {200: response_body_schema}
# Fix #115: if we were passed bare classes we'll go ahead and instantiate
headers_schema = normalize_schema(headers_schema)
request_body_schema = normalize_schema(request_body_schema)
query_string_schema = normalize_schema(query_string_schema)
if response_body_schema:
# Ensure we wrap in appropriate default (200) dict if we were passed a single Schema or class:
if not isinstance(response_body_schema, Mapping):
response_body_schema = {200: response_body_schema}
# use normalize_schema to convert any class reference(s) to instantiated schema(s):
response_body_schema = {
code: normalize_schema(schema)
for (code, schema) in response_body_schema.items()
}

# authenticators can be a list of Authenticators, a single Authenticator, USE_DEFAULT, or None
if isinstance(authenticators, Authenticator) or authenticators is USE_DEFAULT:
Expand Down
3 changes: 3 additions & 0 deletions flask_rebar/swagger_generation/swagger_generator_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ def _convert_schema_to_list_of_parameters(self, schema, converter, in_):
# Pardon the ugliness.
# We need the "explode" key to be at the parameters level, not at the schema level.
explode = jsonschema.pop(sw.explode, None)
description = jsonschema.pop(sw.description, None)

parameter = {
sw.name: prop,
Expand All @@ -334,6 +335,8 @@ def _convert_schema_to_list_of_parameters(self, schema, converter, in_):

if explode is not None:
parameter[sw.explode] = explode
if description is not None:
parameter[sw.description] = description

parameters.append(parameter)

Expand Down
3 changes: 2 additions & 1 deletion flask_rebar/utils/request_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from flask_rebar import compat
from flask_rebar import errors
from flask_rebar import messages
from flask_rebar.utils.defaults import USE_DEFAULT


class HeadersProxy(compat.Mapping):
Expand Down Expand Up @@ -92,7 +93,7 @@ def normalize_schema(schema):
This allows for either an instance of a marshmallow.Schema or the class
itself to be passed to functions.
"""
if not isinstance(schema, marshmallow.Schema):
if schema not in (None, USE_DEFAULT) and not isinstance(schema, marshmallow.Schema):
schema = schema()
return schema

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
if __name__ == "__main__":
setup(
name="flask-rebar",
version="1.10.0",
version="1.10.2",
author="Barak Alon",
author_email="[email protected]",
description="Flask-Rebar combines flask, marshmallow, and swagger for robust REST services.",
Expand All @@ -43,6 +43,7 @@
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
"Topic :: Software Development :: Libraries :: Application Frameworks",
Expand Down
7 changes: 3 additions & 4 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import pytest

import unittest
from flask_rebar.compat import MARSHMALLOW_V2, MARSHMALLOW_V3


skip_if_marshmallow_not_v2 = pytest.mark.skipif(
skip_if_marshmallow_not_v2 = unittest.skipIf(
not MARSHMALLOW_V2, reason="Only applicable for Marshmallow version 2"
)
skip_if_marshmallow_not_v3 = pytest.mark.skipif(
skip_if_marshmallow_not_v3 = unittest.skipIf(
not MARSHMALLOW_V3, reason="Only applicable for Marshmallow version 3"
)
6 changes: 5 additions & 1 deletion tests/swagger_generation/registries/exploded_query_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@


class ExplodedQueryStringSchema(RequestSchema):
foos = QueryParamList(marshmallow.fields.String(), required=True)
foos = QueryParamList(
marshmallow.fields.String(), required=True, description="foo string"
)


@registry.handles(
Expand Down Expand Up @@ -57,6 +59,7 @@ def get_foos():
"collectionFormat": "multi",
"type": "array",
"items": {"type": "string"},
"description": "foo string",
}
],
}
Expand Down Expand Up @@ -99,6 +102,7 @@ def get_foos():
"name": "foos",
"in": "query",
"required": True,
"description": "foo string",
"schema": {"type": "array", "items": {"type": "string"}},
"explode": True,
}
Expand Down
78 changes: 77 additions & 1 deletion tests/test_rebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import marshmallow as m
from flask import Flask
from werkzeug.routing import RequestRedirect

from flask_rebar import messages
from flask_rebar import HeaderApiKeyAuthenticator, SwaggerV3Generator
Expand Down Expand Up @@ -677,3 +676,80 @@ def test_redirects_for_missing_trailing_slash(self):
resp = app.test_client().get(path="/with_trailing_slash")
self.assertIn(resp.status_code, (301, 308))
self.assertTrue(resp.headers["Location"].endswith("/with_trailing_slash/"))

def test_bare_class_schemas_handled(self):
rebar = Rebar()
registry = rebar.create_handler_registry()

expected_foo = FooSchema().load({"uid": "some_uid", "name": "Namey McNamerton"})
expected_headers = {"x-name": "Header Name"}

def get_foo(*args, **kwargs):
return expected_foo

def post_foo(*args, **kwargs):
return expected_foo

register_endpoint(
registry=registry,
method="GET",
path="/my_get_endpoint",
headers_schema=HeadersSchema,
response_body_schema={200: FooSchema},
query_string_schema=FooListSchema,
func=get_foo,
)

register_endpoint(
registry=registry,
method="POST",
path="/my_post_endpoint",
request_body_schema=FooListSchema,
response_body_schema=FooSchema,
func=post_foo,
)

app = create_rebar_app(rebar)
# violate headers schema:
resp = app.test_client().get(path="/my_get_endpoint?name=QuerystringName")
self.assertEqual(resp.status_code, 400)
self.assertEqual(
get_json_from_resp(resp)["message"], messages.header_validation_failed
)
# violate querystring schema:
resp = app.test_client().get(path="/my_get_endpoint", headers=expected_headers)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
get_json_from_resp(resp)["message"], messages.query_string_validation_failed
)
# valid request:
resp = app.test_client().get(
path="/my_get_endpoint?name=QuerystringName", headers=expected_headers
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(get_json_from_resp(resp), expected_foo.data)

resp = app.test_client().post(
path="/my_post_endpoint",
data='{"wrong": "Posted Name"}',
content_type="application/json",
)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
get_json_from_resp(resp)["message"], messages.body_validation_failed
)

resp = app.test_client().post(
path="/my_post_endpoint",
data='{"name": "Posted Name"}',
content_type="application/json",
)
self.assertEqual(resp.status_code, 200)

# ensure Swagger generation doesn't break (Issue #115)
from flask_rebar import SwaggerV2Generator, SwaggerV3Generator

swagger = SwaggerV2Generator().generate(registry)
self.assertIsNotNone(swagger) # really only care that it didn't barf
swagger = SwaggerV3Generator().generate(registry)
self.assertIsNotNone(swagger)

0 comments on commit 69d378c

Please sign in to comment.