Enable Middleware Support for Blueprint Groups (#1399)
* enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * enable indexing of BlueprintGroup object Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * rename blueprint group file to fix spelling error Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add documentation and additional unit tests Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup and optimize headers in unit test file Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix Bluprint Group iteratable method Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add additional unit test to check StopIteration condition Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup iter protocol implemenation for blueprint group and add slots Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix blueprint group middleware invocation identification Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
This commit is contained in:
parent
e5c7589fc0
commit
348964fe12
|
@ -20,6 +20,15 @@ sanic.blueprints module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.blueprint_group module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.blueprint_group
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
sanic.config module
|
sanic.config module
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,7 @@ Blueprints have almost the same functionality as an application instance.
|
||||||
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||||
decorator or `bp.add_websocket_route` method.
|
decorator or `bp.add_websocket_route` method.
|
||||||
|
|
||||||
### Middleware
|
### Blueprint Middleware
|
||||||
|
|
||||||
Using blueprints allows you to also register middleware globally.
|
Using blueprints allows you to also register middleware globally.
|
||||||
|
|
||||||
|
@ -145,6 +145,36 @@ async def halt_response(request, response):
|
||||||
return text('I halted the response')
|
return text('I halted the response')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Blueprint Group Middleware
|
||||||
|
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
|
||||||
|
current blueprint group under consideration.
|
||||||
|
|
||||||
|
```python
|
||||||
|
bp1 = Blueprint('bp1', url_prefix='/bp1')
|
||||||
|
bp2 = Blueprint('bp2', url_prefix='/bp2')
|
||||||
|
|
||||||
|
@bp1.middleware('request')
|
||||||
|
async def bp1_only_middleware(request):
|
||||||
|
print('applied on Blueprint : bp1 Only')
|
||||||
|
|
||||||
|
@bp1.route('/')
|
||||||
|
async def bp1_route(request):
|
||||||
|
return text('bp1')
|
||||||
|
|
||||||
|
@bp2.route('/<param>')
|
||||||
|
async def bp2_route(request, param):
|
||||||
|
return text(param)
|
||||||
|
|
||||||
|
group = Blueprint.group(bp1, bp2)
|
||||||
|
|
||||||
|
@group.middleware('request')
|
||||||
|
async def group_middleware(request):
|
||||||
|
print('common middleware applied for both bp1 and bp2')
|
||||||
|
|
||||||
|
# Register Blueprint group under the app
|
||||||
|
app.blueprint(group)
|
||||||
|
```
|
||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
|
|
||||||
Exceptions can be applied exclusively to blueprints globally.
|
Exceptions can be applied exclusively to blueprints globally.
|
||||||
|
|
|
@ -15,6 +15,7 @@ from typing import Any, Optional, Type, Union
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.config import BASE_LOGO, Config
|
from sanic.config import BASE_LOGO, Config
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
||||||
|
@ -597,8 +598,10 @@ class Sanic:
|
||||||
:return: decorated method
|
:return: decorated method
|
||||||
"""
|
"""
|
||||||
if attach_to == "request":
|
if attach_to == "request":
|
||||||
|
if middleware not in self.request_middleware:
|
||||||
self.request_middleware.append(middleware)
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == "response":
|
if attach_to == "response":
|
||||||
|
if middleware not in self.response_middleware:
|
||||||
self.response_middleware.appendleft(middleware)
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
|
@ -681,7 +684,7 @@ class Sanic:
|
||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple)):
|
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
self.blueprint(item, **options)
|
self.blueprint(item, **options)
|
||||||
return
|
return
|
||||||
|
|
120
sanic/blueprint_group.py
Normal file
120
sanic/blueprint_group.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
from collections import MutableSequence
|
||||||
|
|
||||||
|
|
||||||
|
class BlueprintGroup(MutableSequence):
|
||||||
|
"""
|
||||||
|
This class provides a mechanism to implement a Blueprint Group
|
||||||
|
using the `Blueprint.group` method. To avoid having to re-write
|
||||||
|
some of the existing implementation, this class provides a custom
|
||||||
|
iterator implementation that will let you use the object of this
|
||||||
|
class as a list/tuple inside the existing implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_blueprints", "_url_prefix")
|
||||||
|
|
||||||
|
def __init__(self, url_prefix=None):
|
||||||
|
"""
|
||||||
|
Create a new Blueprint Group
|
||||||
|
|
||||||
|
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
|
||||||
|
"""
|
||||||
|
self._blueprints = []
|
||||||
|
self._url_prefix = url_prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_prefix(self):
|
||||||
|
"""
|
||||||
|
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||||
|
:return: string with url prefix
|
||||||
|
"""
|
||||||
|
return self._url_prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blueprints(self):
|
||||||
|
"""
|
||||||
|
Retrieve a list of all the available blueprints under this group.
|
||||||
|
:return: List of Blueprint instance
|
||||||
|
"""
|
||||||
|
return self._blueprints
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Tun the class Blueprint Group into an Iterable item"""
|
||||||
|
return iter(self._blueprints)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
"""
|
||||||
|
This method returns a blueprint inside the group specified by
|
||||||
|
an index value. This will enable indexing, splice and slicing
|
||||||
|
of the blueprint group like we can do with regular list/tuple.
|
||||||
|
|
||||||
|
This method is provided to ensure backward compatibility with
|
||||||
|
any of the pre-existing usage that might break.
|
||||||
|
|
||||||
|
:param item: Index of the Blueprint item in the group
|
||||||
|
:return: Blueprint object
|
||||||
|
"""
|
||||||
|
return self._blueprints[item]
|
||||||
|
|
||||||
|
def __setitem__(self, index: int, item: object) -> None:
|
||||||
|
"""
|
||||||
|
Abstract method implemented to turn the `BlueprintGroup` class
|
||||||
|
into a list like object to support all the existing behavior.
|
||||||
|
|
||||||
|
This method is used to perform the list's indexed setter operation.
|
||||||
|
|
||||||
|
:param index: Index to use for inserting a new Blueprint item
|
||||||
|
:param item: New `Blueprint` object.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._blueprints[index] = item
|
||||||
|
|
||||||
|
def __delitem__(self, index: int) -> None:
|
||||||
|
"""
|
||||||
|
Abstract method implemented to turn the `BlueprintGroup` class
|
||||||
|
into a list like object to support all the existing behavior.
|
||||||
|
|
||||||
|
This method is used to delete an item from the list of blueprint
|
||||||
|
groups like it can be done on a regular list with index.
|
||||||
|
|
||||||
|
:param index: Index to use for removing a new Blueprint item
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
del self._blueprints[index]
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the Length of the blueprint group object.
|
||||||
|
:return: Length of Blueprint group object
|
||||||
|
"""
|
||||||
|
return len(self._blueprints)
|
||||||
|
|
||||||
|
def insert(self, index: int, item: object) -> None:
|
||||||
|
"""
|
||||||
|
The Abstract class `MutableSequence` leverages this insert method to
|
||||||
|
perform the `BlueprintGroup.append` operation.
|
||||||
|
|
||||||
|
:param index: Index to use for removing a new Blueprint item
|
||||||
|
:param item: New `Blueprint` object.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._blueprints.insert(index, item)
|
||||||
|
|
||||||
|
def middleware(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
A decorator that can be used to implement a Middleware plugin to
|
||||||
|
all of the Blueprints that belongs to this specific Blueprint Group.
|
||||||
|
|
||||||
|
In case of nested Blueprint Groups, the same middleware is applied
|
||||||
|
across each of the Blueprints recursively.
|
||||||
|
|
||||||
|
:param args: Optional positional Parameters to be use middleware
|
||||||
|
:param kwargs: Optional Keyword arg to use with Middleware
|
||||||
|
:return: Partial function to apply the middleware
|
||||||
|
"""
|
||||||
|
kwargs["bp_group"] = True
|
||||||
|
|
||||||
|
def register_middleware_for_blueprints(fn):
|
||||||
|
for blueprint in self.blueprints:
|
||||||
|
blueprint.middleware(fn, *args, **kwargs)
|
||||||
|
|
||||||
|
return register_middleware_for_blueprints
|
|
@ -1,5 +1,6 @@
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
|
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
@ -78,10 +79,12 @@ class Blueprint:
|
||||||
for i in nested:
|
for i in nested:
|
||||||
if isinstance(i, (list, tuple)):
|
if isinstance(i, (list, tuple)):
|
||||||
yield from chain(i)
|
yield from chain(i)
|
||||||
|
elif isinstance(i, BlueprintGroup):
|
||||||
|
yield from i.blueprints
|
||||||
else:
|
else:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
bps = []
|
bps = BlueprintGroup(url_prefix=url_prefix)
|
||||||
for bp in chain(blueprints):
|
for bp in chain(blueprints):
|
||||||
if bp.url_prefix is None:
|
if bp.url_prefix is None:
|
||||||
bp.url_prefix = ""
|
bp.url_prefix = ""
|
||||||
|
@ -326,6 +329,12 @@ class Blueprint:
|
||||||
middleware = args[0]
|
middleware = args[0]
|
||||||
args = []
|
args = []
|
||||||
return register_middleware(middleware)
|
return register_middleware(middleware)
|
||||||
|
else:
|
||||||
|
if kwargs.get("bp_group") and callable(args[0]):
|
||||||
|
middleware = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
kwargs.pop("bp_group")
|
||||||
|
return register_middleware(middleware)
|
||||||
else:
|
else:
|
||||||
return register_middleware
|
return register_middleware
|
||||||
|
|
||||||
|
|
180
tests/test_blueprint_group.py
Normal file
180
tests/test_blueprint_group.py
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from sanic.app import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import text, HTTPResponse
|
||||||
|
|
||||||
|
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
|
||||||
|
|
||||||
|
AUTH = "dGVzdDp0ZXN0Cg=="
|
||||||
|
|
||||||
|
|
||||||
|
def test_bp_group_indexing(app: Sanic):
|
||||||
|
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||||
|
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||||
|
|
||||||
|
group = Blueprint.group(blueprint_1, blueprint_2)
|
||||||
|
assert group[0] == blueprint_1
|
||||||
|
|
||||||
|
with raises(expected_exception=IndexError) as e:
|
||||||
|
_ = group[3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bp_group_with_additional_route_params(app: Sanic):
|
||||||
|
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||||
|
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||||
|
|
||||||
|
@blueprint_1.route(
|
||||||
|
"/request_path", methods=frozenset({"PUT", "POST"}), version=2
|
||||||
|
)
|
||||||
|
def blueprint_1_v2_method_with_put_and_post(request: Request):
|
||||||
|
if request.method == "PUT":
|
||||||
|
return text("PUT_OK")
|
||||||
|
elif request.method == "POST":
|
||||||
|
return text("POST_OK")
|
||||||
|
|
||||||
|
@blueprint_2.route(
|
||||||
|
"/route/<param>", methods=frozenset({"DELETE", "PATCH"}), name="test"
|
||||||
|
)
|
||||||
|
def blueprint_2_named_method(request: Request, param):
|
||||||
|
if request.method == "DELETE":
|
||||||
|
return text("DELETE_{}".format(param))
|
||||||
|
elif request.method == "PATCH":
|
||||||
|
return text("PATCH_{}".format(param))
|
||||||
|
|
||||||
|
blueprint_group = Blueprint.group(
|
||||||
|
blueprint_1, blueprint_2, url_prefix="/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
@blueprint_group.middleware("request")
|
||||||
|
def authenticate_request(request: Request):
|
||||||
|
global AUTH
|
||||||
|
auth = request.headers.get("authorization")
|
||||||
|
if auth:
|
||||||
|
# Dummy auth check. We can have anything here and it's fine.
|
||||||
|
if AUTH not in auth:
|
||||||
|
return text("Unauthorized", status=401)
|
||||||
|
else:
|
||||||
|
return text("Unauthorized", status=401)
|
||||||
|
|
||||||
|
@blueprint_group.middleware("response")
|
||||||
|
def enhance_response_middleware(request: Request, response: HTTPResponse):
|
||||||
|
response.headers.add("x-test-middleware", "value")
|
||||||
|
|
||||||
|
app.blueprint(blueprint_group)
|
||||||
|
|
||||||
|
header = {"authorization": " ".join(["Basic", AUTH])}
|
||||||
|
_, response = app.test_client.put(
|
||||||
|
"/v2/api/bp1/request_path", headers=header
|
||||||
|
)
|
||||||
|
assert response.text == "PUT_OK"
|
||||||
|
assert response.headers.get("x-test-middleware") == "value"
|
||||||
|
|
||||||
|
_, response = app.test_client.post(
|
||||||
|
"/v2/api/bp1/request_path", headers=header
|
||||||
|
)
|
||||||
|
assert response.text == "POST_OK"
|
||||||
|
|
||||||
|
_, response = app.test_client.delete("/api/bp2/route/bp2", headers=header)
|
||||||
|
assert response.text == "DELETE_bp2"
|
||||||
|
|
||||||
|
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
|
||||||
|
assert response.text == "PATCH_bp2"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/v2/api/bp1/request_path")
|
||||||
|
assert response.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_bp_group(app: Sanic):
|
||||||
|
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||||
|
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||||
|
|
||||||
|
@blueprint_1.route("/")
|
||||||
|
def blueprint_1_default_route(request):
|
||||||
|
return text("BP1_OK")
|
||||||
|
|
||||||
|
@blueprint_2.route("/")
|
||||||
|
def blueprint_2_default_route(request):
|
||||||
|
return text("BP2_OK")
|
||||||
|
|
||||||
|
blueprint_group_1 = Blueprint.group(
|
||||||
|
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3")
|
||||||
|
|
||||||
|
@blueprint_group_1.middleware("request")
|
||||||
|
def blueprint_group_1_middleware(request):
|
||||||
|
global MIDDLEWARE_INVOKE_COUNTER
|
||||||
|
MIDDLEWARE_INVOKE_COUNTER["request"] += 1
|
||||||
|
|
||||||
|
@blueprint_3.route("/")
|
||||||
|
def blueprint_3_default_route(request):
|
||||||
|
return text("BP3_OK")
|
||||||
|
|
||||||
|
blueprint_group_2 = Blueprint.group(
|
||||||
|
blueprint_group_1, blueprint_3, url_prefix="/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
@blueprint_group_2.middleware("response")
|
||||||
|
def blueprint_group_2_middleware(request, response):
|
||||||
|
global MIDDLEWARE_INVOKE_COUNTER
|
||||||
|
MIDDLEWARE_INVOKE_COUNTER["response"] += 1
|
||||||
|
|
||||||
|
app.blueprint(blueprint_group_2)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def app_default_route(request):
|
||||||
|
return text("APP_OK")
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/")
|
||||||
|
assert response.text == "APP_OK"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/api/bp/bp1")
|
||||||
|
assert response.text == "BP1_OK"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/api/bp/bp2")
|
||||||
|
assert response.text == "BP2_OK"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/api/bp3")
|
||||||
|
assert response.text == "BP3_OK"
|
||||||
|
|
||||||
|
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
|
||||||
|
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_bp_group_list_operations(app: Sanic):
|
||||||
|
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||||
|
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||||
|
|
||||||
|
@blueprint_1.route("/")
|
||||||
|
def blueprint_1_default_route(request):
|
||||||
|
return text("BP1_OK")
|
||||||
|
|
||||||
|
@blueprint_2.route("/")
|
||||||
|
def blueprint_2_default_route(request):
|
||||||
|
return text("BP2_OK")
|
||||||
|
|
||||||
|
blueprint_group_1 = Blueprint.group(
|
||||||
|
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3")
|
||||||
|
|
||||||
|
@blueprint_3.route("/second")
|
||||||
|
def blueprint_3_second_route(request):
|
||||||
|
return text("BP3_OK")
|
||||||
|
|
||||||
|
assert len(blueprint_group_1) == 2
|
||||||
|
|
||||||
|
blueprint_group_1.append(blueprint_3)
|
||||||
|
assert len(blueprint_group_1) == 3
|
||||||
|
|
||||||
|
del blueprint_group_1[2]
|
||||||
|
assert len(blueprint_group_1) == 2
|
||||||
|
|
||||||
|
blueprint_group_1[1] = blueprint_3
|
||||||
|
assert len(blueprint_group_1) == 2
|
||||||
|
|
||||||
|
assert blueprint_group_1.url_prefix == "/bp"
|
Loading…
Reference in New Issue
Block a user