Conversion of User Guide to the SHH stack (#2781)

This commit is contained in:
Adam Hopkins
2023-09-06 15:44:00 +03:00
committed by GitHub
parent 47215d4635
commit d255d1aae1
332 changed files with 51495 additions and 2013 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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"

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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=[])

View File

@@ -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(

View File

@@ -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()

View File

@@ -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 "

View File

@@ -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. "

View File

@@ -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("&", "&amp;").replace("<", "&lt;")
@@ -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 = {}

View File

@@ -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"

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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")

View File

@@ -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(

View File

@@ -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,

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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:

View File

@@ -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__ = ()

View File

@@ -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 = ""

View File

@@ -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()

View File

@@ -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__(

View File

@@ -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__(

View File

@@ -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 = {}

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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",

View File

@@ -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():

View File

@@ -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):

View File

@@ -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:

View File

@@ -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 "

View File

@@ -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()

View File

@@ -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:

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 = "",

View File

@@ -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):

View File

@@ -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()

View File

@@ -17,6 +17,8 @@ def get_now():
class WorkerProcess:
"""A worker process."""
THRESHOLD = 300 # == 30 seconds
SERVER_LABEL = "Server"