Conversion of User Guide to the SHH stack (#2781)
This commit is contained in:
@@ -13,13 +13,15 @@ from .parameters import RequestParameters
|
||||
|
||||
|
||||
class File(NamedTuple):
|
||||
"""
|
||||
Model for defining a file. It is a ``namedtuple``, therefore you can
|
||||
iterate over the object, or access the parameters by name.
|
||||
"""Model for defining a file.
|
||||
|
||||
:param type: The mimetype, defaults to text/plain
|
||||
:param body: Bytes of the file
|
||||
:param name: The filename
|
||||
It is a `namedtuple`, therefore you can iterate over the object, or
|
||||
access the parameters by name.
|
||||
|
||||
Args:
|
||||
type (str, optional): The mimetype, defaults to "text/plain".
|
||||
body (bytes): Bytes of the file.
|
||||
name (str): The filename.
|
||||
"""
|
||||
|
||||
type: str
|
||||
@@ -28,13 +30,15 @@ class File(NamedTuple):
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parse a request body and returns fields and files
|
||||
"""Parse a request body and returns fields and files
|
||||
|
||||
:param body: bytes request body
|
||||
:param boundary: bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
"""
|
||||
Args:
|
||||
body (bytes): Bytes request body.
|
||||
boundary (bytes): Bytes multipart boundary.
|
||||
|
||||
Returns:
|
||||
Tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`.
|
||||
""" # noqa: E501
|
||||
files = {}
|
||||
fields = {}
|
||||
|
||||
|
||||
@@ -4,19 +4,30 @@ from typing import Any, Optional
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
"""Hosts a dict with lists as values where get returns the first value of the list and getlist returns the whole shebang""" # noqa: E501
|
||||
|
||||
def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]:
|
||||
"""Return the first value, either the default or actual"""
|
||||
"""Return the first value, either the default or actual
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter
|
||||
default (Optional[Any], optional): The default value. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The first value of the list
|
||||
""" # noqa: E501
|
||||
return super().get(name, [default])[0]
|
||||
|
||||
def getlist(
|
||||
self, name: str, default: Optional[Any] = None
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
Return the entire list
|
||||
"""
|
||||
"""Return the entire list
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter
|
||||
default (Optional[Any], optional): The default value. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The entire list
|
||||
""" # noqa: E501
|
||||
return super().get(name, default)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import BaseProtocol
|
||||
from contextvars import ContextVar
|
||||
from inspect import isawaitable
|
||||
from types import SimpleNamespace
|
||||
@@ -86,8 +87,17 @@ ctx_type = TypeVar("ctx_type")
|
||||
|
||||
|
||||
class Request(Generic[sanic_type, ctx_type]):
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""State of HTTP request.
|
||||
|
||||
Args:
|
||||
url_bytes (bytes): Raw URL bytes.
|
||||
headers (Header): Request headers.
|
||||
version (str): HTTP version.
|
||||
method (str): HTTP method.
|
||||
transport (TransportProtocol): Transport protocol.
|
||||
app (Sanic): Sanic instance.
|
||||
head (bytes, optional): Request head. Defaults to `b""`.
|
||||
stream_id (int, optional): HTTP/3 stream ID. Defaults to `0`.
|
||||
"""
|
||||
|
||||
_current: ContextVar[Request] = ContextVar("request")
|
||||
@@ -186,7 +196,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
self.route: Optional[Route] = None
|
||||
self.stream: Optional[Stream] = None
|
||||
self._match_info: Dict[str, Any] = {}
|
||||
self._protocol = None
|
||||
self._protocol: Optional[BaseProtocol] = None
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
@@ -194,53 +204,88 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@staticmethod
|
||||
def make_context() -> ctx_type:
|
||||
"""Create a new context object.
|
||||
|
||||
This method is called when a new request context is pushed. It is
|
||||
a great candidate for overriding in a subclass if you want to
|
||||
control the type of context object that is created.
|
||||
|
||||
By default, it returns a `types.SimpleNamespace` instance.
|
||||
|
||||
Returns:
|
||||
ctx_type: A new context object.
|
||||
"""
|
||||
return cast(ctx_type, SimpleNamespace())
|
||||
|
||||
@classmethod
|
||||
def get_current(cls) -> Request:
|
||||
"""
|
||||
Retrieve the current request object
|
||||
"""Retrieve the current request object
|
||||
|
||||
This implements `Context Variables
|
||||
<https://docs.python.org/3/library/contextvars.html>`_
|
||||
This implements [Context Variables](https://docs.python.org/3/library/contextvars.html)
|
||||
to allow for accessing the current request from anywhere.
|
||||
|
||||
Raises :exc:`sanic.exceptions.ServerError` if it is outside of
|
||||
a request lifecycle.
|
||||
A typical usecase is when you want to access the current request
|
||||
from a function that is not a handler, such as a logging function:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
import logging
|
||||
|
||||
from sanic import Request
|
||||
class LoggingFormater(logging.Formatter):
|
||||
def format(self, record):
|
||||
request = Request.get_current()
|
||||
record.url = request.url
|
||||
record.ip = request.ip
|
||||
return super().format(record)
|
||||
```
|
||||
|
||||
current_request = Request.get_current()
|
||||
Returns:
|
||||
Request: The current request object
|
||||
|
||||
:return: the current :class:`sanic.request.Request`
|
||||
"""
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If it is outside of a request
|
||||
lifecycle.
|
||||
""" # noqa: E501
|
||||
request = cls._current.get(None)
|
||||
if not request:
|
||||
raise ServerError("No current request")
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def generate_id(*_):
|
||||
def generate_id(*_) -> Union[uuid.UUID, str, int]:
|
||||
"""Generate a unique ID for the request.
|
||||
|
||||
This method is called to generate a unique ID for each request.
|
||||
By default, it returns a `uuid.UUID` instance.
|
||||
|
||||
Returns:
|
||||
Union[uuid.UUID, str, int]: A unique ID for the request.
|
||||
"""
|
||||
return uuid.uuid4()
|
||||
|
||||
@property
|
||||
def ctx(self) -> ctx_type:
|
||||
"""
|
||||
:return: The current request context
|
||||
"""The current request context.
|
||||
|
||||
This is a context object for the current request. It is created
|
||||
by `Request.make_context` and is a great place to store data
|
||||
that you want to be accessible during the request lifecycle.
|
||||
|
||||
Returns:
|
||||
ctx_type: The current request context.
|
||||
"""
|
||||
if not self._ctx:
|
||||
self._ctx = self.make_context()
|
||||
return self._ctx
|
||||
|
||||
@property
|
||||
def stream_id(self):
|
||||
"""
|
||||
Access the HTTP/3 stream ID.
|
||||
def stream_id(self) -> int:
|
||||
"""Access the HTTP/3 stream ID.
|
||||
|
||||
Raises :exc:`sanic.exceptions.ServerError` if it is not an
|
||||
HTTP/3 request.
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If the request is not HTTP/3.
|
||||
|
||||
Returns:
|
||||
int: The HTTP/3 stream ID.
|
||||
"""
|
||||
if self.protocol.version is not HTTP.VERSION_3:
|
||||
raise ServerError(
|
||||
@@ -248,7 +293,17 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
)
|
||||
return self._stream_id
|
||||
|
||||
def reset_response(self):
|
||||
def reset_response(self) -> None:
|
||||
"""Reset the response object.
|
||||
|
||||
This clears much of the state of the object. It should
|
||||
generally not be called directly, but is called automatically as
|
||||
part of the request lifecycle.
|
||||
|
||||
Raises:
|
||||
sanic.exceptions.ServerError: If the response has already been
|
||||
sent.
|
||||
"""
|
||||
try:
|
||||
if (
|
||||
self.stream is not None
|
||||
@@ -257,8 +312,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
raise ServerError(
|
||||
"Cannot reset response because previous response was sent."
|
||||
)
|
||||
self.stream.response.stream = None
|
||||
self.stream.response = None
|
||||
self.stream.response.stream = None # type: ignore
|
||||
self.stream.response = None # type: ignore
|
||||
self.responded = False
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -280,44 +335,44 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
**The first typical usecase** is if you wish to respond to the
|
||||
request without returning from the handler:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
data = ... # Process something
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
data = ... # Process something
|
||||
json_response = json({"data": data})
|
||||
await request.respond(json_response)
|
||||
|
||||
json_response = json({"data": data})
|
||||
await request.respond(json_response)
|
||||
|
||||
# You are now free to continue executing other code
|
||||
...
|
||||
|
||||
@app.on_response
|
||||
async def add_header(_, response: HTTPResponse):
|
||||
# Middlewares still get executed as expected
|
||||
response.headers["one"] = "two"
|
||||
@app.on_response
|
||||
async def add_header(_, response: HTTPResponse):
|
||||
# Middlewares still get executed as expected
|
||||
response.headers["one"] = "two"
|
||||
```
|
||||
|
||||
**The second possible usecase** is for when you want to directly
|
||||
respond to the request:
|
||||
|
||||
.. code-block:: python
|
||||
```python
|
||||
response = await request.respond(content_type="text/csv")
|
||||
await response.send("foo,")
|
||||
await response.send("bar")
|
||||
|
||||
response = await request.respond(content_type="text/csv")
|
||||
await response.send("foo,")
|
||||
await response.send("bar")
|
||||
# You can control the completion of the response by calling
|
||||
# the 'eof()' method:
|
||||
await response.eof()
|
||||
```
|
||||
|
||||
# You can control the completion of the response by calling
|
||||
# the 'eof()' method:
|
||||
await response.eof()
|
||||
Args:
|
||||
response (ResponseType): Response instance to send.
|
||||
status (int): Status code to return in the response.
|
||||
headers (Optional[Dict[str, str]]): Headers to return in the response, defaults to None.
|
||||
content_type (Optional[str]): Content-Type header of the response, defaults to None.
|
||||
|
||||
:param response: response instance to send
|
||||
:param status: status code to return in the response
|
||||
:param headers: headers to return in the response
|
||||
:param content_type: Content-Type header of the response
|
||||
:return: final response being sent (may be different from the
|
||||
``response`` parameter because of middlewares) which can be
|
||||
used to manually send data
|
||||
"""
|
||||
Returns:
|
||||
FinalResponseType: Final response being sent (may be different from the
|
||||
"response" parameter because of middlewares), which can be
|
||||
used to manually send data.
|
||||
""" # noqa: E501
|
||||
try:
|
||||
if self.stream is not None and self.stream.response:
|
||||
raise ServerError("Second respond call is not allowed.")
|
||||
@@ -370,17 +425,16 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""
|
||||
The route name
|
||||
"""The route name
|
||||
|
||||
In the following pattern:
|
||||
|
||||
.. code-block::
|
||||
```
|
||||
<AppName>.[<BlueprintName>.]<HandlerName>
|
||||
```
|
||||
|
||||
<AppName>.[<BlueprintName>.]<HandlerName>
|
||||
|
||||
:return: Route name
|
||||
:rtype: Optional[str]
|
||||
Returns:
|
||||
Optional[str]: The route name
|
||||
"""
|
||||
if self._name:
|
||||
return self._name
|
||||
@@ -390,74 +444,85 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def endpoint(self) -> Optional[str]:
|
||||
"""
|
||||
:return: Alias of :attr:`sanic.request.Request.name`
|
||||
:rtype: Optional[str]
|
||||
"""Alias of `sanic.request.Request.name`
|
||||
|
||||
Returns:
|
||||
Optional[str]: The route name
|
||||
"""
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def uri_template(self) -> Optional[str]:
|
||||
"""
|
||||
:return: The defined URI template
|
||||
:rtype: Optional[str]
|
||||
"""The defined URI template
|
||||
|
||||
Returns:
|
||||
Optional[str]: The defined URI template
|
||||
"""
|
||||
if self.route:
|
||||
return f"/{self.route.path}"
|
||||
return None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
"""
|
||||
:return: The HTTP protocol instance
|
||||
def protocol(self) -> TransportProtocol:
|
||||
"""The HTTP protocol instance
|
||||
|
||||
Returns:
|
||||
Protocol: The HTTP protocol instance
|
||||
"""
|
||||
if not self._protocol:
|
||||
self._protocol = self.transport.get_protocol()
|
||||
return self._protocol
|
||||
return self._protocol # type: ignore
|
||||
|
||||
@property
|
||||
def raw_headers(self) -> bytes:
|
||||
"""
|
||||
:return: The unparsed HTTP headers
|
||||
:rtype: bytes
|
||||
"""The unparsed HTTP headers
|
||||
|
||||
Returns:
|
||||
bytes: The unparsed HTTP headers
|
||||
"""
|
||||
_, headers = self.head.split(b"\r\n", 1)
|
||||
return bytes(headers)
|
||||
|
||||
@property
|
||||
def request_line(self) -> bytes:
|
||||
"""
|
||||
:return: The first line of a HTTP request
|
||||
:rtype: bytes
|
||||
"""The first line of a HTTP request
|
||||
|
||||
Returns:
|
||||
bytes: The first line of a HTTP request
|
||||
"""
|
||||
reqline, _ = self.head.split(b"\r\n", 1)
|
||||
return bytes(reqline)
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[Union[uuid.UUID, str, int]]:
|
||||
"""
|
||||
A request ID passed from the client, or generated from the backend.
|
||||
"""A request ID passed from the client, or generated from the backend.
|
||||
|
||||
By default, this will look in a request header defined at:
|
||||
``self.app.config.REQUEST_ID_HEADER``. It defaults to
|
||||
``X-Request-ID``. Sanic will try to cast the ID into a ``UUID`` or an
|
||||
``int``. If there is not a UUID from the client, then Sanic will try
|
||||
to generate an ID by calling ``Request.generate_id()``. The default
|
||||
behavior is to generate a ``UUID``. You can customize this behavior
|
||||
by subclassing ``Request``.
|
||||
`self.app.config.REQUEST_ID_HEADER`. It defaults to
|
||||
`X-Request-ID`. Sanic will try to cast the ID into a `UUID` or an
|
||||
`int`.
|
||||
|
||||
.. code-block:: python
|
||||
If there is not a UUID from the client, then Sanic will try
|
||||
to generate an ID by calling `Request.generate_id()`. The default
|
||||
behavior is to generate a `UUID`. You can customize this behavior
|
||||
by subclassing `Request` and overwriting that method.
|
||||
|
||||
from sanic import Request, Sanic
|
||||
from itertools import count
|
||||
```python
|
||||
from sanic import Request, Sanic
|
||||
from itertools import count
|
||||
|
||||
class IntRequest(Request):
|
||||
counter = count()
|
||||
class IntRequest(Request):
|
||||
counter = count()
|
||||
|
||||
def generate_id(self):
|
||||
return next(self.counter)
|
||||
def generate_id(self):
|
||||
return next(self.counter)
|
||||
|
||||
app = Sanic("MyApp", request_class=IntRequest)
|
||||
app = Sanic("MyApp", request_class=IntRequest)
|
||||
```
|
||||
|
||||
Returns:
|
||||
Optional[Union[uuid.UUID, str, int]]: A request ID passed from the
|
||||
client, or generated from the backend.
|
||||
"""
|
||||
if not self._id:
|
||||
self._id = self.headers.getone(
|
||||
@@ -479,16 +544,28 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def json(self) -> Any:
|
||||
"""
|
||||
:return: The request body parsed as JSON
|
||||
:rtype: Any
|
||||
"""The request body parsed as JSON
|
||||
|
||||
Returns:
|
||||
Any: The request body parsed as JSON
|
||||
"""
|
||||
if self.parsed_json is None:
|
||||
self.load_json()
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
def load_json(self, loads=None):
|
||||
def load_json(self, loads=None) -> Any:
|
||||
"""Load the request body as JSON
|
||||
|
||||
Args:
|
||||
loads (Callable, optional): A custom JSON loader. Defaults to None.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request body cannot be parsed as JSON
|
||||
|
||||
Returns:
|
||||
Any: The request body parsed as JSON
|
||||
"""
|
||||
try:
|
||||
if not loads:
|
||||
loads = self.__class__._loads
|
||||
@@ -508,8 +585,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
A convenience handler for easier RFC-compliant matching of MIME types,
|
||||
parsed as a list that can match wildcards and includes */* by default.
|
||||
|
||||
:return: The ``Accept`` header parsed
|
||||
:rtype: AcceptList
|
||||
Returns:
|
||||
AcceptList: Accepted response content types
|
||||
"""
|
||||
if self.parsed_accept is None:
|
||||
self.parsed_accept = parse_accept(self.headers.get("accept"))
|
||||
@@ -519,7 +596,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def token(self) -> Optional[str]:
|
||||
"""Attempt to return the auth header token.
|
||||
|
||||
:return: token related to request
|
||||
Returns:
|
||||
Optional[str]: The auth header token
|
||||
"""
|
||||
if self.parsed_token is None:
|
||||
prefixes = ("Bearer", "Token")
|
||||
@@ -536,8 +614,9 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication
|
||||
schemas.
|
||||
|
||||
:return: A Credentials object with token, or username and password
|
||||
related to the request
|
||||
Returns:
|
||||
Optional[Credentials]: A Credentials object with token, or username
|
||||
and password related to the request
|
||||
"""
|
||||
if self.parsed_credentials is None:
|
||||
try:
|
||||
@@ -555,15 +634,14 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def get_form(
|
||||
self, keep_blank_values: bool = False
|
||||
) -> Optional[RequestParameters]:
|
||||
"""
|
||||
Method to extract and parse the form data from a request.
|
||||
"""Method to extract and parse the form data from a request.
|
||||
|
||||
:param keep_blank_values:
|
||||
Whether to discard blank values from the form data
|
||||
:type keep_blank_values: bool
|
||||
:return: the parsed form data
|
||||
:rtype: Optional[RequestParameters]
|
||||
"""
|
||||
Args:
|
||||
keep_blank_values (bool): Whether to discard blank values from the form data.
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The parsed form data.
|
||||
""" # noqa: E501
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.getone(
|
||||
@@ -592,9 +670,11 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
return self.parsed_form
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
"""
|
||||
:return: The request body parsed as form data
|
||||
def form(self) -> Optional[RequestParameters]:
|
||||
"""The request body parsed as form data
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The request body parsed as form data
|
||||
"""
|
||||
if self.parsed_form is None:
|
||||
self.get_form()
|
||||
@@ -602,10 +682,12 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
return self.parsed_form
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
"""
|
||||
:return: The request body parsed as uploaded files
|
||||
"""
|
||||
def files(self) -> Optional[RequestParameters]:
|
||||
"""The request body parsed as uploaded files
|
||||
|
||||
Returns:
|
||||
Optional[RequestParameters]: The request body parsed as uploaded files
|
||||
""" # noqa: E501
|
||||
if self.parsed_files is None:
|
||||
self.form # compute form to get files
|
||||
|
||||
@@ -618,32 +700,30 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
encoding: str = "utf-8",
|
||||
errors: str = "replace",
|
||||
) -> RequestParameters:
|
||||
"""
|
||||
Method to parse ``query_string`` using ``urllib.parse.parse_qs``.
|
||||
This methods is used by ``args`` property.
|
||||
Can be used directly if you need to change default parameters.
|
||||
"""Parse `query_string` using `urllib.parse.parse_qs`.
|
||||
|
||||
:param keep_blank_values:
|
||||
flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
:type keep_blank_values: bool
|
||||
:param strict_parsing:
|
||||
flag indicating what to do with parsing errors.
|
||||
If false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
:type strict_parsing: bool
|
||||
:param encoding:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type encoding: str
|
||||
:param errors:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type errors: str
|
||||
:return: RequestParameters
|
||||
This methods is used by the `args` property, but it also
|
||||
can be used directly if you need to change default parameters.
|
||||
|
||||
Args:
|
||||
keep_blank_values (bool): Flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A `True` value indicates that blanks should be retained as
|
||||
blank strings. The default `False` value indicates that
|
||||
blank values are to be ignored and treated as if they were
|
||||
not included.
|
||||
strict_parsing (bool): Flag indicating what to do with parsing
|
||||
errors. If `False` (the default), errors are silently ignored.
|
||||
If `True`, errors raise a `ValueError` exception.
|
||||
encoding (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
errors (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
|
||||
Returns:
|
||||
RequestParameters: A dictionary containing the parsed arguments.
|
||||
"""
|
||||
if (
|
||||
keep_blank_values,
|
||||
@@ -669,9 +749,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
]
|
||||
|
||||
args = property(get_args)
|
||||
"""
|
||||
Convenience property to access :meth:`Request.get_args` with
|
||||
default values.
|
||||
"""Convenience property to access `Request.get_args` with default values.
|
||||
"""
|
||||
|
||||
def get_query_args(
|
||||
@@ -681,32 +759,31 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
encoding: str = "utf-8",
|
||||
errors: str = "replace",
|
||||
) -> list:
|
||||
"""
|
||||
Method to parse `query_string` using `urllib.parse.parse_qsl`.
|
||||
This methods is used by `query_args` property.
|
||||
Can be used directly if you need to change default parameters.
|
||||
"""Parse `query_string` using `urllib.parse.parse_qsl`.
|
||||
|
||||
:param keep_blank_values:
|
||||
flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
:type keep_blank_values: bool
|
||||
:param strict_parsing:
|
||||
flag indicating what to do with parsing errors.
|
||||
If false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
:type strict_parsing: bool
|
||||
:param encoding:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type encoding: str
|
||||
:param errors:
|
||||
specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
:type errors: str
|
||||
:return: list
|
||||
This methods is used by `query_args` propertyn but can be used
|
||||
directly if you need to change default parameters.
|
||||
|
||||
Args:
|
||||
keep_blank_values (bool): Flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings.
|
||||
A `True` value indicates that blanks should be retained as
|
||||
blank strings. The default `False` value indicates that
|
||||
blank values are to be ignored and treated as if they were
|
||||
not included.
|
||||
strict_parsing (bool): Flag indicating what to do with
|
||||
parsing errors. If `False` (the default), errors are
|
||||
silently ignored. If `True`, errors raise a
|
||||
`ValueError` exception.
|
||||
encoding (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
errors (str): Specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the
|
||||
`bytes.decode()` method.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples containing the parsed arguments.
|
||||
"""
|
||||
if (
|
||||
keep_blank_values,
|
||||
@@ -729,10 +806,8 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
]
|
||||
|
||||
query_args = property(get_query_args)
|
||||
"""
|
||||
Convenience property to access :meth:`Request.get_query_args` with
|
||||
default values.
|
||||
"""
|
||||
"""Convenience property to access `Request.get_query_args` with default values.
|
||||
""" # noqa: E501
|
||||
|
||||
def get_cookies(self) -> RequestParameters:
|
||||
cookie = self.headers.getone("cookie", "")
|
||||
@@ -741,9 +816,10 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def cookies(self) -> RequestParameters:
|
||||
"""
|
||||
:return: Incoming cookies on the request
|
||||
:rtype: Dict[str, str]
|
||||
"""Incoming cookies on the request
|
||||
|
||||
Returns:
|
||||
RequestParameters: Incoming cookies on the request
|
||||
"""
|
||||
|
||||
if self.parsed_cookies is None:
|
||||
@@ -752,16 +828,19 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def content_type(self) -> str:
|
||||
"""
|
||||
:return: Content-Type header form the request
|
||||
:rtype: str
|
||||
"""Content-Type header form the request
|
||||
|
||||
Returns:
|
||||
str: Content-Type header form the request
|
||||
"""
|
||||
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
"""
|
||||
:return: matched info after resolving route
|
||||
def match_info(self) -> Dict[str, Any]:
|
||||
"""Matched path parameters after resolving route
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Matched path parameters after resolving route
|
||||
"""
|
||||
return self._match_info
|
||||
|
||||
@@ -769,53 +848,64 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
def match_info(self, value):
|
||||
self._match_info = value
|
||||
|
||||
# Transport properties (obtained from local interface only)
|
||||
|
||||
@property
|
||||
def ip(self) -> str:
|
||||
"""
|
||||
:return: peer ip of the socket
|
||||
:rtype: str
|
||||
"""Peer ip of the socket
|
||||
|
||||
Returns:
|
||||
str: Peer ip of the socket
|
||||
"""
|
||||
return self.conn_info.client_ip if self.conn_info else ""
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
"""
|
||||
:return: peer port of the socket
|
||||
:rtype: int
|
||||
"""Peer port of the socket
|
||||
|
||||
Returns:
|
||||
int: Peer port of the socket
|
||||
"""
|
||||
return self.conn_info.client_port if self.conn_info else 0
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
def socket(self) -> Union[Tuple[str, int], Tuple[None, None]]:
|
||||
"""Information about the connected socket if available
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[int]]: Information about the
|
||||
connected socket if available, in the form of a tuple of
|
||||
(ip, port)
|
||||
"""
|
||||
:return: Information about the connected socket if available
|
||||
"""
|
||||
return self.conn_info.peername if self.conn_info else (None, None)
|
||||
return (
|
||||
self.conn_info.peername
|
||||
if self.conn_info and self.conn_info.peername
|
||||
else (None, None)
|
||||
)
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
"""
|
||||
:return: path of the local HTTP request
|
||||
:rtype: str
|
||||
"""Path of the local HTTP request
|
||||
|
||||
Returns:
|
||||
str: Path of the local HTTP request
|
||||
"""
|
||||
return self._parsed_url.path.decode("utf-8")
|
||||
|
||||
@property
|
||||
def network_paths(self):
|
||||
"""
|
||||
Access the network paths if available
|
||||
def network_paths(self) -> Optional[List[Any]]:
|
||||
"""Access the network paths if available
|
||||
|
||||
Returns:
|
||||
Optional[List[Any]]: Access the network paths if available
|
||||
"""
|
||||
if self.conn_info is None:
|
||||
return None
|
||||
return self.conn_info.network_paths
|
||||
|
||||
# Proxy properties (using SERVER_NAME/forwarded/request/transport info)
|
||||
|
||||
@property
|
||||
def forwarded(self) -> Options:
|
||||
"""
|
||||
Active proxy information obtained from request headers, as specified in
|
||||
Sanic configuration.
|
||||
"""Active proxy information obtained from request headers, as specified in Sanic configuration.
|
||||
|
||||
Field names by, for, proto, host, port and path are normalized.
|
||||
- for and by IPv6 addresses are bracketed
|
||||
@@ -824,9 +914,9 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
Additional values may be available from new style Forwarded headers.
|
||||
|
||||
:return: forwarded address info
|
||||
:rtype: Dict[str, str]
|
||||
"""
|
||||
Returns:
|
||||
Options: proxy information from request headers
|
||||
""" # noqa: E501
|
||||
if self.parsed_forwarded is None:
|
||||
self.parsed_forwarded = (
|
||||
parse_forwarded(self.headers, self.app.config)
|
||||
@@ -837,11 +927,10 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def remote_addr(self) -> str:
|
||||
"""
|
||||
Client IP address, if available from proxy.
|
||||
"""Client IP address, if available from proxy.
|
||||
|
||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
"""
|
||||
if not hasattr(self, "_remote_addr"):
|
||||
self._remote_addr = str(self.forwarded.get("for", ""))
|
||||
@@ -858,21 +947,21 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
client address regardless of whether the service runs behind a proxy
|
||||
or not (proxy deployment needs separate configuration).
|
||||
|
||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||
"""
|
||||
return self.remote_addr or self.ip
|
||||
|
||||
@property
|
||||
def scheme(self) -> str:
|
||||
"""
|
||||
Determine request scheme.
|
||||
"""Determine request scheme.
|
||||
|
||||
1. `config.SERVER_NAME` if in full URL format
|
||||
2. proxied proto/scheme
|
||||
3. local connection protocol
|
||||
|
||||
:return: http|https|ws|wss or arbitrary value given by the headers.
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: http|https|ws|wss or arbitrary value given by the headers.
|
||||
"""
|
||||
if not hasattr(self, "_scheme"):
|
||||
if "//" in self.app.config.get("SERVER_NAME", ""):
|
||||
@@ -896,16 +985,16 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def host(self) -> str:
|
||||
"""
|
||||
The currently effective server 'host' (hostname or hostname:port).
|
||||
"""The currently effective server 'host' (hostname or hostname:port).
|
||||
|
||||
1. `config.SERVER_NAME` overrides any client headers
|
||||
2. proxied host of original request
|
||||
3. request host header
|
||||
hostname and port may be separated by
|
||||
`sanic.headers.parse_host(request.host)`.
|
||||
|
||||
:return: the first matching host found, or empty string
|
||||
:rtype: str
|
||||
Returns:
|
||||
str: the first matching host found, or empty string
|
||||
"""
|
||||
server_name = self.app.config.get("SERVER_NAME")
|
||||
if server_name:
|
||||
@@ -916,39 +1005,40 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
"""
|
||||
:return: hostname the client connected to, by ``request.host``
|
||||
:rtype: str
|
||||
"""hostname the client connected to, by `request.host`
|
||||
|
||||
Returns:
|
||||
str: hostname the client connected to, by `request.host`
|
||||
"""
|
||||
return parse_host(self.host)[0] or ""
|
||||
|
||||
@property
|
||||
def server_port(self) -> int:
|
||||
"""
|
||||
The port the client connected to, by forwarded ``port`` or
|
||||
``request.host``.
|
||||
"""The port the client connected to, by forwarded `port` or `request.host`.
|
||||
|
||||
Default port is returned as 80 and 443 based on ``request.scheme``.
|
||||
Default port is returned as 80 and 443 based on `request.scheme`.
|
||||
|
||||
:return: port number
|
||||
:rtype: int
|
||||
"""
|
||||
Returns:
|
||||
int: The port the client connected to, by forwarded `port` or `request.host`.
|
||||
""" # noqa: E501
|
||||
port = self.forwarded.get("port") or parse_host(self.host)[1]
|
||||
return int(port or (80 if self.scheme in ("http", "ws") else 443))
|
||||
|
||||
@property
|
||||
def server_path(self) -> str:
|
||||
"""
|
||||
:return: full path of current URL; uses proxied or local path
|
||||
:rtype: str
|
||||
"""Full path of current URL; uses proxied or local path
|
||||
|
||||
Returns:
|
||||
str: Full path of current URL; uses proxied or local path
|
||||
"""
|
||||
return str(self.forwarded.get("path") or self.path)
|
||||
|
||||
@property
|
||||
def query_string(self) -> str:
|
||||
"""
|
||||
:return: representation of the requested query
|
||||
:rtype: str
|
||||
"""Representation of the requested query
|
||||
|
||||
Returns:
|
||||
str: Representation of the requested query
|
||||
"""
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode("utf-8")
|
||||
@@ -957,23 +1047,28 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
:return: the URL
|
||||
:rtype: str
|
||||
"""The URL
|
||||
|
||||
Returns:
|
||||
str: The URL
|
||||
"""
|
||||
return urlunparse(
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs) -> str:
|
||||
"""
|
||||
Same as :func:`sanic.Sanic.url_for`, but automatically determine
|
||||
`scheme` and `netloc` base on the request. Since this method is aiming
|
||||
"""Retrieve a URL for a given view name.
|
||||
|
||||
Same as `sanic.Sanic.url_for`, but automatically determine `scheme`
|
||||
and `netloc` base on the request. Since this method is aiming
|
||||
to generate correct schema & netloc, `_external` is implied.
|
||||
|
||||
:param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for`
|
||||
:return: an absolute url to the given view
|
||||
:rtype: str
|
||||
Args:
|
||||
view_name (str): The view name to generate URL for.
|
||||
**kwargs: Arbitrary keyword arguments to build URL query string.
|
||||
|
||||
Returns:
|
||||
str: The generated URL.
|
||||
"""
|
||||
# Full URL SERVER_NAME can only be handled in app.url_for
|
||||
try:
|
||||
@@ -999,10 +1094,13 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def scope(self) -> ASGIScope:
|
||||
"""
|
||||
:return: The ASGI scope of the request.
|
||||
If the app isn't an ASGI app, then raises an exception.
|
||||
:rtype: Optional[ASGIScope]
|
||||
"""The ASGI scope of the request.
|
||||
|
||||
Returns:
|
||||
ASGIScope: The ASGI scope of the request.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the app isn't an ASGI app.
|
||||
"""
|
||||
if not self.app.asgi:
|
||||
raise NotImplementedError(
|
||||
@@ -1014,27 +1112,33 @@ class Request(Generic[sanic_type, ctx_type]):
|
||||
|
||||
@property
|
||||
def is_safe(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is safe.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is safe.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is safe.
|
||||
"""
|
||||
return self.method in SAFE_HTTP_METHODS
|
||||
|
||||
@property
|
||||
def is_idempotent(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is iempotent.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is iempotent.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is iempotent.
|
||||
"""
|
||||
return self.method in IDEMPOTENT_HTTP_METHODS
|
||||
|
||||
@property
|
||||
def is_cacheable(self) -> bool:
|
||||
"""
|
||||
:return: Whether the HTTP method is cacheable.
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
||||
:rtype: bool
|
||||
"""Whether the HTTP method is cacheable.
|
||||
|
||||
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
||||
|
||||
Returns:
|
||||
bool: Whether the HTTP method is cacheable.
|
||||
"""
|
||||
return self.method in CACHEABLE_HTTP_METHODS
|
||||
|
||||
Reference in New Issue
Block a user