Skip to content

Commit

Permalink
GIT-2045: enable versioning and strict slash on BlueprintGroup (#2047)
Browse files Browse the repository at this point in the history
* GIT-2045: enable versioning and strict slash on BlueprintGroup

* GIT-2045: convert named tuple into typed format + unit tests

* GIT-2045: add example code for versioned bpg

* GIT-2045: None value for strict slashes check

* GIT-2045: refactor handler types and add benchmark for urlparse

* GIT-2045: reduce urlparse benchmark iterations

* GIT-2045: add unit test and url merge behavior

* GIT-2045: cleanup example code and remove print

* GIT-2045: add test for slash duplication avoidence

* GIT-2045: fix issue with tailing / getting appended

* GIT-2045: use Optional instead of Union for Typing

* GIT-2045: use string for version arg

* GIT-2045: combine optional with union
  • Loading branch information
harshanarayana authored Mar 7, 2021
1 parent be905e0 commit 2c25af8
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 79 deletions.
35 changes: 35 additions & 0 deletions examples/versioned_blueprint_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json


app = Sanic(name="blue-print-group-version-example")

bp1 = Blueprint(name="ultron", url_prefix="/ultron")
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)

bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)


@bp1.get("/name")
async def bp1_name(request):
"""This will expose an Endpoint GET /v1/sentient/robot/ultron/name"""
return json({"name": "Ultron"})


@bp2.get("/name")
async def bp2_name(request):
"""This will expose an Endpoint GET /v1/sentient/robot/vision/name"""
return json({"name": "vision"})


@bp2.get("/name", version=2)
async def bp2_revised_name(request):
"""This will expose an Endpoint GET /v2/sentient/robot/vision/name"""
return json({"name": "new vision"})


app.blueprint(bpg)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
3 changes: 2 additions & 1 deletion sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
ServerError,
URLBuildError,
)
from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType
from sanic.handlers import ErrorHandler
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
from sanic.mixins.listeners import ListenerEvent
from sanic.models.futures import (
Expand All @@ -50,6 +50,7 @@
FutureRoute,
FutureStatic,
)
from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse
from sanic.router import Router
Expand Down
77 changes: 71 additions & 6 deletions sanic/blueprint_group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections.abc import MutableSequence
from typing import List
from typing import List, Optional, Union

import sanic


class BlueprintGroup(MutableSequence):
Expand All @@ -16,6 +18,11 @@ class as a list/tuple inside the existing implementation.
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
bp3 = Blueprint('bp3', url_prefix='/bp4')
bp3 = Blueprint('bp3', url_prefix='/bp4')
bpg = BlueprintGroup(bp3, bp4, url_prefix="/api", version="v1")
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
Expand All @@ -28,6 +35,14 @@ async def bp1_route(request):
async def bp2_route(request, param):
return text(param)
@bp3.route('/')
async def bp1_route(request):
return text('bp1')
@bp4.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
Expand All @@ -36,18 +51,23 @@ async def group_middleware(request):
# Register Blueprint group under the app
app.blueprint(group)
app.blueprint(bpg)
"""

__slots__ = ("_blueprints", "_url_prefix")
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")

def __init__(self, url_prefix=None):
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
"""
Create a new Blueprint Group
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
:param version: API Version for the blueprint group. This will be inherited by each of the Blueprint
:param strict_slashes: URL Strict slash behavior indicator
"""
self._blueprints = []
self._url_prefix = url_prefix
self._version = version
self._strict_slashes = strict_slashes

@property
def url_prefix(self) -> str:
Expand All @@ -59,14 +79,33 @@ def url_prefix(self) -> str:
return self._url_prefix

@property
def blueprints(self) -> List:
def blueprints(self) -> List["sanic.Blueprint"]:
"""
Retrieve a list of all the available blueprints under this group.
:return: List of Blueprint instance
"""
return self._blueprints

@property
def version(self) -> Optional[Union[str, int, float]]:
"""
API Version for the Blueprint Group. This will be applied only in case if the Blueprint doesn't already have
a version specified
:return: Version information
"""
return self._version

@property
def strict_slashes(self) -> Optional[bool]:
"""
URL Slash termination behavior configuration
:return: bool
"""
return self._strict_slashes

def __iter__(self):
"""
Tun the class Blueprint Group into an Iterable item
Expand Down Expand Up @@ -121,7 +160,33 @@ def __len__(self) -> int:
"""
return len(self._blueprints)

def insert(self, index: int, item: object) -> None:
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
"""
Sanitize the Blueprint Entity to override the Version and strict slash behaviors as required.
:param bp: Sanic Blueprint entity Object
:return: Modified Blueprint
"""
if self._url_prefix:
merged_prefix = "/".join(
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
).rstrip("/")
bp.url_prefix = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(bp, _attr) is None:
setattr(bp, _attr, getattr(self, _attr))
return bp

def append(self, value: "sanic.Blueprint") -> None:
"""
The Abstract class `MutableSequence` leverages this append method to
perform the `BlueprintGroup.append` operation.
:param value: New `Blueprint` object.
:return: None
"""
self._blueprints.append(self._sanitize_blueprint(bp=value))

def insert(self, index: int, item: "sanic.Blueprint") -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
Expand All @@ -130,7 +195,7 @@ def insert(self, index: int, item: object) -> None:
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, item)
self._blueprints.insert(index, self._sanitize_blueprint(item))

def middleware(self, *args, **kwargs):
"""
Expand Down
23 changes: 15 additions & 8 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from collections import defaultdict
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Iterable

