sanic/sanic/blueprints.py

396 lines
13 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
from collections import defaultdict
2021-08-09 23:07:04 +01:00
from copy import deepcopy
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
2021-02-21 19:29:41 +00:00
from sanic_routing.exceptions import NotFound # type: ignore
2021-02-21 19:29:41 +00:00
from sanic_routing.route import Route # type: ignore
2021-01-28 07:18:06 +00:00
from sanic.base import BaseSanic
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>
2019-03-03 22:26:05 +00:00
from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException
2021-08-09 23:07:04 +01:00
from sanic.helpers import Default, _default
2021-03-11 15:09:18 +00:00
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import (
ListenerType,
MiddlewareType,
RouteHandler,
)
2018-10-18 05:20:16 +01:00
if TYPE_CHECKING:
from sanic import Sanic # noqa
2021-01-28 07:18:06 +00:00
class Blueprint(BaseSanic):
2021-01-18 20:10:47 +00:00
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
2021-01-18 20:10:47 +00:00
It is the main tool for grouping functionality and similar endpoints.
`See user guide re: blueprints
2021-01-18 20:10:47 +00:00
<https://sanicframework.org/guide/best-practices/blueprints.html>`__
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
2021-08-09 23:07:04 +01:00
trailing */*
2021-01-18 20:10:47 +00:00
"""
__fake_slots__ = (
"_apps",
"_future_routes",
"_future_statics",
"_future_middleware",
"_future_listeners",
"_future_exceptions",
"_future_signals",
"ctx",
"exceptions",
"host",
"listeners",
"middlewares",
"name",
"routes",
"statics",
"strict_slashes",
"url_prefix",
"version",
2021-05-30 13:37:44 +01:00
"version_prefix",
"websocket_routes",
)
2018-10-14 01:55:33 +01:00
def __init__(
self,
2021-05-30 13:37:44 +01:00
name: str = None,
2021-01-18 20:10:47 +00:00
url_prefix: Optional[str] = None,
host: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
2021-01-18 20:10:47 +00:00
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
2018-10-14 01:55:33 +01:00
):
2021-05-30 13:37:44 +01:00
super().__init__(name=name)
2021-08-09 23:07:04 +01:00
self.reset()
self.ctx = SimpleNamespace()
self.host = host
self.strict_slashes = strict_slashes
self.url_prefix = (
url_prefix[:-1]
if url_prefix and url_prefix.endswith("/")
else url_prefix
)
self.version = version
self.version_prefix = version_prefix
2016-10-15 20:53:51 +01:00
def __repr__(self) -> str:
args = ", ".join(
[
f'{attr}="{getattr(self, attr)}"'
if isinstance(getattr(self, attr), str)
else f"{attr}={getattr(self, attr)}"
for attr in (
"name",
"url_prefix",
"host",
"version",
"strict_slashes",
)
]
)
return f"Blueprint({args})"
@property
def apps(self):
if not self._apps:
raise SanicException(
f"{self} has not yet been registered to an app"
)
return self._apps
2021-01-26 21:14:47 +00:00
def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)
2021-01-27 08:25:05 +00:00
def static(self, *args, **kwargs):
kwargs["apply"] = False
return super().static(*args, **kwargs)
def middleware(self, *args, **kwargs):
kwargs["apply"] = False
return super().middleware(*args, **kwargs)
def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)
def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
2021-08-09 23:07:04 +01:00
def reset(self):
self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.websocket_routes: List[Route] = []
def copy(
self,
name: str,
url_prefix: Optional[Union[str, Default]] = _default,
version: Optional[Union[int, str, float, Default]] = _default,
version_prefix: Union[str, Default] = _default,
strict_slashes: Optional[Union[bool, Default]] = _default,
with_registration: bool = True,
with_ctx: bool = False,
):
"""
Copy a blueprint instance with some optional parameters to
override the values of attributes in the old instance.
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param version: Blueprint Version
:param version_prefix: the prefix of the version number shown in the
URL.
:param strict_slashes: Enforce the API urls are requested with a
trailing */*
:param with_registration: whether register new blueprint instance with
sanic apps that were registered with the old instance or not.
:param with_ctx: whether ``ctx`` will be copied or not.
"""
attrs_backup = {
"_apps": self._apps,
"routes": self.routes,
"websocket_routes": self.websocket_routes,
"middlewares": self.middlewares,
"exceptions": self.exceptions,
"listeners": self.listeners,
"statics": self.statics,
}
self.reset()
new_bp = deepcopy(self)
new_bp.name = name
if not isinstance(url_prefix, Default):
new_bp.url_prefix = url_prefix
if not isinstance(version, Default):
new_bp.version = version
if not isinstance(strict_slashes, Default):
new_bp.strict_slashes = strict_slashes
if not isinstance(version_prefix, Default):
new_bp.version_prefix = version_prefix
for key, value in attrs_backup.items():
setattr(self, key, value)
if with_registration and self._apps:
if new_bp._future_statics:
raise SanicException(
"Static routes registered with the old blueprint instance,"
" cannot be registered again."
)
for app in self._apps:
app.blueprint(new_bp)
if not with_ctx:
new_bp.ctx = SimpleNamespace()
return new_bp
2018-01-19 01:20:51 +00:00
@staticmethod
def group(
*blueprints: Union[Blueprint, BlueprintGroup],
url_prefix: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
2018-01-19 01:20:51 +00:00
: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
2021-03-11 15:09:18 +00:00
:param strict_slashes: Indicate strict slash termination behavior
for URL
2018-01-19 01:20:51 +00:00
"""
2018-10-14 01:55:33 +01:00
def chain(nested) -> Iterable[Blueprint]:
2018-01-19 01:20:51 +00:00
"""itertools.chain() but leaves strings untouched"""
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
else:
yield i
2018-10-14 01:55:33 +01:00
bps = BlueprintGroup(
url_prefix=url_prefix,
version=version,
strict_slashes=strict_slashes,
version_prefix=version_prefix,
)
2018-01-19 01:20:51 +00:00
for bp in chain(blueprints):
bps.append(bp)
return bps
2016-10-15 20:53:51 +01:00
def register(self, app, options):
"""
Register the blueprint to the sanic app.
:param app: Instance of :class:`sanic.app.Sanic` class
:param options: Options to be used while registering the
blueprint into the app.
*url_prefix* - URL Prefix to override the blueprint prefix
"""
self._apps.add(app)
2018-10-14 01:55:33 +01:00
url_prefix = options.get("url_prefix", self.url_prefix)
opt_version = options.get("version", None)
opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix)
error_format = options.get(
"error_format", app.config.FALLBACK_ERROR_FORMAT
)
routes = []
2021-02-21 19:29:41 +00:00
middleware = []
exception_handlers = []
listeners = defaultdict(list)
# Routes
2021-01-26 21:14:47 +00:00
for future in self._future_routes:
2017-02-02 17:21:14 +00:00
# attach the blueprint name to the handler so that it can be
# prefixed properly in the router
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
2017-07-13 04:18:56 +01:00
version_prefix = self.version_prefix
for prefix in (
future.version_prefix,
opt_version_prefix,
):
if prefix and prefix != "/v":
version_prefix = prefix
break
version = self._extract_value(
future.version, opt_version, self.version
)
strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes
2021-02-07 09:38:37 +00:00
)
2021-02-08 10:18:29 +00:00
name = app._generate_name(future.name)
2021-02-07 09:38:37 +00:00
2021-01-26 21:14:47 +00:00
apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
future.host or self.host,
2021-02-07 09:38:37 +00:00
strict_slashes,
2021-01-26 21:14:47 +00:00
future.stream,
version,
2021-02-08 10:18:29 +00:00
name,
2021-01-26 21:14:47 +00:00
future.ignore_body,
2021-02-03 22:42:24 +00:00
future.websocket,
future.subprotocols,
2021-02-07 09:38:37 +00:00
future.unquote,
future.static,
version_prefix,
error_format,
)
2017-07-13 04:18:56 +01:00
2021-01-27 08:25:05 +00:00
route = app._apply_route(apply_route)
2021-02-07 09:38:37 +00:00
operation = (
routes.extend if isinstance(route, list) else routes.append
)
2021-02-03 20:36:44 +00:00
operation(route)
2017-02-21 06:41:53 +00:00
# Static Files
2021-01-27 08:25:05 +00:00
for future in self._future_statics:
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
2021-01-27 08:25:05 +00:00
apply_route = FutureStatic(uri, *future[1:])
route = app._apply_static(apply_route)
routes.append(route)
route_names = [route.name for route in routes if route]
2021-02-03 22:42:24 +00:00
if route_names:
# Middleware
2021-02-03 22:42:24 +00:00
for future in self._future_middleware:
2021-02-21 19:29:41 +00:00
middleware.append(app._apply_middleware(future, route_names))
# Exceptions
for future in self._future_exceptions:
exception_handlers.append(
app._apply_exception_handler(future, route_names)
)
# Event listeners
for listener in self._future_listeners:
2021-02-21 19:29:41 +00:00
listeners[listener.event].append(app._apply_listener(listener))
# Signals
for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)
2021-02-21 19:29:41 +00:00
self.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes = [
route for route in self.routes if route.ctx.websocket
]
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)
async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
condition.update({"blueprint": self.name})
kwargs["condition"] = condition
await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps]
)
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
events = set()
for app in self.apps:
signal = app.signal_router.name_index.get(event)
if not signal:
raise NotFound("Could not find signal %s" % event)
events.add(signal.ctx.event)
return asyncio.wait(
[event.wait() for event in events],
return_when=asyncio.FIRST_COMPLETED,
timeout=timeout,
)
@staticmethod
def _extract_value(*values):
value = values[-1]
for v in values:
if v is not None:
value = v
break
return value