Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GIT-2045: enable versioning and strict slash on BlueprintGroup #2047

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
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,
)

harshanarayana marked this conversation as resolved.
Show resolved Hide resolved

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