from sanic_routing.route import Route # type: ignore

from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.handlers import ListenerType, MiddlewareType, RouteHandler
from sanic.models.handler_types import (
ListenerType,
MiddlewareType,
RouteHandler,
)
from sanic.models.futures import FutureRoute, FutureStatic


Expand Down Expand Up @@ -87,16 +91,18 @@ def exception(self, *args, **kwargs):
return super().exception(*args, **kwargs)

@staticmethod
def group(*blueprints, url_prefix=""):
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
:param version: API Version to be used for Blueprint group
:param strict_slashes: Indicate strict slash termination behavior for URL
"""

def chain(nested):
def chain(nested) -> Iterable[Blueprint]:
"""itertools.chain() but leaves strings untouched"""
for i in nested:
if isinstance(i, (list, tuple)):
Expand All @@ -106,11 +112,12 @@ def chain(nested):
else:
yield i

bps = BlueprintGroup(url_prefix=url_prefix)
bps = BlueprintGroup(
url_prefix=url_prefix,
version=version,
strict_slashes=strict_slashes,
)
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
bp.url_prefix = url_prefix + bp.url_prefix
bps.append(bp)
return bps

Expand Down
21 changes: 1 addition & 20 deletions sanic/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from asyncio.events import AbstractEventLoop
from traceback import format_exc
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union

from sanic.errorpages import exception_response
from sanic.exceptions import (
Expand All @@ -9,24 +7,7 @@
InvalidRangeType,
)
from sanic.log import logger
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse, text


Sanic = TypeVar("Sanic")

MiddlewareResponse = Union[
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
]
RequestMiddlewareType = Callable[[Request], MiddlewareResponse]
ResponseMiddlewareType = Callable[
[Request, BaseHTTPResponse], MiddlewareResponse
]
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
]
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
from sanic.response import text


class ErrorHandler:
Expand Down
89 changes: 51 additions & 38 deletions sanic/models/futures.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
from collections import namedtuple


FutureRoute = namedtuple(
"FutureRoute",
[
"handler",
"uri",
"methods",
"host",
"strict_slashes",
"stream",
"version",
"name",
"ignore_body",
"websocket",
"subprotocols",
"unquote",
"static",
],
)
FutureListener = namedtuple("FutureListener", ["listener", "event"])
FutureMiddleware = namedtuple("FutureMiddleware", ["middleware", "attach_to"])
FutureException = namedtuple("FutureException", ["handler", "exceptions"])
FutureStatic = namedtuple(
"FutureStatic",
[
"uri",
"file_or_directory",
"pattern",
"use_modified_since",
"use_content_range",
"stream_large_files",
"name",
"host",
"strict_slashes",
"content_type",
],
from pathlib import PurePath
from typing import NamedTuple, List, Union, Iterable, Optional

from sanic.models.handler_types import (
ListenerType,
MiddlewareType,
ErrorMiddlewareType,
)


class FutureRoute(NamedTuple):
handler: str
uri: str
methods: Optional[Iterable[str]]
host: str
strict_slashes: bool
stream: bool
version: Optional[int]
name: str
ignore_body: bool
websocket: bool
subprotocols: Optional[List[str]]
unquote: bool
static: bool


class FutureListener(NamedTuple):
listener: ListenerType
event: str


class FutureMiddleware(NamedTuple):
middleware: MiddlewareType
attach_to: str


class FutureException(NamedTuple):
handler: ErrorMiddlewareType
exceptions: List[BaseException]


class FutureStatic(NamedTuple):
uri: str
file_or_directory: Union[str, bytes, PurePath]
pattern: str
use_modified_since: bool
use_content_range: bool
stream_large_files: bool
name: str
host: Optional[str]
strict_slashes: Optional[bool]
content_type: Optional[bool]
25 changes: 25 additions & 0 deletions sanic/models/handler_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from asyncio.events import AbstractEventLoop
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union

from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse


Sanic = TypeVar("Sanic")


MiddlewareResponse = Union[
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
]
RequestMiddlewareType = Callable[[Request], MiddlewareResponse]
ResponseMiddlewareType = Callable[
[Request, BaseHTTPResponse], MiddlewareResponse
]
ErrorMiddlewareType = Callable[
[Request, BaseException], Optional[Coroutine[Any, Any, None]]
]
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
]
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
Loading

0 comments on commit 2c25af8

Please sign in to comment.