Conversion of User Guide to the SHH stack (#2781)
This commit is contained in:
926
sanic/app.py
926
sanic/app.py
File diff suppressed because it is too large
Load Diff
@@ -17,16 +17,22 @@ class StrEnum(str, Enum): # no cov
|
||||
|
||||
|
||||
class Server(StrEnum):
|
||||
"""Server types."""
|
||||
|
||||
SANIC = auto()
|
||||
ASGI = auto()
|
||||
|
||||
|
||||
class Mode(StrEnum):
|
||||
"""Server modes."""
|
||||
|
||||
PRODUCTION = auto()
|
||||
DEBUG = auto()
|
||||
|
||||
|
||||
class ServerStage(IntEnum):
|
||||
"""Server stages."""
|
||||
|
||||
STOPPED = auto()
|
||||
PARTIAL = auto()
|
||||
SERVING = auto()
|
||||
|
||||
@@ -10,6 +10,20 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||
"""Setup Sanic Extensions.
|
||||
|
||||
Requires Sanic Extensions to be installed.
|
||||
|
||||
Args:
|
||||
app (Sanic): Sanic application.
|
||||
fail (bool, optional): Raise an error if Sanic Extensions is not
|
||||
installed. Defaults to `False`.
|
||||
**kwargs: Keyword arguments to pass to `sanic_ext.Extend`.
|
||||
|
||||
Returns:
|
||||
sanic_ext.Extend: Sanic Extensions instance.
|
||||
"""
|
||||
|
||||
if not app.config.AUTO_EXTEND:
|
||||
return
|
||||
|
||||
|
||||
@@ -45,7 +45,18 @@ SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc
|
||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
def get_logo(full=False, coffee=False):
|
||||
def get_logo(full: bool = False, coffee: bool = False) -> str:
|
||||
"""Get the Sanic logo.
|
||||
|
||||
Will return the full color logo if the terminal supports it.
|
||||
|
||||
Args:
|
||||
full (bool, optional): Use the full color logo. Defaults to `False`.
|
||||
coffee (bool, optional): Use the coffee logo. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
str: Sanic logo.
|
||||
"""
|
||||
logo = (
|
||||
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
|
||||
if is_atty()
|
||||
|
||||
@@ -9,6 +9,8 @@ from sanic.log import logger
|
||||
|
||||
|
||||
class MOTD(ABC):
|
||||
"""Base class for the Message of the Day (MOTD) display."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logo: Optional[str],
|
||||
@@ -25,7 +27,7 @@ class MOTD(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def display(self):
|
||||
... # noqa
|
||||
"""Display the MOTD."""
|
||||
|
||||
@classmethod
|
||||
def output(
|
||||
@@ -35,11 +37,24 @@ class MOTD(ABC):
|
||||
data: Dict[str, str],
|
||||
extra: Dict[str, str],
|
||||
) -> None:
|
||||
"""Output the MOTD.
|
||||
|
||||
Args:
|
||||
logo (Optional[str]): Logo to display.
|
||||
serve_location (str): Location to serve.
|
||||
data (Dict[str, str]): Data to display.
|
||||
extra (Dict[str, str]): Extra data to display.
|
||||
"""
|
||||
motd_class = MOTDTTY if is_atty() else MOTDBasic
|
||||
motd_class(logo, serve_location, data, extra).display()
|
||||
|
||||
|
||||
class MOTDBasic(MOTD):
|
||||
"""A basic MOTD display.
|
||||
|
||||
This is used when the terminal does not support ANSI escape codes.
|
||||
"""
|
||||
|
||||
def display(self):
|
||||
if self.logo:
|
||||
logger.debug(self.logo)
|
||||
@@ -55,11 +70,14 @@ class MOTDBasic(MOTD):
|
||||
|
||||
|
||||
class MOTDTTY(MOTD):
|
||||
"""A MOTD display for terminals that support ANSI escape codes."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_variables()
|
||||
|
||||
def set_variables(self): # no cov
|
||||
"""Set the variables used for display."""
|
||||
fallback = (108, 24)
|
||||
terminal_width = max(
|
||||
get_terminal_size(fallback=fallback).columns, fallback[0]
|
||||
@@ -81,6 +99,15 @@ class MOTDTTY(MOTD):
|
||||
self.display_length = self.key_width + self.value_width + 2
|
||||
|
||||
def display(self, version=True, action="Goin' Fast", out=None):
|
||||
"""Display the MOTD.
|
||||
|
||||
Args:
|
||||
version (bool, optional): Display the version. Defaults to `True`.
|
||||
action (str, optional): Action to display. Defaults to
|
||||
`"Goin' Fast"`.
|
||||
out (Optional[Callable], optional): Output function. Defaults to
|
||||
`None`.
|
||||
"""
|
||||
if not out:
|
||||
out = logger.info
|
||||
header = "Sanic"
|
||||
|
||||
@@ -15,6 +15,11 @@ if os.name == "nt": # noqa
|
||||
|
||||
|
||||
class Spinner: # noqa
|
||||
"""Spinner class to show a loading spinner in the terminal.
|
||||
|
||||
Used internally by the `loading` context manager.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message = message
|
||||
self.queue: Queue[int] = Queue()
|
||||
|
||||
@@ -19,6 +19,8 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class ApplicationServerInfo:
|
||||
"""Information about a server instance."""
|
||||
|
||||
settings: Dict[str, Any]
|
||||
stage: ServerStage = field(default=ServerStage.STOPPED)
|
||||
server: Optional[AsyncioServer] = field(default=None)
|
||||
@@ -26,6 +28,12 @@ class ApplicationServerInfo:
|
||||
|
||||
@dataclass
|
||||
class ApplicationState:
|
||||
"""Application state.
|
||||
|
||||
This class is used to store the state of the application. It is
|
||||
instantiated by the application and is available as `app.state`.
|
||||
"""
|
||||
|
||||
app: Sanic
|
||||
asgi: bool = field(default=False)
|
||||
coffee: bool = field(default=False)
|
||||
@@ -69,15 +77,31 @@ class ApplicationState:
|
||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
def set_verbosity(self, value: int):
|
||||
def set_verbosity(self, value: int) -> None:
|
||||
"""Set the verbosity level.
|
||||
|
||||
Args:
|
||||
value (int): Verbosity level.
|
||||
"""
|
||||
VerbosityFilter.verbosity = value
|
||||
|
||||
@property
|
||||
def is_debug(self):
|
||||
def is_debug(self) -> bool:
|
||||
"""Check if the application is in debug mode.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the application is in debug mode, `False`
|
||||
otherwise.
|
||||
"""
|
||||
return self.mode is Mode.DEBUG
|
||||
|
||||
@property
|
||||
def stage(self) -> ServerStage:
|
||||
"""Get the server stage.
|
||||
|
||||
Returns:
|
||||
ServerStage: Server stage.
|
||||
"""
|
||||
if not self.server_info:
|
||||
return ServerStage.STOPPED
|
||||
|
||||
|
||||
@@ -1,277 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableSequence
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
from .blueprints import BlueprintGroup
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
This class provides a mechanism to implement a Blueprint Group
|
||||
using the :meth:`~sanic.blueprints.Blueprint.group` method in
|
||||
:class:`~sanic.blueprints.Blueprint`. 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.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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')
|
||||
|
||||
@bp1.route('/')
|
||||
async def bp1_route(request):
|
||||
return text('bp1')
|
||||
|
||||
@bp2.route('/<param>')
|
||||
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')
|
||||
async def group_middleware(request):
|
||||
print('common middleware applied for both bp1 and bp2')
|
||||
|
||||
# Register Blueprint group under the app
|
||||
app.blueprint(group)
|
||||
app.blueprint(bpg)
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_blueprints",
|
||||
"_url_prefix",
|
||||
"_version",
|
||||
"_strict_slashes",
|
||||
"_version_prefix",
|
||||
"_name_prefix",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
name_prefix: Optional[str] = "",
|
||||
):
|
||||
"""
|
||||
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: List[Blueprint] = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._version_prefix = version_prefix
|
||||
self._strict_slashes = strict_slashes
|
||||
self._name_prefix = name_prefix
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||
"""
|
||||
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||
|
||||
:return: string with url prefix
|
||||
"""
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List[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
|
||||
|
||||
@property
|
||||
def version_prefix(self) -> str:
|
||||
"""
|
||||
Version prefix; defaults to ``/v``
|
||||
|
||||
:return: str
|
||||
"""
|
||||
return self._version_prefix
|
||||
|
||||
@property
|
||||
def name_prefix(self) -> Optional[str]:
|
||||
"""
|
||||
Name prefix for the blueprint group
|
||||
|
||||
:return: str
|
||||
"""
|
||||
return self._name_prefix
|
||||
|
||||
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, item) -> 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) -> 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 append(self, value: 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(value)
|
||||
|
||||
def exception(self, *exceptions, **kwargs):
|
||||
"""
|
||||
A decorator that can be used to implement a global exception handler
|
||||
for all the Blueprints that belong to this Blueprint Group.
|
||||
|
||||
In case of nested Blueprint Groups, the same handler is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
:param args: List of Python exceptions to be caught by the handler
|
||||
:param kwargs: Additional optional arguments to be passed to the
|
||||
exception handler
|
||||
:return: a decorated method to handle global exceptions for any
|
||||
blueprint registered under this group.
|
||||
"""
|
||||
|
||||
def register_exception_handler_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.exception(*exceptions, **kwargs)(fn)
|
||||
|
||||
return register_exception_handler_for_blueprints
|
||||
|
||||
def insert(self, index: int, item: Blueprint) -> 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
|
||||
"""
|
||||
|
||||
def register_middleware_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.middleware(fn, *args, **kwargs)
|
||||
|
||||
if args and callable(args[0]):
|
||||
fn = args[0]
|
||||
args = list(args)[1:]
|
||||
return register_middleware_for_blueprints(fn)
|
||||
return register_middleware_for_blueprints
|
||||
|
||||
def on_request(self, middleware=None):
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="request")
|
||||
|
||||
def on_response(self, middleware=None):
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="response")
|
||||
__all__ = ["BlueprintGroup"] # noqa: F405
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import MutableSequence
|
||||
from copy import deepcopy
|
||||
from functools import wraps
|
||||
from functools import partial, wraps
|
||||
from inspect import isfunction
|
||||
from itertools import chain
|
||||
from types import SimpleNamespace
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from sanic_routing.exceptions import NotFound
|
||||
from sanic_routing.route import Route
|
||||
|
||||
from sanic.base.root import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.helpers import Default, _default
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
@@ -41,6 +45,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def lazy(func, as_decorator=True):
|
||||
"""Decorator to register a function to be called later.
|
||||
|
||||
Args:
|
||||
func (Callable): Function to be called later.
|
||||
as_decorator (bool): Whether the function should be called
|
||||
immediately or not.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def decorator(bp, *args, **kwargs):
|
||||
nonlocal as_decorator
|
||||
@@ -67,23 +79,21 @@ def lazy(func, as_decorator=True):
|
||||
|
||||
|
||||
class Blueprint(BaseSanic):
|
||||
"""
|
||||
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.
|
||||
"""A logical collection of URLs that consist of a similar logical domain.
|
||||
|
||||
It is the main tool for grouping functionality and similar endpoints.
|
||||
A Blueprint object is the main tool for grouping functionality and similar endpoints. It allows the developer to
|
||||
organize routes, exception handlers, middleware, and other web functionalities into separate, modular groups.
|
||||
|
||||
`See user guide re: blueprints
|
||||
<https://sanicframework.org/guide/best-practices/blueprints.html>`__
|
||||
See [Blueprints](/en/guide/best-practices/blueprints) for more information.
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param host: IP Address or FQDN for the sanic server to use.
|
||||
:param version: Blueprint Version
|
||||
:param strict_slashes: Enforce the API urls are requested with a
|
||||
trailing */*
|
||||
"""
|
||||
Args:
|
||||
name (str): The name of the blueprint.
|
||||
url_prefix (Optional[str]): The URL prefix for all routes defined on this blueprint.
|
||||
host (Optional[Union[List[str], str]]): Host or list of hosts that this blueprint should respond to.
|
||||
version (Optional[Union[int, str, float]]): Version number of the API implemented by this blueprint.
|
||||
strict_slashes (Optional[bool]): Whether or not the URL should end with a slash.
|
||||
version_prefix (str): Prefix for the version. Default is "/v".
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = (
|
||||
"_apps",
|
||||
@@ -151,7 +161,16 @@ class Blueprint(BaseSanic):
|
||||
return f"Blueprint({args})"
|
||||
|
||||
@property
|
||||
def apps(self):
|
||||
def apps(self) -> Set[Sanic]:
|
||||
"""Get the set of apps that this blueprint is registered to.
|
||||
|
||||
Returns:
|
||||
Set[Sanic]: Set of apps that this blueprint is registered to.
|
||||
|
||||
Raises:
|
||||
SanicException: If the blueprint has not yet been registered to
|
||||
an app.
|
||||
"""
|
||||
if not self._apps:
|
||||
raise SanicException(
|
||||
f"{self} has not yet been registered to an app"
|
||||
@@ -160,6 +179,12 @@ class Blueprint(BaseSanic):
|
||||
|
||||
@property
|
||||
def registered(self) -> bool:
|
||||
"""Check if the blueprint has been registered to an app.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the blueprint has been registered to an app,
|
||||
`False` otherwise.
|
||||
"""
|
||||
return bool(self._apps)
|
||||
|
||||
exception = lazy(BaseSanic.exception)
|
||||
@@ -169,7 +194,8 @@ class Blueprint(BaseSanic):
|
||||
signal = lazy(BaseSanic.signal)
|
||||
static = lazy(BaseSanic.static, as_decorator=False)
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
"""Reset the blueprint to its initial state."""
|
||||
self._apps: Set[Sanic] = set()
|
||||
self._allow_route_overwrite = False
|
||||
self.exceptions: List[RouteHandler] = []
|
||||
@@ -190,21 +216,21 @@ class Blueprint(BaseSanic):
|
||||
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.
|
||||
"""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.
|
||||
"""
|
||||
Args:
|
||||
name (str): Unique name of the blueprint.
|
||||
url_prefix (Optional[Union[str, Default]]): URL to be prefixed before all route URLs.
|
||||
version (Optional[Union[int, str, float, Default]]): Blueprint version.
|
||||
version_prefix (Union[str, Default]): The prefix of the version number shown in the URL.
|
||||
allow_route_overwrite (Union[bool, Default]): Whether to allow route overwrite or not.
|
||||
strict_slashes (Optional[Union[bool, Default]]): Enforce the API URLs are requested with a trailing "/*".
|
||||
with_registration (bool): Whether to register the new blueprint instance with Sanic apps that were registered with the old instance or not. Default is `True`.
|
||||
with_ctx (bool): Whether the ``ctx`` will be copied or not. Default is `False`.
|
||||
|
||||
Returns:
|
||||
Blueprint: A new Blueprint instance with the specified attributes.
|
||||
""" # noqa: E501
|
||||
|
||||
attrs_backup = {
|
||||
"_apps": self._apps,
|
||||
@@ -258,19 +284,44 @@ class Blueprint(BaseSanic):
|
||||
version_prefix: str = "/v",
|
||||
name_prefix: Optional[str] = "",
|
||||
) -> BlueprintGroup:
|
||||
"""
|
||||
Create a list of blueprints, optionally grouping them under a
|
||||
general URL prefix.
|
||||
"""Group multiple blueprints (or other blueprint groups) together.
|
||||
|
||||
: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
|
||||
Gropuping blueprings is a method for modularizing and organizing
|
||||
your application's code. This can be a powerful tool for creating
|
||||
reusable components, logically structuring your application code,
|
||||
and easily maintaining route definitions in bulk.
|
||||
|
||||
This is the preferred way to group multiple blueprints together.
|
||||
|
||||
Args:
|
||||
blueprints (Union[Blueprint, BlueprintGroup]): Blueprints to be
|
||||
registered as a group.
|
||||
url_prefix (Optional[str]): URL route to be prepended to all
|
||||
sub-prefixes. Default is `None`.
|
||||
version (Optional[Union[int, str, float]]): API Version to be
|
||||
used for Blueprint group. Default is `None`.
|
||||
strict_slashes (Optional[bool]): Indicate strict slash
|
||||
termination behavior for URL. Default is `None`.
|
||||
version_prefix (str): Prefix to be used for the version in the
|
||||
URL. Default is "/v".
|
||||
name_prefix (Optional[str]): Prefix to be used for the name of
|
||||
the blueprints in the group. Default is an empty string.
|
||||
|
||||
Returns:
|
||||
BlueprintGroup: A group of blueprints.
|
||||
|
||||
Example:
|
||||
The resulting group will have the URL prefixes
|
||||
`'/v2/bp1'` and `'/v2/bp2'` for bp1 and bp2, respectively.
|
||||
```python
|
||||
bp1 = Blueprint('bp1', url_prefix='/bp1')
|
||||
bp2 = Blueprint('bp2', url_prefix='/bp2')
|
||||
group = group(bp1, bp2, version=2)
|
||||
```
|
||||
"""
|
||||
|
||||
def chain(nested) -> Iterable[Blueprint]:
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
"""Iterate through nested blueprints"""
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
@@ -289,13 +340,11 @@ class Blueprint(BaseSanic):
|
||||
return bps
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Register the blueprint to the sanic app.
|
||||
"""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
|
||||
Args:
|
||||
app (Sanic): Sanic app to register the blueprint to.
|
||||
options (dict): Options to be passed to the blueprint.
|
||||
"""
|
||||
|
||||
self._apps.add(app)
|
||||
@@ -454,6 +503,12 @@ class Blueprint(BaseSanic):
|
||||
)
|
||||
|
||||
async def dispatch(self, *args, **kwargs):
|
||||
"""Dispatch a signal event
|
||||
|
||||
Args:
|
||||
*args: Arguments to be passed to the signal event.
|
||||
**kwargs: Keyword arguments to be passed to the signal event.
|
||||
"""
|
||||
condition = kwargs.pop("condition", {})
|
||||
condition.update({"__blueprint__": self.name})
|
||||
kwargs["condition"] = condition
|
||||
@@ -462,6 +517,16 @@ class Blueprint(BaseSanic):
|
||||
)
|
||||
|
||||
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
|
||||
"""Wait for a signal event to be dispatched.
|
||||
|
||||
Args:
|
||||
event (str): Name of the signal event.
|
||||
timeout (Optional[Union[int, float]]): Timeout for the event to be
|
||||
dispatched.
|
||||
|
||||
Returns:
|
||||
Awaitable: Awaitable for the event to be dispatched.
|
||||
"""
|
||||
events = set()
|
||||
for app in self.apps:
|
||||
signal = app.signal_router.name_index.get(event)
|
||||
@@ -500,5 +565,401 @@ class Blueprint(BaseSanic):
|
||||
def register_futures(
|
||||
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
|
||||
):
|
||||
"""Register futures to the apps.
|
||||
|
||||
Args:
|
||||
apps (Set[Sanic]): Set of apps to register the futures to.
|
||||
bp (Blueprint): Blueprint that the futures belong to.
|
||||
futures (Sequence[Tuple[Any, ...]]): Sequence of futures to be
|
||||
registered.
|
||||
"""
|
||||
|
||||
for app in apps:
|
||||
app._future_registry.update(set((bp, item) for item in futures))
|
||||
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
bpg_base = MutableSequence
|
||||
else:
|
||||
bpg_base = MutableSequence[Blueprint]
|
||||
|
||||
|
||||
class BlueprintGroup(bpg_base):
|
||||
"""This class provides a mechanism to implement a Blueprint Group.
|
||||
|
||||
The `BlueprintGroup` class allows grouping blueprints under a common
|
||||
URL prefix, version, and other shared attributes. It integrates with
|
||||
Sanic's Blueprint system, offering a custom iterator to treat an
|
||||
object of this class as a list/tuple.
|
||||
|
||||
Although possible to instantiate a group directly, it is recommended
|
||||
to use the `Blueprint.group` method to create a group of blueprints.
|
||||
|
||||
Args:
|
||||
url_prefix (Optional[str]): URL to be prefixed before all the
|
||||
Blueprint Prefixes. Default is `None`.
|
||||
version (Optional[Union[int, str, float]]): API Version for the
|
||||
blueprint group, inherited by each Blueprint. Default is `None`.
|
||||
strict_slashes (Optional[bool]): URL Strict slash behavior
|
||||
indicator. Default is `None`.
|
||||
version_prefix (str): Prefix for the version in the URL.
|
||||
Default is `"/v"`.
|
||||
name_prefix (Optional[str]): Prefix for the name of the blueprints
|
||||
in the group. Default is an empty string.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
bp1 = Blueprint("bp1", url_prefix="/bp1")
|
||||
bp2 = Blueprint("bp2", url_prefix="/bp2")
|
||||
|
||||
bp3 = Blueprint("bp3", url_prefix="/bp4")
|
||||
bp4 = Blueprint("bp3", url_prefix="/bp4")
|
||||
|
||||
|
||||
group1 = Blueprint.group(bp1, bp2)
|
||||
group2 = Blueprint.group(bp3, bp4, version_prefix="/api/v", version="1")
|
||||
|
||||
|
||||
@bp1.on_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)
|
||||
|
||||
|
||||
@bp3.route("/")
|
||||
async def bp3_route(request):
|
||||
return text("bp3")
|
||||
|
||||
|
||||
@bp4.route("/<param>")
|
||||
async def bp4_route(request, param):
|
||||
return text(param)
|
||||
|
||||
|
||||
@group1.on_request
|
||||
async def group_middleware(request):
|
||||
print("common middleware applied for both bp1 and bp2")
|
||||
|
||||
|
||||
# Register Blueprint group under the app
|
||||
app.blueprint(group1)
|
||||
app.blueprint(group2)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = (
|
||||
"_blueprints",
|
||||
"_url_prefix",
|
||||
"_version",
|
||||
"_strict_slashes",
|
||||
"_version_prefix",
|
||||
"_name_prefix",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
name_prefix: Optional[str] = "",
|
||||
):
|
||||
self._blueprints: List[Blueprint] = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._version_prefix = version_prefix
|
||||
self._strict_slashes = strict_slashes
|
||||
self._name_prefix = name_prefix
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||
"""The URL prefix for the Blueprint Group.
|
||||
|
||||
Returns:
|
||||
Optional[Union[int, str, float]]: URL prefix for the Blueprint
|
||||
Group.
|
||||
"""
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List[Blueprint]:
|
||||
"""A list of all the available blueprints under this group.
|
||||
|
||||
Returns:
|
||||
List[Blueprint]: List of all the available blueprints under
|
||||
this group.
|
||||
"""
|
||||
return self._blueprints
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[Union[str, int, float]]:
|
||||
"""API Version for the Blueprint Group, if any.
|
||||
|
||||
Returns:
|
||||
Optional[Union[str, int, float]]: API Version for the Blueprint
|
||||
"""
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def strict_slashes(self) -> Optional[bool]:
|
||||
"""Whether to enforce strict slashes for the Blueprint Group.
|
||||
|
||||
Returns:
|
||||
Optional[bool]: Whether to enforce strict slashes for the
|
||||
"""
|
||||
return self._strict_slashes
|
||||
|
||||
@property
|
||||
def version_prefix(self) -> str:
|
||||
"""Version prefix for the Blueprint Group.
|
||||
|
||||
Returns:
|
||||
str: Version prefix for the Blueprint Group.
|
||||
"""
|
||||
return self._version_prefix
|
||||
|
||||
@property
|
||||
def name_prefix(self) -> Optional[str]:
|
||||
"""Name prefix for the Blueprint Group.
|
||||
|
||||
This is mainly needed when blueprints are copied in order to
|
||||
avoid name conflicts.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Name prefix for the Blueprint Group.
|
||||
"""
|
||||
return self._name_prefix
|
||||
|
||||
def __iter__(self) -> Iterator[Blueprint]:
|
||||
"""Iterate over the list of blueprints in the group.
|
||||
|
||||
Returns:
|
||||
Iterator[Blueprint]: Iterator for the list of blueprints in
|
||||
"""
|
||||
return iter(self._blueprints)
|
||||
|
||||
@overload
|
||||
def __getitem__(self, item: int) -> Blueprint:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, item: slice) -> MutableSequence[Blueprint]:
|
||||
...
|
||||
|
||||
def __getitem__(
|
||||
self, item: Union[int, slice]
|
||||
) -> Union[Blueprint, MutableSequence[Blueprint]]:
|
||||
"""Get the Blueprint object at the specified index.
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
Blueprint: Blueprint object at the specified index.
|
||||
|
||||
Raises:
|
||||
IndexError: If the index is out of range.
|
||||
"""
|
||||
return self._blueprints[item]
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: int, item: Blueprint) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: slice, item: Iterable[Blueprint]) -> None:
|
||||
...
|
||||
|
||||
def __setitem__(
|
||||
self,
|
||||
index: Union[int, slice],
|
||||
item: Union[Blueprint, Iterable[Blueprint]],
|
||||
) -> None:
|
||||
"""Set the Blueprint object at the specified index.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
index (int): Index to use for removing a new Blueprint item
|
||||
item (Blueprint): New `Blueprint` object.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
IndexError: If the index is out of range.
|
||||
"""
|
||||
if isinstance(index, int):
|
||||
if not isinstance(item, Blueprint):
|
||||
raise TypeError("Expected a Blueprint instance")
|
||||
self._blueprints[index] = item
|
||||
elif isinstance(index, slice):
|
||||
if not isinstance(item, Iterable):
|
||||
raise TypeError("Expected an iterable of Blueprint instances")
|
||||
self._blueprints[index] = list(item)
|
||||
else:
|
||||
raise TypeError("Index must be int or slice")
|
||||
|
||||
@overload
|
||||
def __delitem__(self, index: int) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __delitem__(self, index: slice) -> None:
|
||||
...
|
||||
|
||||
def __delitem__(self, index: Union[int, slice]) -> None:
|
||||
"""Delete the Blueprint object at the specified index.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
index (int): Index to use for removing a new Blueprint item
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
IndexError: If the index is out of range.
|
||||
"""
|
||||
del self._blueprints[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the Length of the blueprint group object.
|
||||
|
||||
Returns:
|
||||
int: Length of the blueprint group object.
|
||||
"""
|
||||
return len(self._blueprints)
|
||||
|
||||
def append(self, value: Blueprint) -> None:
|
||||
"""Add a new Blueprint object to the group.
|
||||
|
||||
The Abstract class `MutableSequence` leverages this append method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
|
||||
Args:
|
||||
value (Blueprint): New `Blueprint` object.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self._blueprints.append(value)
|
||||
|
||||
def exception(self, *exceptions: Exception, **kwargs) -> Callable:
|
||||
"""Decorate a function to handle exceptions for all blueprints in the group.
|
||||
|
||||
In case of nested Blueprint Groups, the same handler is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
Args:
|
||||
*exceptions (Exception): Exceptions to handle
|
||||
**kwargs (dict): Optional Keyword arg to use with Middleware
|
||||
|
||||
Returns:
|
||||
Partial function to apply the middleware
|
||||
|
||||
Examples:
|
||||
```python
|
||||
bp1 = Blueprint("bp1", url_prefix="/bp1")
|
||||
bp2 = Blueprint("bp2", url_prefix="/bp2")
|
||||
group1 = Blueprint.group(bp1, bp2)
|
||||
|
||||
@group1.exception(Exception)
|
||||
def handler(request, exception):
|
||||
return text("Exception caught")
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
def register_exception_handler_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.exception(*exceptions, **kwargs)(fn)
|
||||
|
||||
return register_exception_handler_for_blueprints
|
||||
|
||||
def insert(self, index: int, item: Blueprint) -> None:
|
||||
"""Insert a new Blueprint object to the group at the specified index.
|
||||
|
||||
The Abstract class `MutableSequence` leverages this insert method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
|
||||
Args:
|
||||
index (int): Index to use for removing a new Blueprint item
|
||||
item (Blueprint): New `Blueprint` object.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self._blueprints.insert(index, item)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""A decorator that can be used to implement a Middleware for all blueprints in the group.
|
||||
|
||||
In case of nested Blueprint Groups, the same middleware is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
Args:
|
||||
*args (Optional): Optional positional Parameters to be use middleware
|
||||
**kwargs (Optional): Optional Keyword arg to use with Middleware
|
||||
|
||||
Returns:
|
||||
Partial function to apply the middleware
|
||||
""" # noqa: E501
|
||||
|
||||
def register_middleware_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.middleware(fn, *args, **kwargs)
|
||||
|
||||
if args and callable(args[0]):
|
||||
fn = args[0]
|
||||
args = list(args)[1:]
|
||||
return register_middleware_for_blueprints(fn)
|
||||
return register_middleware_for_blueprints
|
||||
|
||||
def on_request(self, middleware=None):
|
||||
"""Convenience method to register a request middleware for all blueprints in the group.
|
||||
|
||||
Args:
|
||||
middleware (Optional): Optional positional Parameters to be use middleware
|
||||
|
||||
Returns:
|
||||
Partial function to apply the middleware
|
||||
""" # noqa: E501
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="request")
|
||||
|
||||
def on_response(self, middleware=None):
|
||||
"""Convenience method to register a response middleware for all blueprints in the group.
|
||||
|
||||
Args:
|
||||
middleware (Optional): Optional positional Parameters to be use middleware
|
||||
|
||||
Returns:
|
||||
Partial function to apply the middleware
|
||||
""" # noqa: E501
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="response")
|
||||
|
||||
@@ -45,6 +45,8 @@ else:
|
||||
|
||||
|
||||
class UpperStrEnum(StrEnum):
|
||||
"""Base class for string enums that are case insensitive."""
|
||||
|
||||
def _generate_next_value_(name, start, count, last_values):
|
||||
return name.upper()
|
||||
|
||||
@@ -109,16 +111,13 @@ def pypy_windows_set_console_cp_patch() -> None:
|
||||
|
||||
|
||||
class Header(CIMultiDict):
|
||||
"""
|
||||
Container used for both request and response headers. It is a subclass of
|
||||
`CIMultiDict
|
||||
<https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy>`_.
|
||||
"""Container used for both request and response headers.
|
||||
It is a subclass of [CIMultiDict](https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy)
|
||||
|
||||
It allows for multiple values for a single key in keeping with the HTTP
|
||||
spec. Also, all keys are *case in-sensitive*.
|
||||
|
||||
Please checkout `the MultiDict documentation
|
||||
<https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_
|
||||
Please checkout [the MultiDict documentation](https://multidict.readthedocs.io/en/stable/multidict.html#multidict)
|
||||
for more details about how to use the object. In general, it should work
|
||||
very similar to a regular dictionary.
|
||||
""" # noqa: E501
|
||||
@@ -130,9 +129,7 @@ class Header(CIMultiDict):
|
||||
return ",".join(self.getall(key, default=[]))
|
||||
|
||||
def get_all(self, key: str):
|
||||
"""
|
||||
Convenience method mapped to ``getall()``.
|
||||
"""
|
||||
"""Convenience method mapped to ``getall()``."""
|
||||
return self.getall(key, default=[])
|
||||
|
||||
|
||||
|
||||
128
sanic/config.py
128
sanic/config.py
@@ -77,6 +77,8 @@ DEFAULT_CONFIG = {
|
||||
|
||||
|
||||
class DescriptorMeta(ABCMeta):
|
||||
"""Metaclass for Config."""
|
||||
|
||||
def __init__(cls, *_):
|
||||
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
||||
|
||||
@@ -86,6 +88,12 @@ class DescriptorMeta(ABCMeta):
|
||||
|
||||
|
||||
class Config(dict, metaclass=DescriptorMeta):
|
||||
"""Configuration object for Sanic.
|
||||
|
||||
You can use this object to both: (1) configure how Sanic will operate, and
|
||||
(2) manage your application's custom configuration values.
|
||||
"""
|
||||
|
||||
ACCESS_LOG: bool
|
||||
AUTO_EXTEND: bool
|
||||
AUTO_RELOAD: bool
|
||||
@@ -171,6 +179,35 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
self.update({attr: value})
|
||||
|
||||
def update(self, *other: Any, **kwargs: Any) -> None:
|
||||
"""Update the config with new values.
|
||||
|
||||
This method will update the config with the values from the provided
|
||||
`other` objects, and then update the config with the provided
|
||||
`kwargs`. The `other` objects can be any object that can be converted
|
||||
to a dictionary, such as a `dict`, `Config` object, or `str` path to a
|
||||
Python file. The `kwargs` must be a dictionary of key-value pairs.
|
||||
|
||||
.. note::
|
||||
Only upper case settings are considered
|
||||
|
||||
Args:
|
||||
*other: Any number of objects that can be converted to a
|
||||
dictionary.
|
||||
**kwargs: Any number of key-value pairs.
|
||||
|
||||
Raises:
|
||||
AttributeError: If a key is not in the config.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
config.update(
|
||||
{"A": 1, "B": 2},
|
||||
{"C": 3, "D": 4},
|
||||
E=5,
|
||||
F=6,
|
||||
)
|
||||
```
|
||||
"""
|
||||
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
||||
setters: Dict[str, Any] = {
|
||||
k: kwargs.pop(k)
|
||||
@@ -243,7 +280,8 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
"""Load environment variables into the config.
|
||||
|
||||
Looks for prefixed environment variables and applies them to the
|
||||
configuration if present. This is called automatically when Sanic
|
||||
starts up to load environment variables into config. Environment
|
||||
@@ -261,19 +299,26 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
:meth:`sanic.config.Config.register_type`. Just make sure that they
|
||||
are registered before you instantiate your application.
|
||||
|
||||
.. code-block:: python
|
||||
You likely won't need to call this method directly.
|
||||
|
||||
class Foo:
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name
|
||||
See [Configuration](/en/guide/deployment/configuration) for more details.
|
||||
|
||||
Args:
|
||||
prefix (str): The prefix to use when looking for environment
|
||||
variables. Defaults to `SANIC_`.
|
||||
|
||||
|
||||
config = Config(converters=[Foo])
|
||||
app = Sanic(__name__, config=config)
|
||||
Examples:
|
||||
```python
|
||||
# Environment variables
|
||||
# SANIC_SERVER_NAME=example.com
|
||||
# SANIC_SERVER_PORT=9999
|
||||
# SANIC_SERVER_AUTORELOAD=true
|
||||
|
||||
`See user guide re: config
|
||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
# Python
|
||||
app.config.load_environment_vars()
|
||||
```
|
||||
""" # noqa: E501
|
||||
for key, value in environ.items():
|
||||
if not key.startswith(prefix) or not key.isupper():
|
||||
continue
|
||||
@@ -288,52 +333,55 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
pass
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""
|
||||
Update app.config.
|
||||
"""Update app.config.
|
||||
|
||||
.. note::
|
||||
|
||||
Only upper case settings are considered
|
||||
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
See [Configuration](/en/guide/deployment/configuration) for more details.
|
||||
|
||||
.. code-block:: python
|
||||
Args:
|
||||
config (Union[bytes, str, dict, Any]): Path to py file holding
|
||||
settings, dict holding settings, or any object holding
|
||||
settings.
|
||||
|
||||
Examples:
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
|
||||
```python
|
||||
# /some/py/file
|
||||
A = 1
|
||||
B = 2
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
```python
|
||||
config.update_config("${some}/py/file")
|
||||
```
|
||||
|
||||
Yes you can put environment variable here, but they must be provided
|
||||
in format: ``${some_env_var}``, and mark that ``$some_env_var`` is
|
||||
treated as plain string.
|
||||
Yes you can put environment variable here, but they must be provided
|
||||
in format: ``${some_env_var}``, and mark that ``$some_env_var`` is
|
||||
treated as plain string.
|
||||
|
||||
You can upload app config by providing dict holding settings.
|
||||
|
||||
.. code-block:: python
|
||||
You can upload app config by providing dict holding settings.
|
||||
|
||||
```python
|
||||
d = {"A": 1, "B": 2}
|
||||
config.update_config(d)
|
||||
```
|
||||
|
||||
You can upload app config by providing any object holding settings,
|
||||
but in such case config.__dict__ will be used as dict holding settings.
|
||||
|
||||
.. code-block:: python
|
||||
You can upload app config by providing any object holding settings,
|
||||
but in such case config.__dict__ will be used as dict holding settings.
|
||||
|
||||
```python
|
||||
class C:
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
config.update_config(C)
|
||||
|
||||
`See user guide re: config
|
||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
|
||||
```
|
||||
""" # noqa: E501
|
||||
if isinstance(config, (bytes, str, Path)):
|
||||
config = load_module_from_file_location(location=config)
|
||||
|
||||
@@ -357,10 +405,24 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
load = update_config
|
||||
|
||||
def register_type(self, converter: Callable[[str], Any]) -> None:
|
||||
"""
|
||||
"""Register a custom type converter.
|
||||
|
||||
Allows for adding custom function to cast from a string value to any
|
||||
other type. The function should raise ValueError if it is not the
|
||||
correct type.
|
||||
|
||||
Args:
|
||||
converter (Callable[[str], Any]): A function that takes a string
|
||||
and returns a value of any type.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
def my_converter(value: str) -> Any:
|
||||
# Do something to convert the value
|
||||
return value
|
||||
|
||||
config.register_type(my_converter)
|
||||
```
|
||||
"""
|
||||
if converter in self._converters:
|
||||
error_logger.warning(
|
||||
|
||||
@@ -4,6 +4,8 @@ from sanic.compat import UpperStrEnum
|
||||
|
||||
|
||||
class HTTPMethod(UpperStrEnum):
|
||||
"""HTTP methods that are commonly used."""
|
||||
|
||||
GET = auto()
|
||||
POST = auto()
|
||||
PUT = auto()
|
||||
@@ -14,6 +16,8 @@ class HTTPMethod(UpperStrEnum):
|
||||
|
||||
|
||||
class LocalCertCreator(UpperStrEnum):
|
||||
"""Local certificate creator."""
|
||||
|
||||
AUTO = auto()
|
||||
TRUSTME = auto()
|
||||
MKCERT = auto()
|
||||
|
||||
@@ -48,8 +48,29 @@ def _unquote(str): # no cov
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def parse_cookie(raw: str):
|
||||
cookies: Dict[str, List] = {}
|
||||
def parse_cookie(raw: str) -> Dict[str, List[str]]:
|
||||
"""Parses a raw cookie string into a dictionary.
|
||||
|
||||
The function takes a raw cookie string (usually from HTTP headers) and
|
||||
returns a dictionary where each key is a cookie name and the value is a
|
||||
list of values for that cookie. The function handles quoted values and
|
||||
skips invalid cookie names.
|
||||
|
||||
Args:
|
||||
raw (str): The raw cookie string to be parsed.
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: A dictionary containing the cookie names as keys
|
||||
and a list of values for each cookie.
|
||||
|
||||
Example:
|
||||
```python
|
||||
raw = 'name1=value1; name2="value2"; name3=value3'
|
||||
cookies = parse_cookie(raw)
|
||||
# cookies will be {'name1': ['value1'], 'name2': ['value2'], 'name3': ['value3']}
|
||||
```
|
||||
""" # noqa: E501
|
||||
cookies: Dict[str, List[str]] = {}
|
||||
|
||||
for token in raw.split(";"):
|
||||
name, __, value = token.partition("=")
|
||||
@@ -74,6 +95,31 @@ def parse_cookie(raw: str):
|
||||
|
||||
|
||||
class CookieRequestParameters(RequestParameters):
|
||||
"""A container for accessing single and multiple cookie values.
|
||||
|
||||
Because the HTTP standard allows for multiple cookies with the same name,
|
||||
a standard dictionary cannot be used to access cookie values. This class
|
||||
provides a way to access cookie values in a way that is similar to a
|
||||
dictionary, but also allows for accessing multiple values for a single
|
||||
cookie name when necessary.
|
||||
|
||||
Args:
|
||||
cookies (Dict[str, List[str]]): A dictionary containing the cookie
|
||||
names as keys and a list of values for each cookie.
|
||||
|
||||
Example:
|
||||
```python
|
||||
raw = 'name1=value1; name2="value2"; name3=value3'
|
||||
cookies = parse_cookie(raw)
|
||||
# cookies will be {'name1': ['value1'], 'name2': ['value2'], 'name3': ['value3']}
|
||||
|
||||
request_cookies = CookieRequestParameters(cookies)
|
||||
request_cookies['name1'] # 'value1'
|
||||
request_cookies.get('name1') # 'value1'
|
||||
request_cookies.getlist('name1') # ['value1']
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
def __getitem__(self, key: str) -> Optional[str]:
|
||||
deprecation(
|
||||
f"You are accessing cookie key '{key}', which is currently in "
|
||||
|
||||
@@ -53,10 +53,14 @@ _is_legal_key = re.compile("[%s]+" % re.escape(LEGAL_CHARS)).fullmatch
|
||||
|
||||
# In v24.3, we should remove this as being a subclass of dict
|
||||
class CookieJar(dict):
|
||||
"""
|
||||
"""A container to manipulate cookies.
|
||||
|
||||
CookieJar dynamically writes headers as cookies are added and removed
|
||||
It gets around the limitation of one header per name by using the
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||
|
||||
Args:
|
||||
headers (Header): The headers object to write cookies to.
|
||||
"""
|
||||
|
||||
HEADER_KEY = "Set-Cookie"
|
||||
@@ -114,6 +118,7 @@ class CookieJar(dict):
|
||||
return super().__iter__()
|
||||
|
||||
def keys(self): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"Accessing CookieJar.keys() has been deprecated and will be "
|
||||
"removed in v24.3. To learn more, please see: "
|
||||
@@ -123,6 +128,7 @@ class CookieJar(dict):
|
||||
return super().keys()
|
||||
|
||||
def values(self): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"Accessing CookieJar.values() has been deprecated and will be "
|
||||
"removed in v24.3. To learn more, please see: "
|
||||
@@ -132,6 +138,7 @@ class CookieJar(dict):
|
||||
return super().values()
|
||||
|
||||
def items(self): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"Accessing CookieJar.items() has been deprecated and will be "
|
||||
"removed in v24.3. To learn more, please see: "
|
||||
@@ -141,6 +148,7 @@ class CookieJar(dict):
|
||||
return super().items()
|
||||
|
||||
def get(self, *args, **kwargs): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"Accessing cookies from the CookieJar using get is deprecated "
|
||||
"and will be removed in v24.3. You should instead use the "
|
||||
@@ -151,6 +159,7 @@ class CookieJar(dict):
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def pop(self, key, *args, **kwargs): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"Using CookieJar.pop() has been deprecated and will be "
|
||||
"removed in v24.3. To learn more, please see: "
|
||||
@@ -162,6 +171,7 @@ class CookieJar(dict):
|
||||
|
||||
@property
|
||||
def header_key(self): # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"The CookieJar.header_key property has been deprecated and will "
|
||||
"be removed in version 24.3. Use CookieJar.HEADER_KEY. ",
|
||||
@@ -171,6 +181,7 @@ class CookieJar(dict):
|
||||
|
||||
@property
|
||||
def cookie_headers(self) -> Dict[str, str]: # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"The CookieJar.coookie_headers property has been deprecated "
|
||||
"and will be removed in version 24.3. If you need to check if a "
|
||||
@@ -181,6 +192,11 @@ class CookieJar(dict):
|
||||
|
||||
@property
|
||||
def cookies(self) -> List[Cookie]:
|
||||
"""A list of cookies in the CookieJar.
|
||||
|
||||
Returns:
|
||||
List[Cookie]: A list of cookies in the CookieJar.
|
||||
"""
|
||||
return self.headers.getall(self.HEADER_KEY)
|
||||
|
||||
def get_cookie(
|
||||
@@ -191,6 +207,22 @@ class CookieJar(dict):
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Optional[Cookie]:
|
||||
"""Fetch a cookie from the CookieJar.
|
||||
|
||||
Args:
|
||||
key (str): The key of the cookie to fetch.
|
||||
path (str, optional): The path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): The domain of the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Optional[Cookie]: The cookie if it exists, otherwise `None`.
|
||||
""" # noqa: E501
|
||||
for cookie in self.cookies:
|
||||
if (
|
||||
cookie.key == Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
@@ -208,6 +240,22 @@ class CookieJar(dict):
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> bool:
|
||||
"""Check if a cookie exists in the CookieJar.
|
||||
|
||||
Args:
|
||||
key (str): The key of the cookie to check.
|
||||
path (str, optional): The path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): The domain of the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the cookie exists.
|
||||
""" # noqa: E501
|
||||
for cookie in self.cookies:
|
||||
if (
|
||||
cookie.key == Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
@@ -234,44 +282,59 @@ class CookieJar(dict):
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Cookie:
|
||||
"""
|
||||
Add a cookie to the CookieJar
|
||||
"""Add a cookie to the CookieJar.
|
||||
|
||||
:param key: Key of the cookie
|
||||
:type key: str
|
||||
:param value: Value of the cookie
|
||||
:type value: str
|
||||
:param path: Path of the cookie, defaults to None
|
||||
:type path: Optional[str], optional
|
||||
:param domain: Domain of the cookie, defaults to None
|
||||
:type domain: Optional[str], optional
|
||||
:param secure: Whether to set it as a secure cookie, defaults to True
|
||||
:type secure: bool
|
||||
:param max_age: Max age of the cookie in seconds; if set to 0 a
|
||||
browser should delete it, defaults to None
|
||||
:type max_age: Optional[int], optional
|
||||
:param expires: When the cookie expires; if set to None browsers
|
||||
should set it as a session cookie, defaults to None
|
||||
:type expires: Optional[datetime], optional
|
||||
:param httponly: Whether to set it as HTTP only, defaults to False
|
||||
:type httponly: bool
|
||||
:param samesite: How to set the samesite property, should be
|
||||
strict, lax or none (case insensitive), defaults to Lax
|
||||
:type samesite: Optional[SameSite], optional
|
||||
:param partitioned: Whether to set it as partitioned, defaults to False
|
||||
:type partitioned: bool
|
||||
:param comment: A cookie comment, defaults to None
|
||||
:type comment: Optional[str], optional
|
||||
:param host_prefix: Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True,
|
||||
defaults to False
|
||||
:type host_prefix: bool
|
||||
:param secure_prefix: Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True, defaults to False
|
||||
:type secure_prefix: bool
|
||||
:return: The instance of the created cookie
|
||||
:rtype: Cookie
|
||||
"""
|
||||
Args:
|
||||
key (str): Key of the cookie.
|
||||
value (str): Value of the cookie.
|
||||
path (str, optional): Path of the cookie. Defaults to "/".
|
||||
domain (Optional[str], optional): Domain of the cookie. Defaults to None.
|
||||
secure (bool, optional): Whether to set it as a secure cookie. Defaults to True.
|
||||
max_age (Optional[int], optional): Max age of the cookie in seconds; if set to 0 a
|
||||
browser should delete it. Defaults to None.
|
||||
expires (Optional[datetime], optional): When the cookie expires; if set to None browsers
|
||||
should set it as a session cookie. Defaults to None.
|
||||
httponly (bool, optional): Whether to set it as HTTP only. Defaults to False.
|
||||
samesite (Optional[SameSite], optional): How to set the samesite property, should be
|
||||
strict, lax, or none (case insensitive). Defaults to "Lax".
|
||||
partitioned (bool, optional): Whether to set it as partitioned. Defaults to False.
|
||||
comment (Optional[str], optional): A cookie comment. Defaults to None.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True. Defaults to False.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Cookie: The instance of the created cookie.
|
||||
|
||||
Raises:
|
||||
ServerError: If host_prefix is set without secure=True.
|
||||
ServerError: If host_prefix is set without path="/" and domain=None.
|
||||
ServerError: If host_prefix is set with domain.
|
||||
ServerError: If secure_prefix is set without secure=True.
|
||||
ServerError: If partitioned is set without host_prefix=True.
|
||||
|
||||
Examples:
|
||||
Basic usage
|
||||
```python
|
||||
cookie = add_cookie('name', 'value')
|
||||
```
|
||||
|
||||
Adding a cookie with a custom path and domain
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', path='/custom', domain='example.com')
|
||||
```
|
||||
|
||||
Adding a secure, HTTP-only cookie with a comment
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', secure=True, httponly=True, comment='My Cookie')
|
||||
```
|
||||
|
||||
Adding a cookie with a max age of 60 seconds
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', max_age=60)
|
||||
```
|
||||
""" # noqa: E501
|
||||
cookie = Cookie(
|
||||
key,
|
||||
value,
|
||||
@@ -359,7 +422,41 @@ class CookieJar(dict):
|
||||
# All of the current property accessors should be removed in favor
|
||||
# of actual slotted properties.
|
||||
class Cookie(dict):
|
||||
"""A stripped down version of Morsel from SimpleCookie"""
|
||||
"""A representation of a HTTP cookie, providing an interface to manipulate cookie attributes intended for a response.
|
||||
|
||||
This class is a simplified representation of a cookie, similar to the Morsel SimpleCookie in Python's standard library.
|
||||
It allows the manipulation of various cookie attributes including path, domain, security settings, and others.
|
||||
|
||||
Several "smart defaults" are provided to make it easier to create cookies that are secure by default. These include:
|
||||
|
||||
- Setting the `secure` flag to `True` by default
|
||||
- Setting the `samesite` flag to `Lax` by default
|
||||
|
||||
Args:
|
||||
key (str): The key (name) of the cookie.
|
||||
value (str): The value of the cookie.
|
||||
path (str, optional): The path for the cookie. Defaults to "/".
|
||||
domain (Optional[str], optional): The domain for the cookie.
|
||||
Defaults to `None`.
|
||||
secure (bool, optional): Whether the cookie is secure.
|
||||
Defaults to `True`.
|
||||
max_age (Optional[int], optional): The maximum age of the cookie
|
||||
in seconds. Defaults to `None`.
|
||||
expires (Optional[datetime], optional): The expiration date of the
|
||||
cookie. Defaults to `None`.
|
||||
httponly (bool, optional): HttpOnly flag for the cookie.
|
||||
Defaults to `False`.
|
||||
samesite (Optional[SameSite], optional): The SameSite attribute for
|
||||
the cookie. Defaults to `"Lax"`.
|
||||
partitioned (bool, optional): Whether the cookie is partitioned.
|
||||
Defaults to `False`.
|
||||
comment (Optional[str], optional): A comment for the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to use the host prefix.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to use the secure prefix.
|
||||
Defaults to `False`.
|
||||
""" # noqa: E501
|
||||
|
||||
HOST_PREFIX = "__Host-"
|
||||
SECURE_PREFIX = "__Secure-"
|
||||
@@ -481,19 +578,23 @@ class Cookie(dict):
|
||||
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
"""
|
||||
Encode the cookie content in a specific type of encoding instructed
|
||||
by the developer. Leverages the :func:`str.encode` method provided
|
||||
by python.
|
||||
def encode(self, encoding: str) -> bytes:
|
||||
"""Encode the cookie content in a specific type of encoding instructed by the developer.
|
||||
|
||||
Leverages the `str.encode` method provided by Python.
|
||||
|
||||
This method can be used to encode and embed ``utf-8`` content into
|
||||
the cookies.
|
||||
|
||||
:param encoding: Encoding to be used with the cookie
|
||||
:return: Cookie encoded in a codec of choosing.
|
||||
:except: UnicodeEncodeError
|
||||
"""
|
||||
.. warning::
|
||||
Direct encoding of a Cookie object has been deprecated and will be removed in v24.3.
|
||||
|
||||
Args:
|
||||
encoding (str): The encoding type to be used.
|
||||
|
||||
Returns:
|
||||
bytes: The encoded cookie content.
|
||||
""" # noqa: E501
|
||||
deprecation(
|
||||
"Direct encoding of a Cookie object has been deprecated and will "
|
||||
"be removed in v24.3.",
|
||||
@@ -531,6 +632,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def path(self) -> str: # no cov
|
||||
"""The path of the cookie. Defaults to `"/"`."""
|
||||
return self["path"]
|
||||
|
||||
@path.setter
|
||||
@@ -539,6 +641,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def expires(self) -> Optional[datetime]: # no cov
|
||||
"""The expiration date of the cookie. Defaults to `None`."""
|
||||
return self.get("expires")
|
||||
|
||||
@expires.setter
|
||||
@@ -547,6 +650,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def comment(self) -> Optional[str]: # no cov
|
||||
"""A comment for the cookie. Defaults to `None`."""
|
||||
return self.get("comment")
|
||||
|
||||
@comment.setter
|
||||
@@ -555,6 +659,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def domain(self) -> Optional[str]: # no cov
|
||||
"""The domain of the cookie. Defaults to `None`."""
|
||||
return self.get("domain")
|
||||
|
||||
@domain.setter
|
||||
@@ -563,6 +668,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def max_age(self) -> Optional[int]: # no cov
|
||||
"""The maximum age of the cookie in seconds. Defaults to `None`."""
|
||||
return self.get("max-age")
|
||||
|
||||
@max_age.setter
|
||||
@@ -571,6 +677,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def secure(self) -> bool: # no cov
|
||||
"""Whether the cookie is secure. Defaults to `True`."""
|
||||
return self.get("secure", False)
|
||||
|
||||
@secure.setter
|
||||
@@ -579,6 +686,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def httponly(self) -> bool: # no cov
|
||||
"""Whether the cookie is HTTP only. Defaults to `False`."""
|
||||
return self.get("httponly", False)
|
||||
|
||||
@httponly.setter
|
||||
@@ -587,6 +695,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def samesite(self) -> Optional[SameSite]: # no cov
|
||||
"""The SameSite attribute for the cookie. Defaults to `"Lax"`."""
|
||||
return self.get("samesite")
|
||||
|
||||
@samesite.setter
|
||||
@@ -595,6 +704,7 @@ class Cookie(dict):
|
||||
|
||||
@property
|
||||
def partitioned(self) -> bool: # no cov
|
||||
"""Whether the cookie is partitioned. Defaults to `False`."""
|
||||
return self.get("partitioned", False)
|
||||
|
||||
@partitioned.setter
|
||||
@@ -605,6 +715,29 @@ class Cookie(dict):
|
||||
def make_key(
|
||||
cls, key: str, host_prefix: bool = False, secure_prefix: bool = False
|
||||
) -> str:
|
||||
"""Create a cookie key with the appropriate prefix.
|
||||
|
||||
Cookies can have one ow two prefixes. The first is `__Host-` which
|
||||
requires that the cookie be set with `path="/", domain=None, and
|
||||
secure=True`. The second is `__Secure-` which requires that
|
||||
`secure=True`.
|
||||
|
||||
They cannot be combined.
|
||||
|
||||
Args:
|
||||
key (str): The key (name) of the cookie.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
ServerError: If both host_prefix and secure_prefix are set.
|
||||
|
||||
Returns:
|
||||
str: The key with the appropriate prefix.
|
||||
""" # noqa: E501
|
||||
if host_prefix and secure_prefix:
|
||||
raise ServerError(
|
||||
"Both host_prefix and secure_prefix were requested. "
|
||||
|
||||
@@ -47,46 +47,63 @@ JSON = "application/json"
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
"""
|
||||
Base class that all renderers must inherit from.
|
||||
"""
|
||||
"""Base class that all renderers must inherit from.
|
||||
|
||||
This class defines the structure for rendering objects, handling the core functionality that specific renderers may extend.
|
||||
|
||||
Attributes:
|
||||
request (Request): The incoming request object that needs rendering.
|
||||
exception (Exception): Any exception that occurred and needs to be rendered.
|
||||
debug (bool): Flag indicating whether to render with debugging information.
|
||||
|
||||
Methods:
|
||||
dumps: A static method that must be overridden by subclasses to define the specific rendering.
|
||||
|
||||
Args:
|
||||
request (Request): The incoming request object that needs rendering.
|
||||
exception (Exception): Any exception that occurred and needs to be rendered.
|
||||
debug (bool): Flag indicating whether to render with debugging information.
|
||||
""" # noqa: E501
|
||||
|
||||
dumps = staticmethod(dumps)
|
||||
|
||||
def __init__(self, request, exception, debug):
|
||||
def __init__(self, request: Request, exception: Exception, debug: bool):
|
||||
self.request = request
|
||||
self.exception = exception
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
def headers(self) -> t.Dict[str, str]:
|
||||
"""The headers to be used for the response."""
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "headers", {})
|
||||
return {}
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""The status code to be used for the response."""
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "status_code", FALLBACK_STATUS)
|
||||
return FALLBACK_STATUS
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""The text to be used for the response."""
|
||||
if self.debug or isinstance(self.exception, SanicException):
|
||||
return str(self.exception)
|
||||
return FALLBACK_TEXT
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""The title to be used for the response."""
|
||||
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
return f"{self.status} — {status_text}"
|
||||
|
||||
def render(self) -> HTTPResponse:
|
||||
"""
|
||||
Outputs the exception as a :class:`HTTPResponse`.
|
||||
"""Outputs the exception as a response.
|
||||
|
||||
:return: The formatted exception
|
||||
:rtype: str
|
||||
Returns:
|
||||
HTTPResponse: The response object.
|
||||
"""
|
||||
output = (
|
||||
self.full
|
||||
@@ -98,23 +115,26 @@ class BaseRenderer:
|
||||
return output
|
||||
|
||||
def minimal(self) -> HTTPResponse: # noqa
|
||||
"""
|
||||
Provide a formatted message that is meant to not show any sensitive
|
||||
data or details.
|
||||
"""
|
||||
"""Provide a formatted message that is meant to not show any sensitive data or details.
|
||||
|
||||
This is the default fallback for production environments.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: The response object.
|
||||
""" # noqa: E501
|
||||
raise NotImplementedError
|
||||
|
||||
def full(self) -> HTTPResponse: # noqa
|
||||
"""
|
||||
Provide a formatted message that has all details and is mean to be used
|
||||
primarily for debugging and non-production environments.
|
||||
"""
|
||||
"""Provide a formatted message that has all details and is mean to be used primarily for debugging and non-production environments.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: The response object.
|
||||
""" # noqa: E501
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as HTML.
|
||||
"""Render an exception as HTML.
|
||||
|
||||
The default fallback type.
|
||||
"""
|
||||
@@ -134,9 +154,7 @@ class HTMLRenderer(BaseRenderer):
|
||||
|
||||
|
||||
class TextRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as plain text.
|
||||
"""
|
||||
"""Render an exception as plain text."""
|
||||
|
||||
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
|
||||
SPACER = " "
|
||||
@@ -211,9 +229,7 @@ class TextRenderer(BaseRenderer):
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as JSON.
|
||||
"""
|
||||
"""Render an exception as JSON."""
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
output = self._generate_output(full=True)
|
||||
@@ -269,9 +285,7 @@ class JSONRenderer(BaseRenderer):
|
||||
|
||||
|
||||
def escape(text):
|
||||
"""
|
||||
Minimal HTML escaping, not for attribute values (unlike html.escape).
|
||||
"""
|
||||
"""Minimal HTML escaping, not for attribute values (unlike html.escape)."""
|
||||
return f"{text}".replace("&", "&").replace("<", "<")
|
||||
|
||||
|
||||
@@ -302,6 +316,7 @@ RESPONSE_MAPPING = {
|
||||
|
||||
|
||||
def check_error_format(format):
|
||||
"""Check that the format is known."""
|
||||
if format not in MIME_BY_CONFIG and format != "auto":
|
||||
raise SanicException(f"Unknown format: {format}")
|
||||
|
||||
@@ -314,9 +329,7 @@ def exception_response(
|
||||
base: t.Type[BaseRenderer],
|
||||
renderer: t.Optional[t.Type[BaseRenderer]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Render a response for the default FALLBACK exception handler.
|
||||
"""
|
||||
"""Render a response for the default FALLBACK exception handler."""
|
||||
if not renderer:
|
||||
mt = guess_mime(request, fallback)
|
||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(mt, base)
|
||||
@@ -326,6 +339,7 @@ def exception_response(
|
||||
|
||||
|
||||
def guess_mime(req: Request, fallback: str) -> str:
|
||||
"""Guess the MIME type for the response based upon the request."""
|
||||
# Attempt to find a suitable MIME format for the response.
|
||||
# Insertion-ordered map of formats["html"] = "source of that suggestion"
|
||||
formats = {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from asyncio import CancelledError, Protocol
|
||||
from asyncio import CancelledError
|
||||
from os import PathLike
|
||||
from typing import Any, Dict, Optional, Sequence, Union
|
||||
|
||||
from sanic.helpers import STATUS_CODES
|
||||
from sanic.models.protocol_types import Range
|
||||
|
||||
|
||||
class RequestCancelled(CancelledError):
|
||||
@@ -10,54 +11,46 @@ class RequestCancelled(CancelledError):
|
||||
|
||||
|
||||
class ServerKilled(Exception):
|
||||
"""
|
||||
Exception Sanic server uses when killing a server process for something
|
||||
unexpected happening.
|
||||
"""
|
||||
"""Exception Sanic server uses when killing a server process for something unexpected happening.""" # noqa: E501
|
||||
|
||||
quiet = True
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
"""
|
||||
Generic exception that will generate an HTTP response when raised
|
||||
in the context of a request lifecycle.
|
||||
"""Generic exception that will generate an HTTP response when raised in the context of a request lifecycle.
|
||||
|
||||
Usually it is best practice to use one of the more specific exceptions
|
||||
than this generic. Even when trying to raise a 500, it is generally
|
||||
preferrable to use :class:`.ServerError`
|
||||
Usually, it is best practice to use one of the more specific exceptions
|
||||
than this generic one. Even when trying to raise a 500, it is generally
|
||||
preferable to use `ServerError`.
|
||||
|
||||
.. code-block:: python
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`,
|
||||
then the appropriate HTTP response status message will be used instead. Defaults to `None`.
|
||||
status_code (Optional[int], optional): The HTTP response code to send, if applicable. If `None`,
|
||||
then it will be 500. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed from the logs.
|
||||
Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
raise SanicException(
|
||||
"Something went wrong",
|
||||
status_code=999,
|
||||
context={
|
||||
"info": "Some additional details",
|
||||
"info": "Some additional details to send to the client",
|
||||
},
|
||||
headers={
|
||||
"X-Foo": "bar"
|
||||
}
|
||||
)
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the appropriate HTTP response status message will be used
|
||||
instead, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param status_code: The HTTP response code to send, if applicable. If
|
||||
``None``, then it will be 500, defaults to None
|
||||
:type status_code: Optional[int], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
status_code: int = 500
|
||||
quiet: Optional[bool] = False
|
||||
@@ -96,9 +89,7 @@ class SanicException(Exception):
|
||||
|
||||
|
||||
class HTTPException(SanicException):
|
||||
"""
|
||||
A base class for other exceptions and should not be called directly.
|
||||
"""
|
||||
"""A base class for other exceptions and should not be called directly."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -119,50 +110,40 @@ class HTTPException(SanicException):
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""A base class for other exceptions and should not be called directly.
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Not Found' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`,
|
||||
then the appropriate HTTP response status message will be used instead. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed from the logs.
|
||||
Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 404
|
||||
quiet = True
|
||||
|
||||
|
||||
class BadRequest(HTTPException):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""400 Bad Request
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 400
|
||||
quiet = True
|
||||
@@ -173,31 +154,23 @@ BadURL = BadRequest
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
"""405 Method Not Allowed
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Method Not Allowed' will be sent,
|
||||
defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param method: The HTTP method that was used, defaults to an empty string
|
||||
:type method: Optional[str], optional
|
||||
:param allowed_methods: The HTTP methods that can be used instead of the
|
||||
one that was attempted
|
||||
:type allowed_methods: Optional[Sequence[str]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Method Not Allowed' will be sent. Defaults to `None`.
|
||||
method (Optional[str], optional): The HTTP method that was used. Defaults to an empty string.
|
||||
allowed_methods (Optional[Sequence[str]], optional): The HTTP methods that can be used instead of the
|
||||
one that was attempted.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 405
|
||||
quiet = True
|
||||
@@ -233,29 +206,23 @@ MethodNotSupported = MethodNotAllowed
|
||||
|
||||
|
||||
class ServerError(HTTPException):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""500 Internal Server Error
|
||||
|
||||
A general server-side error has occurred. If no other HTTP exception is
|
||||
appropriate, then this should be used
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Internal Server Error' will be sent,
|
||||
defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 500
|
||||
|
||||
@@ -264,71 +231,70 @@ InternalServerError = ServerError
|
||||
|
||||
|
||||
class ServiceUnavailable(HTTPException):
|
||||
"""
|
||||
**Status**: 503 Service Unavailable
|
||||
"""503 Service Unavailable
|
||||
|
||||
The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state.
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 503
|
||||
quiet = True
|
||||
|
||||
|
||||
class URLBuildError(HTTPException):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""500 Internal Server Error
|
||||
|
||||
An exception used by Sanic internals when unable to build a URL.
|
||||
"""
|
||||
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""404 Not Found
|
||||
|
||||
A specific form of :class:`.NotFound` that is specifically when looking
|
||||
for a file on the file system at a known path.
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Not Found' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param path: The path, if any, to the file that could not
|
||||
be found, defaults to None
|
||||
:type path: Optional[PathLike], optional
|
||||
:param relative_url: A relative URL of the file, defaults to None
|
||||
:type relative_url: Optional[str], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Not Found' will be sent. Defaults to `None`.
|
||||
path (Optional[PathLike], optional): The path, if any, to the file that could not
|
||||
be found. Defaults to `None`.
|
||||
relative_url (Optional[str], optional): A relative URL of the file. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -353,7 +319,8 @@ class FileNotFound(NotFound):
|
||||
|
||||
|
||||
class RequestTimeout(HTTPException):
|
||||
"""
|
||||
"""408 Request Timeout
|
||||
|
||||
The Web server (running the Web site) thinks that there has been too
|
||||
long an interval of time between 1) the establishment of an IP
|
||||
connection (socket) between the client and the server and
|
||||
@@ -363,96 +330,98 @@ class RequestTimeout(HTTPException):
|
||||
|
||||
This is an internal exception thrown by Sanic and should not be used
|
||||
directly.
|
||||
"""
|
||||
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 408
|
||||
quiet = True
|
||||
|
||||
|
||||
class PayloadTooLarge(HTTPException):
|
||||
"""
|
||||
**Status**: 413 Payload Too Large
|
||||
"""413 Payload Too Large
|
||||
|
||||
This is an internal exception thrown by Sanic and should not be used
|
||||
directly.
|
||||
"""
|
||||
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 413
|
||||
quiet = True
|
||||
|
||||
|
||||
class HeaderNotFound(BadRequest):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""400 Bad Request
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
class InvalidHeader(BadRequest):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""400 Bad Request
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
|
||||
|
||||
class ContentRange(Protocol):
|
||||
total: int
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
class RangeNotSatisfiable(HTTPException):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""416 Range Not Satisfiable
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Range Not Satisfiable' will be sent,
|
||||
defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param content_range: An object meeting the :class:`.ContentRange` protocol
|
||||
that has a ``total`` property, defaults to None
|
||||
:type content_range: Optional[ContentRange], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Range Not Satisfiable' will be sent. Defaults to `None`.
|
||||
content_range (Optional[ContentRange], optional): An object meeting the :class:`.ContentRange` protocol
|
||||
that has a `total` property. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 416
|
||||
quiet = True
|
||||
@@ -460,7 +429,7 @@ class RangeNotSatisfiable(HTTPException):
|
||||
def __init__(
|
||||
self,
|
||||
message: Optional[Union[str, bytes]] = None,
|
||||
content_range: Optional[ContentRange] = None,
|
||||
content_range: Optional[Range] = None,
|
||||
*,
|
||||
quiet: Optional[bool] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
@@ -485,26 +454,20 @@ ContentRangeError = RangeNotSatisfiable
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPException):
|
||||
"""
|
||||
**Status**: 417 Expectation Failed
|
||||
"""417 Expectation Failed
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Expectation Failed' will be sent,
|
||||
defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 417
|
||||
quiet = True
|
||||
@@ -514,34 +477,40 @@ HeaderExpectationFailed = ExpectationFailed
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""
|
||||
**Status**: 403 Forbidden
|
||||
"""403 Forbidden
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Forbidden' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 403
|
||||
quiet = True
|
||||
|
||||
|
||||
class InvalidRangeType(RangeNotSatisfiable):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
"""416 Range Not Satisfiable
|
||||
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 416
|
||||
quiet = True
|
||||
@@ -575,48 +544,58 @@ class Unauthorized(HTTPException):
|
||||
When present, additional keyword arguments may be used to complete
|
||||
the WWW-Authentication header.
|
||||
|
||||
Examples::
|
||||
Args:
|
||||
message (Optional[Union[str, bytes]], optional): The message to be sent to the client. If `None`
|
||||
then the HTTP status 'Bad Request' will be sent. Defaults to `None`.
|
||||
scheme (Optional[str], optional): Name of the authentication scheme to be used. Defaults to `None`.
|
||||
quiet (Optional[bool], optional): When `True`, the error traceback will be suppressed
|
||||
from the logs. Defaults to `None`.
|
||||
context (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will be
|
||||
sent to the client upon exception. Defaults to `None`.
|
||||
extra (Optional[Dict[str, Any]], optional): Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode. Defaults to `None`.
|
||||
headers (Optional[Dict[str, Any]], optional): Additional headers that should be sent with the HTTP
|
||||
response. Defaults to `None`.
|
||||
**challenges (Dict[str, Any]): Additional keyword arguments that will be used to complete the
|
||||
WWW-Authentication header. Defaults to `None`.
|
||||
|
||||
# With a Basic auth-scheme, realm MUST be present:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Basic",
|
||||
realm="Restricted Area")
|
||||
Examples:
|
||||
With a Basic auth-scheme, realm MUST be present:
|
||||
```python
|
||||
raise Unauthorized(
|
||||
"Auth required.",
|
||||
scheme="Basic",
|
||||
realm="Restricted Area"
|
||||
)
|
||||
```
|
||||
|
||||
# With a Digest auth-scheme, things are a bit more complicated:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Digest",
|
||||
realm="Restricted Area",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
With a Digest auth-scheme, things are a bit more complicated:
|
||||
```python
|
||||
raise Unauthorized(
|
||||
"Auth required.",
|
||||
scheme="Digest",
|
||||
realm="Restricted Area",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu"
|
||||
)
|
||||
```
|
||||
|
||||
# With a Bearer auth-scheme, realm is optional so you can write:
|
||||
With a Bearer auth-scheme, realm is optional so you can write:
|
||||
```python
|
||||
raise Unauthorized("Auth required.", scheme="Bearer")
|
||||
```
|
||||
|
||||
# or, if you want to specify the realm:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
|
||||
:param message: The message to be sent to the client. If ``None``
|
||||
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||
:type message: Optional[Union[str, bytes]], optional
|
||||
:param scheme: Name of the authentication scheme to be used.
|
||||
:type scheme: Optional[str], optional
|
||||
:param quiet: When ``True``, the error traceback will be suppressed
|
||||
from the logs, defaults to None
|
||||
:type quiet: Optional[bool], optional
|
||||
:param context: Additional mapping of key/value data that will be
|
||||
sent to the client upon exception, defaults to None
|
||||
:type context: Optional[Dict[str, Any]], optional
|
||||
:param extra: Additional mapping of key/value data that will NOT be
|
||||
sent to the client when in PRODUCTION mode, defaults to None
|
||||
:type extra: Optional[Dict[str, Any]], optional
|
||||
:param headers: Additional headers that should be sent with the HTTP
|
||||
response, defaults to None
|
||||
:type headers: Optional[Dict[str, Any]], optional
|
||||
"""
|
||||
or, if you want to specify the realm:
|
||||
```python
|
||||
raise Unauthorized(
|
||||
"Auth required.",
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area"
|
||||
)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
status_code = 401
|
||||
quiet = True
|
||||
@@ -654,13 +633,15 @@ class Unauthorized(HTTPException):
|
||||
|
||||
|
||||
class LoadFileException(SanicException):
|
||||
pass
|
||||
"""Exception raised when a file cannot be loaded."""
|
||||
|
||||
|
||||
class InvalidSignal(SanicException):
|
||||
pass
|
||||
"""Exception raised when an invalid signal is sent."""
|
||||
|
||||
|
||||
class WebsocketClosed(SanicException):
|
||||
"""Exception raised when a websocket is closed."""
|
||||
|
||||
quiet = True
|
||||
message = "Client has closed the websocket connection"
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sanic.exceptions import (
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
RangeNotSatisfiable,
|
||||
)
|
||||
from sanic.models.protocol_types import Range
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""
|
||||
A mechanism to parse and process the incoming request headers to
|
||||
extract the content range information.
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Request
|
||||
|
||||
:param request: Incoming api request
|
||||
:param stats: Stats related to the content
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type stats: :class:`posix.stat_result`
|
||||
class ContentRangeHandler(Range):
|
||||
"""Parse and process the incoming request headers to extract the content range information.
|
||||
|
||||
:ivar start: Content Range start
|
||||
:ivar end: Content Range end
|
||||
:ivar size: Length of the content
|
||||
:ivar total: Total size identified by the :class:`posix.stat_result`
|
||||
instance
|
||||
:ivar ContentRangeHandler.headers: Content range header ``dict``
|
||||
"""
|
||||
Args:
|
||||
request (Request): The incoming request object.
|
||||
stats (os.stat_result): The stats of the file being served.
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = ("start", "end", "size", "total", "headers")
|
||||
|
||||
def __init__(self, request, stats):
|
||||
def __init__(self, request: Request, stats: os.stat_result) -> None:
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.getone("range", None)
|
||||
if _range is None:
|
||||
|
||||
@@ -13,6 +13,16 @@ from sanic.response import file, html, redirect
|
||||
|
||||
|
||||
class DirectoryHandler:
|
||||
"""Serve files from a directory.
|
||||
|
||||
Args:
|
||||
uri (str): The URI to serve the files at.
|
||||
directory (Path): The directory to serve files from.
|
||||
directory_view (bool): Whether to show a directory listing or not.
|
||||
index (Optional[Union[str, Sequence[str]]]): The index file(s) to
|
||||
serve if the directory is requested. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uri: str,
|
||||
@@ -30,6 +40,19 @@ class DirectoryHandler:
|
||||
self.index = tuple(index)
|
||||
|
||||
async def handle(self, request: Request, path: str):
|
||||
"""Handle the request.
|
||||
|
||||
Args:
|
||||
request (Request): The incoming request object.
|
||||
path (str): The path to the file to serve.
|
||||
|
||||
Raises:
|
||||
NotFound: If the file is not found.
|
||||
IsADirectoryError: If the path is a directory and directory_view is False.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
""" # noqa: E501
|
||||
current = path.strip("/")[len(self.base) :].strip("/") # noqa: E203
|
||||
for file_name in self.index:
|
||||
index_file = self.directory / current / file_name
|
||||
|
||||
@@ -12,16 +12,16 @@ from sanic.response.types import HTTPResponse
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
"""
|
||||
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
|
||||
and process any and all uncaught exceptions in a way the application
|
||||
developer will set fit.
|
||||
"""Process and handle all uncaught exceptions.
|
||||
|
||||
This error handling framework is built into the core that can be extended
|
||||
by the developers to perform a wide range of tasks from recording the error
|
||||
stats to reporting them to an external service that can be used for
|
||||
realtime alerting system.
|
||||
"""
|
||||
|
||||
Args:
|
||||
base (BaseRenderer): The renderer to use for the error pages.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -54,18 +54,18 @@ class ErrorHandler:
|
||||
self.cached_handlers[key] = handler
|
||||
|
||||
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
||||
"""
|
||||
Add a new exception handler to an already existing handler object.
|
||||
"""Add a new exception handler to an already existing handler object.
|
||||
|
||||
:param exception: Type of exception that need to be handled
|
||||
:param handler: Reference to the method that will handle the exception
|
||||
Args:
|
||||
exception (sanic.exceptions.SanicException or Exception): Type
|
||||
of exception that needs to be handled.
|
||||
handler (function): Reference to the function that will
|
||||
handle the exception.
|
||||
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
:type handler: ``function``
|
||||
Returns:
|
||||
None
|
||||
|
||||
:return: None
|
||||
"""
|
||||
""" # noqa: E501
|
||||
if route_names:
|
||||
for route in route_names:
|
||||
self._add((exception, route), handler)
|
||||
@@ -73,19 +73,18 @@ class ErrorHandler:
|
||||
self._add((exception, None), handler)
|
||||
|
||||
def lookup(self, exception, route_name: Optional[str] = None):
|
||||
"""
|
||||
Lookup the existing instance of :class:`ErrorHandler` and fetch the
|
||||
registered handler for a specific type of exception.
|
||||
"""Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception.
|
||||
|
||||
This method leverages a dict lookup to speedup the retrieval process.
|
||||
|
||||
:param exception: Type of exception
|
||||
Args:
|
||||
exception (sanic.exceptions.SanicException or Exception): Type
|
||||
of exception.
|
||||
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
Returns:
|
||||
Registered function if found, ``None`` otherwise.
|
||||
|
||||
:return: Registered function if found ``None`` otherwise
|
||||
"""
|
||||
""" # noqa: E501
|
||||
exception_class = type(exception)
|
||||
|
||||
for name in (route_name, None):
|
||||
@@ -113,19 +112,16 @@ class ErrorHandler:
|
||||
_lookup = _full_lookup
|
||||
|
||||
def response(self, request, exception):
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
"""Fetch and executes an exception handler and returns a response object.
|
||||
|
||||
:param request: Instance of :class:`sanic.request.Request`
|
||||
:param exception: Exception to handle
|
||||
Args:
|
||||
request (sanic.request.Request): Instance of the request.
|
||||
exception (sanic.exceptions.SanicException or Exception): Exception to handle.
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
Returns:
|
||||
Wrap the return value obtained from the `default` function or the registered handler for that type of exception.
|
||||
|
||||
:return: Wrap the return value obtained from :func:`default`
|
||||
or registered handler for that type of exception.
|
||||
"""
|
||||
""" # noqa: E501
|
||||
route_name = request.name if request else None
|
||||
handler = self._lookup(exception, route_name)
|
||||
response = None
|
||||
@@ -151,20 +147,30 @@ class ErrorHandler:
|
||||
return response
|
||||
|
||||
def default(self, request: Request, exception: Exception) -> HTTPResponse:
|
||||
"""
|
||||
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
||||
If a developer chooses to extent the :class:`ErrorHandler` they can
|
||||
"""Provide a default behavior for the objects of ErrorHandler.
|
||||
|
||||
If a developer chooses to extend the ErrorHandler, they can
|
||||
provide a custom implementation for this method to behave in a way
|
||||
they see fit.
|
||||
|
||||
:param request: Incoming request
|
||||
:param exception: Exception object
|
||||
Args:
|
||||
request (sanic.request.Request): Incoming request.
|
||||
exception (sanic.exceptions.SanicException or Exception): Exception object.
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
:return:
|
||||
"""
|
||||
Returns:
|
||||
HTTPResponse: The response object.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
class CustomErrorHandler(ErrorHandler):
|
||||
def default(self, request: Request, exception: Exception) -> HTTPResponse:
|
||||
# Custom logic for handling the exception and creating a response
|
||||
custom_response = my_custom_logic(request, exception)
|
||||
return custom_response
|
||||
|
||||
app = Sanic("MyApp", error_handler=CustomErrorHandler())
|
||||
```
|
||||
""" # noqa: E501
|
||||
self.log(request, exception)
|
||||
fallback = request.app.config.FALLBACK_ERROR_FORMAT
|
||||
return exception_response(
|
||||
@@ -176,7 +182,16 @@ class ErrorHandler:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log(request, exception):
|
||||
def log(request: Request, exception: Exception) -> None:
|
||||
"""Logs information about an incoming request and the associated exception.
|
||||
|
||||
Args:
|
||||
request (Request): The incoming request to be logged.
|
||||
exception (Exception): The exception that occurred during the handling of the request.
|
||||
|
||||
Returns:
|
||||
None
|
||||
""" # noqa: E501
|
||||
quiet = getattr(exception, "quiet", False)
|
||||
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
|
||||
if quiet is False or noisy is True:
|
||||
|
||||
144
sanic/headers.py
144
sanic/headers.py
@@ -33,7 +33,21 @@ _host_re = re.compile(
|
||||
|
||||
|
||||
class MediaType:
|
||||
"""A media type, as used in the Accept header."""
|
||||
"""A media type, as used in the Accept header.
|
||||
|
||||
This class is a representation of a media type, as used in the Accept
|
||||
header. It encapsulates the type, subtype and any parameters, and
|
||||
provides methods for matching against other media types.
|
||||
|
||||
Two separate methods are provided for searching the list:
|
||||
- 'match' for finding the most preferred match (wildcards supported)
|
||||
- operator 'in' for checking explicit matches (wildcards as literals)
|
||||
|
||||
Args:
|
||||
type_ (str): The type of the media type.
|
||||
subtype (str): The subtype of the media type.
|
||||
**params (str): Any parameters for the media type.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -73,14 +87,24 @@ class MediaType:
|
||||
self,
|
||||
mime_with_params: Union[str, MediaType],
|
||||
) -> Optional[MediaType]:
|
||||
"""Check if this media type matches the given mime type/subtype.
|
||||
"""Match this media type against another media type.
|
||||
|
||||
Check if this media type matches the given mime type/subtype.
|
||||
Wildcards are supported both ways on both type and subtype.
|
||||
If mime contains a semicolon, optionally followed by parameters,
|
||||
the parameters of the two media types must match exactly.
|
||||
Note: Use the `==` operator instead to check for literal matches
|
||||
without expanding wildcards.
|
||||
@param media_type: A type/subtype string to match.
|
||||
@return `self` if the media types are compatible, else `None`
|
||||
|
||||
.. note::
|
||||
Use the `==` operator instead to check for literal matches
|
||||
without expanding wildcards.
|
||||
|
||||
|
||||
Args:
|
||||
media_type (str): A type/subtype string to match.
|
||||
|
||||
Returns:
|
||||
MediaType: Returns `self` if the media types are compatible.
|
||||
None: Returns `None` if the media types are not compatible.
|
||||
"""
|
||||
mt = (
|
||||
MediaType._parse(mime_with_params)
|
||||
@@ -109,7 +133,11 @@ class MediaType:
|
||||
|
||||
@property
|
||||
def has_wildcard(self) -> bool:
|
||||
"""Return True if this media type has a wildcard in it."""
|
||||
"""Return True if this media type has a wildcard in it.
|
||||
|
||||
Returns:
|
||||
bool: True if this media type has a wildcard in it.
|
||||
"""
|
||||
return any(part == "*" for part in (self.subtype, self.type))
|
||||
|
||||
@classmethod
|
||||
@@ -134,7 +162,16 @@ class MediaType:
|
||||
|
||||
|
||||
class Matched:
|
||||
"""A matching result of a MIME string against a header."""
|
||||
"""A matching result of a MIME string against a header.
|
||||
|
||||
This class is a representation of a matching result of a MIME string
|
||||
against a header. It encapsulates the MIME string, the header, and
|
||||
provides methods for matching against other MIME strings.
|
||||
|
||||
Args:
|
||||
mime (str): The MIME string to match.
|
||||
header (MediaType): The header to match against, if any.
|
||||
"""
|
||||
|
||||
def __init__(self, mime: str, header: Optional[MediaType]):
|
||||
self.mime = mime
|
||||
@@ -179,6 +216,17 @@ class Matched:
|
||||
)
|
||||
|
||||
def match(self, other: Union[str, Matched]) -> Optional[Matched]:
|
||||
"""Match this MIME string against another MIME string.
|
||||
|
||||
Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype.
|
||||
|
||||
Args:
|
||||
other (str): A MIME string to match.
|
||||
|
||||
Returns:
|
||||
Matched: Returns `self` if the MIME strings are compatible.
|
||||
None: Returns `None` if the MIME strings are not compatible.
|
||||
""" # noqa: E501
|
||||
accept = Matched.parse(other) if isinstance(other, str) else other
|
||||
if not self.header or not accept.header:
|
||||
return None
|
||||
@@ -202,6 +250,9 @@ class AcceptList(list):
|
||||
Two separate methods are provided for searching the list:
|
||||
- 'match' for finding the most preferred match (wildcards supported)
|
||||
- operator 'in' for checking explicit matches (wildcards as literals)
|
||||
|
||||
Args:
|
||||
*args (MediaType): Any number of MediaType objects.
|
||||
"""
|
||||
|
||||
def match(self, *mimes: str, accept_wildcards=True) -> Matched:
|
||||
@@ -224,10 +275,13 @@ class AcceptList(list):
|
||||
that matched, and is empty/falsy if no match was found. The matched
|
||||
header entry `MediaType` or `None` is available as the `m` attribute.
|
||||
|
||||
@param mimes: Any MIME types to search for in order of preference.
|
||||
@param accept_wildcards: Match Accept entries with wildcards in them.
|
||||
@return A match object with the mime string and the MediaType object.
|
||||
"""
|
||||
Args:
|
||||
mimes (List[str]): Any MIME types to search for in order of preference.
|
||||
accept_wildcards (bool): Match Accept entries with wildcards in them.
|
||||
|
||||
Returns:
|
||||
Match: A match object with the mime string and the MediaType object.
|
||||
""" # noqa: E501
|
||||
a = sorted(
|
||||
(-acc.q, i, j, mime, acc)
|
||||
for j, acc in enumerate(self)
|
||||
@@ -243,10 +297,19 @@ class AcceptList(list):
|
||||
|
||||
|
||||
def parse_accept(accept: Optional[str]) -> AcceptList:
|
||||
"""Parse an Accept header and order the acceptable media types in
|
||||
according to RFC 7231, s. 5.3.2
|
||||
"""Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
"""
|
||||
|
||||
Args:
|
||||
accept (str): The Accept header value to parse.
|
||||
|
||||
Returns:
|
||||
AcceptList: A list of MediaType objects, ordered by preference.
|
||||
|
||||
Raises:
|
||||
InvalidHeader: If the header value is invalid.
|
||||
""" # noqa: E501
|
||||
if not accept:
|
||||
if accept == "":
|
||||
return AcceptList() # Empty header, accept nothing
|
||||
@@ -274,6 +337,12 @@ def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
but runs faster and handles special characters better.
|
||||
|
||||
Unescapes %22 to `"` and %0D%0A to `\n` in field values.
|
||||
|
||||
Args:
|
||||
value (str): The header value to parse.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Options]: The header value and a dict of options.
|
||||
"""
|
||||
pos = value.find(";")
|
||||
if pos == -1:
|
||||
@@ -375,7 +444,14 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
|
||||
|
||||
def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||
"""Normalize and convert values extracted from forwarded headers."""
|
||||
"""Normalize and convert values extracted from forwarded headers.
|
||||
|
||||
Args:
|
||||
fwd (OptionsIterable): An iterable of key-value pairs.
|
||||
|
||||
Returns:
|
||||
Options: A dict of normalized key-value pairs.
|
||||
"""
|
||||
ret: Dict[str, Union[int, str]] = {}
|
||||
for key, val in fwd:
|
||||
if val is not None:
|
||||
@@ -396,7 +472,14 @@ def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||
|
||||
|
||||
def fwd_normalize_address(addr: str) -> str:
|
||||
"""Normalize address fields of proxy headers."""
|
||||
"""Normalize address fields of proxy headers.
|
||||
|
||||
Args:
|
||||
addr (str): An address string.
|
||||
|
||||
Returns:
|
||||
str: A normalized address string.
|
||||
"""
|
||||
if addr == "unknown":
|
||||
raise ValueError() # omit unknown value identifiers
|
||||
if addr.startswith("_"):
|
||||
@@ -408,7 +491,12 @@ def fwd_normalize_address(addr: str) -> str:
|
||||
|
||||
def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
"""Split host:port into hostname and port.
|
||||
:return: None in place of missing elements
|
||||
|
||||
Args:
|
||||
host (str): A host string.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[int]]: A tuple of hostname and port.
|
||||
"""
|
||||
m = _host_re.fullmatch(host)
|
||||
if not m:
|
||||
@@ -424,7 +512,15 @@ _HTTP1_STATUSLINES = [
|
||||
|
||||
|
||||
def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||
"""Format a HTTP/1.1 response header."""
|
||||
"""Format a HTTP/1.1 response header.
|
||||
|
||||
Args:
|
||||
status (int): The HTTP status code.
|
||||
headers (HeaderBytesIterable): An iterable of header tuples.
|
||||
|
||||
Returns:
|
||||
bytes: The formatted response header.
|
||||
"""
|
||||
# Note: benchmarks show that here bytes concat is faster than bytearray,
|
||||
# b"".join() or %-formatting. %timeit any changes you make.
|
||||
ret = _HTTP1_STATUSLINES[status]
|
||||
@@ -438,7 +534,15 @@ def parse_credentials(
|
||||
header: Optional[str],
|
||||
prefixes: Optional[Union[List, Tuple, Set]] = None,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Parses any header with the aim to retrieve any credentials from it."""
|
||||
"""Parses any header with the aim to retrieve any credentials from it.
|
||||
|
||||
Args:
|
||||
header (Optional[str]): The header to parse.
|
||||
prefixes (Optional[Union[List, Tuple, Set]], optional): The prefixes to look for. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[str]]: The prefix and the credentials.
|
||||
""" # noqa: E501
|
||||
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
|
||||
prefixes = ("Basic", "Bearer", "Token")
|
||||
if header is not None:
|
||||
|
||||
@@ -171,7 +171,11 @@ class Default:
|
||||
default value, and `object()` is hard to be typed.
|
||||
"""
|
||||
|
||||
pass
|
||||
def __repr__(self):
|
||||
return "<Default>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
_default = Default()
|
||||
|
||||
@@ -2,8 +2,7 @@ from enum import Enum, IntEnum
|
||||
|
||||
|
||||
class Stage(Enum):
|
||||
"""
|
||||
Enum for representing the stage of the request/response cycle
|
||||
"""Enum for representing the stage of the request/response cycle
|
||||
|
||||
| ``IDLE`` Waiting for request
|
||||
| ``REQUEST`` Request headers being received
|
||||
@@ -21,6 +20,8 @@ class Stage(Enum):
|
||||
|
||||
|
||||
class HTTP(IntEnum):
|
||||
"""Enum for representing HTTP versions"""
|
||||
|
||||
VERSION_1 = 1
|
||||
VERSION_3 = 3
|
||||
|
||||
|
||||
@@ -30,22 +30,16 @@ HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||
|
||||
|
||||
class Http(Stream, metaclass=TouchUpMeta):
|
||||
"""
|
||||
Internal helper for managing the HTTP/1.1 request/response cycle
|
||||
""" "Internal helper for managing the HTTP/1.1 request/response cycle.
|
||||
|
||||
:raises ServerError:
|
||||
:raises PayloadTooLarge:
|
||||
:raises Exception:
|
||||
:raises BadRequest:
|
||||
:raises ExpectationFailed:
|
||||
:raises RuntimeError:
|
||||
:raises ServerError:
|
||||
:raises ServerError:
|
||||
:raises BadRequest:
|
||||
:raises BadRequest:
|
||||
:raises BadRequest:
|
||||
:raises PayloadTooLarge:
|
||||
:raises RuntimeError:
|
||||
Raises:
|
||||
BadRequest: If the request body is malformed.
|
||||
Exception: If the request is malformed.
|
||||
ExpectationFailed: If the request is malformed.
|
||||
PayloadTooLarge: If the request body exceeds the size limit.
|
||||
RuntimeError: If the response status is invalid.
|
||||
ServerError: If the handler does not produce a response.
|
||||
ServerError: If the response is bigger than the content-length.
|
||||
"""
|
||||
|
||||
HEADER_CEILING = 16_384
|
||||
@@ -106,9 +100,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
return self.stage in (Stage.HANDLER, Stage.RESPONSE)
|
||||
|
||||
async def http1(self):
|
||||
"""
|
||||
HTTP 1.1 connection handler
|
||||
"""
|
||||
"""HTTP 1.1 connection handler"""
|
||||
# Handle requests while the connection stays reusable
|
||||
while self.keep_alive and self.stage is Stage.IDLE:
|
||||
self.init_for_request()
|
||||
@@ -178,9 +170,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
self.response.stream = None
|
||||
|
||||
async def http1_request_header(self): # no cov
|
||||
"""
|
||||
Receive and parse request header into self.request.
|
||||
"""
|
||||
"""Receive and parse request header into self.request."""
|
||||
# Receive until full header is in buffer
|
||||
buf = self.recv_buffer
|
||||
pos = 0
|
||||
@@ -291,6 +281,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
async def http1_response_header(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None: # no cov
|
||||
"""Format response header and send it."""
|
||||
res = self.response
|
||||
|
||||
# Compatibility with simple response body
|
||||
@@ -367,9 +358,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
|
||||
|
||||
def head_response_ignored(self, data: bytes, end_stream: bool) -> None:
|
||||
"""
|
||||
HEAD response: body data silently ignored.
|
||||
"""
|
||||
"""HEAD response: body data silently ignored."""
|
||||
if end_stream:
|
||||
self.response_func = None
|
||||
self.stage = Stage.IDLE
|
||||
@@ -377,9 +366,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
async def http1_response_chunked(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format a part of response body in chunked encoding.
|
||||
"""
|
||||
"""Format a part of response body in chunked encoding."""
|
||||
# Chunked encoding
|
||||
size = len(data)
|
||||
if end_stream:
|
||||
@@ -396,9 +383,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
async def http1_response_normal(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format / keep track of non-chunked response.
|
||||
"""
|
||||
"""Format / keep track of non-chunked response."""
|
||||
bytes_left = self.response_bytes_left - len(data)
|
||||
if bytes_left <= 0:
|
||||
if bytes_left < 0:
|
||||
@@ -415,9 +400,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
self.response_bytes_left = bytes_left
|
||||
|
||||
async def error_response(self, exception: Exception) -> None:
|
||||
"""
|
||||
Handle response when exception encountered
|
||||
"""
|
||||
"""Handle response when exception encountered"""
|
||||
# Disconnect after an error if in any other state than handler
|
||||
if self.stage is not Stage.HANDLER:
|
||||
self.keep_alive = False
|
||||
@@ -444,7 +427,8 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
await app.handle_exception(self.request, e, False)
|
||||
|
||||
def create_empty_request(self) -> None:
|
||||
"""
|
||||
"""Create an empty request object for error handling use.
|
||||
|
||||
Current error handling code needs a request object that won't exist
|
||||
if an error occurred during before a request was received. Create a
|
||||
bogus response for error handling use.
|
||||
@@ -471,10 +455,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
self.request.stream = self
|
||||
|
||||
def log_response(self) -> None:
|
||||
"""
|
||||
Helper method provided to enable the logging of responses in case if
|
||||
the :attr:`HttpProtocol.access_log` is enabled.
|
||||
"""
|
||||
"""Helper method provided to enable the logging of responses in case if the `HttpProtocol.access_log` is enabled.""" # noqa: E501
|
||||
req, res = self.request, self.response
|
||||
extra = {
|
||||
"status": getattr(res, "status", 0),
|
||||
@@ -493,9 +474,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
# Request methods
|
||||
|
||||
async def __aiter__(self):
|
||||
"""
|
||||
Async iterate over request body.
|
||||
"""
|
||||
"""Async iterate over request body."""
|
||||
while self.request_body:
|
||||
data = await self.read()
|
||||
|
||||
@@ -503,9 +482,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
yield data
|
||||
|
||||
async def read(self) -> Optional[bytes]: # no cov
|
||||
"""
|
||||
Read some bytes of request body.
|
||||
"""
|
||||
"""Read some bytes of request body."""
|
||||
|
||||
# Send a 100-continue if needed
|
||||
if self.expecting_continue:
|
||||
@@ -587,8 +564,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
# Response methods
|
||||
|
||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
|
||||
"""
|
||||
Initiate new streaming response.
|
||||
"""Initiate new streaming response.
|
||||
|
||||
Nothing is sent until the first send() call on the returned object, and
|
||||
calling this function multiple times will just alter the response to be
|
||||
|
||||
@@ -60,6 +60,8 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class HTTP3Transport(TransportProtocol):
|
||||
"""HTTP/3 transport implementation."""
|
||||
|
||||
__slots__ = ("_protocol",)
|
||||
|
||||
def __init__(self, protocol: Http3Protocol):
|
||||
@@ -82,6 +84,8 @@ class HTTP3Transport(TransportProtocol):
|
||||
|
||||
|
||||
class Receiver(ABC):
|
||||
"""HTTP/3 receiver base class."""
|
||||
|
||||
future: asyncio.Future
|
||||
|
||||
def __init__(self, transmit, protocol, request: Request) -> None:
|
||||
@@ -95,6 +99,8 @@ class Receiver(ABC):
|
||||
|
||||
|
||||
class HTTPReceiver(Receiver, Stream):
|
||||
"""HTTP/3 receiver implementation."""
|
||||
|
||||
stage: Stage
|
||||
request: Request
|
||||
|
||||
@@ -108,6 +114,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
self.request_bytes = 0
|
||||
|
||||
async def run(self, exception: Optional[Exception] = None):
|
||||
"""Handle the request and response cycle."""
|
||||
self.stage = Stage.HANDLER
|
||||
self.head_only = self.request.method.upper() == "HEAD"
|
||||
|
||||
@@ -133,9 +140,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
self.stage = Stage.IDLE
|
||||
|
||||
async def error_response(self, exception: Exception) -> None:
|
||||
"""
|
||||
Handle response when exception encountered
|
||||
"""
|
||||
"""Handle response when exception encountered"""
|
||||
# From request and handler states we can respond, otherwise be silent
|
||||
app = self.protocol.app
|
||||
|
||||
@@ -172,6 +177,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
return headers
|
||||
|
||||
def send_headers(self) -> None:
|
||||
"""Send response headers to client"""
|
||||
logger.debug( # no cov
|
||||
f"{Colors.BLUE}[send]: {Colors.GREEN}HEADERS{Colors.END}",
|
||||
extra={"verbosity": 2},
|
||||
@@ -195,6 +201,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
self.future.cancel()
|
||||
|
||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
|
||||
"""Prepare response to client"""
|
||||
logger.debug( # no cov
|
||||
f"{Colors.BLUE}[respond]:{Colors.END} {response}",
|
||||
extra={"verbosity": 2},
|
||||
@@ -213,6 +220,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
return response
|
||||
|
||||
def receive_body(self, data: bytes) -> None:
|
||||
"""Receive request body from client"""
|
||||
self.request_bytes += len(data)
|
||||
if self.request_bytes > self.request_max_size:
|
||||
raise PayloadTooLarge("Request body exceeds the size limit")
|
||||
@@ -220,6 +228,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
self.request.body += data
|
||||
|
||||
async def send(self, data: bytes, end_stream: bool) -> None:
|
||||
"""Send data to client"""
|
||||
logger.debug( # no cov
|
||||
f"{Colors.BLUE}[send]: {Colors.GREEN}data={data.decode()} "
|
||||
f"end_stream={end_stream}{Colors.END}",
|
||||
@@ -264,19 +273,21 @@ class HTTPReceiver(Receiver, Stream):
|
||||
|
||||
|
||||
class WebsocketReceiver(Receiver): # noqa
|
||||
"""Websocket receiver implementation."""
|
||||
|
||||
async def run(self):
|
||||
...
|
||||
|
||||
|
||||
class WebTransportReceiver(Receiver): # noqa
|
||||
"""WebTransport receiver implementation."""
|
||||
|
||||
async def run(self):
|
||||
...
|
||||
|
||||
|
||||
class Http3:
|
||||
"""
|
||||
Internal helper for managing the HTTP/3 request/response cycle
|
||||
"""
|
||||
"""Internal helper for managing the HTTP/3 request/response cycle"""
|
||||
|
||||
if HTTP3_AVAILABLE:
|
||||
HANDLER_PROPERTY_MAPPING = {
|
||||
|
||||
@@ -14,7 +14,19 @@ class MiddlewareLocation(IntEnum):
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""Middleware object that is used to encapsulate middleware functions.
|
||||
|
||||
This should generally not be instantiated directly, but rather through
|
||||
the `sanic.Sanic.middleware` decorator and its variants.
|
||||
|
||||
Args:
|
||||
func (MiddlewareType): The middleware function to be called.
|
||||
location (MiddlewareLocation): The location of the middleware.
|
||||
priority (int): The priority of the middleware.
|
||||
"""
|
||||
|
||||
_counter = count()
|
||||
count: int
|
||||
|
||||
__slots__ = ("func", "priority", "location", "definition")
|
||||
|
||||
@@ -44,7 +56,14 @@ class Middleware:
|
||||
)
|
||||
|
||||
@property
|
||||
def order(self):
|
||||
def order(self) -> tuple[int, int]:
|
||||
"""Return a tuple of the priority and definition order.
|
||||
|
||||
This is used to sort the middleware.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: The priority and definition order.
|
||||
"""
|
||||
return (self.priority, -self.definition)
|
||||
|
||||
@classmethod
|
||||
@@ -53,6 +72,16 @@ class Middleware:
|
||||
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
|
||||
location: MiddlewareLocation,
|
||||
) -> Deque[Middleware]:
|
||||
"""Convert middleware collections to a deque of Middleware objects.
|
||||
|
||||
Args:
|
||||
*middleware_collections (Sequence[Union[Middleware, MiddlewareType]]):
|
||||
The middleware collections to convert.
|
||||
location (MiddlewareLocation): The location of the middleware.
|
||||
|
||||
Returns:
|
||||
Deque[Middleware]: The converted middleware.
|
||||
""" # noqa: E501
|
||||
return deque(
|
||||
[
|
||||
middleware
|
||||
@@ -64,6 +93,13 @@ class Middleware:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def reset_count(cls):
|
||||
def reset_count(cls) -> None:
|
||||
"""Reset the counter for the middleware definition order.
|
||||
|
||||
This is used for testing.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
cls._counter = count()
|
||||
cls.count = next(cls._counter)
|
||||
|
||||
@@ -4,6 +4,8 @@ from sanic.base.meta import SanicMeta
|
||||
|
||||
|
||||
class BaseMixin(metaclass=SanicMeta):
|
||||
"""Base class for some other mixins."""
|
||||
|
||||
name: str
|
||||
strict_slashes: Optional[bool]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Set
|
||||
from typing import Any, Callable, List, Set, Type, Union
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.models.futures import FutureException
|
||||
@@ -11,18 +11,58 @@ class ExceptionMixin(metaclass=SanicMeta):
|
||||
def _apply_exception_handler(self, handler: FutureException):
|
||||
raise NotImplementedError # noqa
|
||||
|
||||
def exception(self, *exceptions, apply=True):
|
||||
"""
|
||||
This method enables the process of creating a global exception
|
||||
handler for the current blueprint under question.
|
||||
def exception(
|
||||
self,
|
||||
*exceptions: Union[Type[Exception], List[Type[Exception]]],
|
||||
apply: bool = True
|
||||
) -> Callable:
|
||||
"""Decorator used to register an exception handler for the current application or blueprint instance.
|
||||
|
||||
:param args: List of Python exceptions to be caught by the handler
|
||||
:param kwargs: Additional optional arguments to be passed to the
|
||||
exception handler
|
||||
This method allows you to define a handler for specific exceptions that
|
||||
may be raised within the routes of this blueprint. You can specify one
|
||||
or more exception types to catch, and the handler will be applied to
|
||||
those exceptions.
|
||||
|
||||
:return a decorated method to handle global exceptions for any
|
||||
route registered under this blueprint.
|
||||
"""
|
||||
When used on a Blueprint, the handler will only be applied to routes
|
||||
registered under that blueprint. That means they only apply to
|
||||
requests that have been matched, and the exception is raised within
|
||||
the handler function (or middleware) for that route.
|
||||
|
||||
A general exception like `NotFound` should only be registered on the
|
||||
application instance, not on a blueprint.
|
||||
|
||||
See [Exceptions](/en/guide/best-practices/exceptions.html) for more information.
|
||||
|
||||
Args:
|
||||
exceptions (Union[Type[Exception], List[Type[Exception]]]): List of
|
||||
Python exceptions to be caught by the handler.
|
||||
apply (bool, optional): Whether the exception handler should be
|
||||
applied. Defaults to True.
|
||||
|
||||
Returns:
|
||||
Callable: A decorated method to handle global exceptions for any route
|
||||
registered under this blueprint.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from sanic import Blueprint, text
|
||||
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.exception(Exception)
|
||||
def handle_exception(request, exception):
|
||||
return text("Oops, something went wrong!", status=500)
|
||||
```
|
||||
|
||||
```python
|
||||
from sanic import Sanic, NotFound, text
|
||||
|
||||
app = Sanic('MyApp')
|
||||
|
||||
@app.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return text(f"Yep, I totally found the page: {request.url}")
|
||||
""" # noqa: E501
|
||||
|
||||
def decorator(handler):
|
||||
nonlocal apply
|
||||
@@ -39,14 +79,30 @@ class ExceptionMixin(metaclass=SanicMeta):
|
||||
|
||||
return decorator
|
||||
|
||||
def all_exceptions(self, handler):
|
||||
"""
|
||||
This method enables the process of creating a global exception
|
||||
handler for the current blueprint under question.
|
||||
def all_exceptions(
|
||||
self, handler: Callable[..., Any]
|
||||
) -> Callable[..., Any]:
|
||||
"""Enables the process of creating a global exception handler as a convenience.
|
||||
|
||||
:param handler: A coroutine function to handle exceptions
|
||||
This following two examples are equivalent:
|
||||
|
||||
:return a decorated method to handle global exceptions for any
|
||||
route registered under this blueprint.
|
||||
"""
|
||||
```python
|
||||
@app.exception(Exception)
|
||||
async def handler(request: Request, exception: Exception) -> HTTPResponse:
|
||||
return text(f"Exception raised: {exception}")
|
||||
```
|
||||
|
||||
```python
|
||||
@app.all_exceptions
|
||||
async def handler(request: Request, exception: Exception) -> HTTPResponse:
|
||||
return text(f"Exception raised: {exception}")
|
||||
```
|
||||
|
||||
Args:
|
||||
handler (Callable[..., Any]): A coroutine function to handle exceptions.
|
||||
|
||||
Returns:
|
||||
Callable[..., Any]: A decorated method to handle global exceptions for
|
||||
any route registered under this blueprint.
|
||||
""" # noqa: E501
|
||||
return self.exception(Exception)(handler)
|
||||
|
||||
@@ -59,26 +59,57 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
ListenerType[Sanic],
|
||||
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
|
||||
]:
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
"""Create a listener for a specific event in the application's lifecycle.
|
||||
|
||||
To be used as a decorator:
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
.. code-block:: python
|
||||
.. note::
|
||||
Overloaded signatures allow for different ways of calling this method, depending on the types of the arguments.
|
||||
|
||||
Usually, it is prederred to use one of the convenience methods such as `before_server_start` or `after_server_stop` instead of calling this method directly.
|
||||
|
||||
```python
|
||||
@app.before_server_start
|
||||
async def prefered_method(_):
|
||||
...
|
||||
|
||||
@app.listener("before_server_start")
|
||||
async def not_prefered_method(_):
|
||||
...
|
||||
|
||||
Args:
|
||||
listener_or_event (Union[ListenerType[Sanic], str]): A listener function or an event name.
|
||||
event_or_none (Optional[str]): The event name to listen for if `listener_or_event` is a function. Defaults to `None`.
|
||||
apply (bool): Whether to apply the listener immediately. Defaults to `True`.
|
||||
|
||||
Returns:
|
||||
Union[ListenerType[Sanic], Callable[[ListenerType[Sanic]], ListenerType[Sanic]]]: The listener or a callable that takes a listener.
|
||||
|
||||
Example:
|
||||
The following code snippet shows how you can use this method as a decorator:
|
||||
|
||||
```python
|
||||
@bp.listener("before_server_start")
|
||||
async def before_server_start(app, loop):
|
||||
...
|
||||
|
||||
`See user guide re: listeners
|
||||
<https://sanicframework.org/guide/basics/listeners.html#listeners>`__
|
||||
|
||||
:param event: event to listen to
|
||||
"""
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
def register_listener(
|
||||
listener: ListenerType[Sanic], event: str
|
||||
) -> ListenerType[Sanic]:
|
||||
"""A helper function to register a listener for an event.
|
||||
|
||||
Typically will not be called directly.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener function to
|
||||
register.
|
||||
event (str): The event name to listen for.
|
||||
|
||||
Returns:
|
||||
ListenerType[Sanic]: The listener function that was registered.
|
||||
"""
|
||||
nonlocal apply
|
||||
|
||||
future_listener = FutureListener(listener, event)
|
||||
@@ -99,54 +130,275 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
def main_process_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the main_process_start event.
|
||||
|
||||
This event is fired only on the main process and **NOT** on any
|
||||
worker processes. You should typically use this event to initialize
|
||||
resources that are shared across workers, or to initialize resources
|
||||
that are not safe to be initialized in a worker process.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.main_process_start
|
||||
async def on_main_process_start(app: Sanic):
|
||||
print("Main process started")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "main_process_start")
|
||||
|
||||
def main_process_ready(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the main_process_ready event.
|
||||
|
||||
This event is fired only on the main process and **NOT** on any
|
||||
worker processes. It is fired after the main process has started and
|
||||
the Worker Manager has been initialized (ie, you will have access to
|
||||
`app.manager` instance). The typical use case for this event is to
|
||||
add a managed process to the Worker Manager.
|
||||
|
||||
See [Running custom processes](/en/guide/deployment/manager.html#running-custom-processes) and [Listeners](/en/guide/basics/listeners.html) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.main_process_ready
|
||||
async def on_main_process_ready(app: Sanic):
|
||||
print("Main process ready")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "main_process_ready")
|
||||
|
||||
def main_process_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the main_process_stop event.
|
||||
|
||||
This event is fired only on the main process and **NOT** on any
|
||||
worker processes. You should typically use this event to clean up
|
||||
resources that were initialized in the main_process_start event.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.main_process_stop
|
||||
async def on_main_process_stop(app: Sanic):
|
||||
print("Main process stopped")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "main_process_stop")
|
||||
|
||||
def reload_process_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the reload_process_start event.
|
||||
|
||||
This event is fired only on the reload process and **NOT** on any
|
||||
worker processes. This is similar to the main_process_start event,
|
||||
except that it is fired only when the reload process is started.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.reload_process_start
|
||||
async def on_reload_process_start(app: Sanic):
|
||||
print("Reload process started")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "reload_process_start")
|
||||
|
||||
def reload_process_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the reload_process_stop event.
|
||||
|
||||
This event is fired only on the reload process and **NOT** on any
|
||||
worker processes. This is similar to the main_process_stop event,
|
||||
except that it is fired only when the reload process is stopped.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.reload_process_stop
|
||||
async def on_reload_process_stop(app: Sanic):
|
||||
print("Reload process stopped")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "reload_process_stop")
|
||||
|
||||
def before_reload_trigger(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the before_reload_trigger event.
|
||||
|
||||
This event is fired only on the reload process and **NOT** on any
|
||||
worker processes. This event is fired before the reload process
|
||||
triggers the reload. A change event has been detected and the reload
|
||||
process is about to be triggered.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.before_reload_trigger
|
||||
async def on_before_reload_trigger(app: Sanic):
|
||||
print("Before reload trigger")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "before_reload_trigger")
|
||||
|
||||
def after_reload_trigger(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the after_reload_trigger event.
|
||||
|
||||
This event is fired only on the reload process and **NOT** on any
|
||||
worker processes. This event is fired after the reload process
|
||||
triggers the reload. A change event has been detected and the reload
|
||||
process has been triggered.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.after_reload_trigger
|
||||
async def on_after_reload_trigger(app: Sanic, changed: set[str]):
|
||||
print("After reload trigger, changed files: ", changed)
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "after_reload_trigger")
|
||||
|
||||
def before_server_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the before_server_start event.
|
||||
|
||||
This event is fired on all worker processes. You should typically
|
||||
use this event to initialize resources that are global in nature, or
|
||||
will be shared across requests and various parts of the application.
|
||||
|
||||
A common use case for this event is to initialize a database connection
|
||||
pool, or to initialize a cache client.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.before_server_start
|
||||
async def on_before_server_start(app: Sanic):
|
||||
print("Before server start")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "before_server_start")
|
||||
|
||||
def after_server_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the after_server_start event.
|
||||
|
||||
This event is fired on all worker processes. You should typically
|
||||
use this event to run background tasks, or perform other actions that
|
||||
are not directly related to handling requests. In theory, it is
|
||||
possible that some requests may be handled before this event is fired,
|
||||
so you should not use this event to initialize resources that are
|
||||
required for handling requests.
|
||||
|
||||
A common use case for this event is to start a background task that
|
||||
periodically performs some action, such as clearing a cache or
|
||||
performing a health check.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.after_server_start
|
||||
async def on_after_server_start(app: Sanic):
|
||||
print("After server start")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "after_server_start")
|
||||
|
||||
def before_server_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the before_server_stop event.
|
||||
|
||||
This event is fired on all worker processes. This event is fired
|
||||
before the server starts shutting down. You should not use this event
|
||||
to perform any actions that are required for handling requests, as
|
||||
some requests may continue to be handled after this event is fired.
|
||||
|
||||
A common use case for this event is to stop a background task that
|
||||
was started in the after_server_start event.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.before_server_stop
|
||||
async def on_before_server_stop(app: Sanic):
|
||||
print("Before server stop")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "before_server_stop")
|
||||
|
||||
def after_server_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
"""Decorator for registering a listener for the after_server_stop event.
|
||||
|
||||
This event is fired on all worker processes. This event is fired
|
||||
after the server has stopped shutting down, and all requests have
|
||||
been handled. You should typically use this event to clean up
|
||||
resources that were initialized in the before_server_start event.
|
||||
|
||||
A common use case for this event is to close a database connection
|
||||
pool, or to close a cache client.
|
||||
|
||||
See [Listeners](/en/guide/basics/listeners) for more details.
|
||||
|
||||
Args:
|
||||
listener (ListenerType[Sanic]): The listener handler to attach.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.after_server_stop
|
||||
async def on_after_server_stop(app: Sanic):
|
||||
print("After server stop")
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.listener(listener, "after_server_stop")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
from typing import List
|
||||
from typing import Callable, List, Union, overload
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.middleware import Middleware, MiddlewareLocation
|
||||
from sanic.models.futures import FutureMiddleware
|
||||
from sanic.models.futures import FutureMiddleware, MiddlewareType
|
||||
from sanic.router import Router
|
||||
|
||||
|
||||
@@ -18,24 +18,68 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
def _apply_middleware(self, middleware: FutureMiddleware):
|
||||
raise NotImplementedError # noqa
|
||||
|
||||
@overload
|
||||
def middleware(
|
||||
self,
|
||||
middleware_or_request,
|
||||
attach_to="request",
|
||||
apply=True,
|
||||
middleware_or_request: MiddlewareType,
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority=0
|
||||
):
|
||||
"""
|
||||
Decorate and register middleware to be called before a request
|
||||
is handled or after a response is created. Can either be called as
|
||||
*@app.middleware* or *@app.middleware('request')*.
|
||||
priority: int = 0
|
||||
) -> MiddlewareType:
|
||||
...
|
||||
|
||||
`See user guide re: middleware
|
||||
<https://sanicframework.org/guide/basics/middleware.html>`__
|
||||
@overload
|
||||
def middleware(
|
||||
self,
|
||||
middleware_or_request: str,
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0
|
||||
) -> Callable[[MiddlewareType], MiddlewareType]:
|
||||
...
|
||||
|
||||
:param: middleware_or_request: Optional parameter to use for
|
||||
identifying which type of middleware is being registered.
|
||||
def middleware(
|
||||
self,
|
||||
middleware_or_request: Union[MiddlewareType, str],
|
||||
attach_to: str = "request",
|
||||
apply: bool = True,
|
||||
*,
|
||||
priority: int = 0
|
||||
) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]:
|
||||
"""Decorator for registering middleware.
|
||||
|
||||
Decorate and register middleware to be called before a request is
|
||||
handled or after a response is created. Can either be called as
|
||||
*@app.middleware* or *@app.middleware('request')*. Although, it is
|
||||
recommended to use *@app.on_request* or *@app.on_response* instead
|
||||
for clarity and convenience.
|
||||
|
||||
See [Middleware](/guide/basics/middleware) for more information.
|
||||
|
||||
Args:
|
||||
middleware_or_request (Union[Callable, str]): Middleware function
|
||||
or the keyword 'request' or 'response'.
|
||||
attach_to (str, optional): When to apply the middleware;
|
||||
either 'request' (before the request is handled) or 'response'
|
||||
(after the response is created). Defaults to `'request'`.
|
||||
apply (bool, optional): Whether the middleware should be applied.
|
||||
Defaults to `True`.
|
||||
priority (int, optional): The priority level of the middleware.
|
||||
Lower numbers are executed first. Defaults to `0`.
|
||||
|
||||
Returns:
|
||||
Union[Callable, Callable[[Callable], Callable]]: The decorated
|
||||
middleware function or a partial function depending on how
|
||||
the method was called.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@app.middleware('request')
|
||||
async def custom_middleware(request):
|
||||
...
|
||||
```
|
||||
"""
|
||||
|
||||
def register_middleware(middleware, attach_to="request"):
|
||||
@@ -63,17 +107,30 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
register_middleware, attach_to=middleware_or_request
|
||||
)
|
||||
|
||||
def on_request(self, middleware=None, *, priority=0):
|
||||
def on_request(self, middleware=None, *, priority=0) -> MiddlewareType:
|
||||
"""Register a middleware to be called before a request is handled.
|
||||
|
||||
This is the same as *@app.middleware('request')*.
|
||||
|
||||
:param: middleware: A callable that takes in request.
|
||||
Args:
|
||||
middleware (Callable, optional): A callable that takes in a
|
||||
request. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Callable: The decorated middleware function or a partial function
|
||||
depending on how the method was called.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.on_request
|
||||
async def custom_middleware(request):
|
||||
request.ctx.custom = 'value'
|
||||
```
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request", priority=priority)
|
||||
else:
|
||||
return partial(
|
||||
return partial( # type: ignore
|
||||
self.middleware, attach_to="request", priority=priority
|
||||
)
|
||||
|
||||
@@ -82,8 +139,20 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
|
||||
This is the same as *@app.middleware('response')*.
|
||||
|
||||
:param: middleware:
|
||||
A callable that takes in a request and its response.
|
||||
Args:
|
||||
middleware (Callable, optional): A callable that takes in a
|
||||
request and response. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Callable: The decorated middleware function or a partial function
|
||||
depending on how the method was called.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@app.on_response
|
||||
async def custom_middleware(request, response):
|
||||
response.headers['X-Server'] = 'Sanic'
|
||||
```
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response", priority=priority)
|
||||
@@ -92,16 +161,37 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
self.middleware, attach_to="response", priority=priority
|
||||
)
|
||||
|
||||
def finalize_middleware(self):
|
||||
def finalize_middleware(self) -> None:
|
||||
"""Finalize the middleware configuration for the Sanic application.
|
||||
|
||||
This method completes the middleware setup for the application.
|
||||
Middleware in Sanic is used to process requests globally before they
|
||||
reach individual routes or after routes have been processed.
|
||||
|
||||
Finalization consists of identifying defined routes and optimizing
|
||||
Sanic's performance to meet the application's specific needs. If
|
||||
you are manually adding routes, after Sanic has started, you will
|
||||
typically want to use the `amend` context manager rather than
|
||||
calling this method directly.
|
||||
|
||||
.. note::
|
||||
This method is usually called internally during the server setup
|
||||
process and does not typically need to be invoked manually.
|
||||
|
||||
Example:
|
||||
```python
|
||||
app.finalize_middleware()
|
||||
```
|
||||
"""
|
||||
for route in self.router.routes:
|
||||
request_middleware = Middleware.convert(
|
||||
self.request_middleware,
|
||||
self.named_request_middleware.get(route.name, deque()),
|
||||
self.request_middleware, # type: ignore
|
||||
self.named_request_middleware.get(route.name, deque()), # type: ignore # noqa: E501
|
||||
location=MiddlewareLocation.REQUEST,
|
||||
)
|
||||
response_middleware = Middleware.convert(
|
||||
self.response_middleware,
|
||||
self.named_response_middleware.get(route.name, deque()),
|
||||
self.response_middleware, # type: ignore
|
||||
self.named_response_middleware.get(route.name, deque()), # type: ignore # noqa: E501
|
||||
location=MiddlewareLocation.RESPONSE,
|
||||
)
|
||||
route.extra.request_middleware = deque(
|
||||
@@ -119,11 +209,11 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
)[::-1]
|
||||
)
|
||||
request_middleware = Middleware.convert(
|
||||
self.request_middleware,
|
||||
self.request_middleware, # type: ignore
|
||||
location=MiddlewareLocation.REQUEST,
|
||||
)
|
||||
response_middleware = Middleware.convert(
|
||||
self.response_middleware,
|
||||
self.response_middleware, # type: ignore
|
||||
location=MiddlewareLocation.RESPONSE,
|
||||
)
|
||||
self.request_middleware = deque(
|
||||
|
||||
@@ -58,32 +58,52 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Decorate a function to be registered as a route
|
||||
"""Decorate a function to be registered as a route.
|
||||
|
||||
Args:
|
||||
uri (str): Path of the URL.
|
||||
methods (Optional[Iterable[str]]): List or tuple of
|
||||
methods allowed.
|
||||
host (Optional[Union[str, List[str]]]): The host, if required.
|
||||
strict_slashes (Optional[bool]): Whether to apply strict slashes
|
||||
to the route.
|
||||
stream (bool): Whether to allow the request to stream its body.
|
||||
version (Optional[Union[int, str, float]]): Route specific
|
||||
versioning.
|
||||
name (Optional[str]): User-defined route name for url_for.
|
||||
ignore_body (bool): Whether the handler should ignore request
|
||||
body (e.g. `GET` requests).
|
||||
apply (bool): Apply middleware to the route.
|
||||
subprotocols (Optional[List[str]]): List of subprotocols.
|
||||
websocket (bool): Enable WebSocket support.
|
||||
unquote (bool): Unquote special characters in the URL path.
|
||||
static (bool): Enable static route.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value; default: `"/v"`.
|
||||
error_format (Optional[str]): Error format for the route.
|
||||
ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*`
|
||||
prefix will be appended to the route context (`route.ctx`).
|
||||
|
||||
**Example using context kwargs**
|
||||
Returns:
|
||||
RouteWrapper: Tuple of routes, decorated function.
|
||||
|
||||
.. code-block:: python
|
||||
Examples:
|
||||
Using the method to define a GET endpoint:
|
||||
|
||||
@app.route(..., ctx_foo="foobar")
|
||||
async def route_handler(request: Request):
|
||||
assert request.route.ctx.foo == "foobar"
|
||||
```python
|
||||
@app.route("/hello")
|
||||
async def hello(request: Request):
|
||||
return text("Hello, World!")
|
||||
```
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:param host: the host, if required
|
||||
:param strict_slashes: whether to apply strict slashes to the route
|
||||
:param stream: whether to allow the request to stream its body
|
||||
:param version: route specific versioning
|
||||
:param name: user defined route name for url_for
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests)
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: tuple of routes, decorated function
|
||||
Adding context kwargs to the route:
|
||||
|
||||
```python
|
||||
@app.route("/greet", ctx_name="World")
|
||||
async def greet(request: Request):
|
||||
name = request.route.ctx.name
|
||||
return text(f"Hello, {name}!")
|
||||
```
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
@@ -209,25 +229,56 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
unquote: bool = False,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
"""A helper method to register class-based view or functions as a handler to the application url routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:param stream: boolean specifying if the handler is a stream handler
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: function or class instance
|
||||
"""
|
||||
Args:
|
||||
handler (RouteHandler): Function or class-based view used as a route handler.
|
||||
uri (str): Path of the URL.
|
||||
methods (Iterable[str]): List or tuple of methods allowed; these are overridden if using an HTTPMethodView.
|
||||
host (Optional[Union[str, List[str]]]): Hostname or hostnames to match for this route.
|
||||
strict_slashes (Optional[bool]): If set, a route's slashes will be strict. E.g. `/foo` will not match `/foo/`.
|
||||
version (Optional[Union[int, str, float]]): Version of the API for this route.
|
||||
name (Optional[str]): User-defined route name for `url_for`.
|
||||
stream (bool): Boolean specifying if the handler is a stream handler.
|
||||
version_prefix (str): URL path that should be before the version value; default: ``/v``.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
unquote (bool): Boolean specifying if the handler requires unquoting.
|
||||
ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*` prefix will be appended to the route context (``route.ctx``). See below for examples.
|
||||
|
||||
Returns:
|
||||
RouteHandler: The route handler.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from sanic import Sanic, text
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
async def handler(request):
|
||||
return text("OK")
|
||||
|
||||
app.add_route(handler, "/test", methods=["GET", "POST"])
|
||||
```
|
||||
|
||||
You can use `ctx_kwargs` to add custom context to the route. This
|
||||
can often be useful when wanting to add metadata to a route that
|
||||
can be used by other parts of the application (like middleware).
|
||||
|
||||
```python
|
||||
from sanic import Sanic, text
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
async def handler(request):
|
||||
return text("OK")
|
||||
|
||||
async def custom_middleware(request):
|
||||
if request.route.ctx.monitor:
|
||||
do_some_monitoring()
|
||||
|
||||
app.add_route(handler, "/test", methods=["GET", "POST"], ctx_monitor=True)
|
||||
app.register_middleware(custom_middleware)
|
||||
""" # noqa: E501
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, "view_class"):
|
||||
methods = set()
|
||||
@@ -271,21 +322,31 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **GET** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **GET** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **GET** method of *HTTP*
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to GET method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
ignore_body (bool): Whether the handler should ignore request
|
||||
body. This means the body of the request, if sent, will not
|
||||
be consumed. In that instance, you will see a warning in
|
||||
the logs. Defaults to `True`, meaning do not consume the body.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_* prefix` will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -314,21 +375,29 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **POST** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **POST** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **POST** method of *HTTP*
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to POST method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
stream (bool): Whether or not to stream the request body.
|
||||
Defaults to `False`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_*` prefix will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -357,21 +426,29 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **PUT** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **PUT** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **PUT** method of *HTTP*
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to PUT method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
stream (bool): Whether or not to stream the request body.
|
||||
Defaults to `False`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_*` prefix will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -400,29 +477,31 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **HEAD** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **HEAD** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **HEAD** method of *HTTP*
|
||||
:type uri: str
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:type host: Optional[str], optional
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:type strict_slashes: Optional[bool], optional
|
||||
:param version: API Version
|
||||
:type version: Optional[str], optional
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:type name: Optional[str], optional
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to HEAD method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
ignore_body (bool): Whether the handler should ignore request
|
||||
body. This means the body of the request, if sent, will not
|
||||
be consumed. In that instance, you will see a warning in
|
||||
the logs. Defaults to `True`, meaning do not consume the body.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_*` prefix will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -451,29 +530,31 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **OPTIONS** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **OPTIONS** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
|
||||
:type uri: str
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:type host: Optional[str], optional
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:type strict_slashes: Optional[bool], optional
|
||||
:param version: API Version
|
||||
:type version: Optional[str], optional
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:type name: Optional[str], optional
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to OPTIONS method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
ignore_body (bool): Whether the handler should ignore request
|
||||
body. This means the body of the request, if sent, will not
|
||||
be consumed. In that instance, you will see a warning in
|
||||
the logs. Defaults to `True`, meaning do not consume the body.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_*` prefix will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -502,31 +583,29 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **PATCH** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **PATCH** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **PATCH** method of *HTTP*
|
||||
:type uri: str
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:type host: Optional[str], optional
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:type strict_slashes: Optional[bool], optional
|
||||
:param stream: whether to allow the request to stream its body
|
||||
:type stream: Optional[bool], optional
|
||||
:param version: API Version
|
||||
:type version: Optional[str], optional
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:type name: Optional[str], optional
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to PATCH method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for
|
||||
the service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a `/`.
|
||||
stream (bool): Set to `True` if full request streaming is needed,
|
||||
`False` otherwise. Defaults to `False`.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the route.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a
|
||||
`ctx_*` prefix will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -555,21 +634,28 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteHandler:
|
||||
"""
|
||||
Add an API URL under the **DELETE** *HTTP* method
|
||||
"""Decorate a function handler to create a route definition using the **DELETE** HTTP method.
|
||||
|
||||
:param uri: URL to be tagged to **DELETE** method of *HTTP*
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
Args:
|
||||
uri (str): URL to be tagged to the DELETE method of HTTP.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN for the
|
||||
service to use.
|
||||
strict_slashes (Optional[bool]): Instruct Sanic to check if the
|
||||
request URLs need to terminate with a */*.
|
||||
version (Optional[Union[int, str, float]]): API Version.
|
||||
name (Optional[str]): Unique name that can be used to identify
|
||||
the Route.
|
||||
ignore_body (bool): Whether or not to ignore the body in the
|
||||
request. Defaults to `False`.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*`
|
||||
prefix will be appended to the route context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
RouteHandler: Object decorated with route method.
|
||||
""" # noqa: E501
|
||||
return cast(
|
||||
RouteHandler,
|
||||
self.route(
|
||||
@@ -599,21 +685,30 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a websocket route
|
||||
"""Decorate a function to be registered as a websocket route.
|
||||
|
||||
:param uri: path of the URL
|
||||
:param host: Host IP or FQDN details
|
||||
:param strict_slashes: If the API endpoint needs to terminate
|
||||
with a "/" or not
|
||||
:param subprotocols: optional list of str with supported subprotocols
|
||||
:param name: A unique name assigned to the URL so that it can
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: tuple of routes, decorated function
|
||||
Args:
|
||||
uri (str): Path of the URL.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN details.
|
||||
strict_slashes (Optional[bool]): If the API endpoint needs to
|
||||
terminate with a `"/"` or not.
|
||||
subprotocols (Optional[List[str]]): Optional list of str with
|
||||
supported subprotocols.
|
||||
version (Optional[Union[int, str, float]]): WebSocket
|
||||
protocol version.
|
||||
name (Optional[str]): A unique name assigned to the URL so that
|
||||
it can be used with url_for.
|
||||
apply (bool): If set to False, it doesn't apply the route to the
|
||||
app. Default is `True`.
|
||||
version_prefix (str): URL path that should be before the version
|
||||
value. Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Custom error format string.
|
||||
**ctx_kwargs (Any): Keyword arguments that begin with
|
||||
a `ctx_* prefix` will be appended to the route
|
||||
context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
tuple: Tuple of routes, decorated function.
|
||||
"""
|
||||
return self.route(
|
||||
uri=uri,
|
||||
@@ -643,26 +738,27 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
):
|
||||
"""
|
||||
A helper method to register a function as a websocket route.
|
||||
"""A helper method to register a function as a websocket route.
|
||||
|
||||
:param handler: a callable function or instance of a class
|
||||
that can handle the websocket request
|
||||
:param host: Host IP or FQDN details
|
||||
:param uri: URL path that will be mapped to the websocket
|
||||
handler
|
||||
handler
|
||||
:param strict_slashes: If the API endpoint needs to terminate
|
||||
with a "/" or not
|
||||
:param subprotocols: Subprotocols to be used with websocket
|
||||
handshake
|
||||
:param name: A unique name assigned to the URL so that it can
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Objected decorated by :func:`websocket`
|
||||
Args:
|
||||
handler (Callable): A callable function or instance of a class
|
||||
that can handle the websocket request.
|
||||
uri (str): URL path that will be mapped to the websocket handler.
|
||||
host (Optional[Union[str, List[str]]]): Host IP or FQDN details.
|
||||
strict_slashes (Optional[bool]): If the API endpoint needs to
|
||||
terminate with a `"/"` or not.
|
||||
subprotocols (Optional[List[str]]): Subprotocols to be used with
|
||||
websocket handshake.
|
||||
version (Optional[Union[int, str, float]]): Versioning information.
|
||||
name (Optional[str]): A unique name assigned to the URL.
|
||||
version_prefix (str): URL path before the version value.
|
||||
Defaults to `"/v"`.
|
||||
error_format (Optional[str]): Format for error handling.
|
||||
**ctx_kwargs (Any): Keyword arguments beginning with `ctx_*`
|
||||
prefix will be appended to the route context (`route.ctx`).
|
||||
|
||||
Returns:
|
||||
Callable: Object passed as the handler.
|
||||
"""
|
||||
return self.websocket(
|
||||
uri=uri,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Set, Union
|
||||
from typing import Any, Callable, Coroutine, Dict, Optional, Set, Union
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.models.futures import FutureSignal
|
||||
@@ -66,7 +68,24 @@ class SignalMixin(metaclass=SanicMeta):
|
||||
event: str,
|
||||
condition: Optional[Dict[str, Any]] = None,
|
||||
exclusive: bool = True,
|
||||
):
|
||||
) -> Callable[..., Any]:
|
||||
"""Registers a signal handler for a specific event.
|
||||
|
||||
Args:
|
||||
handler (Optional[Callable[..., Any]]): The function to be called
|
||||
when the event occurs. Defaults to a noop if not provided.
|
||||
event (str): The name of the event to listen for.
|
||||
condition (Optional[Dict[str, Any]]): Optional condition to filter
|
||||
the event triggering. Defaults to `None`.
|
||||
exclusive (bool): Whether or not the handler is exclusive. When
|
||||
`True`, the signal can only be dispatched when the
|
||||
`condition` has been met. *This is inapplicable to blueprint
|
||||
signals, which are **ALWAYS** non-exclusive.* Defaults
|
||||
to `True`.
|
||||
|
||||
Returns:
|
||||
Callable[..., Any]: The handler that was registered.
|
||||
"""
|
||||
if not handler:
|
||||
|
||||
async def noop():
|
||||
@@ -81,8 +100,40 @@ class SignalMixin(metaclass=SanicMeta):
|
||||
def event(self, event: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def catch_exception(self, handler):
|
||||
def catch_exception(
|
||||
self,
|
||||
handler: Callable[[SignalMixin, Exception], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""Register an exception handler for logging or processing.
|
||||
|
||||
This method allows the registration of a custom exception handler to
|
||||
catch and process exceptions that occur in the application. Unlike a
|
||||
typical exception handler that might modify the response to the client,
|
||||
this is intended to capture exceptions for logging or other internal
|
||||
processing, such as sending them to an error reporting utility.
|
||||
|
||||
Args:
|
||||
handler (Callable): A coroutine function that takes the application
|
||||
instance and the exception as arguments. It will be called when
|
||||
an exception occurs within the application's lifecycle.
|
||||
|
||||
Example:
|
||||
```python
|
||||
app = Sanic("TestApp")
|
||||
|
||||
@app.catch_exception
|
||||
async def report_exception(app: Sanic, exception: Exception):
|
||||
logging.error(f"An exception occurred: {exception}")
|
||||
|
||||
# Send to an error reporting service
|
||||
await error_service.report(exception)
|
||||
|
||||
# Any unhandled exceptions within the application will now be
|
||||
# logged and reported to the error service.
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
async def signal_handler(exception: Exception):
|
||||
await handler(self, exception)
|
||||
|
||||
self.signal(Event.SERVER_LIFECYCLE_EXCEPTION)(signal_handler)
|
||||
self.signal(Event.SERVER_EXCEPTION_REPORT)(signal_handler)
|
||||
|
||||
@@ -54,7 +54,7 @@ from sanic.helpers import Default, _default, is_atty
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.http.tls import get_ssl_context, process_to_context
|
||||
from sanic.http.tls.context import SanicSSLContext
|
||||
from sanic.log import Colors, error_logger, logger
|
||||
from sanic.log import Colors, deprecation, error_logger, logger
|
||||
from sanic.models.handler_types import ListenerType
|
||||
from sanic.server import Signal as ServerSignal
|
||||
from sanic.server import try_use_uvloop
|
||||
@@ -90,6 +90,7 @@ else: # no cov
|
||||
class StartupMixin(metaclass=SanicMeta):
|
||||
_app_registry: ClassVar[Dict[str, Sanic]]
|
||||
|
||||
asgi: bool
|
||||
config: Config
|
||||
listeners: Dict[str, List[ListenerType[Any]]]
|
||||
state: ApplicationState
|
||||
@@ -100,7 +101,15 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
start_method: ClassVar[StartMethod] = _default
|
||||
START_METHOD_SET: ClassVar[bool] = False
|
||||
|
||||
def setup_loop(self):
|
||||
def setup_loop(self) -> None:
|
||||
"""Set up the event loop.
|
||||
|
||||
An internal method that sets up the event loop to uvloop if
|
||||
possible, or a Windows selector loop if on Windows.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not self.asgi:
|
||||
if self.config.USE_UVLOOP is True or (
|
||||
isinstance(self.config.USE_UVLOOP, Default)
|
||||
@@ -112,10 +121,39 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
|
||||
@property
|
||||
def m(self) -> WorkerMultiplexer:
|
||||
"""Interface for interacting with the worker processes"""
|
||||
"""Interface for interacting with the worker processes
|
||||
|
||||
This is a shortcut for `app.multiplexer`. It is available only in a
|
||||
worker process using the Sanic server. It allows you to interact with
|
||||
the worker processes, such as sending messages and commands.
|
||||
|
||||
See [Access to the multiplexer](/en/guide/deployment/manager#access-to-the-multiplexer) for more information.
|
||||
|
||||
Returns:
|
||||
WorkerMultiplexer: The worker multiplexer instance
|
||||
|
||||
Examples:
|
||||
```python
|
||||
app.m.restart() # restarts the worker
|
||||
app.m.terminate() # terminates the worker
|
||||
app.m.scale(4) # scales the number of workers to 4
|
||||
```
|
||||
""" # noqa: E501
|
||||
return self.multiplexer
|
||||
|
||||
def make_coffee(self, *args, **kwargs):
|
||||
"""
|
||||
Try for yourself! `sanic server:app --coffee`
|
||||
|
||||
```
|
||||
▄████████▄
|
||||
██ ██▀▀▄
|
||||
███████████ █
|
||||
███████████▄▄▀
|
||||
▀███████▀
|
||||
|
||||
```
|
||||
"""
|
||||
self.state.coffee = True
|
||||
self.run(*args, **kwargs)
|
||||
|
||||
@@ -146,42 +184,77 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
auto_tls: bool = False,
|
||||
single_process: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param auto_reload: Reload app whenever its source code is changed.
|
||||
Enabled by default in debug mode.
|
||||
:type auto_relaod: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl: str, dict, SSLContext or list
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param workers: Number of processes received before it is respected
|
||||
:type workers: int
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:type backlog: int
|
||||
:param register_sys_signals: Register SIG* events
|
||||
:type register_sys_signals: bool
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:param unix: Unix socket to listen on instead of TCP port
|
||||
:type unix: str
|
||||
:param noisy_exceptions: Log exceptions that are normally considered
|
||||
to be quiet/silent
|
||||
:type noisy_exceptions: bool
|
||||
:return: Nothing
|
||||
"""
|
||||
.. note::
|
||||
When you need control over running the Sanic instance, this is the method to use.
|
||||
However, in most cases the preferred method is to use the CLI command:
|
||||
|
||||
```sh
|
||||
sanic server:app`
|
||||
```
|
||||
|
||||
If you are using this method to run Sanic, make sure you do the following:
|
||||
|
||||
1. Use `if __name__ == "__main__"` to guard the code.
|
||||
2. Do **NOT** define the app instance inside the `if` block.
|
||||
|
||||
See [Dynamic Applications](/en/guide/deployment/app-loader) for more information about the second point.
|
||||
|
||||
Args:
|
||||
host (Optional[str]): Address to host on.
|
||||
port (Optional[int]): Port to host on.
|
||||
dev (bool): Run the server in development mode.
|
||||
debug (bool): Enables debug output (slows server).
|
||||
auto_reload (Optional[bool]): Reload app whenever its source code is changed.
|
||||
Enabled by default in debug mode.
|
||||
version (HTTPVersion): HTTP Version.
|
||||
ssl (Union[None, SSLContext, dict, str, list, tuple]): SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s).
|
||||
sock (Optional[socket]): Socket for the server to accept connections from.
|
||||
workers (int): Number of processes received before it is respected.
|
||||
protocol (Optional[Type[Protocol]]): Subclass of asyncio Protocol class.
|
||||
backlog (int): A number of unaccepted connections that the system will allow
|
||||
before refusing new connections.
|
||||
register_sys_signals (bool): Register SIG* events.
|
||||
access_log (Optional[bool]): Enables writing access logs (slows server).
|
||||
unix (Optional[str]): Unix socket to listen on instead of TCP port.
|
||||
loop (Optional[AbstractEventLoop]): AsyncIO event loop.
|
||||
reload_dir (Optional[Union[List[str], str]]): Directory to watch for code changes, if auto_reload is True.
|
||||
noisy_exceptions (Optional[bool]): Log exceptions that are normally considered to be quiet/silent.
|
||||
motd (bool): Display Message of the Day.
|
||||
fast (bool): Enable fast mode.
|
||||
verbosity (int): Verbosity level.
|
||||
motd_display (Optional[Dict[str, str]]): Customize Message of the Day display.
|
||||
auto_tls (bool): Enable automatic TLS certificate handling.
|
||||
single_process (bool): Enable single process mode.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised when attempting to serve HTTP/3 as a secondary server.
|
||||
RuntimeError: Raised when attempting to use both `fast` and `workers`.
|
||||
RuntimeError: Raised when attempting to use `single_process` with `fast`, `workers`, or `auto_reload`.
|
||||
TypeError: Raised when attempting to use `loop` with `create_server`.
|
||||
ValueError: Raised when `PROXIES_COUNT` is negative.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from sanic import Sanic, Request, json
|
||||
|
||||
app = Sanic("TestApp")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
return json({"foo": "bar"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(port=9999, dev=True)
|
||||
```
|
||||
""" # noqa: E501
|
||||
self.prepare(
|
||||
host=host,
|
||||
port=port,
|
||||
@@ -242,6 +315,53 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
auto_tls: bool = False,
|
||||
single_process: bool = False,
|
||||
) -> None:
|
||||
"""Prepares one or more Sanic applications to be served simultaneously.
|
||||
|
||||
This low-level API is typically used when you need to run multiple Sanic applications at the same time. Once prepared, `Sanic.serve()` should be called in the `if __name__ == "__main__"` block.
|
||||
|
||||
.. note::
|
||||
"Preparing" and "serving" with this function is equivalent to using `app.run` for a single instance. This should only be used when running multiple applications at the same time.
|
||||
|
||||
Args:
|
||||
host (Optional[str], optional): Hostname to listen on. Defaults to `None`.
|
||||
port (Optional[int], optional): Port to listen on. Defaults to `None`.
|
||||
dev (bool, optional): Development mode. Defaults to `False`.
|
||||
debug (bool, optional): Debug mode. Defaults to `False`.
|
||||
auto_reload (Optional[bool], optional): Auto reload feature. Defaults to `None`.
|
||||
version (HTTPVersion, optional): HTTP version to use. Defaults to `HTTP.VERSION_1`.
|
||||
ssl (Union[None, SSLContext, dict, str, list, tuple], optional): SSL configuration. Defaults to `None`.
|
||||
sock (Optional[socket], optional): Socket to bind to. Defaults to `None`.
|
||||
workers (int, optional): Number of worker processes. Defaults to `1`.
|
||||
protocol (Optional[Type[Protocol]], optional): Custom protocol class. Defaults to `None`.
|
||||
backlog (int, optional): Maximum number of pending connections. Defaults to `100`.
|
||||
register_sys_signals (bool, optional): Register system signals. Defaults to `True`.
|
||||
access_log (Optional[bool], optional): Access log. Defaults to `None`.
|
||||
unix (Optional[str], optional): Unix socket. Defaults to `None`.
|
||||
loop (Optional[AbstractEventLoop], optional): Event loop. Defaults to `None`.
|
||||
reload_dir (Optional[Union[List[str], str]], optional): Reload directory. Defaults to `None`.
|
||||
noisy_exceptions (Optional[bool], optional): Display exceptions. Defaults to `None`.
|
||||
motd (bool, optional): Display message of the day. Defaults to `True`.
|
||||
fast (bool, optional): Fast mode. Defaults to `False`.
|
||||
verbosity (int, optional): Verbosity level. Defaults to `0`.
|
||||
motd_display (Optional[Dict[str, str]], optional): Custom MOTD display. Defaults to `None`.
|
||||
coffee (bool, optional): Coffee mode. Defaults to `False`.
|
||||
auto_tls (bool, optional): Auto TLS. Defaults to `False`.
|
||||
single_process (bool, optional): Single process mode. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised when attempting to serve HTTP/3 as a secondary server.
|
||||
RuntimeError: Raised when attempting to use both `fast` and `workers`.
|
||||
RuntimeError: Raised when attempting to use `single_process` with `fast`, `workers`, or `auto_reload`.
|
||||
TypeError: Raised when attempting to use `loop` with `create_server`.
|
||||
ValueError: Raised when `PROXIES_COUNT` is negative.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.prepare()
|
||||
app.serve()
|
||||
```
|
||||
""" # noqa: E501
|
||||
if version == 3 and self.state.server_info:
|
||||
raise RuntimeError(
|
||||
"Serving HTTP/3 instances as a secondary server is "
|
||||
@@ -361,50 +481,77 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
backlog: int = 100,
|
||||
access_log: Optional[bool] = None,
|
||||
unix: Optional[str] = None,
|
||||
return_asyncio_server: bool = False,
|
||||
return_asyncio_server: bool = True,
|
||||
asyncio_server_kwargs: Optional[Dict[str, Any]] = None,
|
||||
noisy_exceptions: Optional[bool] = None,
|
||||
) -> Optional[AsyncioServer]:
|
||||
"""
|
||||
Asynchronous version of :func:`run`.
|
||||
Low level API for creating a Sanic Server instance.
|
||||
|
||||
This method will take care of the operations necessary to invoke
|
||||
the *before_start* events via :func:`trigger_events` method invocation
|
||||
before starting the *sanic* app in Async mode.
|
||||
This method will create a Sanic Server instance, but will not start
|
||||
it. This is useful for integrating Sanic into other systems. But, you
|
||||
should take caution when using it as it is a low level API and does
|
||||
not perform any of the lifecycle events.
|
||||
|
||||
.. note::
|
||||
This does not support multiprocessing and is not the preferred
|
||||
way to run a :class:`Sanic` application.
|
||||
way to run a Sanic application. Proceed with caution.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl: SSLContext or dict
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:type backlog: int
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:param return_asyncio_server: flag that defines whether there's a need
|
||||
to return asyncio.Server or
|
||||
start it serving right away
|
||||
:type return_asyncio_server: bool
|
||||
:param asyncio_server_kwargs: key-value arguments for
|
||||
asyncio/uvloop create_server method
|
||||
:type asyncio_server_kwargs: dict
|
||||
:param noisy_exceptions: Log exceptions that are normally considered
|
||||
to be quiet/silent
|
||||
:type noisy_exceptions: bool
|
||||
:return: AsyncioServer if return_asyncio_server is true, else Nothing
|
||||
You will need to start the server yourself as shown in the example
|
||||
below. You are responsible for the lifecycle of the server, including
|
||||
app startup using `await app.startup()`. No events will be triggered
|
||||
for you, so you will need to trigger them yourself if wanted.
|
||||
|
||||
Args:
|
||||
host (Optional[str]): Address to host on.
|
||||
port (Optional[int]): Port to host on.
|
||||
debug (bool): Enables debug output (slows server).
|
||||
ssl (Union[None, SSLContext, dict, str, list, tuple]): SSLContext,
|
||||
or location of certificate and key for SSL encryption
|
||||
of worker(s).
|
||||
sock (Optional[socket]): Socket for the server to accept
|
||||
connections from.
|
||||
protocol (Optional[Type[Protocol]]): Subclass of
|
||||
`asyncio.Protocol` class.
|
||||
backlog (int): Number of unaccepted connections that the system
|
||||
will allow before refusing new connections.
|
||||
access_log (Optional[bool]): Enables writing access logs
|
||||
(slows server).
|
||||
return_asyncio_server (bool): _DEPRECATED_
|
||||
asyncio_server_kwargs (Optional[Dict[str, Any]]): Key-value
|
||||
arguments for asyncio/uvloop `create_server` method.
|
||||
noisy_exceptions (Optional[bool]): Log exceptions that are normally
|
||||
considered to be quiet/silent.
|
||||
|
||||
Returns:
|
||||
Optional[AsyncioServer]: AsyncioServer if `return_asyncio_server`
|
||||
is `True` else `None`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import asyncio
|
||||
import uvloop
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
|
||||
async def main():
|
||||
server = await app.create_server()
|
||||
await server.startup()
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
asyncio.run(main())
|
||||
```
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
@@ -423,6 +570,14 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
if value is not None:
|
||||
setattr(self.config, attribute, value)
|
||||
|
||||
if not return_asyncio_server:
|
||||
return_asyncio_server = True
|
||||
deprecation(
|
||||
"The `return_asyncio_server` argument is deprecated and "
|
||||
"ignored. It will be removed in v24.3.",
|
||||
24.3,
|
||||
)
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host,
|
||||
port=port,
|
||||
@@ -456,9 +611,16 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||
)
|
||||
|
||||
def stop(self, terminate: bool = True, unregister: bool = False):
|
||||
"""
|
||||
This kills the Sanic
|
||||
def stop(self, terminate: bool = True, unregister: bool = False) -> None:
|
||||
"""This kills the Sanic server, cleaning up after itself.
|
||||
|
||||
Args:
|
||||
terminate (bool): Force kill all requests immediately without
|
||||
allowing them to finish processing.
|
||||
unregister (bool): Unregister the app from the global registry.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if terminate and hasattr(self, "multiplexer"):
|
||||
self.multiplexer.terminate()
|
||||
@@ -565,7 +727,19 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
def motd(
|
||||
self,
|
||||
server_settings: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
) -> None:
|
||||
"""Outputs the message of the day (MOTD).
|
||||
|
||||
It generally can only be called once per process, and is usually
|
||||
called by the `run` method in the main process.
|
||||
|
||||
Args:
|
||||
server_settings (Optional[Dict[str, Any]], optional): Settings for
|
||||
the server. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if (
|
||||
os.environ.get("SANIC_WORKER_NAME")
|
||||
or os.environ.get("SANIC_MOTD_OUTPUT")
|
||||
@@ -583,6 +757,17 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
def get_motd_data(
|
||||
self, server_settings: Optional[Dict[str, Any]] = None
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||
"""Retrieves the message of the day (MOTD) data.
|
||||
|
||||
Args:
|
||||
server_settings (Optional[Dict[str, Any]], optional): Settings for
|
||||
the server. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, Any], Dict[str, Any]]: A tuple containing two
|
||||
dictionaries with the relevant MOTD data.
|
||||
"""
|
||||
|
||||
mode = [f"{self.state.mode},"]
|
||||
if self.state.fast:
|
||||
mode.append("goin' fast")
|
||||
@@ -646,6 +831,11 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
|
||||
@property
|
||||
def serve_location(self) -> str:
|
||||
"""Retrieve the server location.
|
||||
|
||||
Returns:
|
||||
str: The server location.
|
||||
"""
|
||||
try:
|
||||
server_settings = self.state.server_info[0].settings
|
||||
return self.get_server_location(server_settings)
|
||||
@@ -657,6 +847,15 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
def get_server_location(
|
||||
server_settings: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""Using the server settings, retrieve the server location.
|
||||
|
||||
Args:
|
||||
server_settings (Optional[Dict[str, Any]], optional): Settings for
|
||||
the server. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
str: The server location.
|
||||
"""
|
||||
serve_location = ""
|
||||
proto = "http"
|
||||
if not server_settings:
|
||||
@@ -686,12 +885,29 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
version: HTTPVersion = HTTP.VERSION_1,
|
||||
auto_tls: bool = False,
|
||||
) -> Tuple[str, int]:
|
||||
"""Retrieve the host address and port, with default values based on the given parameters.
|
||||
|
||||
Args:
|
||||
host (Optional[str]): Host IP or FQDN for the service to use. Defaults to `"127.0.0.1"`.
|
||||
port (Optional[int]): Port number. Defaults to `8443` if version is 3 or `auto_tls=True`, else `8000`
|
||||
version (HTTPVersion, optional): HTTP Version. Defaults to `HTTP.VERSION_1` (HTTP/1.1).
|
||||
auto_tls (bool, optional): Automatic TLS flag. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Tuple[str, int]: Tuple containing the host and port
|
||||
""" # noqa: E501
|
||||
host = host or "127.0.0.1"
|
||||
port = port or (8443 if (version == 3 or auto_tls) else 8000)
|
||||
return host, port
|
||||
|
||||
@classmethod
|
||||
def should_auto_reload(cls) -> bool:
|
||||
"""Check if any applications have auto-reload enabled.
|
||||
|
||||
Returns:
|
||||
bool: `True` if any applications have auto-reload enabled, else
|
||||
`False`.
|
||||
"""
|
||||
return any(app.state.auto_reload for app in cls._app_registry.values())
|
||||
|
||||
@classmethod
|
||||
@@ -731,6 +947,42 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
app_loader: Optional[AppLoader] = None,
|
||||
factory: Optional[Callable[[], Sanic]] = None,
|
||||
) -> None:
|
||||
"""Serve one or more Sanic applications.
|
||||
|
||||
This is the main entry point for running Sanic applications. It
|
||||
should be called in the `if __name__ == "__main__"` block.
|
||||
|
||||
Args:
|
||||
primary (Optional[Sanic], optional): The primary Sanic application
|
||||
to serve. Defaults to `None`.
|
||||
app_loader (Optional[AppLoader], optional): An AppLoader instance
|
||||
to use for loading applications. Defaults to `None`.
|
||||
factory (Optional[Callable[[], Sanic]], optional): A factory
|
||||
function to use for loading applications. Defaults to `None`.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised when no applications are found.
|
||||
RuntimeError: Raised when no server information is found for the
|
||||
primary application.
|
||||
RuntimeError: Raised when attempting to use `loop` with
|
||||
`create_server`.
|
||||
RuntimeError: Raised when attempting to use `single_process` with
|
||||
`fast`, `workers`, or `auto_reload`.
|
||||
RuntimeError: Raised when attempting to serve HTTP/3 as a
|
||||
secondary server.
|
||||
RuntimeError: Raised when attempting to use both `fast` and
|
||||
`workers`.
|
||||
TypeError: Raised when attempting to use `loop` with
|
||||
`create_server`.
|
||||
ValueError: Raised when `PROXIES_COUNT` is negative.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.prepare()
|
||||
Sanic.serve()
|
||||
```
|
||||
"""
|
||||
cls._set_startup_method()
|
||||
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||
apps = list(cls._app_registry.values())
|
||||
@@ -913,6 +1165,39 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
|
||||
@classmethod
|
||||
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
|
||||
"""Serve a single process of a Sanic application.
|
||||
|
||||
Similar to `serve`, but only serves a single process. When used,
|
||||
certain features are disabled, such as `fast`, `workers`,
|
||||
`multiplexer`, `auto_reload`, and the Inspector. It is almost
|
||||
never needed to use this method directly. Instead, you should
|
||||
use the CLI:
|
||||
|
||||
```sh
|
||||
sanic app.sanic:app --single-process
|
||||
```
|
||||
|
||||
Or, if you need to do it programmatically, you should use the
|
||||
`single_process` argument of `run`:
|
||||
|
||||
```python
|
||||
app.run(single_process=True)
|
||||
```
|
||||
|
||||
Args:
|
||||
primary (Optional[Sanic], optional): The primary Sanic application
|
||||
to serve. Defaults to `None`.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised when no applications are found.
|
||||
RuntimeError: Raised when no server information is found for the
|
||||
primary application.
|
||||
RuntimeError: Raised when attempting to serve HTTP/3 as a
|
||||
secondary server.
|
||||
RuntimeError: Raised when attempting to use both `fast` and
|
||||
`workers`.
|
||||
ValueError: Raised when `PROXIES_COUNT` is negative.
|
||||
"""
|
||||
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||
apps = list(cls._app_registry.values())
|
||||
|
||||
|
||||
@@ -46,43 +46,71 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta):
|
||||
directory_view: bool = False,
|
||||
directory_handler: Optional[DirectoryHandler] = None,
|
||||
):
|
||||
"""
|
||||
Register a root to serve files from. The input can either be a
|
||||
file or a directory. This method will enable an easy and simple way
|
||||
to setup the :class:`Route` necessary to serve the static files.
|
||||
"""Register a root to serve files from. The input can either be a file or a directory.
|
||||
|
||||
:param uri: URL path to be used for serving static content
|
||||
:param file_or_directory: Path for the Static file/directory with
|
||||
static files
|
||||
:param pattern: Regex Pattern identifying the valid static files
|
||||
:param use_modified_since: If true, send file modified time, and return
|
||||
not modified if the browser's matches the server's
|
||||
:param use_content_range: If true, process header for range requests
|
||||
and sends the file part that is requested
|
||||
:param stream_large_files: If true, use the
|
||||
:func:`StreamingHTTPResponse.file_stream` handler rather
|
||||
than the :func:`HTTPResponse.file` handler to send the file.
|
||||
If this is an integer, this represents the threshold size to
|
||||
switch to :func:`StreamingHTTPResponse.file_stream`
|
||||
:param name: user defined name used for url_for
|
||||
:param host: Host IP or FQDN for the service to use
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param content_type: user defined content type for header
|
||||
:param apply: If true, will register the route immediately
|
||||
:param resource_type: Explicitly declare a resource to be a "
|
||||
file" or a "dir"
|
||||
:param index: When exposing against a directory, index is the name that
|
||||
will be served as the default file. When multiple files names are
|
||||
passed, then they will be tried in order.
|
||||
:param directory_view: Whether to fallback to showing the directory
|
||||
viewer when exposing a directory
|
||||
:param directory_handler: An instance of :class:`DirectoryHandler`
|
||||
that can be used for explicitly controlling and subclassing the
|
||||
behavior of the default directory handler
|
||||
:return: routes registered on the router
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
This method provides an easy and simple way to set up the route necessary to serve static files.
|
||||
|
||||
Args:
|
||||
uri (str): URL path to be used for serving static content.
|
||||
file_or_directory (Union[PathLike, str]): Path to the static file
|
||||
or directory with static files.
|
||||
pattern (str, optional): Regex pattern identifying the valid
|
||||
static files. Defaults to `r"/?.+"`.
|
||||
use_modified_since (bool, optional): If true, send file modified
|
||||
time, and return not modified if the browser's matches the
|
||||
server's. Defaults to `True`.
|
||||
use_content_range (bool, optional): If true, process header for
|
||||
range requests and sends the file part that is requested.
|
||||
Defaults to `False`.
|
||||
stream_large_files (Union[bool, int], optional): If `True`, use
|
||||
the `StreamingHTTPResponse.file_stream` handler rather than
|
||||
the `HTTPResponse.file handler` to send the file. If this
|
||||
is an integer, it represents the threshold size to switch
|
||||
to `StreamingHTTPResponse.file_stream`. Defaults to `False`,
|
||||
which means that the response will not be streamed.
|
||||
name (str, optional): User-defined name used for url_for.
|
||||
Defaults to `"static"`.
|
||||
host (Optional[str], optional): Host IP or FQDN for the
|
||||
service to use.
|
||||
strict_slashes (Optional[bool], optional): Instruct Sanic to
|
||||
check if the request URLs need to terminate with a slash.
|
||||
content_type (Optional[str], optional): User-defined content type
|
||||
for header.
|
||||
apply (bool, optional): If true, will register the route
|
||||
immediately. Defaults to `True`.
|
||||
resource_type (Optional[str], optional): Explicitly declare a
|
||||
resource to be a `"file"` or a `"dir"`.
|
||||
index (Optional[Union[str, Sequence[str]]], optional): When
|
||||
exposing against a directory, index is the name that will
|
||||
be served as the default file. When multiple file names are
|
||||
passed, then they will be tried in order.
|
||||
directory_view (bool, optional): Whether to fallback to showing
|
||||
the directory viewer when exposing a directory. Defaults
|
||||
to `False`.
|
||||
directory_handler (Optional[DirectoryHandler], optional): An
|
||||
instance of DirectoryHandler that can be used for explicitly
|
||||
controlling and subclassing the behavior of the default
|
||||
directory handler.
|
||||
|
||||
Returns:
|
||||
List[sanic.router.Route]: Routes registered on the router.
|
||||
|
||||
Examples:
|
||||
Serving a single file:
|
||||
```python
|
||||
app.static('/foo', 'path/to/static/file.txt')
|
||||
```
|
||||
|
||||
Serving all files from a directory:
|
||||
```python
|
||||
app.static('/static', 'path/to/static/directory')
|
||||
```
|
||||
|
||||
Serving large files with a specific threshold:
|
||||
```python
|
||||
app.static('/static', 'path/to/large/files', stream_large_files=1000000)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
name = self._generate_name(name)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class MockTransport(TransportProtocol): # no cov
|
||||
self._receive = receive
|
||||
self._send = send
|
||||
self._protocol = None
|
||||
self.loop = None
|
||||
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
def get_protocol(self) -> MockProtocol: # type: ignore
|
||||
if not self._protocol:
|
||||
|
||||
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import sys
|
||||
|
||||
from asyncio import BaseTransport
|
||||
from typing import TYPE_CHECKING, Any, AnyStr
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.models.asgi import ASGIScope
|
||||
|
||||
|
||||
@@ -25,19 +26,14 @@ else:
|
||||
...
|
||||
|
||||
class Range(Protocol):
|
||||
def start(self) -> int:
|
||||
...
|
||||
|
||||
def end(self) -> int:
|
||||
...
|
||||
|
||||
def size(self) -> int:
|
||||
...
|
||||
|
||||
def total(self) -> int:
|
||||
...
|
||||
start: Optional[int]
|
||||
end: Optional[int]
|
||||
size: Optional[int]
|
||||
total: Optional[int]
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class TransportProtocol(BaseTransport):
|
||||
scope: ASGIScope
|
||||
version: HTTP
|
||||
__slots__ = ()
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from ssl import SSLContext, SSLObject
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
|
||||
@@ -35,7 +35,7 @@ class ConnInfo:
|
||||
def __init__(self, transport: TransportProtocol, unix=None):
|
||||
self.ctx = SimpleNamespace()
|
||||
self.lost = False
|
||||
self.peername = None
|
||||
self.peername: Optional[Tuple[str, int]] = None
|
||||
self.server = self.client = ""
|
||||
self.server_port = self.client_port = 0
|
||||
self.client_ip = ""
|
||||
|
||||
@@ -8,6 +8,8 @@ from sanic.pages.css import CSS
|
||||
|
||||
|
||||
class BasePage(ABC, metaclass=CSS): # no cov
|
||||
"""Base page for Sanic pages."""
|
||||
|
||||
TITLE = "Sanic"
|
||||
HEADING = None
|
||||
CSS: str
|
||||
@@ -18,9 +20,19 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||
|
||||
@property
|
||||
def style(self) -> str:
|
||||
"""Returns the CSS for the page.
|
||||
|
||||
Returns:
|
||||
str: The CSS for the page.
|
||||
"""
|
||||
return self.CSS
|
||||
|
||||
def render(self) -> str:
|
||||
"""Renders the page.
|
||||
|
||||
Returns:
|
||||
str: The rendered page.
|
||||
"""
|
||||
self.doc = Document(self.TITLE, lang="en", id="sanic")
|
||||
self._head()
|
||||
self._body()
|
||||
|
||||
@@ -14,6 +14,8 @@ else:
|
||||
from typing import TypedDict
|
||||
|
||||
class FileInfo(TypedDict):
|
||||
"""Type for file info."""
|
||||
|
||||
icon: str
|
||||
file_name: str
|
||||
file_access: str
|
||||
@@ -21,6 +23,8 @@ else:
|
||||
|
||||
|
||||
class DirectoryPage(BasePage): # no cov
|
||||
"""Page for viewing a directory."""
|
||||
|
||||
TITLE = "Directory Viewer"
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -22,6 +22,8 @@ for the inconvenience and appreciate your patience.\
|
||||
|
||||
|
||||
class ErrorPage(BasePage):
|
||||
"""Page for displaying an error."""
|
||||
|
||||
STYLE_APPEND = tracerite.html.style
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -13,13 +13,15 @@ from .parameters import RequestParameters
|
||||
|
||||
|
||||
class File(NamedTuple):
|
||||
"""
|
||||
Model for defining a file. It is a ``namedtuple``, therefore you can
|
||||
iterate over the object, or access the parameters by name.
|
||||
"""Model for defining a file.
|
||||
|
||||
:param type: The mimetype, defaults to text/plain
|
||||
:param body: Bytes of the file
|
||||
:param name: The filename
|
||||
It is a `namedtuple`, therefore you can iterate over the object, or
|
||||
access the parameters by name.
|
||||
|
||||
Args:
|
||||
type (str, optional): The mimetype, defaults to "text/plain".
|
||||
body (bytes): Bytes of the file.
|
||||
name (str): The filename.
|
||||
"""
|
||||
|
||||
type: str
|
||||
@@ -28,13 +30,15 @@ class File(NamedTuple):
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parse a request body and returns fields and files
|
||||
"""Parse a request body and returns fields and files
|
||||
|
||||
:param body: bytes request body
|
||||
:param boundary: bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
"""
|
||||
Args:
|
||||
body (bytes): Bytes request body.
|
||||
boundary (bytes): Bytes multipart boundary.
|
||||
|
||||
Returns:
|
||||
Tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`.
|
||||
""" # noqa: E501
|
||||
files = {}
|
||||
fields = {}
|
||||
|
||||
|
||||
@@ -4,19 +4,30 @@ from typing import Any, Optional
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
"""Hosts a dict with lists as values where get returns the first value of the list and getlist returns the whole shebang""" # noqa: E501
|
||||
|
||||
def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]:
|
||||
"""Return the first value, either the default or actual"""
|
||||
"""Return the first value, either the default or actual
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter
|
||||
default (Optional[Any], optional): The default value. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The first value of the list
|
||||
""" # noqa: E501
|
||||
return super().get(name, [default])[0]
|
||||
|
||||
def getlist(
|
||||
self, name: str, default: Optional[Any] = None
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
Return the entire list
|
||||
"""
|
||||
"""Return the entire list
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter
|
||||
default (Optional[Any], optional): The default value. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The entire list
|
||||
""" # noqa: E501
|
||||
return super().get(name, default)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import BaseProtocol
|
||||
from contextvars import ContextVar
|
||||
from inspect import isawaitable
|
||||
from types import SimpleNamespace
|
||||
@@ -86,8 +87,17 @@ ctx_type = TypeVar("ctx_type")
|
||||
|
||||
|
||||
class Request(Generic[sanic_type, ctx_type]):
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""State of HTTP request.
|
||||
|
||||
Args:
|
||||
url_bytes (bytes): Raw URL bytes.
|
||||
headers (Header): Request headers.
|
||||
version (str): HTTP version.
|
||||
method (str): HTTP method.
|
||||
transport (TransportProtocol): Transport protocol.
|
||||
app (Sanic): Sanic instance.
|
||||
head (bytes, optional): Request head. Defaults to `b""`.
|
||||
stream_id (int, optional): HTTP/3 stream ID. Defaults to `0`.
|
||||
"""
|
||||
|
||||
_current: ContextVar[Request] = ContextVar("request")
|
||||
@@ -186,7 +196,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
self.route: Optional[Route] = None
|
||||
self.stream: Optional[Stream] = None
|
||||
self._match_info: Dict[str, Any] = {}
|
||||
self._protocol = None
|
||||
self._protocol: Optional[BaseProtocol] = None
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
@@ -194,53 +204,88 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@staticmethod
|
||||
def make_context() -> ctx_type:
|
||||
"""Create a new context object.
|
||||
|
||||
This method is called when a new request context is pushed. It is
|
||||
a great candidate for overriding in a subclass if you want to
|
||||
control the type of context object that is created.
|
||||
|
||||
By default, it returns a `types.SimpleNamespace` instance.
|
||||
|
||||
Returns:
|
||||
ctx_type: A new context object.
|
||||
"""
|
||||
return cast(ctx_type, SimpleNamespace())
|
||||
|
||||
@classmethod
|
||||
def get_current(cls) -> Request:
|
||||
"""
|
||||
Retrieve the current request object
|
||||
"""Retrieve the current request object
|
||||
|
||||
This implements `Context Variables
|
||||
<https://docs.python.org/3/library/contextvars.html>`_
|
||||
This implements [Context Variables](https://docs.python.org/3/library/contextvars.html)
|
||||
to allow for accessing the current request from anywhere.
|
||||
|
||||
Raises :exc:`sanic.exceptions.ServerError` if it is outside of
|
||||
a request lifecycle.
|
||||
A typical usecase is when you want to access the current request
|
||||
from a function that is not a handler, such as a logging function:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
import logging
|
||||
|
||||
from sanic import Request
|
||||
class LoggingFormater(logging.Formatter):
|
||||
def format(self, record):
|
||||
request = Request.get_current()
|
||||
record.url = request.url
|
||||
record.ip = request.ip
|
||||
return super().format(record)
|
||||
```
|
||||
|
||||
current_request = Request.get_current()
|
||||
Returns:
|
||||
Request: The current request object
|
||||
|
||||
:return: the current :class:`sanic.request.Request`
|
||||
"""
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If it is outside of a request
|
||||
lifecycle.
|
||||
""" # noqa: E501
|
||||
request = cls._current.get(None)
|
||||
if not request:
|
||||
raise ServerError("No current request")
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def generate_id(*_):
|
||||
def generate_id(*_) -> Union[uuid.UUID, str, int]:
|
||||
"""Generate a unique ID for the request.
|
||||
|
||||
This method is called to generate a unique ID for each request.
|
||||
By default, it returns a `uuid.UUID` instance.
|
||||
|
||||
Returns:
|
||||
Union[uuid.UUID, str, int]: A unique ID for the request.
|
||||
"""
|
||||
return uuid.uuid4()
|
||||
|
||||
@property
|
||||
def ctx(self) -> ctx_type:
|
||||
"""
|
||||
:return: The current request context
|
||||
"""The current request context.
|
||||
|
||||
This is a context object for the current request. It is created
|
||||
by `Request.make_context` and is a great place to store data
|
||||
that you want to be accessible during the request lifecycle.
|
||||
|
||||
Returns:
|
||||
ctx_type: The current request context.
|
||||
"""
|
||||
if not self._ctx:
|
||||
self._ctx = self.make_context()
|
||||
return self._ctx
|
||||
|
||||
@property
|
||||
def stream_id(self):
|
||||
"""
|
||||
Access the HTTP/3 stream ID.
|
||||
def stream_id(self) -> int:
|
||||
"""Access the HTTP/3 stream ID.
|
||||
|
||||
Raises :exc:`sanic.exceptions.ServerError` if it is not an
|
||||
HTTP/3 request.
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If the request is not HTTP/3.
|
||||
|
||||
Returns:
|
||||
int: The HTTP/3 stream ID.
|
||||
"""
|
||||
if self.protocol.version is not HTTP.VERSION_3:
|
||||
raise ServerError(
|
||||
@@ -248,7 +293,17 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
)
|
||||
return self._stream_id
|
||||
|
||||
def reset_response(self):
|
||||
def reset_response(self) -> None:
|
||||
"""Reset the response object.
|
||||
|
||||
This clears much of the state of the object. It should
|
||||
generally not be called directly, but is called automatically as
|
||||
part of the request lifecycle.
|
||||
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If the response has already been
|
||||
sent.
|
||||
"""
|
||||
try:
|
||||
if (
|
||||
self.stream is not None
|
||||
@@ -257,8 +312,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
raise ServerError(
|
||||
"Cannot reset response because previous response was sent."
|
||||
)
|
||||
self.stream.response.stream = None
|
||||
self.stream.response = None
|
||||
self.stream.response.stream = None # type: ignore
|
||||
self.stream.response = None # type: ignore
|
||||
self.responded = False
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -280,44 +335,44 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
**The first typical usecase** is if you wish to respond to the
|
||||
request without returning from the handler:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
data = ... # Process something
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
data = ... # Process something
|
||||
json_response = json({"data": data})
|
||||
await request.respond(json_response)
|
||||
|
||||
json_response = json({"data": data})
|
||||
await request.respond(json_response)
|
||||
|
||||
# You are now free to continue executing other code
|
||||
...
|
||||
|
||||
@app.on_response
|
||||
async def add_header(_, response: HTTPResponse):
|
||||
# Middlewares still get executed as expected
|
||||
response.headers["one"] = "two"
|
||||
@app.on_response
|
||||
async def add_header(_, response: HTTPResponse):
|
||||
# Middlewares still get executed as expected
|
||||
response.headers["one"] = "two"
|
||||
```
|
||||
|
||||
**The second possible usecase** is for when you want to directly
|
||||
respond to the request:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
response = await request.respond(content_type="text/csv")
|
||||
await response.send("foo,")
|
||||
await response.send("bar")
|
||||
|
||||
response = await request.respond(content_type="text/csv")
|
||||
await response.send("foo,")
|
||||
await response.send("bar")
|
||||
# You can control the completion of the response by calling
|
||||
# the 'eof()' method:
|
||||
await response.eof()
|
||||
```
|
||||
|
||||
# You can control the completion of the response by calling
|
||||
# the 'eof()' method:
|
||||
await response.eof()
|
||||
Args:
|
||||
response (ResponseType): Response instance to send.
|
||||
status (int): Status code to return in the response.
|
||||
headers (Optional[Dict[str, str]]): Headers to return in the response, defaults to None.
|
||||
content_type (Optional[str]): Content-Type header of the response, defaults to None.
|
||||
|
||||
:param response: response instance to send
|
||||
:param status: status code to return in the response
|
||||
:param headers: headers to return in the response
|
||||
:param content_type: Content-Type header of the response
|
||||
:return: final response being sent (may be different from the
|
||||
``response`` parameter because of middlewares) which can be
|
||||
used to manually send data
|
||||
"""
|
||||
Returns:
|
||||
FinalResponseType: Final response being sent (may be different from the
|
||||
"response" parameter because of middlewares), which can be
|
||||
used to manually send data.
|
||||
""" # noqa: E501
|
||||
try:
|
||||
if self.stream is not None and self.stream.response:
|
||||
raise ServerError("Second respond call is not allowed.")
|
||||
@@ -370,17 +425,16 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""
|
||||
The route name
|
||||
"""The route name
|
||||
|
||||
In the following pattern:
|
||||
|
||||
.. code-block::
|
||||
```
|
||||
<AppName>.[<BlueprintName>.]<HandlerName>
|
||||
```
|
||||
|
||||
<AppName>.[<BlueprintName>.]<HandlerName>
|
||||
|
||||
:return: Route name
|
||||
:rtype: Optional[str]
|
||||
Returns:
|
||||
Optional[str]: The route name
|
||||
"""
|
||||
if self._name:
|
||||
return self._name
|
||||
@@ -390,74 +444,85 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def endpoint(self) -> Optional[str]:
|
||||
"""
|
||||
:return: Alias of :attr:`sanic.request.Request.name`
|
||||
:rtype: Optional[str]
|
||||
"""Alias of `sanic.request.Request.name`
|
||||
|
||||
Returns:
|
||||
Optional[str]: The route name
|
||||
"""
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def uri_template(self) -> Optional[str]:
|
||||
"""
|
||||
:return: The defined URI template
|
||||
:rtype: Optional[str]
|
||||
"""The defined URI template
|
||||
|
||||
Returns:
|
||||
Optional[str]: The defined URI template
|
||||
"""
|
||||
if self.route:
|
||||
return f"/{self.route.path}"
|
||||
return None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
"""
|
||||
:return: The HTTP protocol instance
|
||||
def protocol(self) -> TransportProtocol:
|
||||
"""The HTTP protocol instance
|
||||
|
||||
Returns:
|
||||
Protocol: The HTTP protocol instance
|
||||
"""
|
||||
if not self._protocol:
|
||||
self._protocol = self.transport.get_protocol()
|
||||
return self._protocol
|
||||
return self._protocol # type: ignore
|
||||
|
||||
@property
|
||||
def raw_headers(self) -> bytes:
|
||||
"""
|
||||
:return: The unparsed HTTP headers
|
||||
:rtype: bytes
|
||||
"""The unparsed HTTP headers
|
||||
|
||||
Returns:
|
||||
bytes: The unparsed HTTP headers
|
||||
"""
|
||||
_, headers = self.head.split(b"\r\n", 1)
|
||||
return bytes(headers)
|
||||
|
||||
@property
|
||||
def request_line(self) -> bytes:
|
||||
"""
|
||||
:return: The first line of a HTTP request
|
||||
:rtype: bytes
|
||||
"""The first line of a HTTP request
|
||||
|
||||
Returns:
|
||||
bytes: The first line of a HTTP request
|
||||
"""
|
||||
reqline, _ = self.head.split(b"\r\n", 1)
|
||||
return bytes(reqline)
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[Union[uuid.UUID, str, int]]:
|
||||
"""
|
||||
A request ID passed from the client, or generated from the backend.
|
||||
"""A request ID passed from the client, or generated from the backend.
|
||||
|
||||
By default, this will look in a request header defined at:
|
||||
``self.app.config.REQUEST_ID_HEADER``. It defaults to
|
||||
``X-Request-ID``. Sanic will try to cast the ID into a ``UUID`` or an
|
||||
``int``. If there is not a UUID from the client, then Sanic will try
|
||||
to generate an ID by calling ``Request.generate_id()``. The default
|
||||
behavior is to generate a ``UUID``. You can customize this behavior
|
||||
by subclassing ``Request``.
|
||||
`self.app.config.REQUEST_ID_HEADER`. It defaults to
|
||||
`X-Request-ID`. Sanic will try to cast the ID into a `UUID` or an
|
||||
`int`.
|
||||
|
||||
.. code-block:: python
|
||||
If there is not a UUID from the client, then Sanic will try
|
||||
to generate an ID by calling `Request.generate_id()`. The default
|
||||
behavior is to generate a `UUID`. You can customize this behavior
|
||||
by subclassing `Request` and overwriting that method.
|
||||
|
||||
from sanic import Request, Sanic
|
||||
from itertools import count
|
||||
```python
|
||||
from sanic import Request, Sanic
|
||||
from itertools import count
|
||||
|
||||
class IntRequest(Request):
|
||||
counter = count()
|
||||
class IntRequest(Request):
|
||||
counter = count()
|
||||
|
||||
def generate_id(self):
|
||||
return next(self.counter)
|
||||
def generate_id(self):
|
||||
return next(self.counter)
|
||||
|
||||
app = Sanic("MyApp", request_class=IntRequest)
|
||||
app = Sanic("MyApp", request_class=IntRequest)
|
||||
```
|
||||
|
||||
Returns:
|
||||
Optional[Union[uuid.UUID, str, int]]: A request ID passed from the
|
||||
client, or generated from the backend.
|
||||
"""
|
||||
if not self._id:
|
||||
self._id = self.headers.getone(
|
||||
@@ -479,16 +544,28 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def json(self) -> Any:
|
||||
"""
|
||||
:return: The request body parsed as JSON
|
||||
:rtype: Any
|
||||
"""The request body parsed as JSON
|
||||
|
||||
Returns:
|
||||
Any: The request body parsed as JSON
|
||||
"""
|
||||
if self.parsed_json is None:
|
||||
self.load_json()
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
def load_json(self, loads=None):
|
||||
def load_json(self, loads=None) -> Any:
|
||||
"""Load the request body as JSON
|
||||
|
||||
Args:
|
||||
loads (Callable, optional): A custom JSON loader. Defaults to None.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request body cannot be parsed as JSON
|
||||
|
||||
Returns:
|
||||
Any: The request body parsed as JSON
|
||||
"""
|
||||
try:
|
||||
if not loads:
|
||||
loads = self.__class__._loads
|
||||
@@ -508,8 +585,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
A convenience handler for easier RFC-compliant matching of MIME types,
|
||||
parsed as a list that can match wildcards and includes */* by default.
|
||||
|
||||
:return: The ``Accept`` header parsed
|
||||
:rtype: AcceptList
|
||||
Returns:
|
||||
AcceptList: Accepted response content types
|
||||
"""
|
||||
if self.parsed_accept is None:
|
||||
self.parsed_accept = parse_accept(self.headers.get("accept"))
|
||||
@@ -519,7 +596,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def token(self) -> Optional[str]:
|
||||
"""Attempt to return the auth header token.
|
||||
|
||||
:return: token related to request
|
||||
Returns:
|
||||
Optional[str]: The auth header token
|
||||
"""
|
||||
if self.parsed_token is None:
|
||||
prefixes = ("Bearer", "Token")
|
||||
@@ -536,8 +614,9 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication
|
||||
schemas.
|
||||
|
||||
:return: A Credentials object with token, or username and password
|
||||
related to the request
|
||||
Returns:
|
||||
Optional[Credentials]: A Credentials object with token, or username
|
||||
and password related to the request
|
||||
"""
|
||||
if self.parsed_credentials is None:
|
||||
try:
|
||||
@@ -555,15 +634,14 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def get_form(
|
||||
self, keep_blank_values: bool = False
|
||||
) -> Optional[RequestParameters]:
|
||||
"""
|
||||
Method to extract and parse the form data from a request.
|
||||
"""Method to extract and parse the form data from a request.
|
||||
|
||||
:param keep_blank_values:
|
||||
Whether to discard blank values from the form data
|
||||
:type keep_blank_values: bool
|
||||
:return: the parsed form data
|
||||
:rtype: Optional[RequestParameters]
|
||||
"""
|
||||
Args:
|
||||
keep_blank_values (bool): Whether to discard blank values from the form data.
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The parsed form data.
|
||||
""" # noqa: E501
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.getone(
|
||||
@@ -592,9 +670,11 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
return self.parsed_form
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
"""
|
||||
:return: The request body parsed as form data
|
||||
def form(self) -> Optional[RequestParameters]:
|
||||
"""The request body parsed as form data
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The request body parsed as form data
|
||||
"""
|
||||
if self.parsed_form is None:
|
||||
self.get_form()
|
||||
@@ -602,10 +682,12 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
return self.parsed_form
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
"""
|
||||
:return: The request body parsed as uploaded files
|
||||
"""
|
||||
def files(self) -> Optional[RequestParameters]:
|
||||
"""The request body parsed as uploaded files
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The request body parsed as uploaded files
|
||||
""" # noqa: E501
|
||||
if self.parsed_files is None:
|
||||
self.form # compute form to get files
|
||||
|
||||
@@ -618,32 +700,30 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
encoding: str = "utf-8",
|
||||
errors: str = "replace",
|
||||
) -> RequestParameters:
|
||||
"""
|
||||
Method to parse ``query_string`` using ``urllib.parse.parse_qs``.
|
||||
This methods is used by ``args`` property.
|
||||
Can be used directly if you need to change default parameters.
|
||||
"""Parse `query_string` using `urllib.parse.parse_qs`.
|
||||
|
||||
:param keep_blank_values:
|
||||
flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
:type keep_blank_values: bool
|
||||
:param strict_parsing:
|
||||
flag indicating what to do with parsing errors.
|
||||
If false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
:type strict_parsing: bool
|
||||
:param encoding:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type encoding: str
|
||||
:param errors:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type errors: str
|
||||
:return: RequestParameters
|
||||
This methods is used by the `args` property, but it also
|
||||
can be used directly if you need to change default parameters.
|
||||
|
||||
Args:
|
||||
keep_blank_values (bool): Flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A `True` value indicates that blanks should be retained as
|
||||
blank strings. The default `False` value indicates that
|
||||
blank values are to be ignored and treated as if they were
|
||||
not included.
|
||||
strict_parsing (bool): Flag indicating what to do with parsing
|
||||
errors. If `False` (the default), errors are silently ignored.
|
||||
If `True`, errors raise a `ValueError` exception.
|
||||
encoding (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
errors (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
|
||||
Returns:
|
||||
RequestParameters: A dictionary containing the parsed arguments.
|
||||
"""
|
||||
if (
|
||||
keep_blank_values,
|
||||
@@ -669,9 +749,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
]
|
||||
|
||||
args = property(get_args)
|
||||
"""
|
||||
Convenience property to access :meth:`Request.get_args` with
|
||||
default values.
|
||||
"""Convenience property to access `Request.get_args` with default values.
|
||||
"""
|
||||
|
||||
def get_query_args(
|
||||
@@ -681,32 +759,31 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
encoding: str = "utf-8",
|
||||
errors: str = "replace",
|
||||
) -> list:
|
||||
"""
|
||||
Method to parse `query_string` using `urllib.parse.parse_qsl`.
|
||||
This methods is used by `query_args` property.
|
||||
Can be used directly if you need to change default parameters.
|
||||
"""Parse `query_string` using `urllib.parse.parse_qsl`.
|
||||
|
||||
:param keep_blank_values:
|
||||
flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
:type keep_blank_values: bool
|
||||
:param strict_parsing:
|
||||
flag indicating what to do with parsing errors.
|
||||
If false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
:type strict_parsing: bool
|
||||
:param encoding:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type encoding: str
|
||||
:param errors:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type errors: str
|
||||
:return: list
|
||||
This methods is used by `query_args` propertyn but can be used
|
||||
directly if you need to change default parameters.
|
||||
|
||||
Args:
|
||||
keep_blank_values (bool): Flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A `True` value indicates that blanks should be retained as
|
||||
blank strings. The default `False` value indicates that
|
||||
blank values are to be ignored and treated as if they were
|
||||
not included.
|
||||
strict_parsing (bool): Flag indicating what to do with
|
||||
parsing errors. If `False` (the default), errors are
|
||||
silently ignored. If `True`, errors raise a
|
||||
`ValueError` exception.
|
||||
encoding (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
errors (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples containing the parsed arguments.
|
||||
"""
|
||||
if (
|
||||
keep_blank_values,
|
||||
@@ -729,10 +806,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
]
|
||||
|
||||
query_args = property(get_query_args)
|
||||
"""
|
||||
Convenience property to access :meth:`Request.get_query_args` with
|
||||
default values.
|
||||
"""
|
||||
"""Convenience property to access `Request.get_query_args` with default values.
|
||||
""" # noqa: E501
|
||||
|
||||
def get_cookies(self) -> RequestParameters:
|
||||
cookie = self.headers.getone("cookie", "")
|
||||
@@ -741,9 +816,10 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def cookies(self) -> RequestParameters:
|
||||
"""
|
||||
:return: Incoming cookies on the request
|
||||
:rtype: Dict[str, str]
|
||||
"""Incoming cookies on the request
|
||||
|
||||
Returns:
|
||||
RequestParameters: Incoming cookies on the request
|
||||
"""
|
||||
|
||||
if self.parsed_cookies is None:
|
||||
@@ -752,16 +828,19 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def content_type(self) -> str:
|
||||
"""
|
||||
:return: Content-Type header form the request
|
||||
:rtype: str
|
||||
"""Content-Type header form the request
|
||||
|
||||
Returns:
|
||||
str: Content-Type header form the request
|
||||
"""
|
||||
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
"""
|
||||
:return: matched info after resolving route
|
||||
def match_info(self) -> Dict[str, Any]:
|
||||
"""Matched path parameters after resolving route
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Matched path parameters after resolving route
|
||||
"""
|
||||
return self._match_info
|
||||
|
||||
@@ -769,53 +848,64 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def match_info(self, value):
|
||||
self._match_info = value
|
||||
|
||||
# Transport properties (obtained from local interface only)
|
||||
|
||||
@property
|
||||
def ip(self) -> str:
|
||||
"""
|
||||
:return: peer ip of the socket
|
||||
:rtype: str
|
||||
"""Peer ip of the socket
|
||||
|
||||
Returns:
|
||||
str: Peer ip of the socket
|
||||
"""
|
||||
return self.conn_info.client_ip if self.conn_info else ""
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
"""
|
||||
:return: peer port of the socket
|
||||
:rtype: int
|
||||
"""Peer port of the socket
|
||||
|
||||
Returns:
|
||||
int: Peer port of the socket
|
||||
"""
|
||||
return self.conn_info.client_port if self.conn_info else 0
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
def socket(self) -> Union[Tuple[str, int], Tuple[None, None]]:
|
||||
"""Information about the connected socket if available
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[int]]: Information about the
|
||||
connected socket if available, in the form of a tuple of
|
||||
(ip, port)
|
||||
"""
|
||||
:return: Information about the connected socket if available
|
||||
"""
|
||||
return self.conn_info.peername if self.conn_info else (None, None)
|
||||
return (
|
||||
self.conn_info.peername
|
||||
if self.conn_info and self.conn_info.peername
|
||||
else (None, None)
|
||||
)
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
"""
|
||||
:return: path of the local HTTP request
|
||||
:rtype: str
|
||||
"""Path of the local HTTP request
|
||||
|
||||
Returns:
|
||||
str: Path of the local HTTP request
|
||||
"""
|
||||
return self._parsed_url.path.decode("utf-8")
|
||||
|
||||
@property
|
||||
def network_paths(self):
|
||||
"""
|
||||
Access the network paths if available
|
||||
def network_paths(self) -> Optional[List[Any]]:
|
||||
"""Access the network paths if available
|
||||
|
||||
Returns:
|
||||
Optional[List[Any]]: Access the network paths if available
|
||||
"""
|
||||
if self.conn_info is None:
|
||||
return None
|
||||
return self.conn_info.network_paths
|
||||
|
||||
# Proxy properties (using SERVER_NAME/forwarded/request/transport info)
|
||||
|
||||
@property
|
||||
def forwarded(self) -> Options:
|
||||
"""
|
||||
Active proxy information obtained from request headers, as specified in
|
||||
Sanic configuration.
|
||||
"""Active proxy information obtained from request headers, as specified in Sanic configuration.
|
||||
|
||||
Field names by, for, proto, host, port and path are normalized.
|
||||
- for and by IPv6 addresses are bracketed
|
||||
@@ -824,9 +914,9 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
Additional values may be available from new style Forwarded headers.
|
||||
|
||||
:return: forwarded address info
|
||||
:rtype: Dict[str, str]
|
||||
"""
|
||||
Returns:
|
||||
Options: proxy information from request headers
|
||||
""" # noqa: E501
|
||||
if self.parsed_forwarded is None:
|
||||
self.parsed_forwarded = (
|
||||
parse_forwarded(self.headers, self.app.config)
|
||||
@@ -837,11 +927,10 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def remote_addr(self) -> str:
|
||||
"""
|
||||
Client IP address, if available from proxy.
|
||||
"""Client IP address, if available from proxy.
|
||||
|
||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
"""
|
||||
if not hasattr(self, "_remote_addr"):
|
||||
self._remote_addr = str(self.forwarded.get("for", ""))
|
||||
@@ -858,21 +947,21 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
client address regardless of whether the service runs behind a proxy
|
||||
or not (proxy deployment needs separate configuration).
|
||||
|
||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
"""
|
||||
return self.remote_addr or self.ip
|
||||
|
||||
@property
|
||||
def scheme(self) -> str:
|
||||
"""
|
||||
Determine request scheme.
|
||||
"""Determine request scheme.
|
||||
|
||||
1. `config.SERVER_NAME` if in full URL format
|
||||
2. proxied proto/scheme
|
||||
3. local connection protocol
|
||||
|
||||
:return: http|https|ws|wss or arbitrary value given by the headers.
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: http|https|ws|wss or arbitrary value given by the headers.
|
||||
"""
|
||||
if not hasattr(self, "_scheme"):
|
||||
if "//" in self.app.config.get("SERVER_NAME", ""):
|
||||
@@ -896,16 +985,16 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def host(self) -> str:
|
||||
"""
|
||||
The currently effective server 'host' (hostname or hostname:port).
|
||||
"""The currently effective server 'host' (hostname or hostname:port).
|
||||
|
||||
1. `config.SERVER_NAME` overrides any client headers
|
||||
2. proxied host of original request
|
||||
3. request host header
|
||||
hostname and port may be separated by
|
||||
`sanic.headers.parse_host(request.host)`.
|
||||
|
||||
:return: the first matching host found, or empty string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: the first matching host found, or empty string
|
||||
"""
|
||||
server_name = self.app.config.get("SERVER_NAME")
|
||||
if server_name:
|
||||
@@ -916,39 +1005,40 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
"""
|
||||
:return: hostname the client connected to, by ``request.host``
|
||||
:rtype: str
|
||||
"""hostname the client connected to, by `request.host`
|
||||
|
||||
Returns:
|
||||
str: hostname the client connected to, by `request.host`
|
||||
"""
|
||||
return parse_host(self.host)[0] or ""
|
||||
|
||||
@property
|
||||
def server_port(self) -> int:
|
||||
"""
|
||||
The port the client connected to, by forwarded ``port`` or
|
||||
``request.host``.
|
||||
"""The port the client connected to, by forwarded `port` or `request.host`.
|
||||
|
||||
Default port is returned as 80 and 443 based on ``request.scheme``.
|
||||
Default port is returned as 80 and 443 based on `request.scheme`.
|
||||
|
||||
:return: port number
|
||||
:rtype: int
|
||||
"""
|
||||
Returns:
|
||||
int: The port the client connected to, by forwarded `port` or `request.host`.
|
||||
""" # noqa: E501
|
||||
port = self.forwarded.get("port") or parse_host(self.host)[1]
|
||||
return int(port or (80 if self.scheme in ("http", "ws") else 443))
|
||||
|
||||
@property
|
||||
def server_path(self) -> str:
|
||||
"""
|
||||
:return: full path of current URL; uses proxied or local path
|
||||
:rtype: str
|
||||
"""Full path of current URL; uses proxied or local path
|
||||
|
||||
Returns:
|
||||
str: Full path of current URL; uses proxied or local path
|
||||
"""
|
||||
return str(self.forwarded.get("path") or self.path)
|
||||
|
||||
@property
|
||||
def query_string(self) -> str:
|
||||
"""
|
||||
:return: representation of the requested query
|
||||
:rtype: str
|
||||
"""Representation of the requested query
|
||||
|
||||
Returns:
|
||||
str: Representation of the requested query
|
||||
"""
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode("utf-8")
|
||||
@@ -957,23 +1047,28 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
:return: the URL
|
||||
:rtype: str
|
||||
"""The URL
|
||||
|
||||
Returns:
|
||||
str: The URL
|
||||
"""
|
||||
return urlunparse(
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs) -> str:
|
||||
"""
|
||||
Same as :func:`sanic.Sanic.url_for`, but automatically determine
|
||||
`scheme` and `netloc` base on the request. Since this method is aiming
|
||||
"""Retrieve a URL for a given view name.
|
||||
|
||||
Same as `sanic.Sanic.url_for`, but automatically determine `scheme`
|
||||
and `netloc` base on the request. Since this method is aiming
|
||||
to generate correct schema & netloc, `_external` is implied.
|
||||
|
||||
:param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for`
|
||||
:return: an absolute url to the given view
|
||||
:rtype: str
|
||||
Args:
|
||||
view_name (str): The view name to generate URL for.
|
||||
**kwargs: Arbitrary keyword arguments to build URL query string.
|
||||
|
||||
Returns:
|
||||
str: The generated URL.
|
||||
"""
|
||||
# Full URL SERVER_NAME can only be handled in app.url_for
|
||||
try:
|
||||
@@ -999,10 +1094,13 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def scope(self) -> ASGIScope:
|
||||
"""
|
||||
:return: The ASGI scope of the request.
|
||||
If the app isn't an ASGI app, then raises an exception.
|
||||
:rtype: Optional[ASGIScope]
|
||||
"""The ASGI scope of the request.
|
||||
|
||||
Returns:
|
||||
ASGIScope: The ASGI scope of the request.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the app isn't an ASGI app.
|
||||
"""
|
||||
if not self.app.asgi:
|
||||
raise NotImplementedError(
|
||||
@@ -1014,27 +1112,33 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def is_safe(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is safe.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is safe.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is safe.
|
||||
"""
|
||||
return self.method in SAFE_HTTP_METHODS
|
||||
|
||||
@property
|
||||
def is_idempotent(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is iempotent.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is iempotent.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is iempotent.
|
||||
"""
|
||||
return self.method in IDEMPOTENT_HTTP_METHODS
|
||||
|
||||
@property
|
||||
def is_cacheable(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is cacheable.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is cacheable.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is cacheable.
|
||||
"""
|
||||
return self.method in CACHEABLE_HTTP_METHODS
|
||||
|
||||
@@ -21,11 +21,14 @@ from .types import HTTPResponse, JSONResponse, ResponseStream
|
||||
def empty(
|
||||
status: int = 204, headers: Optional[Dict[str, str]] = None
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Returns an empty response to the client.
|
||||
"""Returns an empty response to the client.
|
||||
|
||||
:param status Response code.
|
||||
:param headers Custom Headers.
|
||||
Args:
|
||||
status (int, optional): HTTP response code. Defaults to `204`.
|
||||
headers ([type], optional): Custom HTTP headers. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: An empty response to the client.
|
||||
"""
|
||||
return HTTPResponse(body=b"", status=status, headers=headers)
|
||||
|
||||
@@ -38,15 +41,19 @@ def json(
|
||||
dumps: Optional[Callable[..., str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
"""Returns response object with body in json format.
|
||||
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
Args:
|
||||
body (Any): Response data to be serialized.
|
||||
status (int, optional): HTTP response code. Defaults to `200`.
|
||||
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
|
||||
content_type (str, optional): The content type (string) of the response. Defaults to `"application/json"`.
|
||||
dumps (Callable[..., str], optional): A custom json dumps function. Defaults to `None`.
|
||||
**kwargs (Any): Remaining arguments that are passed to the json encoder.
|
||||
|
||||
Returns:
|
||||
JSONResponse: A response object with body in json format.
|
||||
""" # noqa: E501
|
||||
return JSONResponse(
|
||||
body,
|
||||
status=status,
|
||||
@@ -63,14 +70,20 @@ def text(
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
"""Returns response object with body in text format.
|
||||
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
Args:
|
||||
body (str): Response data.
|
||||
status (int, optional): HTTP response code. Defaults to `200`.
|
||||
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
|
||||
content_type (str, optional): The content type (string) of the response. Defaults to `"text/plain; charset=utf-8"`.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: A response object with body in text format.
|
||||
|
||||
Raises:
|
||||
TypeError: If the body is not a string.
|
||||
""" # noqa: E501
|
||||
if not isinstance(body, str):
|
||||
raise TypeError(
|
||||
f"Bad body type. Expected str, got {type(body).__name__})"
|
||||
@@ -87,14 +100,17 @@ def raw(
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
content_type: str = DEFAULT_HTTP_CONTENT_TYPE,
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
"""Returns response object without encoding the body.
|
||||
|
||||
:param body: Response data.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
Args:
|
||||
body (Optional[AnyStr]): Response data.
|
||||
status (int, optional): HTTP response code. Defaults to `200`.
|
||||
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
|
||||
content_type (str, optional): The content type (string) of the response. Defaults to `"application/octet-stream"`.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: A response object without encoding the body.
|
||||
""" # noqa: E501
|
||||
return HTTPResponse(
|
||||
body=body,
|
||||
status=status,
|
||||
@@ -108,20 +124,25 @@ def html(
|
||||
status: int = 200,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Returns response object with body in html format.
|
||||
"""Returns response object with body in html format.
|
||||
|
||||
:param body: str or bytes-ish, or an object with __html__ or _repr_html_.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
Body should be a `str` or `bytes` like object, or an object with `__html__` or `_repr_html_`.
|
||||
|
||||
Args:
|
||||
body (Union[str, bytes, HTMLProtocol]): Response data.
|
||||
status (int, optional): HTTP response code. Defaults to `200`.
|
||||
headers (Dict[str, str], optional): Custom HTTP headers. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
HTTPResponse: A response object with body in html format.
|
||||
""" # noqa: E501
|
||||
if not isinstance(body, (str, bytes)):
|
||||
if hasattr(body, "__html__"):
|
||||
body = body.__html__()
|
||||
elif hasattr(body, "_repr_html_"):
|
||||
body = body._repr_html_()
|
||||
|
||||
return HTTPResponse( # type: ignore
|
||||
return HTTPResponse(
|
||||
body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
@@ -131,11 +152,20 @@ def html(
|
||||
|
||||
async def validate_file(
|
||||
request_headers: Header, last_modified: Union[datetime, float, int]
|
||||
):
|
||||
) -> Optional[HTTPResponse]:
|
||||
"""Validate file based on request headers.
|
||||
|
||||
Args:
|
||||
request_headers (Header): The request headers.
|
||||
last_modified (Union[datetime, float, int]): The last modified date and time of the file.
|
||||
|
||||
Returns:
|
||||
Optional[HTTPResponse]: A response object with status 304 if the file is not modified.
|
||||
""" # noqa: E501
|
||||
try:
|
||||
if_modified_since = request_headers.getone("If-Modified-Since")
|
||||
except KeyError:
|
||||
return
|
||||
return None
|
||||
try:
|
||||
if_modified_since = parsedate_to_datetime(if_modified_since)
|
||||
except (TypeError, ValueError):
|
||||
@@ -143,7 +173,7 @@ async def validate_file(
|
||||
"Ignorning invalid If-Modified-Since header received: " "'%s'",
|
||||
if_modified_since,
|
||||
)
|
||||
return
|
||||
return None
|
||||
if not isinstance(last_modified, datetime):
|
||||
last_modified = datetime.fromtimestamp(
|
||||
float(last_modified), tz=timezone.utc
|
||||
@@ -170,6 +200,8 @@ async def validate_file(
|
||||
if last_modified.timestamp() <= if_modified_since.timestamp():
|
||||
return HTTPResponse(status=304)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def file(
|
||||
location: Union[str, PurePath],
|
||||
@@ -185,21 +217,23 @@ async def file(
|
||||
_range: Optional[Range] = None,
|
||||
) -> HTTPResponse:
|
||||
"""Return a response object with file data.
|
||||
:param status: HTTP response code. Won't enforce the passed in
|
||||
status if only a part of the content will be sent (206)
|
||||
or file is being validated (304).
|
||||
:param request_headers: The request headers.
|
||||
:param validate_when_requested: If True, will validate the
|
||||
file when requested.
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param last_modified: The last modified date and time of the file.
|
||||
:param max_age: Max age for cache control.
|
||||
:param no_store: Any cache should not store this response.
|
||||
:param _range:
|
||||
"""
|
||||
|
||||
Args:
|
||||
location (Union[str, PurePath]): Location of file on system.
|
||||
status (int, optional): HTTP response code. Won't enforce the passed in status if only a part of the content will be sent (206) or file is being validated (304). Defaults to 200.
|
||||
request_headers (Optional[Header], optional): The request headers.
|
||||
validate_when_requested (bool, optional): If `True`, will validate the file when requested. Defaults to True.
|
||||
mime_type (Optional[str], optional): Specific mime_type.
|
||||
headers (Optional[Dict[str, str]], optional): Custom Headers.
|
||||
filename (Optional[str], optional): Override filename.
|
||||
last_modified (Optional[Union[datetime, float, int, Default]], optional): The last modified date and time of the file.
|
||||
max_age (Optional[Union[float, int]], optional): Max age for cache control.
|
||||
no_store (Optional[bool], optional): Any cache should not store this response. Defaults to None.
|
||||
_range (Optional[Range], optional):
|
||||
|
||||
Returns:
|
||||
HTTPResponse: The response object with the file data.
|
||||
""" # noqa: E501
|
||||
|
||||
if isinstance(last_modified, datetime):
|
||||
last_modified = last_modified.replace(microsecond=0).timestamp()
|
||||
@@ -271,15 +305,17 @@ def redirect(
|
||||
status: int = 302,
|
||||
content_type: str = "text/html; charset=utf-8",
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Abort execution and cause a 302 redirect (by default) by setting a
|
||||
Location header.
|
||||
"""Cause a HTTP redirect (302 by default) by setting a Location header.
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
:param headers: optional dict of headers to include in the new request
|
||||
:param status: status code (int) of the new request, defaults to 302
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
Args:
|
||||
to (str): path or fully qualified URL to redirect to
|
||||
headers (Optional[Dict[str, str]], optional): optional dict of headers to include in the new request. Defaults to None.
|
||||
status (int, optional): status code (int) of the new request, defaults to 302. Defaults to 302.
|
||||
content_type (str, optional): the content type (string) of the response. Defaults to "text/html; charset=utf-8".
|
||||
|
||||
Returns:
|
||||
HTTPResponse: A response object with the redirect.
|
||||
""" # noqa: E501
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
@@ -310,7 +346,16 @@ async def file_stream(
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
|
||||
Args:
|
||||
location (Union[str, PurePath]): Location of file on system.
|
||||
status (int, optional): HTTP response code. Won't enforce the passed in status if only a part of the content will be sent (206) or file is being validated (304). Defaults to `200`.
|
||||
chunk_size (int, optional): The size of each chunk in the stream (in bytes). Defaults to `4096`.
|
||||
mime_type (Optional[str], optional): Specific mime_type.
|
||||
headers (Optional[Dict[str, str]], optional): Custom HTTP headers.
|
||||
filename (Optional[str], optional): Override filename.
|
||||
_range (Optional[Range], optional): The range of bytes to send.
|
||||
""" # noqa: E501
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
|
||||
@@ -50,9 +50,7 @@ except ImportError:
|
||||
|
||||
|
||||
class BaseHTTPResponse:
|
||||
"""
|
||||
The base class for all HTTP Responses
|
||||
"""
|
||||
"""The base class for all HTTP Responses"""
|
||||
|
||||
__slots__ = (
|
||||
"asgi",
|
||||
@@ -88,20 +86,12 @@ class BaseHTTPResponse:
|
||||
|
||||
@property
|
||||
def cookies(self) -> CookieJar:
|
||||
"""
|
||||
The response cookies. Cookies should be set and written as follows:
|
||||
"""The response cookies.
|
||||
|
||||
.. code-block:: python
|
||||
See [Cookies](/en/guide/basics/cookies.html)
|
||||
|
||||
response.cookies["test"] = "It worked!"
|
||||
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
|
||||
response.cookies["test"]["httponly"] = True
|
||||
|
||||
`See user guide re: cookies
|
||||
<https://sanic.dev/en/guide/basics/cookies.html>`
|
||||
|
||||
:return: the cookie jar
|
||||
:rtype: CookieJar
|
||||
Returns:
|
||||
CookieJar: The response cookies
|
||||
"""
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
@@ -109,14 +99,13 @@ class BaseHTTPResponse:
|
||||
|
||||
@property
|
||||
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
|
||||
"""
|
||||
Obtain a list of header tuples encoded in bytes for sending.
|
||||
"""Obtain a list of header tuples encoded in bytes for sending.
|
||||
|
||||
Add and remove headers based on status and content_type.
|
||||
|
||||
:return: response headers
|
||||
:rtype: Tuple[Tuple[bytes, bytes], ...]
|
||||
"""
|
||||
Returns:
|
||||
Iterator[Tuple[bytes, bytes]]: A list of header tuples encoded in bytes for sending
|
||||
""" # noqa: E501
|
||||
# TODO: Make a blacklist set of header names and then filter with that
|
||||
if self.status in (304, 412): # Not Modified, Precondition Failed
|
||||
self.headers = remove_entity_headers(self.headers)
|
||||
@@ -133,12 +122,12 @@ class BaseHTTPResponse:
|
||||
data: Optional[AnyStr] = None,
|
||||
end_stream: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send any pending response headers and the given data as body.
|
||||
"""Send any pending response headers and the given data as body.
|
||||
|
||||
:param data: str or bytes to be written
|
||||
:param end_stream: whether to close the stream after this block
|
||||
"""
|
||||
Args:
|
||||
data (Optional[AnyStr], optional): str or bytes to be written. Defaults to `None`.
|
||||
end_stream (Optional[bool], optional): whether to close the stream after this block. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
if data is None and end_stream is None:
|
||||
end_stream = True
|
||||
if self.stream is None:
|
||||
@@ -179,44 +168,28 @@ class BaseHTTPResponse:
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Cookie:
|
||||
"""
|
||||
Add a cookie to the CookieJar
|
||||
"""Add a cookie to the CookieJar
|
||||
|
||||
:param key: Key of the cookie
|
||||
:type key: str
|
||||
:param value: Value of the cookie
|
||||
:type value: str
|
||||
:param path: Path of the cookie, defaults to None
|
||||
:type path: Optional[str], optional
|
||||
:param domain: Domain of the cookie, defaults to None
|
||||
:type domain: Optional[str], optional
|
||||
:param secure: Whether to set it as a secure cookie, defaults to True
|
||||
:type secure: bool
|
||||
:param max_age: Max age of the cookie in seconds; if set to 0 a
|
||||
browser should delete it, defaults to None
|
||||
:type max_age: Optional[int], optional
|
||||
:param expires: When the cookie expires; if set to None browsers
|
||||
should set it as a session cookie, defaults to None
|
||||
:type expires: Optional[datetime], optional
|
||||
:param httponly: Whether to set it as HTTP only, defaults to False
|
||||
:type httponly: bool
|
||||
:param samesite: How to set the samesite property, should be
|
||||
strict, lax or none (case insensitive), defaults to Lax
|
||||
:type samesite: Optional[SameSite], optional
|
||||
:param partitioned: Whether to set it as partitioned, defaults to False
|
||||
:type partitioned: bool
|
||||
:param comment: A cookie comment, defaults to None
|
||||
:type comment: Optional[str], optional
|
||||
:param host_prefix: Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True,
|
||||
defaults to False
|
||||
:type host_prefix: bool
|
||||
:param secure_prefix: Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True, defaults to False
|
||||
:type secure_prefix: bool
|
||||
:return: The instance of the created cookie
|
||||
:rtype: Cookie
|
||||
"""
|
||||
See [Cookies](/en/guide/basics/cookies.html)
|
||||
|
||||
Args:
|
||||
key (str): The key to be added
|
||||
value (str): The value to be added
|
||||
path (str, optional): Path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): Domain of the cookie. Defaults to `None`.
|
||||
secure (bool, optional): Whether the cookie is secure. Defaults to `True`.
|
||||
max_age (Optional[int], optional): Max age of the cookie. Defaults to `None`.
|
||||
expires (Optional[datetime], optional): Expiry date of the cookie. Defaults to `None`.
|
||||
httponly (bool, optional): Whether the cookie is http only. Defaults to `False`.
|
||||
samesite (Optional[SameSite], optional): SameSite policy of the cookie. Defaults to `"Lax"`.
|
||||
partitioned (bool, optional): Whether the cookie is partitioned. Defaults to `False`.
|
||||
comment (Optional[str], optional): Comment of the cookie. Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key. This requires that path="/", domain=None, and secure=True. Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key. This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Cookie: The cookie that was added
|
||||
""" # noqa: E501
|
||||
return self.cookies.add_cookie(
|
||||
key=key,
|
||||
value=value,
|
||||
@@ -242,8 +215,7 @@ class BaseHTTPResponse:
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a cookie
|
||||
"""Delete a cookie
|
||||
|
||||
This will effectively set it as Max-Age: 0, which a browser should
|
||||
interpret it to mean: "delete the cookie".
|
||||
@@ -251,20 +223,15 @@ class BaseHTTPResponse:
|
||||
Since it is a browser/client implementation, your results may vary
|
||||
depending upon which client is being used.
|
||||
|
||||
:param key: The key to be deleted
|
||||
:type key: str
|
||||
:param path: Path of the cookie, defaults to None
|
||||
:type path: Optional[str], optional
|
||||
:param domain: Domain of the cookie, defaults to None
|
||||
:type domain: Optional[str], optional
|
||||
:param host_prefix: Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True,
|
||||
defaults to False
|
||||
:type host_prefix: bool
|
||||
:param secure_prefix: Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True, defaults to False
|
||||
:type secure_prefix: bool
|
||||
"""
|
||||
See [Cookies](/en/guide/basics/cookies.html)
|
||||
|
||||
Args:
|
||||
key (str): The key to be deleted
|
||||
path (str, optional): Path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): Domain of the cookie. Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key. This requires that path="/", domain=None, and secure=True. Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key. This requires that secure=True. Defaults to `False`.
|
||||
""" # noqa: E501
|
||||
self.cookies.delete_cookie(
|
||||
key=key,
|
||||
path=path,
|
||||
@@ -275,18 +242,14 @@ class BaseHTTPResponse:
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
HTTP response to be sent back to the client.
|
||||
"""HTTP response to be sent back to the client.
|
||||
|
||||
:param body: the body content to be returned
|
||||
:type body: Optional[bytes]
|
||||
:param status: HTTP response number. **Default=200**
|
||||
:type status: int
|
||||
:param headers: headers to be returned
|
||||
:type headers: Optional;
|
||||
:param content_type: content type to be returned (as a header)
|
||||
:type content_type: Optional[str]
|
||||
"""
|
||||
Args:
|
||||
body (Optional[Any], optional): The body content to be returned. Defaults to `None`.
|
||||
status (int, optional): HTTP response number. Defaults to `200`.
|
||||
headers (Optional[Union[Header, Dict[str, str]]], optional): Headers to be returned. Defaults to `None`.
|
||||
content_type (Optional[str], optional): Content type to be returned (as a header). Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@@ -306,6 +269,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
self._cookies = None
|
||||
|
||||
async def eof(self):
|
||||
"""Send a EOF (End of File) message to the client."""
|
||||
await self.send("", True)
|
||||
|
||||
async def __aenter__(self):
|
||||
@@ -316,22 +280,20 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
|
||||
|
||||
class JSONResponse(HTTPResponse):
|
||||
"""
|
||||
"""Convenience class for JSON responses
|
||||
|
||||
HTTP response to be sent back to the client, when the response
|
||||
is of json type. Offers several utilities to manipulate common
|
||||
json data types.
|
||||
|
||||
:param body: the body content to be returned
|
||||
:type body: Optional[Any]
|
||||
:param status: HTTP response number. **Default=200**
|
||||
:type status: int
|
||||
:param headers: headers to be returned
|
||||
:type headers: Optional
|
||||
:param content_type: content type to be returned (as a header)
|
||||
:type content_type: Optional[str]
|
||||
:param dumps: json.dumps function to use
|
||||
:type dumps: Optional[Callable]
|
||||
"""
|
||||
Args:
|
||||
body (Optional[Any], optional): The body content to be returned. Defaults to `None`.
|
||||
status (int, optional): HTTP response number. Defaults to `200`.
|
||||
headers (Optional[Union[Header, Dict[str, str]]], optional): Headers to be returned. Defaults to `None`.
|
||||
content_type (str, optional): Content type to be returned (as a header). Defaults to `"application/json"`.
|
||||
dumps (Optional[Callable[..., str]], optional): The function to use for json encoding. Defaults to `None`.
|
||||
**kwargs (Any, optional): The kwargs to pass to the json encoding function. Defaults to `{}`.
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = (
|
||||
"_body",
|
||||
@@ -376,16 +338,17 @@ class JSONResponse(HTTPResponse):
|
||||
|
||||
@property
|
||||
def raw_body(self) -> Optional[Any]:
|
||||
"""Returns the raw body, as long as body has not been manually
|
||||
set previously.
|
||||
"""Returns the raw body, as long as body has not been manually set previously.
|
||||
|
||||
NOTE: This object should not be mutated, as it will not be
|
||||
reflected in the response body. If you need to mutate the
|
||||
response body, consider using one of the provided methods in
|
||||
this class or alternatively call set_body() with the mutated
|
||||
object afterwards or set the raw_body property to it.
|
||||
"""
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The raw body
|
||||
""" # noqa: E501
|
||||
self._check_body_not_manually_set()
|
||||
return self._raw_body
|
||||
|
||||
@@ -399,6 +362,11 @@ class JSONResponse(HTTPResponse):
|
||||
|
||||
@property # type: ignore
|
||||
def body(self) -> Optional[bytes]: # type: ignore
|
||||
"""Returns the response body.
|
||||
|
||||
Returns:
|
||||
Optional[bytes]: The response body
|
||||
"""
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
@@ -414,11 +382,24 @@ class JSONResponse(HTTPResponse):
|
||||
dumps: Optional[Callable[..., str]] = None,
|
||||
**dumps_kwargs: Any,
|
||||
) -> None:
|
||||
"""Sets a new response body using the given dumps function
|
||||
"""Set the response body to the given value, using the given dumps function
|
||||
|
||||
Sets a new response body using the given dumps function
|
||||
and kwargs, or falling back to the defaults given when
|
||||
creating the object if none are specified.
|
||||
"""
|
||||
|
||||
Args:
|
||||
body (Any): The body to set
|
||||
dumps (Optional[Callable[..., str]], optional): The function to use for json encoding. Defaults to `None`.
|
||||
**dumps_kwargs (Any, optional): The kwargs to pass to the json encoding function. Defaults to `{}`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
response = JSONResponse({"foo": "bar"})
|
||||
response.set_body({"bar": "baz"})
|
||||
assert response.body == b'{"bar": "baz"}'
|
||||
```
|
||||
""" # noqa: E501
|
||||
self._body_manually_set = False
|
||||
self._raw_body = body
|
||||
|
||||
@@ -428,10 +409,16 @@ class JSONResponse(HTTPResponse):
|
||||
self._body = self._encode_body(use_dumps(body, **use_dumps_kwargs))
|
||||
|
||||
def append(self, value: Any) -> None:
|
||||
"""Appends a value to the response raw_body, ensuring that
|
||||
body is kept up to date. This can only be used if raw_body
|
||||
is a list.
|
||||
"""
|
||||
"""Appends a value to the response raw_body, ensuring that body is kept up to date.
|
||||
|
||||
This can only be used if raw_body is a list.
|
||||
|
||||
Args:
|
||||
value (Any): The value to append
|
||||
|
||||
Raises:
|
||||
SanicException: If the body is not a list
|
||||
""" # noqa: E501
|
||||
|
||||
self._check_body_not_manually_set()
|
||||
|
||||
@@ -442,10 +429,16 @@ class JSONResponse(HTTPResponse):
|
||||
self.raw_body = self._raw_body
|
||||
|
||||
def extend(self, value: Any) -> None:
|
||||
"""Extends the response's raw_body with the given values, ensuring
|
||||
that body is kept up to date. This can only be used if raw_body is
|
||||
a list.
|
||||
"""
|
||||
"""Extends the response's raw_body with the given values, ensuring that body is kept up to date.
|
||||
|
||||
This can only be used if raw_body is a list.
|
||||
|
||||
Args:
|
||||
value (Any): The values to extend with
|
||||
|
||||
Raises:
|
||||
SanicException: If the body is not a list
|
||||
""" # noqa: E501
|
||||
|
||||
self._check_body_not_manually_set()
|
||||
|
||||
@@ -456,10 +449,17 @@ class JSONResponse(HTTPResponse):
|
||||
self.raw_body = self._raw_body
|
||||
|
||||
def update(self, *args, **kwargs) -> None:
|
||||
"""Updates the response's raw_body with the given values, ensuring
|
||||
that body is kept up to date. This can only be used if raw_body is
|
||||
a dict.
|
||||
"""
|
||||
"""Updates the response's raw_body with the given values, ensuring that body is kept up to date.
|
||||
|
||||
This can only be used if raw_body is a dict.
|
||||
|
||||
Args:
|
||||
*args: The args to update with
|
||||
**kwargs: The kwargs to update with
|
||||
|
||||
Raises:
|
||||
SanicException: If the body is not a dict
|
||||
""" # noqa: E501
|
||||
|
||||
self._check_body_not_manually_set()
|
||||
|
||||
@@ -470,10 +470,21 @@ class JSONResponse(HTTPResponse):
|
||||
self.raw_body = self._raw_body
|
||||
|
||||
def pop(self, key: Any, default: Any = _default) -> Any:
|
||||
"""Pops a key from the response's raw_body, ensuring that body is
|
||||
kept up to date. This can only be used if raw_body is a dict or a
|
||||
list.
|
||||
"""
|
||||
"""Pops a key from the response's raw_body, ensuring that body is kept up to date.
|
||||
|
||||
This can only be used if raw_body is a dict or a list.
|
||||
|
||||
Args:
|
||||
key (Any): The key to pop
|
||||
default (Any, optional): The default value to return if the key is not found. Defaults to `_default`.
|
||||
|
||||
Raises:
|
||||
SanicException: If the body is not a dict or a list
|
||||
TypeError: If the body is a list and a default value is provided
|
||||
|
||||
Returns:
|
||||
Any: The value that was popped
|
||||
""" # noqa: E501
|
||||
|
||||
self._check_body_not_manually_set()
|
||||
|
||||
@@ -495,12 +506,12 @@ class JSONResponse(HTTPResponse):
|
||||
|
||||
|
||||
class ResponseStream:
|
||||
"""
|
||||
ResponseStream is a compat layer to bridge the gap after the deprecation
|
||||
of StreamingHTTPResponse. It will be removed when:
|
||||
"""A compat layer to bridge the gap after the deprecation of StreamingHTTPResponse.
|
||||
|
||||
It will be removed when:
|
||||
- file_stream is moved to new style streaming
|
||||
- file and file_stream are combined into a single API
|
||||
"""
|
||||
""" # noqa: E501
|
||||
|
||||
__slots__ = (
|
||||
"_cookies",
|
||||
|
||||
129
sanic/router.py
129
sanic/router.py
@@ -21,10 +21,7 @@ ALLOWED_LABELS = ("__file_uri__",)
|
||||
|
||||
|
||||
class Router(BaseRouter):
|
||||
"""
|
||||
The router implementation responsible for routing a :class:`Request` object
|
||||
to the appropriate handler.
|
||||
"""
|
||||
"""The router implementation responsible for routing a `Request` object to the appropriate handler.""" # noqa: E501
|
||||
|
||||
DEFAULT_METHOD = "GET"
|
||||
ALLOWED_METHODS = HTTP_METHODS
|
||||
@@ -53,16 +50,26 @@ class Router(BaseRouter):
|
||||
def get( # type: ignore
|
||||
self, path: str, method: str, host: Optional[str]
|
||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve a `Route` object containing the details about how to handle
|
||||
a response for a given request
|
||||
"""Retrieve a `Route` object containing the details about how to handle a response for a given request
|
||||
|
||||
:param request: the incoming request object
|
||||
:type request: Request
|
||||
:return: details needed for handling the request and returning the
|
||||
correct response
|
||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||
"""
|
||||
|
||||
Args:
|
||||
path (str): the path of the route
|
||||
method (str): the HTTP method of the route
|
||||
host (Optional[str]): the host of the route
|
||||
|
||||
Raises:
|
||||
NotFound: if the route is not found
|
||||
MethodNotAllowed: if the method is not allowed for the route
|
||||
|
||||
Returns:
|
||||
Tuple[Route, RouteHandler, Dict[str, Any]]: the route, handler, and match info
|
||||
""" # noqa: E501
|
||||
__tracebackhide__ = True
|
||||
return self._get(path, method, host)
|
||||
|
||||
@@ -83,33 +90,25 @@ class Router(BaseRouter):
|
||||
overwrite: bool = False,
|
||||
error_format: Optional[str] = None,
|
||||
) -> Union[Route, List[Route]]:
|
||||
"""
|
||||
Add a handler to the router
|
||||
"""Add a handler to the router
|
||||
|
||||
Args:
|
||||
uri (str): The path of the route.
|
||||
methods (Iterable[str]): The types of HTTP methods that should be attached,
|
||||
example: ["GET", "POST", "OPTIONS"].
|
||||
handler (RouteHandler): The sync or async function to be executed.
|
||||
host (Optional[str], optional): Host that the route should be on. Defaults to None.
|
||||
strict_slashes (bool, optional): Whether to apply strict slashes. Defaults to False.
|
||||
stream (bool, optional): Whether to stream the response. Defaults to False.
|
||||
ignore_body (bool, optional): Whether the incoming request body should be read.
|
||||
Defaults to False.
|
||||
version (Union[str, float, int], optional): A version modifier for the uri. Defaults to None.
|
||||
name (Optional[str], optional): An identifying name of the route. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Route: The route object.
|
||||
""" # noqa: E501
|
||||
|
||||
:param uri: the path of the route
|
||||
:type uri: str
|
||||
:param methods: the types of HTTP methods that should be attached,
|
||||
example: ``["GET", "POST", "OPTIONS"]``
|
||||
:type methods: Iterable[str]
|
||||
:param handler: the sync or async function to be executed
|
||||
:type handler: RouteHandler
|
||||
:param host: host that the route should be on, defaults to None
|
||||
:type host: Optional[str], optional
|
||||
:param strict_slashes: whether to apply strict slashes, defaults
|
||||
to False
|
||||
:type strict_slashes: bool, optional
|
||||
:param stream: whether to stream the response, defaults to False
|
||||
:type stream: bool, optional
|
||||
:param ignore_body: whether the incoming request body should be read,
|
||||
defaults to False
|
||||
:type ignore_body: bool, optional
|
||||
:param version: a version modifier for the uri, defaults to None
|
||||
:type version: Union[str, float, int], optional
|
||||
:param name: an identifying name of the route, defaults to None
|
||||
:type name: Optional[str], optional
|
||||
:return: the route object
|
||||
:rtype: Route
|
||||
"""
|
||||
if version is not None:
|
||||
version = str(version).strip("/").lstrip("v")
|
||||
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
||||
@@ -163,14 +162,18 @@ class Router(BaseRouter):
|
||||
return routes
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""
|
||||
Find a route in the router based on the specified view name.
|
||||
def find_route_by_view_name(
|
||||
self, view_name: str, name: Optional[str] = None
|
||||
) -> Optional[Route]:
|
||||
"""Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:param kwargs: additional params, usually for static files
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
Args:
|
||||
view_name (str): the name of the view to search for
|
||||
name (Optional[str], optional): the name of the route. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Optional[Route]: the route object
|
||||
""" # noqa: E501
|
||||
if not view_name:
|
||||
return None
|
||||
|
||||
@@ -185,22 +188,56 @@ class Router(BaseRouter):
|
||||
return route
|
||||
|
||||
@property
|
||||
def routes_all(self):
|
||||
def routes_all(self) -> Dict[Tuple[str, ...], Route]:
|
||||
"""Return all routes in the router.
|
||||
|
||||
Returns:
|
||||
Dict[Tuple[str, ...], Route]: a dictionary of routes
|
||||
"""
|
||||
return {route.parts: route for route in self.routes}
|
||||
|
||||
@property
|
||||
def routes_static(self):
|
||||
def routes_static(self) -> Dict[Tuple[str, ...], Route]:
|
||||
"""Return all static routes in the router.
|
||||
|
||||
_In this context "static" routes do not refer to the `app.static()`
|
||||
method. Instead, they refer to routes that do not contain
|
||||
any path parameters._
|
||||
|
||||
Returns:
|
||||
Dict[Tuple[str, ...], Route]: a dictionary of routes
|
||||
"""
|
||||
return self.static_routes
|
||||
|
||||
@property
|
||||
def routes_dynamic(self):
|
||||
def routes_dynamic(self) -> Dict[Tuple[str, ...], Route]:
|
||||
"""Return all dynamic routes in the router.
|
||||
|
||||
_Dynamic routes are routes that contain path parameters._
|
||||
|
||||
Returns:
|
||||
Dict[Tuple[str, ...], Route]: a dictionary of routes
|
||||
"""
|
||||
return self.dynamic_routes
|
||||
|
||||
@property
|
||||
def routes_regex(self):
|
||||
def routes_regex(self) -> Dict[Tuple[str, ...], Route]:
|
||||
"""Return all regex routes in the router.
|
||||
|
||||
_Regex routes are routes that contain path parameters with regex
|
||||
expressions, or otherwise need regex to resolve._
|
||||
|
||||
Returns:
|
||||
Dict[Tuple[str, ...], Route]: a dictionary of routes
|
||||
"""
|
||||
return self.regex_routes
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
def finalize(self, *args, **kwargs) -> None:
|
||||
"""Finalize the router.
|
||||
|
||||
Raises:
|
||||
SanicException: if a route contains a parameter name that starts with "__" and is not in ALLOWED_LABELS
|
||||
""" # noqa: E501
|
||||
super().finalize(*args, **kwargs)
|
||||
|
||||
for route in self.dynamic_routes.values():
|
||||
|
||||
@@ -12,10 +12,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class AsyncioServer:
|
||||
"""
|
||||
Wraps an asyncio server with functionality that might be useful to
|
||||
a user who needs to manage the server lifecycle manually.
|
||||
"""
|
||||
"""Wraps an asyncio server with functionality that might be useful to a user who needs to manage the server lifecycle manually.""" # noqa: E501
|
||||
|
||||
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
|
||||
|
||||
@@ -35,45 +32,38 @@ class AsyncioServer:
|
||||
self.server = None
|
||||
|
||||
def startup(self):
|
||||
"""
|
||||
Trigger "before_server_start" events
|
||||
"""
|
||||
"""Trigger "startup" operations on the app"""
|
||||
return self.app._startup()
|
||||
|
||||
def before_start(self):
|
||||
"""
|
||||
Trigger "before_server_start" events
|
||||
"""
|
||||
"""Trigger "before_server_start" events"""
|
||||
return self._server_event("init", "before")
|
||||
|
||||
def after_start(self):
|
||||
"""
|
||||
Trigger "after_server_start" events
|
||||
"""
|
||||
"""Trigger "after_server_start" events"""
|
||||
return self._server_event("init", "after")
|
||||
|
||||
def before_stop(self):
|
||||
"""
|
||||
Trigger "before_server_stop" events
|
||||
"""
|
||||
"""Trigger "before_server_stop" events"""
|
||||
return self._server_event("shutdown", "before")
|
||||
|
||||
def after_stop(self):
|
||||
"""
|
||||
Trigger "after_server_stop" events
|
||||
"""
|
||||
"""Trigger "after_server_stop" events"""
|
||||
return self._server_event("shutdown", "after")
|
||||
|
||||
def is_serving(self) -> bool:
|
||||
"""Returns True if the server is running, False otherwise"""
|
||||
if self.server:
|
||||
return self.server.is_serving()
|
||||
return False
|
||||
|
||||
def wait_closed(self):
|
||||
"""Wait until the server is closed"""
|
||||
if self.server:
|
||||
return self.server.wait_closed()
|
||||
|
||||
def close(self):
|
||||
"""Close the server"""
|
||||
if self.server:
|
||||
self.server.close()
|
||||
coro = self.wait_closed()
|
||||
@@ -81,9 +71,11 @@ class AsyncioServer:
|
||||
return task
|
||||
|
||||
def start_serving(self):
|
||||
"""Start serving requests"""
|
||||
return self._serve(self.server.start_serving)
|
||||
|
||||
def serve_forever(self):
|
||||
"""Serve requests until the server is stopped"""
|
||||
return self._serve(self.server.serve_forever)
|
||||
|
||||
def _serve(self, serve_func):
|
||||
|
||||
@@ -13,11 +13,12 @@ def trigger_events(
|
||||
loop,
|
||||
app: Optional[Sanic] = None,
|
||||
):
|
||||
"""
|
||||
Trigger event callbacks (functions or async)
|
||||
"""Trigger event callbacks (functions or async)
|
||||
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
Args:
|
||||
events (Optional[Iterable[Callable[..., Any]]]): [description]
|
||||
loop ([type]): [description]
|
||||
app (Optional[Sanic], optional): [description]. Defaults to None.
|
||||
"""
|
||||
if events:
|
||||
for event in events:
|
||||
|
||||
@@ -9,9 +9,7 @@ from sanic.utils import str_to_bool
|
||||
|
||||
|
||||
def try_use_uvloop() -> None:
|
||||
"""
|
||||
Use uvloop instead of the default asyncio loop.
|
||||
"""
|
||||
"""Use uvloop instead of the default asyncio loop."""
|
||||
if OS_IS_WINDOWS:
|
||||
error_logger.warning(
|
||||
"You are trying to use uvloop, but uvloop is not compatible "
|
||||
@@ -51,6 +49,7 @@ def try_use_uvloop() -> None:
|
||||
|
||||
|
||||
def try_windows_loop():
|
||||
"""Try to use the WindowsSelectorEventLoopPolicy instead of the default"""
|
||||
if not OS_IS_WINDOWS:
|
||||
error_logger.warning(
|
||||
"You are trying to use an event loop policy that is not "
|
||||
|
||||
@@ -82,7 +82,44 @@ def serve(
|
||||
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||
create_server method
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
Args:
|
||||
host (str): Address to host on
|
||||
port (int): Port to host on
|
||||
app (Sanic): Sanic app instance
|
||||
ssl (Optional[SSLContext], optional): SSLContext. Defaults to `None`.
|
||||
sock (Optional[socket.socket], optional): Socket for the server to
|
||||
accept connections from. Defaults to `None`.
|
||||
unix (Optional[str], optional): Unix socket to listen on instead of
|
||||
TCP port. Defaults to `None`.
|
||||
reuse_port (bool, optional): `True` for multiple workers. Defaults
|
||||
to `False`.
|
||||
loop: asyncio compatible event loop. Defaults
|
||||
to `None`.
|
||||
protocol (Type[asyncio.Protocol], optional): Protocol to use. Defaults
|
||||
to `HttpProtocol`.
|
||||
backlog (int, optional): The maximum number of queued connections
|
||||
passed to socket.listen(). Defaults to `100`.
|
||||
register_sys_signals (bool, optional): Register SIGINT and SIGTERM.
|
||||
Defaults to `True`.
|
||||
run_multiple (bool, optional): Run multiple workers. Defaults
|
||||
to `False`.
|
||||
run_async (bool, optional): Return an AsyncServer object.
|
||||
Defaults to `False`.
|
||||
connections: Connections. Defaults to `None`.
|
||||
signal (Signal, optional): Signal. Defaults to `Signal()`.
|
||||
state: State. Defaults to `None`.
|
||||
asyncio_server_kwargs (Optional[Dict[str, Union[int, float]]], optional):
|
||||
key-value args for asyncio/uvloop create_server method. Defaults
|
||||
to `None`.
|
||||
version (str, optional): HTTP version. Defaults to `HTTP.VERSION_1`.
|
||||
|
||||
Raises:
|
||||
ServerError: Cannot run HTTP/3 server without aioquic installed.
|
||||
|
||||
Returns:
|
||||
AsyncioServer: AsyncioServer object if `run_async` is `True`.
|
||||
""" # noqa: E501
|
||||
if not run_async and not loop:
|
||||
# create new event_loop after fork
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
@@ -16,6 +16,8 @@ from sanic.models.handler_types import SignalHandler
|
||||
|
||||
|
||||
class Event(Enum):
|
||||
"""Event names for the SignalRouter"""
|
||||
|
||||
SERVER_EXCEPTION_REPORT = "server.exception.report"
|
||||
SERVER_INIT_AFTER = "server.init.after"
|
||||
SERVER_INIT_BEFORE = "server.init.before"
|
||||
@@ -71,14 +73,16 @@ def _blank():
|
||||
|
||||
|
||||
class Signal(Route):
|
||||
...
|
||||
"""A `Route` that is used to dispatch signals to handlers"""
|
||||
|
||||
|
||||
class SignalGroup(RouteGroup):
|
||||
...
|
||||
"""A `RouteGroup` that is used to dispatch signals to handlers"""
|
||||
|
||||
|
||||
class SignalRouter(BaseRouter):
|
||||
"""A `BaseRouter` that is used to dispatch signals to handlers"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
delimiter=".",
|
||||
@@ -94,6 +98,18 @@ class SignalRouter(BaseRouter):
|
||||
event: str,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
"""Get the handlers for a signal
|
||||
|
||||
Args:
|
||||
event (str): The event to get the handlers for
|
||||
condition (Optional[Dict[str, str]], optional): A dictionary of conditions to match against the handlers. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Tuple[SignalGroup, List[SignalHandler], Dict[str, Any]]: A tuple of the `SignalGroup` that matched, a list of the handlers that matched, and a dictionary of the params that matched
|
||||
|
||||
Raises:
|
||||
NotFound: If no handlers are found
|
||||
""" # noqa: E501
|
||||
extra = condition or {}
|
||||
try:
|
||||
group, param_basket = self.find_route(
|
||||
@@ -195,6 +211,23 @@ class SignalRouter(BaseRouter):
|
||||
inline: bool = False,
|
||||
reverse: bool = False,
|
||||
) -> Union[asyncio.Task, Any]:
|
||||
"""Dispatch a signal to all handlers that match the event
|
||||
|
||||
Args:
|
||||
event (str): The event to dispatch
|
||||
context (Optional[Dict[str, Any]], optional): A dictionary of context to pass to the handlers. Defaults to `None`.
|
||||
condition (Optional[Dict[str, str]], optional): A dictionary of conditions to match against the handlers. Defaults to `None`.
|
||||
fail_not_found (bool, optional): Whether to raise an exception if no handlers are found. Defaults to `True`.
|
||||
inline (bool, optional): Whether to run the handlers inline. An inline run means it will return the value of the signal handler. When `False` (which is the default) the signal handler will run in a background task. Defaults to `False`.
|
||||
reverse (bool, optional): Whether to run the handlers in reverse order. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Union[asyncio.Task, Any]: If `inline` is `True` then the return value of the signal handler will be returned. If `inline` is `False` then an `asyncio.Task` will be returned.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the signal is dispatched outside of an event loop
|
||||
""" # noqa: E501
|
||||
|
||||
dispatch = self._dispatch(
|
||||
event,
|
||||
context=context,
|
||||
@@ -245,6 +278,18 @@ class SignalRouter(BaseRouter):
|
||||
return cast(Signal, signal)
|
||||
|
||||
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
||||
"""Finalize the router and compile the routes
|
||||
|
||||
Args:
|
||||
do_compile (bool, optional): Whether to compile the routes. Defaults to `True`.
|
||||
do_optimize (bool, optional): Whether to optimize the routes. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
SignalRouter: The router
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the router is finalized outside of an event loop
|
||||
""" # noqa: E501
|
||||
self.add(_blank, "sanic.__signal__.__init__")
|
||||
|
||||
try:
|
||||
|
||||
155
sanic/views.py
155
sanic/views.py
@@ -11,6 +11,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.request.types import Request
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -19,39 +20,103 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class HTTPMethodView:
|
||||
"""Simple class based implementation of view for the sanic.
|
||||
You should implement methods (get, post, put, patch, delete) for the class
|
||||
to every HTTP method you want to support.
|
||||
"""Class based implementation for creating and grouping handlers
|
||||
|
||||
Class-based views (CBVs) are an alternative to function-based views. They
|
||||
allow you to reuse common logic, and group related views, while keeping
|
||||
the flexibility of function-based views.
|
||||
|
||||
|
||||
To use a class-based view, subclass the method handler, and implement
|
||||
methods (`get`, `post`, `put`, `patch`, `delete`) for the class
|
||||
to correspond to each HTTP method you want to support.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request: Request):
|
||||
return text('I am get method')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return text('I am get method')
|
||||
def put(self, request, *args, **kwargs):
|
||||
return text('I am put method')
|
||||
def put(self, request: Request):
|
||||
return text('I am put method')
|
||||
```
|
||||
|
||||
If someone tries to use a non-implemented method, there will be a
|
||||
If someone tries to use a non-implemented method, they will reveive a
|
||||
405 response.
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
If you need any url params just include them in method signature, like
|
||||
you would for function-based views.
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request: Request, my_param_here: str):
|
||||
return text(f"I am get method with {my_param_here}")
|
||||
```
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, my_param_here, *args, **kwargs):
|
||||
return text('I am get method with %s' % my_param_here)
|
||||
Next, you need to attach the view to the app or blueprint. You can do this
|
||||
in the exact same way as you would for a function-based view, except you
|
||||
should you use `MyView.as_view()` instead of `my_view_handler`.
|
||||
|
||||
To add the view into the routing you could use
|
||||
```python
|
||||
app.add_route(DummyView.as_view(), "/<my_param_here>")
|
||||
```
|
||||
|
||||
1) ``app.add_route(DummyView.as_view(), '/')``, OR
|
||||
2) ``app.route('/')(DummyView.as_view())``
|
||||
Alternatively, you can use the `attach` method:
|
||||
|
||||
To add any decorator you could set it into decorators variable
|
||||
```python
|
||||
DummyView.attach(app, "/<my_param_here>")
|
||||
```
|
||||
|
||||
Or, at the time of subclassing:
|
||||
|
||||
```python
|
||||
class DummyView(HTTPMethodView, attach=app, uri="/<my_param_here>"):
|
||||
...
|
||||
```
|
||||
|
||||
To add a decorator, you can either:
|
||||
|
||||
1. Add it to the `decorators` list on the class, which will apply it to
|
||||
all methods on the class; or
|
||||
2. Add it to the method directly, which will only apply it to that method.
|
||||
|
||||
```python
|
||||
class DummyView(HTTPMethodView):
|
||||
decorators = [my_decorator]
|
||||
...
|
||||
|
||||
# or
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
@my_decorator
|
||||
def get(self, request: Request):
|
||||
...
|
||||
```
|
||||
|
||||
One catch is that you need to be mindful that the call inside the decorator
|
||||
may need to account for the `self` argument, which is passed to the method
|
||||
as the first argument. Alternatively, you may want to also mark your method
|
||||
as `staticmethod` to avoid this.
|
||||
|
||||
Available attributes at the time of subclassing:
|
||||
- **attach** (Optional[Union[Sanic, Blueprint]]): The app or blueprint to
|
||||
attach the view to.
|
||||
- **uri** (str): The uri to attach the view to.
|
||||
- **methods** (Iterable[str]): The HTTP methods to attach the view to.
|
||||
Defaults to `{"GET"}`.
|
||||
- **host** (Optional[str]): The host to attach the view to.
|
||||
- **strict_slashes** (Optional[bool]): Whether to add a redirect rule for
|
||||
the uri with a trailing slash.
|
||||
- **version** (Optional[int]): The version to attach the view to.
|
||||
- **name** (Optional[str]): The name to attach the view to.
|
||||
- **stream** (bool): Whether the view is a stream handler.
|
||||
- **version_prefix** (str): The prefix to use for the version. Defaults
|
||||
to `"/v"`.
|
||||
"""
|
||||
|
||||
get: Optional[Callable[..., Any]]
|
||||
|
||||
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
|
||||
|
||||
def __init_subclass__(
|
||||
@@ -79,17 +144,46 @@ class HTTPMethodView:
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def dispatch_request(self, request, *args, **kwargs):
|
||||
def dispatch_request(self, request: Request, *args, **kwargs):
|
||||
"""Dispatch request to appropriate handler method."""
|
||||
handler = getattr(self, request.method.lower(), None)
|
||||
if not handler and request.method == "HEAD":
|
||||
handler = self.get
|
||||
if not handler:
|
||||
# The router will never allow us to get here, but this is
|
||||
# included as a fallback and for completeness.
|
||||
raise NotImplementedError(
|
||||
f"{request.method} is not supported for this endpoint."
|
||||
)
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, *class_args: Any, **class_kwargs: Any) -> RouteHandler:
|
||||
"""Return view function for use with the routing system, that
|
||||
dispatches request to appropriate handler method.
|
||||
"""
|
||||
"""Return view function for use with the routing system, that dispatches request to appropriate handler method.
|
||||
|
||||
If you need to pass arguments to the class's constructor, you can
|
||||
pass the arguments to `as_view` and they will be passed to the class
|
||||
`__init__` method.
|
||||
|
||||
Args:
|
||||
*class_args: Variable length argument list for the class instantiation.
|
||||
**class_kwargs: Arbitrary keyword arguments for the class instantiation.
|
||||
|
||||
Returns:
|
||||
RouteHandler: The view function.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
class DummyView(HTTPMethodView):
|
||||
def __init__(self, foo: MyFoo):
|
||||
self.foo = foo
|
||||
|
||||
async def get(self, request: Request):
|
||||
return text(self.foo.bar)
|
||||
|
||||
app.add_route(DummyView.as_view(foo=MyFoo()), "/")
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
@@ -119,6 +213,20 @@ class HTTPMethodView:
|
||||
stream: bool = False,
|
||||
version_prefix: str = "/v",
|
||||
) -> None:
|
||||
"""Attaches the view to a Sanic app or Blueprint at the specified URI.
|
||||
|
||||
Args:
|
||||
cls: The class that this method is part of.
|
||||
to (Union[Sanic, Blueprint]): The Sanic application or Blueprint to attach to.
|
||||
uri (str): The URI to bind the view to.
|
||||
methods (Iterable[str], optional): A collection of HTTP methods that the view should respond to. Defaults to `frozenset({"GET"})`.
|
||||
host (Optional[str], optional): A specific host or hosts to bind the view to. Defaults to `None`.
|
||||
strict_slashes (Optional[bool], optional): Enforce or not the trailing slash. Defaults to `None`.
|
||||
version (Optional[int], optional): Version of the API if versioning is used. Defaults to `None`.
|
||||
name (Optional[str], optional): Unique name for the route. Defaults to `None`.
|
||||
stream (bool, optional): Enable or disable streaming for the view. Defaults to `False`.
|
||||
version_prefix (str, optional): The prefix for the version, if versioning is used. Defaults to `"/v"`.
|
||||
""" # noqa: E501
|
||||
to.add_route(
|
||||
cls.as_view(),
|
||||
uri=uri,
|
||||
@@ -133,5 +241,6 @@ class HTTPMethodView:
|
||||
|
||||
|
||||
def stream(func):
|
||||
"""Decorator to mark a function as a stream handler."""
|
||||
func.is_stream = True
|
||||
return func
|
||||
|
||||
@@ -4,11 +4,15 @@ from sanic.compat import UpperStrEnum
|
||||
|
||||
|
||||
class RestartOrder(UpperStrEnum):
|
||||
"""Available restart orders."""
|
||||
|
||||
SHUTDOWN_FIRST = auto()
|
||||
STARTUP_FIRST = auto()
|
||||
|
||||
|
||||
class ProcessState(IntEnum):
|
||||
"""Process states."""
|
||||
|
||||
IDLE = auto()
|
||||
RESTARTING = auto()
|
||||
STARTING = auto()
|
||||
|
||||
@@ -15,6 +15,27 @@ from sanic.response import json
|
||||
|
||||
|
||||
class Inspector:
|
||||
"""Inspector for Sanic workers.
|
||||
|
||||
This class is used to create an inspector for Sanic workers. It is
|
||||
instantiated by the worker class and is used to create a Sanic app
|
||||
that can be used to inspect and control the workers and the server.
|
||||
|
||||
It is not intended to be used directly by the user.
|
||||
|
||||
See [Inspector](/en/guide/deployment/inspector) for more information.
|
||||
|
||||
Args:
|
||||
publisher (Connection): The connection to the worker.
|
||||
app_info (Dict[str, Any]): Information about the app.
|
||||
worker_state (Mapping[str, Any]): The state of the worker.
|
||||
host (str): The host to bind the inspector to.
|
||||
port (int): The port to bind the inspector to.
|
||||
api_key (str): The API key to use for authentication.
|
||||
tls_key (Union[Path, str, Default]): The path to the TLS key file.
|
||||
tls_cert (Union[Path, str, Default]): The path to the TLS cert file.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
publisher: Connection,
|
||||
@@ -100,12 +121,26 @@ class Inspector:
|
||||
return obj
|
||||
|
||||
def reload(self, zero_downtime: bool = False) -> None:
|
||||
"""Reload the workers
|
||||
|
||||
Args:
|
||||
zero_downtime (bool, optional): Whether to use zero downtime
|
||||
reload. Defaults to `False`.
|
||||
"""
|
||||
message = "__ALL_PROCESSES__:"
|
||||
if zero_downtime:
|
||||
message += ":STARTUP_FIRST"
|
||||
self._publisher.send(message)
|
||||
|
||||
def scale(self, replicas) -> str:
|
||||
def scale(self, replicas: Union[str, int]) -> str:
|
||||
"""Scale the number of workers
|
||||
|
||||
Args:
|
||||
replicas (Union[str, int]): The number of workers to scale to.
|
||||
|
||||
Returns:
|
||||
str: A log message.
|
||||
"""
|
||||
num_workers = 1
|
||||
if replicas:
|
||||
num_workers = int(replicas)
|
||||
@@ -116,5 +151,6 @@ class Inspector:
|
||||
return log_msg
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Shutdown the workers"""
|
||||
message = "__TERMINATE__"
|
||||
self._publisher.send(message)
|
||||
|
||||
@@ -21,6 +21,20 @@ DEFAULT_APP_NAME = "app"
|
||||
|
||||
|
||||
class AppLoader:
|
||||
"""A helper to load application instances.
|
||||
|
||||
Generally used by the worker to load the application instance.
|
||||
|
||||
See [Dynamic Applications](/en/guide/deployment/app-loader) for information on when you may need to use this.
|
||||
|
||||
Args:
|
||||
module_input (str): The module to load the application from.
|
||||
as_factory (bool): Whether the application is a factory.
|
||||
as_simple (bool): Whether the application is a simple server.
|
||||
args (Any): Arguments to pass to the application factory.
|
||||
factory (Callable[[], SanicApp]): A callable that returns a Sanic application instance.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module_input: str = "",
|
||||
|
||||
@@ -21,6 +21,30 @@ else:
|
||||
|
||||
|
||||
class WorkerManager:
|
||||
"""Manage all of the processes.
|
||||
|
||||
This class is used to manage all of the processes. It is instantiated
|
||||
by Sanic when in multiprocess mode (which is OOTB default) and is used
|
||||
to start, stop, and restart the worker processes.
|
||||
|
||||
You can access it to interact with it **ONLY** when on the main process.
|
||||
|
||||
Therefore, you should really only access it from within the
|
||||
`main_process_ready` event listener.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic("MyApp")
|
||||
|
||||
@app.main_process_ready
|
||||
async def ready(app: Sanic, _):
|
||||
app.manager.manage("MyProcess", my_process, {"foo": "bar"})
|
||||
```
|
||||
|
||||
See [Worker Manager](/en/guide/deployment/manager) for more information.
|
||||
"""
|
||||
|
||||
THRESHOLD = WorkerProcess.THRESHOLD
|
||||
MAIN_IDENT = "Sanic-Main"
|
||||
|
||||
@@ -62,24 +86,20 @@ class WorkerManager:
|
||||
transient: bool = False,
|
||||
workers: int = 1,
|
||||
) -> Worker:
|
||||
"""
|
||||
Instruct Sanic to manage a custom process.
|
||||
"""Instruct Sanic to manage a custom process.
|
||||
|
||||
:param ident: A name for the worker process
|
||||
:type ident: str
|
||||
:param func: The function to call in the background process
|
||||
:type func: Callable[..., Any]
|
||||
:param kwargs: Arguments to pass to the function
|
||||
:type kwargs: Dict[str, Any]
|
||||
:param transient: Whether to mark the process as transient. If True
|
||||
then the Worker Manager will restart the process along
|
||||
with any global restart (ex: auto-reload), defaults to False
|
||||
:type transient: bool, optional
|
||||
:param workers: The number of worker processes to run, defaults to 1
|
||||
:type workers: int, optional
|
||||
:return: The Worker instance
|
||||
:rtype: Worker
|
||||
"""
|
||||
Args:
|
||||
ident (str): A name for the worker process
|
||||
func (Callable[..., Any]): The function to call in the background process
|
||||
kwargs (Dict[str, Any]): Arguments to pass to the function
|
||||
transient (bool, optional): Whether to mark the process as transient. If `True`
|
||||
then the Worker Manager will restart the process along
|
||||
with any global restart (ex: auto-reload), defaults to `False`
|
||||
workers (int, optional): The number of worker processes to run. Defaults to `1`.
|
||||
|
||||
Returns:
|
||||
Worker: The Worker instance
|
||||
""" # noqa: E501
|
||||
container = self.transient if transient else self.durable
|
||||
worker = Worker(
|
||||
ident, func, kwargs, self.context, self.worker_state, workers
|
||||
@@ -88,6 +108,11 @@ class WorkerManager:
|
||||
return worker
|
||||
|
||||
def create_server(self) -> Worker:
|
||||
"""Create a new server process.
|
||||
|
||||
Returns:
|
||||
Worker: The Worker instance
|
||||
"""
|
||||
server_number = next(self._server_count)
|
||||
return self.manage(
|
||||
f"{WorkerProcess.SERVER_LABEL}-{server_number}",
|
||||
@@ -97,6 +122,12 @@ class WorkerManager:
|
||||
)
|
||||
|
||||
def shutdown_server(self, ident: Optional[str] = None) -> None:
|
||||
"""Shutdown a server process.
|
||||
|
||||
Args:
|
||||
ident (Optional[str], optional): The name of the server process to shutdown.
|
||||
If `None` then a random server will be chosen. Defaults to `None`.
|
||||
""" # noqa: E501
|
||||
if not ident:
|
||||
servers = [
|
||||
worker
|
||||
@@ -118,16 +149,19 @@ class WorkerManager:
|
||||
del self.transient[worker.ident]
|
||||
|
||||
def run(self):
|
||||
"""Run the worker manager."""
|
||||
self.start()
|
||||
self.monitor()
|
||||
self.join()
|
||||
self.terminate()
|
||||
|
||||
def start(self):
|
||||
"""Start the worker processes."""
|
||||
for process in self.processes:
|
||||
process.start()
|
||||
|
||||
def join(self):
|
||||
"""Join the worker processes."""
|
||||
logger.debug("Joining processes", extra={"verbosity": 1})
|
||||
joined = set()
|
||||
for process in self.processes:
|
||||
@@ -143,6 +177,7 @@ class WorkerManager:
|
||||
self.join()
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate the worker processes."""
|
||||
if not self._shutting_down:
|
||||
for process in self.processes:
|
||||
process.terminate()
|
||||
@@ -153,6 +188,14 @@ class WorkerManager:
|
||||
restart_order=RestartOrder.SHUTDOWN_FIRST,
|
||||
**kwargs,
|
||||
):
|
||||
"""Restart the worker processes.
|
||||
|
||||
Args:
|
||||
process_names (Optional[List[str]], optional): The names of the processes to restart.
|
||||
If `None` then all processes will be restarted. Defaults to `None`.
|
||||
restart_order (RestartOrder, optional): The order in which to restart the processes.
|
||||
Defaults to `RestartOrder.SHUTDOWN_FIRST`.
|
||||
""" # noqa: E501
|
||||
for process in self.transient_processes:
|
||||
if not process_names or process.name in process_names:
|
||||
process.restart(restart_order=restart_order, **kwargs)
|
||||
@@ -180,6 +223,17 @@ class WorkerManager:
|
||||
self.num_server = num_worker
|
||||
|
||||
def monitor(self):
|
||||
"""Monitor the worker processes.
|
||||
|
||||
First, wait for all of the workers to acknowledge that they are ready.
|
||||
Then, wait for messages from the workers. If a message is received
|
||||
then it is processed and the state of the worker is updated.
|
||||
|
||||
Also used to restart, shutdown, and scale the workers.
|
||||
|
||||
Raises:
|
||||
ServerKilled: Raised when a worker fails to come online.
|
||||
"""
|
||||
self.wait_for_ack()
|
||||
while True:
|
||||
try:
|
||||
@@ -228,6 +282,7 @@ class WorkerManager:
|
||||
break
|
||||
|
||||
def wait_for_ack(self): # no cov
|
||||
"""Wait for all of the workers to acknowledge that they are ready."""
|
||||
misses = 0
|
||||
message = (
|
||||
"It seems that one or more of your workers failed to come "
|
||||
@@ -262,27 +317,32 @@ class WorkerManager:
|
||||
|
||||
@property
|
||||
def workers(self) -> List[Worker]:
|
||||
"""Get all of the workers."""
|
||||
return list(self.transient.values()) + list(self.durable.values())
|
||||
|
||||
@property
|
||||
def processes(self):
|
||||
"""Get all of the processes."""
|
||||
for worker in self.workers:
|
||||
for process in worker.processes:
|
||||
yield process
|
||||
|
||||
@property
|
||||
def transient_processes(self):
|
||||
"""Get all of the transient processes."""
|
||||
for worker in self.transient.values():
|
||||
for process in worker.processes:
|
||||
yield process
|
||||
|
||||
def kill(self):
|
||||
"""Kill all of the processes."""
|
||||
for process in self.processes:
|
||||
logger.info("Killing %s [%s]", process.name, process.pid)
|
||||
os.kill(process.pid, SIGKILL)
|
||||
raise ServerKilled
|
||||
|
||||
def shutdown_signal(self, signal, frame):
|
||||
"""Handle the shutdown signal."""
|
||||
if self._shutting_down:
|
||||
logger.info("Shutdown interrupted. Killing.")
|
||||
with suppress(ServerKilled):
|
||||
@@ -293,6 +353,7 @@ class WorkerManager:
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the worker manager."""
|
||||
for process in self.processes:
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
@@ -300,6 +361,7 @@ class WorkerManager:
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
"""Get the process ID of the main process."""
|
||||
return os.getpid()
|
||||
|
||||
def _all_workers_ack(self):
|
||||
|
||||
@@ -8,6 +8,16 @@ from sanic.worker.state import WorkerState
|
||||
|
||||
|
||||
class WorkerMultiplexer:
|
||||
"""Multiplexer for Sanic workers.
|
||||
|
||||
This is instantiated inside of worker porocesses only. It is used to
|
||||
communicate with the monitor process.
|
||||
|
||||
Args:
|
||||
monitor_publisher (Connection): The connection to the monitor.
|
||||
worker_state (Dict[str, Any]): The state of the worker.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
monitor_publisher: Connection,
|
||||
@@ -17,6 +27,7 @@ class WorkerMultiplexer:
|
||||
self._state = WorkerState(worker_state, self.name)
|
||||
|
||||
def ack(self):
|
||||
"""Acknowledge the worker is ready."""
|
||||
logger.debug(
|
||||
f"{Colors.BLUE}Process ack: {Colors.BOLD}{Colors.SANIC}"
|
||||
f"%s {Colors.BLUE}[%s]{Colors.END}",
|
||||
@@ -34,6 +45,13 @@ class WorkerMultiplexer:
|
||||
all_workers: bool = False,
|
||||
zero_downtime: bool = False,
|
||||
):
|
||||
"""Restart the worker.
|
||||
|
||||
Args:
|
||||
name (str): The name of the process to restart.
|
||||
all_workers (bool): Whether to restart all workers.
|
||||
zero_downtime (bool): Whether to restart with zero downtime.
|
||||
"""
|
||||
if name and all_workers:
|
||||
raise ValueError(
|
||||
"Ambiguous restart with both a named process and"
|
||||
@@ -48,27 +66,42 @@ class WorkerMultiplexer:
|
||||
self._monitor_publisher.send(name)
|
||||
|
||||
reload = restart # no cov
|
||||
"""Alias for restart."""
|
||||
|
||||
def scale(self, num_workers: int):
|
||||
"""Scale the number of workers.
|
||||
|
||||
Args:
|
||||
num_workers (int): The number of workers to scale to.
|
||||
"""
|
||||
message = f"__SCALE__:{num_workers}"
|
||||
self._monitor_publisher.send(message)
|
||||
|
||||
def terminate(self, early: bool = False):
|
||||
"""Terminate the worker.
|
||||
|
||||
Args:
|
||||
early (bool): Whether to terminate early.
|
||||
"""
|
||||
message = "__TERMINATE_EARLY__" if early else "__TERMINATE__"
|
||||
self._monitor_publisher.send(message)
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
"""The process ID of the worker."""
|
||||
return getpid()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of the worker."""
|
||||
return environ.get("SANIC_WORKER_NAME", "")
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""The state of the worker."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def workers(self) -> Dict[str, Any]:
|
||||
"""The state of all workers."""
|
||||
return self.state.full()
|
||||
|
||||
@@ -17,6 +17,8 @@ def get_now():
|
||||
|
||||
|
||||
class WorkerProcess:
|
||||
"""A worker process."""
|
||||
|
||||
THRESHOLD = 300 # == 30 seconds
|
||||
SERVER_LABEL = "Server"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user