Additonal annotations
This commit is contained in:
parent
65b76f2762
commit
b958cdc151
3
Makefile
3
Makefile
|
@ -70,9 +70,10 @@ endif
|
|||
black:
|
||||
black --config ./.black.toml sanic tests
|
||||
|
||||
fix-import: black
|
||||
isort:
|
||||
isort sanic tests
|
||||
|
||||
pretty: black isort
|
||||
|
||||
docs-clean:
|
||||
cd docs && make clean
|
||||
|
|
|
@ -156,7 +156,12 @@ epub_exclude_files = ["search.html"]
|
|||
|
||||
suppress_warnings = ["image.nonlocal_uri"]
|
||||
|
||||
autodoc_member_order = "bysource"
|
||||
|
||||
autodoc_typehints = "description"
|
||||
autodoc_default_options = {
|
||||
"member-order": "groupwise",
|
||||
}
|
||||
|
||||
|
||||
# app setup hook
|
||||
def setup(app):
|
||||
|
|
|
@ -4,7 +4,7 @@ Guides
|
|||
======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
:maxdepth: 4
|
||||
|
||||
sanic/api_reference
|
||||
|
||||
|
|
|
@ -31,109 +31,92 @@ sanic.compat
|
|||
.. automodule:: sanic.compat
|
||||
:members:
|
||||
|
||||
|
||||
sanic.config
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.constants
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic.constants
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.cookies
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.cookies
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.errorpages
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.errorpages
|
||||
:members:
|
||||
|
||||
sanic.exceptions
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.handlers
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.handlers
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.http
|
||||
----------
|
||||
|
||||
.. automodule:: sanic.http
|
||||
:members:
|
||||
|
||||
sanic.log
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.request
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.request
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.response
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.response
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.router
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.server
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.server
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.static
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.static
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.views
|
||||
-----------
|
||||
|
||||
.. automodule:: sanic.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.websocket
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
sanic.worker
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.worker
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
from sanic.__version__ import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
|
||||
__all__ = ["Sanic", "Blueprint", "__version__"]
|
||||
__all__ = (
|
||||
"__version__",
|
||||
"Sanic",
|
||||
"Blueprint",
|
||||
"HTTPResponse",
|
||||
"Request",
|
||||
"html",
|
||||
"json",
|
||||
"text",
|
||||
)
|
||||
|
|
|
@ -66,7 +66,16 @@ class Config(dict):
|
|||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
them to the configuration if present. This is called automatically when
|
||||
Sanic starts up to load environment variables into config.
|
||||
|
||||
It will automatically hyrdate the following types:
|
||||
|
||||
- ``int``
|
||||
- ``float``
|
||||
- ``bool``
|
||||
|
||||
Anything else will be imported as a ``str``.
|
||||
"""
|
||||
for k, v in environ.items():
|
||||
if k.startswith(prefix):
|
||||
|
@ -86,7 +95,9 @@ class Config(dict):
|
|||
"""
|
||||
Update app.config.
|
||||
|
||||
..note:: only upper case settings are considered.
|
||||
.. note::
|
||||
|
||||
Only upper case settings are considered
|
||||
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
|
@ -102,7 +113,7 @@ class Config(dict):
|
|||
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
|
||||
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.
|
||||
|
@ -122,6 +133,8 @@ class Config(dict):
|
|||
B = 2
|
||||
|
||||
config.update_config(C)
|
||||
|
||||
`See user guide <https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
|
||||
if isinstance(config, (bytes, str, Path)):
|
||||
|
|
|
@ -2,6 +2,7 @@ import re
|
|||
import string
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
|
@ -41,16 +42,17 @@ _is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
|||
|
||||
|
||||
class CookieJar(dict):
|
||||
"""CookieJar dynamically writes headers as cookies are added and removed
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
self.headers = headers
|
||||
self.cookie_headers = {}
|
||||
self.header_key = "Set-Cookie"
|
||||
self.headers: Dict[str, str] = headers
|
||||
self.cookie_headers: Dict[str, str] = {}
|
||||
self.header_key: str = "Set-Cookie"
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
"""
|
||||
Sanic `provides a pattern <https://sanicframework.org/guide/best-practices/exceptions.html#using-sanic-exceptions>`_
|
||||
for providing a response when an exception occurs. However, if you do no handle
|
||||
an exception, it will provide a fallback. There are three fallback types:
|
||||
|
||||
- HTML - *default*
|
||||
- Text
|
||||
- JSON
|
||||
|
||||
Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that
|
||||
will attempt to provide an appropriate response format based upon the
|
||||
request type.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
|
@ -26,6 +40,10 @@ FALLBACK_STATUS = 500
|
|||
|
||||
|
||||
class BaseRenderer:
|
||||
"""
|
||||
Base class that all renderers must inherit from.
|
||||
"""
|
||||
|
||||
def __init__(self, request, exception, debug):
|
||||
self.request = request
|
||||
self.exception = exception
|
||||
|
@ -54,7 +72,13 @@ class BaseRenderer:
|
|||
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
return f"{self.status} — {status_text}"
|
||||
|
||||
def render(self):
|
||||
def render(self) -> str:
|
||||
"""
|
||||
Outputs the exception as a ``str`` for response.
|
||||
|
||||
:return: The formatted exception
|
||||
:rtype: str
|
||||
"""
|
||||
output = (
|
||||
self.full
|
||||
if self.debug and not getattr(self.exception, "quiet", False)
|
||||
|
@ -62,14 +86,28 @@ class BaseRenderer:
|
|||
)
|
||||
return output()
|
||||
|
||||
def minimal(self): # noqa
|
||||
def minimal(self) -> str: # noqa
|
||||
"""
|
||||
Provide a formatted message that is meant to not show any sensitive
|
||||
data or details.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def full(self): # noqa
|
||||
def full(self) -> str: # noqa
|
||||
"""
|
||||
Provide a formatted message that has all details and is mean to be used
|
||||
primarily for debugging and non-production environments.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as HTML.
|
||||
|
||||
The default fallback type.
|
||||
"""
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
html { font-family: sans-serif }
|
||||
h2 { color: #888; }
|
||||
|
@ -172,6 +210,10 @@ class HTMLRenderer(BaseRenderer):
|
|||
|
||||
|
||||
class TextRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as plain text.
|
||||
"""
|
||||
|
||||
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
|
||||
SPACER = " "
|
||||
|
||||
|
@ -231,6 +273,10 @@ class TextRenderer(BaseRenderer):
|
|||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as JSON.
|
||||
"""
|
||||
|
||||
def full(self):
|
||||
output = self._generate_output(full=True)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
|
@ -280,7 +326,9 @@ class JSONRenderer(BaseRenderer):
|
|||
|
||||
|
||||
def escape(text):
|
||||
"""Minimal HTML escaping, not for attribute values (unlike html.escape)."""
|
||||
"""
|
||||
Minimal HTML escaping, not for attribute values (unlike html.escape).
|
||||
"""
|
||||
return f"{text}".replace("&", "&").replace("<", "<")
|
||||
|
||||
|
||||
|
@ -303,7 +351,9 @@ def exception_response(
|
|||
debug: bool,
|
||||
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""Render a response for the default FALLBACK exception handler"""
|
||||
"""
|
||||
Render a response for the default FALLBACK exception handler.
|
||||
"""
|
||||
|
||||
if not renderer:
|
||||
renderer = HTMLRenderer
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Optional, Union
|
||||
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
|
@ -33,16 +35,28 @@ class SanicException(Exception):
|
|||
|
||||
@add_status_code(404)
|
||||
class NotFound(SanicException):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(400)
|
||||
class InvalidUsage(SanicException):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
"""
|
||||
|
||||
def __init__(self, message, method, allowed_methods):
|
||||
super().__init__(message)
|
||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
||||
|
@ -50,22 +64,38 @@ class MethodNotSupported(SanicException):
|
|||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
"""
|
||||
**Status**: 503 Service Unavailable
|
||||
|
||||
The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
|
@ -87,15 +117,27 @@ class RequestTimeout(SanicException):
|
|||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
"""
|
||||
**Status**: 413 Payload Too Large
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
||||
|
@ -103,15 +145,27 @@ class ContentRangeError(SanicException):
|
|||
|
||||
@add_status_code(417)
|
||||
class HeaderExpectationFailed(SanicException):
|
||||
"""
|
||||
**Status**: 417 Expectation Failed
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
"""
|
||||
**Status**: 403 Forbidden
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
@ -123,7 +177,7 @@ class PyFileError(Exception):
|
|||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
**Status**: 401 Unauthorized
|
||||
|
||||
:param message: Message describing the exception.
|
||||
:param status_code: HTTP Status code.
|
||||
|
@ -173,7 +227,7 @@ class LoadFileException(SanicException):
|
|||
pass
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
message appropriate for the given status code, unless provided.
|
||||
|
@ -184,8 +238,8 @@ def abort(status_code, message=None):
|
|||
:param message: The HTTP response body. Defaults to the messages in
|
||||
"""
|
||||
if message is None:
|
||||
message = STATUS_CODES.get(status_code)
|
||||
msg: bytes = STATUS_CODES[status_code]
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode("utf8")
|
||||
message = msg.decode("utf8")
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
from importlib import import_module
|
||||
from inspect import ismodule
|
||||
from typing import Dict
|
||||
|
||||
|
||||
STATUS_CODES = {
|
||||
STATUS_CODES: Dict[int, bytes] = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
102: b"Processing",
|
||||
|
|
127
sanic/http.py
127
sanic/http.py
|
@ -1,3 +1,13 @@
|
|||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
|
||||
from asyncio import CancelledError, sleep
|
||||
from enum import Enum
|
||||
|
||||
|
@ -15,6 +25,17 @@ from sanic.log import access_logger, logger
|
|||
|
||||
|
||||
class Stage(Enum):
|
||||
"""
|
||||
Enum for representing the stage of the request/response cycle
|
||||
|
||||
| ``IDLE`` Waiting for request
|
||||
| ``REQUEST`` Request headers being received
|
||||
| ``HANDLER`` Headers done, handler running
|
||||
| ``RESPONSE`` Response headers sent, body in progress
|
||||
| ``FAILED`` Unrecoverable state (error while sending response)
|
||||
|
|
||||
"""
|
||||
|
||||
IDLE = 0 # Waiting for request
|
||||
REQUEST = 1 # Request headers being received
|
||||
HANDLER = 3 # Headers done, handler running
|
||||
|
@ -26,6 +47,24 @@ HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
|||
|
||||
|
||||
class Http:
|
||||
"""
|
||||
Internal helper for managing the HTTP request/response cycle
|
||||
|
||||
:raises ServerError:
|
||||
:raises PayloadTooLarge:
|
||||
:raises Exception:
|
||||
:raises InvalidUsage:
|
||||
:raises HeaderExpectationFailed:
|
||||
:raises RuntimeError:
|
||||
:raises ServerError:
|
||||
:raises ServerError:
|
||||
:raises InvalidUsage:
|
||||
:raises InvalidUsage:
|
||||
:raises InvalidUsage:
|
||||
:raises PayloadTooLarge:
|
||||
:raises RuntimeError:
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
"_send",
|
||||
"_receive_more",
|
||||
|
@ -53,16 +92,16 @@ class Http:
|
|||
self._receive_more = protocol.receive_more
|
||||
self.recv_buffer = protocol.recv_buffer
|
||||
self.protocol = protocol
|
||||
self.expecting_continue = False
|
||||
self.stage = Stage.IDLE
|
||||
self.expecting_continue: bool = False
|
||||
self.stage: Stage = Stage.IDLE
|
||||
self.request_body = None
|
||||
self.request_bytes = None
|
||||
self.request_bytes_left = None
|
||||
self.request_max_size = protocol.request_max_size
|
||||
self.keep_alive = True
|
||||
self.head_only = None
|
||||
self.request = None
|
||||
self.response = None
|
||||
self.request: Request = None
|
||||
self.response: BaseHTTPResponse = None
|
||||
self.exception = None
|
||||
self.url = None
|
||||
self.upgrade_websocket = False
|
||||
|
@ -72,7 +111,9 @@ class Http:
|
|||
return self.stage in (Stage.HANDLER, Stage.RESPONSE)
|
||||
|
||||
async def http1(self):
|
||||
"""HTTP 1.1 connection handler"""
|
||||
"""
|
||||
HTTP 1.1 connection handler
|
||||
"""
|
||||
while True: # As long as connection stays keep-alive
|
||||
try:
|
||||
# Receive and handle a request
|
||||
|
@ -125,7 +166,9 @@ class Http:
|
|||
await self._receive_more()
|
||||
|
||||
async def http1_request_header(self):
|
||||
"""Receive and parse request header into self.request."""
|
||||
"""
|
||||
Receive and parse request header into self.request.
|
||||
"""
|
||||
HEADER_MAX_SIZE = min(8192, self.request_max_size)
|
||||
# Receive until full header is in buffer
|
||||
buf = self.recv_buffer
|
||||
|
@ -214,7 +257,9 @@ class Http:
|
|||
self.request, request.stream = request, self
|
||||
self.protocol.state["requests_count"] += 1
|
||||
|
||||
async def http1_response_header(self, data, end_stream):
|
||||
async def http1_response_header(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
res = self.response
|
||||
|
||||
# Compatibility with simple response body
|
||||
|
@ -257,7 +302,7 @@ class Http:
|
|||
else:
|
||||
# Length not known, use chunked encoding
|
||||
headers["transfer-encoding"] = "chunked"
|
||||
data = b"%x\r\n%b\r\n" % (size, data) if size else None
|
||||
data = b"%x\r\n%b\r\n" % (size, data) if size else b""
|
||||
self.response_func = self.http1_response_chunked
|
||||
|
||||
if self.head_only:
|
||||
|
@ -283,14 +328,20 @@ class Http:
|
|||
await self._send(ret)
|
||||
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
|
||||
|
||||
def head_response_ignored(self, data, end_stream):
|
||||
"""HEAD response: body data silently ignored."""
|
||||
def head_response_ignored(self, data: bytes, end_stream: bool) -> None:
|
||||
"""
|
||||
HEAD response: body data silently ignored.
|
||||
"""
|
||||
if end_stream:
|
||||
self.response_func = None
|
||||
self.stage = Stage.IDLE
|
||||
|
||||
async def http1_response_chunked(self, data, end_stream):
|
||||
"""Format a part of response body in chunked encoding."""
|
||||
async def http1_response_chunked(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format a part of response body in chunked encoding.
|
||||
"""
|
||||
# Chunked encoding
|
||||
size = len(data)
|
||||
if end_stream:
|
||||
|
@ -304,8 +355,12 @@ class Http:
|
|||
elif size:
|
||||
await self._send(b"%x\r\n%b\r\n" % (size, data))
|
||||
|
||||
async def http1_response_normal(self, data: bytes, end_stream: bool):
|
||||
"""Format / keep track of non-chunked response."""
|
||||
async def http1_response_normal(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format / keep track of non-chunked response.
|
||||
"""
|
||||
bytes_left = self.response_bytes_left - len(data)
|
||||
if bytes_left <= 0:
|
||||
if bytes_left < 0:
|
||||
|
@ -321,7 +376,10 @@ class Http:
|
|||
await self._send(data)
|
||||
self.response_bytes_left = bytes_left
|
||||
|
||||
async def error_response(self, exception):
|
||||
async def error_response(self, exception: Exception) -> None:
|
||||
"""
|
||||
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
|
||||
|
@ -339,10 +397,13 @@ class Http:
|
|||
|
||||
await app.handle_exception(self.request, exception)
|
||||
|
||||
def create_empty_request(self):
|
||||
"""Current error handling code needs a request object that won't exist
|
||||
def create_empty_request(self) -> None:
|
||||
"""
|
||||
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."""
|
||||
bogus response for error handling use.
|
||||
"""
|
||||
|
||||
# FIXME: Avoid this by refactoring error handling and response code
|
||||
self.request = self.protocol.request_class(
|
||||
url_bytes=self.url.encode() if self.url else b"*",
|
||||
|
@ -354,17 +415,10 @@ class Http:
|
|||
)
|
||||
self.request.stream = self
|
||||
|
||||
def log_response(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.
|
||||
|
||||
:param response: Response generated for the current request
|
||||
|
||||
:type response: :class:`sanic.response.HTTPResponse` or
|
||||
:class:`sanic.response.StreamingHTTPResponse`
|
||||
|
||||
:return: None
|
||||
"""
|
||||
req, res = self.request, self.response
|
||||
extra = {
|
||||
|
@ -382,15 +436,20 @@ class Http:
|
|||
# Request methods
|
||||
|
||||
async def __aiter__(self):
|
||||
"""Async iterate over request body."""
|
||||
"""
|
||||
Async iterate over request body.
|
||||
"""
|
||||
while self.request_body:
|
||||
data = await self.read()
|
||||
|
||||
if data:
|
||||
yield data
|
||||
|
||||
async def read(self):
|
||||
"""Read some bytes of request body."""
|
||||
async def read(self) -> Optional[bytes]:
|
||||
"""
|
||||
Read some bytes of request body.
|
||||
"""
|
||||
|
||||
# Send a 100-continue if needed
|
||||
if self.expecting_continue:
|
||||
self.expecting_continue = False
|
||||
|
@ -440,7 +499,7 @@ class Http:
|
|||
# End of request body?
|
||||
if not self.request_bytes_left:
|
||||
self.request_body = None
|
||||
return
|
||||
return None
|
||||
|
||||
# At this point we are good to read/return up to request_bytes_left
|
||||
if not buf:
|
||||
|
@ -457,12 +516,14 @@ class Http:
|
|||
|
||||
# Response methods
|
||||
|
||||
def respond(self, response):
|
||||
"""Initiate new streaming response.
|
||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
|
||||
"""
|
||||
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
|
||||
given."""
|
||||
given.
|
||||
"""
|
||||
if self.stage is not Stage.HANDLER:
|
||||
self.stage = Stage.FAILED
|
||||
raise RuntimeError("Response already started")
|
||||
|
|
11
sanic/log.py
11
sanic/log.py
|
@ -54,5 +54,16 @@ LOGGING_CONFIG_DEFAULTS = dict(
|
|||
|
||||
|
||||
logger = logging.getLogger("sanic.root")
|
||||
"""
|
||||
General Sanic logger
|
||||
"""
|
||||
|
||||
error_logger = logging.getLogger("sanic.error")
|
||||
"""
|
||||
Logger used by Sanic for error logging
|
||||
"""
|
||||
|
||||
access_logger = logging.getLogger("sanic.access")
|
||||
"""
|
||||
Logger used by Sanic for access logging
|
||||
"""
|
||||
|
|
110
sanic/request.py
110
sanic/request.py
|
@ -1,14 +1,30 @@
|
|||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
List,
|
||||
NamedTuple,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.app import Sanic
|
||||
|
||||
from asyncio.transports import BaseTransport
|
||||
import email.utils
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections import defaultdict
|
||||
from http.cookies import SimpleCookie
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||
|
||||
from httptools import parse_url # type: ignore
|
||||
|
||||
from sanic.compat import CancelledErrors
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.headers import (
|
||||
parse_content_header,
|
||||
|
@ -47,7 +63,9 @@ class RequestParameters(dict):
|
|||
|
||||
|
||||
class Request:
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"__weakref__",
|
||||
|
@ -80,7 +98,15 @@ class Request:
|
|||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport, app):
|
||||
def __init__(
|
||||
self,
|
||||
url_bytes: bytes,
|
||||
headers: Header,
|
||||
version: str,
|
||||
method: str,
|
||||
transport: BaseTransport,
|
||||
app: Sanic,
|
||||
):
|
||||
self.raw_url = url_bytes
|
||||
# TODO: Content-Encoding detection
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
|
@ -94,18 +120,22 @@ class Request:
|
|||
|
||||
# Init but do not inhale
|
||||
self.body = b""
|
||||
self.conn_info = None
|
||||
self.conn_info: Optional[ConnInfo] = None
|
||||
self.ctx = SimpleNamespace()
|
||||
self.name = None
|
||||
self.name: Optional[str] = None
|
||||
self.parsed_forwarded = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = defaultdict(RequestParameters)
|
||||
self.parsed_not_grouped_args = defaultdict(list)
|
||||
self.parsed_args: DefaultDict[
|
||||
Tuple[bool, bool, str, str], RequestParameters
|
||||
] = defaultdict(RequestParameters)
|
||||
self.parsed_not_grouped_args: DefaultDict[
|
||||
Tuple[bool, bool, str, str], List[Tuple[str, str]]
|
||||
] = defaultdict(list)
|
||||
self.uri_template = None
|
||||
self.request_middleware_started = False
|
||||
self._cookies = None
|
||||
self._cookies: Dict[str, str] = {}
|
||||
self.stream = None
|
||||
self.endpoint = None
|
||||
|
||||
|
@ -350,11 +380,11 @@ class Request:
|
|||
query_args = property(get_query_args)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
def cookies(self) -> Dict[str, str]:
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get("Cookie")
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies: SimpleCookie = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
self._cookies = {
|
||||
name: cookie.value for name, cookie in cookies.items()
|
||||
|
@ -364,27 +394,35 @@ class Request:
|
|||
return self._cookies
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
def content_type(self) -> str:
|
||||
"""
|
||||
:return: Content-Type header form the request
|
||||
:rtype: str
|
||||
"""
|
||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
"""return matched info after resolving route"""
|
||||
"""
|
||||
:return: matched info after resolving route
|
||||
"""
|
||||
return self.app.router.get(self)[2]
|
||||
|
||||
# Transport properties (obtained from local interface only)
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
def ip(self) -> str:
|
||||
"""
|
||||
:return: peer ip of the socket
|
||||
:rtype: str
|
||||
"""
|
||||
return self.conn_info.client if self.conn_info else ""
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
def port(self) -> int:
|
||||
"""
|
||||
:return: peer port of the socket
|
||||
:rtype: int
|
||||
"""
|
||||
return self.conn_info.client_port if self.conn_info else 0
|
||||
|
||||
|
@ -477,39 +515,49 @@ class Request:
|
|||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
"""The hostname the client connected to, by `request.host`."""
|
||||
"""
|
||||
The 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``.
|
||||
"""
|
||||
port = self.forwarded.get("port") or parse_host(self.host)[1]
|
||||
return port or (80 if self.scheme in ("http", "ws") else 443)
|
||||
|
||||
@property
|
||||
def server_path(self) -> str:
|
||||
"""Full path of current URL. Uses proxied or local path."""
|
||||
"""
|
||||
Full path of current URL. Uses proxied or local path.
|
||||
"""
|
||||
return self.forwarded.get("path") or self.path
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
def query_string(self) -> str:
|
||||
"""
|
||||
Representation of the requested query
|
||||
"""
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode("utf-8")
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def url(self) -> str:
|
||||
"""
|
||||
The URL
|
||||
"""
|
||||
return urlunparse(
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
def url_for(self, view_name, **kwargs):
|
||||
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
|
||||
|
@ -542,11 +590,23 @@ class Request:
|
|||
)
|
||||
|
||||
|
||||
File = namedtuple("File", ["type", "body", "name"])
|
||||
class File(NamedTuple):
|
||||
"""
|
||||
Model for defining a file
|
||||
|
||||
:param type: The mimetype, defaults to text/plain
|
||||
:param body: Bytes of the file
|
||||
:param name: The filename
|
||||
"""
|
||||
|
||||
type: str
|
||||
body: bytes
|
||||
name: str
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from functools import partial
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from typing import Optional
|
||||
from urllib.parse import quote_plus
|
||||
from warnings import warn
|
||||
|
||||
from sanic.compat import Header, open_async
|
||||
from sanic.cookies import CookieJar
|
||||
from sanic.helpers import has_message_body, remove_entity_headers
|
||||
from sanic.http import Http
|
||||
|
||||
|
||||
try:
|
||||
|
@ -21,7 +23,10 @@ except ImportError:
|
|||
|
||||
class BaseHTTPResponse:
|
||||
def __init__(self):
|
||||
self.asgi = False
|
||||
self.asgi: bool = False
|
||||
self.body: Optional[bytes] = None
|
||||
self.stream: Http = None
|
||||
self.status: int = None
|
||||
|
||||
def _encode_body(self, data):
|
||||
if data is None:
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
List,
|
||||
NamedTuple,
|
||||
Tuple,
|
||||
Dict,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.http import Http
|
||||
from sanic.app import Sanic
|
||||
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import os
|
||||
|
@ -6,6 +24,7 @@ import socket
|
|||
import stat
|
||||
import sys
|
||||
|
||||
from sanic.http import Stage
|
||||
from asyncio import CancelledError
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
|
@ -13,15 +32,13 @@ from ipaddress import ip_address
|
|||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from time import monotonic as current_time
|
||||
from typing import Dict, Type, Union
|
||||
from sanic.request import Request
|
||||
|
||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
||||
from sanic.http import Http, Stage
|
||||
from sanic.log import logger
|
||||
from sanic.request import Request
|
||||
|
||||
from asyncio.transports import BaseTransport
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore
|
||||
|
@ -49,7 +66,7 @@ class ConnInfo:
|
|||
"ssl",
|
||||
)
|
||||
|
||||
def __init__(self, transport, unix=None):
|
||||
def __init__(self, transport: BaseTransport, unix=None):
|
||||
self.ssl = bool(transport.get_extra_info("sslcontext"))
|
||||
self.server = self.client = ""
|
||||
self.server_port = self.client_port = 0
|
||||
|
@ -126,7 +143,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
asyncio.set_event_loop(loop)
|
||||
self.loop = loop
|
||||
deprecated_loop = self.loop if sys.version_info < (3, 7) else None
|
||||
self.app = app
|
||||
self.app: Sanic = app
|
||||
self.url = None
|
||||
self.transport = None
|
||||
self.conn_info = None
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import inspect
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from time import gmtime, strftime
|
||||
|
||||
|
@ -93,8 +94,8 @@ def test_static_file_pathlib(app, static_file_directory, file_name):
|
|||
[b"test.file", b"decode me.txt", b"python.png"],
|
||||
)
|
||||
def test_static_file_bytes(app, static_file_directory, file_name):
|
||||
bsep = os.path.sep.encode('utf-8')
|
||||
file_path = static_file_directory.encode('utf-8') + bsep + file_name
|
||||
bsep = os.path.sep.encode("utf-8")
|
||||
file_path = static_file_directory.encode("utf-8") + bsep + file_name
|
||||
app.static("/testing.file", file_path)
|
||||
request, response = app.test_client.get("/testing.file")
|
||||
assert response.status == 200
|
||||
|
|
Loading…
Reference in New Issue
Block a user