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