Merge pull request #2006 from sanic-org/docs-annotations

API Docs & Annotations
This commit is contained in:
Adam Hopkins 2021-02-28 19:59:02 +02:00 committed by GitHub
commit eef44ae179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1379 additions and 4245 deletions

View File

@ -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 --profile=black isort sanic tests --profile=black
pretty: black isort
docs-clean: docs-clean:
cd docs && make clean cd docs && make clean
@ -83,6 +84,10 @@ docs: docs-clean
docs-test: docs-clean docs-test: docs-clean
cd docs && make dummy cd docs && make dummy
docs-serve:
# python -m http.server --directory=./docs/_build/html 9999
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
changelog: changelog:
python scripts/changelog.py python scripts/changelog.py

4
docs/_static/custom.css vendored Normal file
View File

@ -0,0 +1,4 @@
.wy-side-nav-search,
.wy-nav-top {
background: #444444;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -12,8 +12,10 @@ import sys
# Add support for auto-doc # Add support for auto-doc
import recommonmark import recommonmark
from recommonmark.transform import AutoStructify from recommonmark.transform import AutoStructify
# Ensure that sanic is present in the path, to allow sphinx-apidoc to # Ensure that sanic is present in the path, to allow sphinx-apidoc to
# autogenerate documentation from docstrings # autogenerate documentation from docstrings
root_directory = os.path.dirname(os.getcwd()) root_directory = os.path.dirname(os.getcwd())
@ -21,23 +23,25 @@ sys.path.insert(0, root_directory)
import sanic import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc', "recommonmark"] extensions = ["sphinx.ext.autodoc", "recommonmark"]
templates_path = ['_templates'] templates_path = ["_templates"]
# Enable support for both Restructured Text and Markdown # Enable support for both Restructured Text and Markdown
source_suffix = ['.rst', '.md'] source_suffix = [".rst", ".md"]
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = 'Sanic' project = "Sanic"
copyright = '2018, Sanic contributors' copyright = "2021, Sanic Community Organization"
author = 'Sanic contributors' author = "Sanic Community Organization"
html_logo = "./_static/sanic-framework-logo-white-400x97.png"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
@ -52,7 +56,7 @@ release = sanic.__version__
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = 'en' language = "en"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@ -60,10 +64,10 @@ language = 'en'
# #
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses # modules.rst is generated by sphinx-apidoc but is unused. This suppresses
# a warning about it. # a warning about it.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst'] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "modules.rst"]
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
@ -72,17 +76,17 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'sphinx_rtd_theme' html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
html_css_files = ["custom.css"]
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc' htmlhelp_basename = "Sanicdoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -90,15 +94,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@ -107,14 +108,21 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation', latex_documents = [
'Sanic contributors', 'manual'), ] (
master_doc,
"Sanic.tex",
"Sanic Documentation",
"Sanic contributors",
"manual",
),
]
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)] man_pages = [(master_doc, "sanic", "Sanic Documentation", [author], 1)]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -122,8 +130,15 @@ man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic', (
'One line description of project.', 'Miscellaneous'), master_doc,
"Sanic",
"Sanic Documentation",
author,
"Sanic",
"One line description of project.",
"Miscellaneous",
),
] ]
# -- Options for Epub output ---------------------------------------------- # -- Options for Epub output ----------------------------------------------
@ -135,17 +150,27 @@ epub_publisher = author
epub_copyright = copyright epub_copyright = copyright
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"]
# -- Custom Settings ------------------------------------------------------- # -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri'] suppress_warnings = ["image.nonlocal_uri"]
autodoc_typehints = "description"
autodoc_default_options = {
"member-order": "groupwise",
}
# app setup hook # app setup hook
def setup(app): def setup(app):
app.add_config_value('recommonmark_config', { app.add_config_value(
'enable_eval_rst': True, "recommonmark_config",
'enable_auto_doc_ref': False, {
}, True) "enable_eval_rst": True,
"enable_auto_doc_ref": False,
},
True,
)
app.add_transform(AutoStructify) app.add_transform(AutoStructify)

View File

@ -4,37 +4,9 @@ Guides
====== ======
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 4
sanic/getting_started
sanic/config
sanic/logging
sanic/request_data
sanic/response
sanic/cookies
sanic/routing
sanic/blueprints
sanic/static_files
sanic/versioning
sanic/exceptions
sanic/middleware
sanic/websocket
sanic/decorators
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/sockets
sanic/ssl
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/nginx
sanic/extensions
sanic/examples
sanic/changelog
sanic/contributing
sanic/api_reference sanic/api_reference
sanic/asyncio_python37
Module Documentation Module Documentation

View File

@ -1,159 +1,132 @@
API Reference API Reference
============= =============
Submodules sanic.app
---------- ---------
sanic.app module
----------------
.. automodule:: sanic.app .. automodule:: sanic.app
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
:inherited-members:
sanic.blueprints module sanic.blueprints
----------------------- ----------------
.. automodule:: sanic.blueprints .. automodule:: sanic.blueprints
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
:inherited-members:
sanic.blueprint_group module sanic.blueprint_group
---------------------------- ---------------------
.. automodule:: sanic.blueprint_group .. automodule:: sanic.blueprint_group
:members: :members:
:undoc-members: :special-members:
sanic.compat
------------
.. automodule:: sanic.compat
:members:
:show-inheritance: :show-inheritance:
sanic.config
sanic.config module ------------
-------------------
.. automodule:: sanic.config .. automodule:: sanic.config
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.constants module sanic.cookies
---------------------- -------------
.. automodule:: sanic.constants
:members:
:undoc-members:
:show-inheritance:
sanic.cookies module
--------------------
.. automodule:: sanic.cookies .. automodule:: sanic.cookies
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.exceptions module sanic.errorpages
----------------------- ----------------
.. automodule:: sanic.errorpages
:members:
:show-inheritance:
sanic.exceptions
----------------
.. automodule:: sanic.exceptions .. automodule:: sanic.exceptions
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.handlers module sanic.handlers
--------------------- --------------
.. automodule:: sanic.handlers .. automodule:: sanic.handlers
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.log module sanic.http
---------------- ----------
.. automodule:: sanic.http
:members:
:show-inheritance:
sanic.log
---------
.. automodule:: sanic.log .. automodule:: sanic.log
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.request module sanic.request
-------------------- -------------
.. automodule:: sanic.request .. automodule:: sanic.request
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.response module sanic.response
--------------------- --------------
.. automodule:: sanic.response .. automodule:: sanic.response
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.router module sanic.router
------------------- ------------
.. automodule:: sanic.router .. automodule:: sanic.router
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.server module sanic.server
------------------- ------------
.. automodule:: sanic.server .. automodule:: sanic.server
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.static module
-------------------
.. automodule:: sanic.static sanic.views
:members: -----------
:undoc-members:
:show-inheritance:
sanic.testing module
--------------------
.. automodule:: sanic.testing
:members:
:undoc-members:
:show-inheritance:
sanic.views module
------------------
.. automodule:: sanic.views .. automodule:: sanic.views
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.websocket module sanic.websocket
---------------------- ---------------
.. automodule:: sanic.websocket .. automodule:: sanic.websocket
:members: :members:
:undoc-members:
:show-inheritance: :show-inheritance:
sanic.worker module sanic.worker
------------------- ------------
.. automodule:: sanic.worker .. automodule:: sanic.worker
:members: :members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: sanic
:members:
:undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -1,58 +0,0 @@
Python 3.7 AsyncIO examples
###########################
With Python 3.7 AsyncIO got major update for the following types:
- asyncio.AbstractEventLoop
- asyncio.AbstractServer
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
.. code:: python
import asyncio
import socket
import os
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"hello": "world"})
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
os.remove(server_socket)
finally:
sock.bind(server_socket)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
srv_coro = app.create_server(
sock=sock,
return_asyncio_server=True,
asyncio_server_kwargs=dict(
start_serving=False
)
)
srv = loop.run_until_complete(srv_coro)
try:
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
loop.run_until_complete(srv.serve_forever())
except KeyboardInterrupt:
srv.close()
loop.close()
Please note that uvloop does not support these features yet.

View File

@ -1,301 +0,0 @@
Blueprints
==========
Blueprints are objects that can be used for sub-routing within an application.
Instead of adding routes to the application instance, blueprints define similar
methods for adding routes, which are then registered with the application in a
flexible and pluggable manner.
Blueprints are especially useful for larger applications, where your
application logic can be broken down into several groups or areas of
responsibility.
My First Blueprint
------------------
The following shows a very simple blueprint that registers a handler-function at
the root `/` of your application.
Suppose you save this file as `my_blueprint.py`, which can be imported into your
main application later.
.. code-block:: python
from sanic.response import json
from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root(request):
return json({'my': 'blueprint'})
Registering blueprints
----------------------
Blueprints must be registered with the application.
.. code-block:: python
from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.blueprint(bp)
app.run(host='0.0.0.0', port=8000, debug=True)
This will add the blueprint to the application and register any routes defined
by that blueprint. In this example, the registered routes in the `app.router`
will look like:
.. code-block:: python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
Blueprint groups and nesting
----------------------------
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
| api/
| ├──content/
| │ ├──authors.py
| │ ├──static.py
| │ └──__init__.py
| ├──info.py
| └──__init__.py
| app.py
Initialization of this app's blueprint hierarchy could go as follows:
.. code-block:: python
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint('content_authors', url_prefix='/authors')
.. code-block:: python
# api/content/static.py
from sanic import Blueprint
static = Blueprint('content_static', url_prefix='/static')
.. code-block:: python
# api/content/__init__.py
from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(static, authors, url_prefix='/content')
.. code-block:: python
# api/info.py
from sanic import Blueprint
info = Blueprint('info', url_prefix='/info')
.. code-block:: python
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix='/api')
And registering these blueprints in `app.py` can now be done like so:
.. code-block:: python
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
Using Blueprints
----------------
Blueprints have almost the same functionality as an application instance.
WebSocket routes
~~~~~~~~~~~~~~~~
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
Blueprint Middleware
~~~~~~~~~~~~~~~~~~~~
Using blueprints allows you to also register middleware globally.
.. code-block:: python
@bp.middleware
async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
async def halt_request(request):
return text('I halted the request')
@bp.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
Blueprint Group Middleware
~~~~~~~~~~~~~~~~~~~~~~~~~~
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.
.. code-block:: python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
Exceptions
~~~~~~~~~~
Exceptions can be applied exclusively to blueprints globally.
.. code-block:: python
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
Static files
~~~~~~~~~~~~
Static files can be served globally, under the blueprint prefix.
.. code-block:: python
# suppose bp.name == 'bp'
bp.static('/web/path', '/folder/to/serve')
# also you can pass name parameter to it for url_for
bp.static('/web/path', '/folder/to/server', name='uploads')
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
Start and stop
--------------
Blueprints can run functions during the start and stop process of the server.
If running in multiprocessor mode (more than 1 worker), these are triggered
after the workers fork.
Available events are:
- `before_server_start`: Executed before the server begins to accept connections
- `after_server_start`: Executed after the server begins to accept connections
- `before_server_stop`: Executed before the server stops accepting connections
- `after_server_stop`: Executed after the server is stopped and all requests are complete
.. code-block:: python
bp = Blueprint('my_blueprint')
@bp.listener('before_server_start')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
Use-case: API versioning
------------------------
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `version` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
.. code-block:: python
# blueprints.py
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1")
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2")
@blueprint_v1.route('/')
async def api_v1_root(request):
return text('Welcome to version 1 of our documentation')
@blueprint_v2.route('/')
async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')
When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
.. code-block:: python
# main.py
from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.run(host='0.0.0.0', port=8000, debug=True)
URL Building with `url_for`
---------------------------
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
takes the format `<blueprint_name>.<handler_name>`. For example:
.. code-block:: python
@blueprint_v1.route('/')
async def root(request):
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
return redirect(url)
@blueprint_v1.route('/post/<post_id>')
async def post_handler(request, post_id):
return text('Post {} in Blueprint V1'.format(post_id))

View File

@ -1,4 +0,0 @@
Changelog
---------
.. include:: ../../CHANGELOG.rst

View File

@ -1,154 +0,0 @@
Class-Based Views
=================
Class-based views are simply classes which implement response behaviour to
requests. They provide a way to compartmentalise handling of different HTTP
request types at the same endpoint. Rather than defining and decorating three
different handler functions, one for each of an endpoint's supported request
type, the endpoint can be assigned a class-based view.
Defining views
--------------
A class-based view should subclass `HTTPMethodView`. You can then implement
class methods for every HTTP request type you want to support. If a request is
received that has no defined method, a `405: Method not allowed` response will
be generated.
To register a class-based view on an endpoint, the `app.add_route` method is
used. The first argument should be the defined class with the method `as_view`
invoked, and the second should be the URL endpoint.
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
using all these methods would look like the following.
.. code-block:: python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic("class_views_example")
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
# You can also use async syntax
async def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView.as_view(), '/')
URL parameters
--------------
If you need any URL parameters, as discussed in the routing guide, include them
in the method definition.
.. code-block:: python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView.as_view(), '/<name>')
Decorators
----------
If you want to add any decorators to the class, you can set the `decorators`
class variable. These will be applied to the class when `as_view` is called.
.. code-block:: python
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
def post(self, request, name):
return text("Hello I also have a decorator")
app.add_route(ViewWithDecorator.as_view(), '/url')
But if you just want to decorate some functions and not all functions, you can do as follows:
.. code-block:: python
class ViewWithSomeDecorator(HTTPMethodView):
@staticmethod
@some_decorator_here
def get(request, name):
return text("Hello I have a decorator")
def post(self, request, name):
return text("Hello I don't have any decorators")
URL Building
------------
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
that you will pass into `url_for`. For example:
.. code-block:: python
@app.route('/')
def index(request):
url = app.url_for('SpecialClassView')
return redirect(url)
class SpecialClassView(HTTPMethodView):
def get(self, request):
return text('Hello from the Special Class View!')
app.add_route(SpecialClassView.as_view(), '/special_class_view')
Using CompositionView
---------------------
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
move handler functions outside of the view class.
Handler functions for each supported HTTP method are defined elsewhere in the
source, and then added to the view using the `CompositionView.add` method. The
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
and the second is the handler function. The following example shows
`CompositionView` usage with both an external handler function and an inline
lambda:
.. code-block:: python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.response import text
app = Sanic("composition_example")
def get_handler(request):
return text('I am a get method')
view = CompositionView()
view.add(['GET'], get_handler)
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
# Use the new view to handle requests to the base URL
app.add_route(view, '/')
Note: currently you cannot build a URL for a CompositionView using `url_for`.

View File

@ -1,339 +0,0 @@
Configuration
=============
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
Basics
------
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
.. code-block:: python
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config['DB_USER'] = 'appuser'
Since the config object has a type that inherits from dictionary, you can use its ``update`` method in order to set several values at once:
.. code-block:: python
db_settings = {
'DB_HOST': 'localhost',
'DB_NAME': 'appdb',
'DB_USER': 'appuser'
}
app.config.update(db_settings)
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
Loading Configuration
---------------------
There are several ways how to load configuration.
From Environment Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
.. code-block:: python
app = Sanic(__name__, load_env='MYAPP_')
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
.. code-block:: python
app = Sanic(__name__, load_env=False)
From file, dict, or any object (having __dict__ attribute).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can store app configurations in: (1) a Python file, (2) a dictionary, or (3) in some other type of custom object.
In order to load configuration from ove of those, you can use ``app.upload_config()``.
**1) From file**
Let's say you have ``my_config.py`` file that looks like this:
.. code-block:: python
# my_config.py
A = 1
B = 2
Loading config from this file is as easy as:
.. code-block:: python
app.update_config("/path/to/my_config.py")
You can also use environment variables in the path name here.
Let's say you have an environment variable like this:
.. code-block:: shell
$ export my_path="/path/to"
Then you can use it like this:
.. code-block:: python
app.update_config("${my_path}/my_config.py")
.. note::
Just remember that you have to provide environment variables in the format ${environment_variable} and that $environment_variable is not expanded (is treated as "plain" text).
**2) From dict**
You can also set your app config by providing a ``dict``:
.. code-block:: python
d = {"A": 1, "B": 2}
app.update_config(d)
**3) From _any_ object**
App config can be taken from an object. Internally, it uses ``__dict__`` to retrieve keys and values.
For example, pass the class:
.. code-block:: python
class C:
A = 1
B = 2
app.update_config(C)
or, it can be instantiated:
.. code-block:: python
c = C()
app.update_config(c)
- From an object (having __dict__ attribute)
From an Object
~~~~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
.. code-block:: python
import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
or also by path to config:
.. code-block:: python
app = Sanic('myapp')
app.config.from_object('config.path.config.Class')
You could use a class or any other object as well.
From a File
~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
.. code-block:: python
app = Sanic('myapp')
app.config.from_envvar('MYAPP_SETTINGS')
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
.. code-block:: python
#$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
#INFO: Goin' Fast @ http://0.0.0.0:8000
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
.. code-block:: python
# config_file
DB_HOST = 'localhost'
DB_NAME = 'appdb'
DB_USER = 'appuser'
Builtin Configuration Values
----------------------------
Out of the box there are just a few predefined values which can be overwritten when creating the application. Note that websocket configuration values will have no impact if running in ASGI mode.
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| Variable | Default | Description |
+===========================+===================+=============================================================================+
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| KEEP_ALIVE | True | Disables keep-alive when False |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_SIZE | 2^20 | Maximum size for incoming messages (bytes) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_QUEUE | 32 | Maximum length of the queue that holds incoming messages |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_READ_LIMIT | 2^16 | High-water limit of the buffer for incoming bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_INTERVAL | 20 | A Ping frame is sent every ping_interval seconds. |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_TIMEOUT | 20 | Connection is closed when Pong is not received after ping_timeout seconds |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_SECRET | None | Used to securely identify a specific proxy server (see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| PROXIES_COUNT | None | The number of proxy servers in front of the app (e.g. nginx; see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REAL_IP_HEADER | None | The name of "X-Real-IP" HTTP header that contains real client ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
The different Timeout variables:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`REQUEST_TIMEOUT`
#################
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
`RESPONSE_TIMEOUT`
##################
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
`KEEP_ALIVE_TIMEOUT`
####################
What is Keep Alive? And what does the Keep Alive Timeout value do?
******************************************************************
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
* Apache httpd server default keepalive timeout = 5 seconds
* Nginx server default keepalive timeout = 75 seconds
* Nginx performance tuning guidelines uses keepalive = 15 seconds
* IE (5-9) client hard keepalive limit = 60 seconds
* Firefox client hard keepalive limit = 115 seconds
* Opera 11 client hard keepalive limit = 120 seconds
* Chrome 13+ client keepalive limit > 300+ seconds
Proxy configuration
~~~~~~~~~~~~~~~~~~~
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy,
typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP,
available as `request.remote_addr`. The full external URL is also constructed from header fields if available.
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`.
Forwarded header
################
.. Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy", for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy"
* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest.
The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the
information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value
of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in
`RFC 7239 section 6.3 <https://tools.ietf.org/html/rfc7239#section-6.3>`_, while `secret` has no such restrictions.
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
Traditional proxy headers
#########################
.. X-Real-IP: 1.2.3.4
X-Forwarded-For: 1.2.3.4, 10.0.0.1
X-Forwarded-Proto: https
X-Forwarded-Host: yoursite.com
* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header.
* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`).
If client IP is found by one of these methods, Sanic uses the following headers for URL parts:
* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`.
Proxy config if using ...
#########################
* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header
* Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret`
* NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret`
* NGINX: :ref:`nginx`.
* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header
* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP
* no proxies: no configuration required!
Changes in Sanic 19.9
#####################
Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed.

View File

@ -1 +0,0 @@
.. include:: ../../CONTRIBUTING.rst

View File

@ -1,87 +0,0 @@
Cookies
=======
Cookies are pieces of data which persist inside a user's browser. Sanic can
both read and write cookies, which are stored as key-value pairs.
.. warning::
Cookies can be freely altered by the client. Therefore you cannot just store
data such as login information in cookies as-is, as they can be freely altered
by the client. To ensure data you store in cookies is not forged or tampered
with by the client, use something like `itsdangerous`_ to cryptographically
sign the data.
Reading cookies
---------------
A user's cookies can be accessed via the ``Request`` object's ``cookies`` dictionary.
.. code-block:: python
from sanic.response import text
@app.route("/cookie")
async def test(request):
test_cookie = request.cookies.get('test')
return text("Test cookie set to: {}".format(test_cookie))
Writing cookies
---------------
When returning a response, cookies can be set on the ``Response`` object.
.. code-block:: python
from sanic.response import text
@app.route("/cookie")
async def test(request):
response = text("There's a cookie up in this response")
response.cookies['test'] = 'It worked!'
response.cookies['test']['domain'] = '.gotta-go-fast.com'
response.cookies['test']['httponly'] = True
return response
Deleting cookies
----------------
Cookies can be removed semantically or explicitly.
.. code-block:: python
from sanic.response import text
@app.route("/cookie")
async def test(request):
response = text("Time to eat some cookies muahaha")
# This cookie will be set to expire in 0 seconds
del response.cookies['kill_me']
# This cookie will self destruct in 5 seconds
response.cookies['short_life'] = 'Glad to be here'
response.cookies['short_life']['max-age'] = 5
del response.cookies['favorite_color']
# This cookie will remain unchanged
response.cookies['favorite_color'] = 'blue'
response.cookies['favorite_color'] = 'pink'
del response.cookies['favorite_color']
return response
Response cookies can be set like dictionary values and have the following
parameters available:
- ``expires`` (datetime): The time for the cookie to expire on the client's browser.
- ``path`` (string): The subset of URLs to which this cookie applies. Defaults to /.
- ``comment`` (string): A comment (metadata).
- ``domain`` (string): Specifies the domain for which the cookie is valid. An
explicitly specified domain must always start with a dot.
- ``max-age`` (number): Number of seconds the cookie should live for.
- ``secure`` (boolean): Specifies whether the cookie will only be sent via HTTPS.
- ``httponly`` (boolean): Specifies whether the cookie cannot be read by Javascript.
.. _itsdangerous: https://pythonhosted.org/itsdangerous/

View File

@ -1,76 +0,0 @@
Custom Protocols
================
.. note::
This is advanced usage, and most readers will not need such functionality.
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
This protocol can then be passed as the keyword argument ``protocol`` to the ``sanic.run`` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- ``loop``: an ``asyncio``-compatible event loop.
- ``connections``: a ``set`` to store protocol objects. When Sanic receives
``SIGINT`` or ``SIGTERM``, it executes ``protocol.close_if_idle`` for all protocol
objects stored in this set.
- ``signal``: a ``sanic.server.Signal`` object with the ``stopped`` attribute. When
Sanic receives ``SIGINT`` or ``SIGTERM``, ``signal.stopped`` is assigned ``True``.
- ``request_handler``: a coroutine that takes a ``sanic.request.Request`` object
and a ``response`` callback as arguments.
- ``error_handler``: a ``sanic.exceptions.Handler`` which is called when exceptions
are raised.
- ``request_timeout``: the number of seconds before a request times out.
- ``request_max_size``: an integer specifying the maximum size of a request, in bytes.
Example
-------
An error occurs in the default protocol if a handler function does not return
an ``HTTPResponse`` object.
By overriding the ``write_response`` protocol method, if a handler returns a
string it will be converted to an ``HTTPResponse object``.
.. code:: python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)

View File

@ -1,53 +0,0 @@
Debug Mode
=============
When enabling Sanic's debug mode, Sanic will provide a more verbose logging output
and by default will enable the Auto Reload feature.
.. warning::
Sanic's debug more will slow down the server's performance
and is therefore advised to enable it only in development environments.
Setting the debug mode
----------------------
By setting the ``debug`` mode a more verbose output from Sanic will be output
and the Automatic Reloader will be activated.
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route('/')
async def hello_world(request):
return json({"hello": "world"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)
Manually setting auto reload
----------------------------
Sanic offers a way to enable or disable the Automatic Reloader manually,
the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route('/')
async def hello_world(request):
return json({"hello": "world"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, auto_reload=True)

View File

@ -1,40 +0,0 @@
Handler Decorators
==================
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
Authorization Decorator
-----------------------
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
.. code-block:: python
from functools import wraps
from sanic.response import json
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({'status': 'authorized'})

View File

@ -1,195 +0,0 @@
Deploying
=========
Sanic has three serving options: the inbuilt webserver,
an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`.
Sanic's own webserver is the fastest option, and it can be securely run on
the Internet. Still, it is also very common to place Sanic behind a reverse
proxy, as shown in :ref:`nginx`.
Running via Sanic webserver
---------------------------
After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on.
- `unix` *(default `None`)*: Unix socket name to host the server on (instead of TCP).
- `debug` *(default `False`)*: Enables debug output (slows server).
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from.
- `workers` *(default `1`)*: Number of worker processes to spawn.
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is specified, Sanic creates its own event loop.
- `protocol` *(default `HttpProtocol`)*: Subclass of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
.. code-block:: python
app.run(host='0.0.0.0', port=1337, access_log=False)
In the above example, we decided to turn off the access log in order to increase performance.
Workers
~~~~~~~
By default, Sanic listens in the main process using only one CPU core. To crank
up the juice, just specify the number of workers in the `run` arguments.
.. code-block:: python
app.run(host='0.0.0.0', port=1337, workers=4)
Sanic will automatically spin up multiple processes and route traffic between
them. We recommend as many workers as you have available cores.
Running via command
~~~~~~~~~~~~~~~~~~~
If you like using command line arguments, you can launch a Sanic webserver by
executing the module. For example, if you initialized Sanic as `app` in a file
named `server.py`, you could run the server like so:
::
sanic server.app --host=0.0.0.0 --port=1337 --workers=4
It can also be called directly as a module.
::
python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4
With this way of running sanic, it is not necessary to invoke `app.run` in your
Python file. If you do, make sure you wrap it so that it only executes when
directly run by the interpreter.
.. code-block:: python
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
Running via ASGI
----------------
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
to run Sanic. The three main implementations of ASGI are
`Daphne <http://github.com/django/daphne>`_, `Uvicorn <https://www.uvicorn.org/>`_,
and `Hypercorn <https://pgjones.gitlab.io/hypercorn/index.html>`_.
Follow their documentation for the proper way to run them, but it should look
something like:
::
daphne myapp:app
uvicorn myapp:app
hypercorn myapp:app
A couple things to note when using ASGI:
1. When using the Sanic webserver, websockets will run using the `websockets <https://websockets.readthedocs.io/>`_ package.
In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
2. The ASGI `lifespan protocol <https://asgi.readthedocs.io/en/latest/specs/lifespan.html>`, supports
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
`before_server_stop`.
Sanic has experimental support for running on `Trio <https://trio.readthedocs.io/en/stable/>`_ with::
hypercorn -k trio myapp:app
Running via Gunicorn
--------------------
`Gunicorn <http://gunicorn.org/>`_ Green Unicorn is a WSGI HTTP Server for UNIX.
Its a pre-fork worker model ported from Rubys Unicorn project.
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
for Gunicorn `worker-class` argument:
::
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
after it has processed a given number of requests. This can be a convenient way to help limit the effects
of the memory leak.
See the `Gunicorn Docs <http://docs.gunicorn.org/en/latest/settings.html#max-requests>`_ for more information.
Other deployment considerations
-------------------------------
Disable debug logging for performance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
.. code-block:: python
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
::
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
Or you can rewrite app config directly
.. code-block:: python
app.config.ACCESS_LOG = False
Asynchronous support and sharing the loop
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`.
However, be advised that this method does not support using multiple processes, and is not the preferred way
to run the app in general.
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
.. code-block:: python
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
Caveat: using this method, calling `app.create_server()` will trigger "before_server_start" server events, but not
"after_server_start", "before_server_stop", or "after_server_stop" server events.
For more advanced use-cases, you can trigger these events using the AsyncioServer object, returned by awaiting
the server task.
Here is an incomplete example (please see `run_async_advanced.py` in examples for something more complete):
.. code-block:: python
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
server = loop.run_until_complete(serv_task)
server.after_start()
try:
loop.run_forever()
except KeyboardInterrupt as e:
loop.stop()
finally:
server.before_stop()
# Wait for server to close
close_task = server.close()
loop.run_until_complete(close_task)
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
server.after_stop()

View File

@ -1,167 +0,0 @@
Examples
========
This section of the documentation is a simple collection of example code that can help you get a quick start
on your application development. Most of these examples are categorized and provide you with a link to the
working code example in the `Sanic Repository <https://github.com/huge-success/sanic/tree/master/examples>`_
Basic Examples
--------------
This section of the examples are a collection of code that provide a simple use case example of the sanic application.
Simple Apps
~~~~~~~~~~~~
A simple sanic application with a single ``async`` method with ``text`` and ``json`` type response.
.. literalinclude:: ../../examples/teapot.py
.. literalinclude:: ../../examples/simple_server.py
Simple App with ``Sanic Views``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Showcasing the simple mechanism of using :class:`sanic.views.HTTPMethodView` as well as a way to extend the same
into providing a custom ``async`` behavior for ``view``.
.. literalinclude:: ../../examples/simple_async_view.py
URL Redirect
~~~~~~~~~~~~
.. literalinclude:: ../../examples/redirect_example.py
Named URL redirection
~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an easy to use way of redirecting the requests via a helper method called ``url_for`` that takes a
unique url name as argument and returns you the actual route assigned for it. This will help in simplifying the
efforts required in redirecting the user between different section of the application.
.. literalinclude:: ../../examples/url_for_example.py
Blueprints
~~~~~~~~~~
``Sanic`` provides an amazing feature to group your APIs and routes under a logical collection that can easily be
imported and plugged into any of your sanic application and it's called ``blueprints``
.. literalinclude:: ../../examples/blueprints.py
Logging Enhancements
~~~~~~~~~~~~~~~~~~~~
Even though ``Sanic`` comes with a battery of Logging support it allows the end users to customize the way logging
is handled in the application runtime.
.. literalinclude:: ../../examples/override_logging.py
The following sample provides an example code that demonstrates the usage of :func:`sanic.app.Sanic.middleware` in order
to provide a mechanism to assign a unique request ID for each of the incoming requests and log them via
`aiotask-context <https://github.com/Skyscanner/aiotask-context>`_.
.. literalinclude:: ../../examples/log_request_id.py
Sanic Streaming Support
~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` framework comes with in-built support for streaming large files and the following code explains the process
to setup a ``Sanic`` application with streaming support.
.. literalinclude:: ../../examples/request_stream/server.py
Sample Client app to show the usage of streaming application by a client code.
.. literalinclude:: ../../examples/request_stream/client.py
Sanic Concurrency Support
~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` supports the ability to start an app with multiple worker support. However, it's important to be able to limit
the concurrency per process/loop in order to ensure an efficient execution. The following section of the code provides a
brief example of how to limit the concurrency with the help of :class:`asyncio.Semaphore`
.. literalinclude:: ../../examples/limit_concurrency.py
Sanic Deployment via Docker
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Deploying a ``sanic`` app via ``docker`` and ``docker-compose`` is an easy task to achieve and the following example
provides a deployment of the sample ``simple_server.py``
.. literalinclude:: ../../examples/Dockerfile
.. literalinclude:: ../../examples/docker-compose.yml
Monitoring and Error Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an extendable bare minimum implementation of a global exception handler via
:class:`sanic.handlers.ErrorHandler`. This example shows how to extend it to enable some custom behaviors.
.. literalinclude:: ../../examples/exception_monitoring.py
Monitoring using external Service Providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `LogDNA <https://logdna.com/>`_
.. literalinclude:: ../../examples/logdna_example.py
* `RayGun <https://raygun.com/>`_
.. literalinclude:: ../../examples/raygun_example.py
* `Rollbar <https://rollbar.com>`_
.. literalinclude:: ../../examples/rollbar_example.py
* `Sentry <http://sentry.io>`_
.. literalinclude:: ../../examples/sentry_example.py
Security
~~~~~~~~
The following sample code shows a simple decorator based authentication and authorization mechanism that can be setup
to secure your ``sanic`` api endpoints.
.. literalinclude:: ../../examples/authorized_sanic.py
Sanic Websocket
~~~~~~~~~~~~~~~
``Sanic`` provides an ability to easily add a route and map it to a ``websocket`` handlers.
.. literalinclude:: ../../examples/websocket.html
.. literalinclude:: ../../examples/websocket.py
vhost Suppport
~~~~~~~~~~~~~~
.. literalinclude:: ../../examples/vhosts.py
Unit Testing With Parallel Test Run Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following example shows you how to get up and running with unit testing ``sanic`` application with parallel test
execution support provided by the ``pytest-xdist`` plugin.
.. literalinclude:: ../../examples/pytest_xdist.py
Amending Request Object
~~~~~~~~~~~~~~~~~~~~~~~
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
.. literalinclude:: ../../examples/amending_request_object.py
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_

View File

@ -1,92 +0,0 @@
Exceptions
==========
Exceptions can be thrown from within request handlers and will automatically be
handled by Sanic. Exceptions take a message as their first argument, and can
also take a status code to be passed back in the HTTP response.
Throwing an exception
---------------------
To throw an exception, simply `raise` the relevant exception from the
`sanic.exceptions` module.
.. code-block:: python
from sanic.exceptions import ServerError
@app.route('/killme')
async def i_am_ready_to_die(request):
raise ServerError("Something bad happened", status_code=500)
You can also use the `abort` function with the appropriate status code:
.. code-block:: python
from sanic.exceptions import abort
from sanic.response import text
@app.route('/youshallnotpass')
async def no_no(request):
abort(401)
# this won't happen
text("OK")
Handling exceptions
-------------------
To override Sanic's default handling of an exception, the `@app.exception`
decorator is used. The decorator expects a list of exceptions to handle as
arguments. You can pass `SanicException` to catch them all! The decorated
exception handler function must take a `Request` and `Exception` object as
arguments.
.. code-block:: python
from sanic.response import text
from sanic.exceptions import NotFound
@app.exception(NotFound)
async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
You can also add an exception handler as such:
.. code-block:: python
from sanic import Sanic
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic("error_handler_example")
app.error_handler.add(Exception, server_error_handler)
In some cases, you might want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:
.. code-block:: python
from sanic import Sanic
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request, exception):
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
return super().default(request, exception)
app = Sanic("custom_error_handler_example")
app.error_handler = CustomErrorHandler()
Useful exceptions
-----------------
Some of the most useful exceptions are presented below:
- `NotFound`: called when a suitable route for the request isn't found.
- `ServerError`: called when something goes wrong inside the server. This
usually occurs if there is an exception raised in user code.
See the `sanic.exceptions` module for the full list of exceptions to throw.

View File

@ -1,4 +0,0 @@
Extensions
==========
Moved to the `awesome-sanic <https://github.com/mekicha/awesome-sanic>`_ list.

View File

@ -1,85 +0,0 @@
Getting Started
===============
Make sure you have both `pip <https://pip.pypa.io/en/stable/installing/>`_ and at
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
1. Install Sanic
----------------
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
.. code-block:: bash
pip3 install sanic
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` ( with`X` = `UVLOOP`/`UJSON`)
to true will stop that features installation.
.. code-block:: bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install --no-binary :all: sanic
You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_
.. code-block:: bash
conda config --add channels conda-forge
conda install sanic
2. Create a file called `main.py`
---------------------------------
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic("hello_example")
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
3. Run the server
-----------------
.. code-block:: bash
python3 main.py
4. Check your browser
---------------------
Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!
5. Application registry
-----------------------
When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.
.. code-block:: python
# ./path/to/server.py
from sanic import Sanic
app = Sanic("my_awesome_server")
# ./path/to/somewhere_else.py
from sanic import Sanic
app = Sanic.get_app("my_awesome_server")
If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name:
.. code-block:: python
app = Sanic.get_app("my_awesome_server", force_create=True)

View File

@ -1,103 +0,0 @@
Logging
=======
Sanic allows you to do different types of logging (access log, error
log) on the requests based on the `python3 logging API`_. You should
have some basic knowledge on python3 logging if you want to create a new
configuration.
Quick Start
~~~~~~~~~~~
A simple example using default settings would be like this:
.. code:: python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('logging_example')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
After the server is running, you can see some messages looks like:
::
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
You can send a request to server and it will print the log messages:
::
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
To use your own logging config, simply use
``logging.config.dictConfig``, or pass ``log_config`` when you
initialize ``Sanic`` app:
.. code:: python
app = Sanic('logging_example', log_config=LOGGING_CONFIG)
And to close logging, simply assign access_log=False:
.. code:: python
if __name__ == "__main__":
app.run(access_log=False)
This would skip calling logging functions when handling requests. And
you could even do further in production to gain extra speed:
.. code:: python
if __name__ == "__main__":
# disable debug messages
app.run(debug=False, access_log=False)
Configuration
~~~~~~~~~~~~~
By default, ``log_config`` parameter is set to use
``sanic.log.LOGGING_CONFIG_DEFAULTS`` dictionary for configuration.
There are three ``loggers`` used in sanic, and **must be defined if you
want to create your own logging configuration**:
================ ==============================
Logger Name Usecase
================ ==============================
``sanic.root`` Used to log internal messages.
``sanic.error`` Used to log error logs.
``sanic.access`` Used to log access logs.
================ ==============================
Log format:
^^^^^^^^^^^
In addition to default parameters provided by python (``asctime``,
``levelname``, ``message``), Sanic provides additional parameters for
access logger with:
===================== ========================================== ========
Log Context Parameter Parameter Value Datatype
===================== ========================================== ========
``host`` ``request.ip`` str
``request`` ``request.method`` + " " + ``request.url`` str
``status`` ``response.status`` int
``byte`` ``len(response.body)`` int
===================== ========================================== ========
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
.. _python3 logging API: https://docs.python.org/3/howto/logging.html

View File

@ -1,188 +0,0 @@
Middleware And Listeners
========================
Middleware are functions which are executed before or after requests to the
server. They can be used to modify the *request to* or *response from*
user-defined handler functions.
Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
Middleware
----------
There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`.
* Request middleware receives only the `request` as an argument and are executed in the order they were added.
* Response middleware receives both the `request` and `response` and are executed in *reverse* order.
The simplest middleware doesn't modify the request or response at all:
.. code-block:: python
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@app.middleware('response')
async def print_on_response(request, response):
print("I print when a response is returned by the server")
Modifying the request or response
---------------------------------
Middleware can modify the request or response parameter it is given, *as long
as it does not return it*. The following example shows a practical use-case for
this.
.. code-block:: python
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Arbitrary data may be stored in request context:
request.ctx.foo = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
@app.get("/")
async def index(request):
return sanic.response.text(request.ctx.foo)
app.run(host="0.0.0.0", port=8000)
The three middlewares are executed in the following order:
1. The first request middleware **add_key** adds a new key `foo` into request context.
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
3. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
4. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server*
Responding early
----------------
If middleware returns a `HTTPResponse` object, the request will stop processing
and the response will be returned. If this occurs to a request before the
relevant user route handler is reached, the handler will never be called.
Returning a response will also prevent any further middleware from running.
.. code-block:: python
@app.middleware('request')
async def halt_request(request):
return text('I halted the request')
@app.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
Custom context
--------------
Arbitrary data may be stored in `request.ctx`. A typical use case
would be to store the user object acquired from database in an authentication
middleware. Keys added are accessible to all later middleware as well as
the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes
no use of it.
Listeners
---------
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
.. code-block:: python
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@app.listener('after_server_start')
async def notify_server_started(app, loop):
print('Server successfully started!')
@app.listener('before_server_stop')
async def notify_server_stopping(app, loop):
print('Server shutting down!')
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.db.close()
Note:
The listeners are deconstructed in the reverse order of being constructed.
For example:
If the first listener in before_server_start handler setups a database connection,
ones registered after it can rely on that connection being alive both when they are started
and stopped, because stopping is done in reverse order, and the database connection is
torn down last.
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
.. code-block:: python
app = Sanic(__name__)
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
.. code-block:: python
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
.. code-block:: python
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds)
Or you can pass the app explicitly for the same effect:
.. code-block:: python
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds(app))

View File

@ -1,222 +0,0 @@
.. _nginx:
Nginx Deployment
================
Introduction
~~~~~~~~~~~~
Although Sanic can be run directly on Internet, it may be useful to use a proxy
server such as Nginx in front of it. This is particularly useful for running
multiple virtual hosts on the same IP, serving NodeJS or other services beside
a single Sanic app, and it also allows for efficient serving of static files.
SSL and HTTP/2 are also easily implemented on such proxy.
We are setting the Sanic app to serve only locally at `127.0.0.1:8000`, while the
Nginx installation is responsible for providing the service to public Internet
on domain `example.com`. Static files will be served from `/var/www/`.
Proxied Sanic app
~~~~~~~~~~~~~~~~~
The app needs to be setup with a secret key used to identify a trusted proxy,
so that real client IP and other information can be identified. This protects
against anyone on the Internet sending fake headers to spoof their IP addresses
and other details. Choose any random string and configure it both on the app
and in Nginx config.
.. code-block:: python
from sanic import Sanic
from sanic.response import text
app = Sanic("proxied_example")
app.config.FORWARDED_SECRET = "YOUR SECRET"
@app.get("/")
def index(request):
# This should display external (public) addresses:
return text(
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, workers=8, access_log=False)
Since this is going to be a system service, save your code to
`/srv/sanicexample/sanicexample.py`.
For testing, run your app in a terminal.
Nginx configuration
~~~~~~~~~~~~~~~~~~~
Quite much configuration is required to allow fast transparent proxying, but
for the most part these don't need to be modified, so bear with me.
Upstream servers need to be configured in a separate `upstream` block to enable
HTTP keep-alive, which can drastically improve performance, so we use this
instead of directly providing an upstream address in `proxy_pass` directive. In
this example, the upstream section is named by `server_name`, i.e. the public
domain name, which then also gets passed to Sanic in the `Host` header. You may
change the naming as you see fit. Multiple servers may also be provided for
load balancing and failover.
Change the two occurrences of `example.com` to your true domain name, and
instead of `YOUR SECRET` use the secret you chose for your app.
::
upstream example.com {
keepalive 100;
server 127.0.0.1:8000;
#server unix:/tmp/sanic.sock;
}
server {
server_name example.com;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# Serve static files if found, otherwise proxy to Sanic
location / {
root /var/www;
try_files $uri @sanic;
}
location @sanic {
proxy_pass http://$server_name;
# Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered)
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
# Proxy forwarding (password configured in app.config.FORWARDED_SECRET)
proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Allow websockets
proxy_set_header connection "upgrade";
proxy_set_header upgrade $http_upgrade;
}
}
To avoid cookie visibility issues and inconsistent addresses on search engines,
it is a good idea to redirect all visitors to one true domain, always using
HTTPS:
::
# Redirect all HTTP to HTTPS with no-WWW
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ~^(?:www\.)?(.*)$;
return 301 https://$1$request_uri;
}
# Redirect WWW to no-WWW
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ~^www\.(.*)$;
return 301 $scheme://$1$request_uri;
}
The above config sections may be placed in `/etc/nginx/sites-available/default`
or in other site configs (be sure to symlink them to `sites-enabled` if you
create new ones).
Make sure that your SSL certificates are configured in the main config, or
add the `ssl_certificate` and `ssl_certificate_key` directives to each
`server` section that listens on SSL.
Additionally, copy&paste all of this into `nginx/conf.d/forwarded.conf`:
::
# RFC 7239 Forwarded header for Nginx proxy_pass
# Add within your server or location block:
# proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Configure your upstream web server to identify this proxy by that password
# because otherwise anyone on the Internet could spoof these headers and fake
# their real IP address and other information to your service.
# Provide the full proxy chain in $proxy_forwarded
map $proxy_add_forwarded $proxy_forwarded {
default "$proxy_add_forwarded;by=\"_$hostname\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\"";
}
# The following mappings are based on
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
map $remote_addr $proxy_forwarded_elem {
# IPv4 addresses can be sent as-is
~^[0-9.]+$ "for=$remote_addr";
# IPv6 addresses need to be bracketed and quoted
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it
default "$proxy_forwarded_elem";
}
For installs that don't use `conf.d` and `sites-available`, all of the above
configs may also be placed inside the `http` section of the main `nginx.conf`.
Reload Nginx config after changes:
::
sudo nginx -s reload
Now you should be able to connect your app on `https://example.com/`. Any 404
errors and such will be handled by Sanic's error pages, and whenever a static
file is present at a given path, it will be served by Nginx.
SSL certificates
~~~~~~~~~~~~~~~~
If you haven't already configured valid certificates on your server, now is a
good time to do so. Install `certbot` and `python3-certbot-nginx`, then run
::
certbot --nginx -d example.com -d www.example.com
`<https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/>`_
Running as a service
~~~~~~~~~~~~~~~~~~~~
This part is for Linux distributions based on `systemd`. Create a unit file
`/etc/systemd/system/sanicexample.service`::
[Unit]
Description=Sanic Example
[Service]
User=nobody
WorkingDirectory=/srv/sanicexample
ExecStart=/usr/bin/env python3 sanicexample.py
Restart=always
[Install]
WantedBy=multi-user.target
Then reload service files, start your service and enable it on boot::
sudo systemctl daemon-reload
sudo systemctl start sanicexample
sudo systemctl enable sanicexample

View File

@ -1,270 +0,0 @@
Request Data
============
When an endpoint receives a HTTP request, the route function is passed a
`Request` object.
The following variables are accessible as properties on `Request` objects:
- `json` (any) - JSON body
.. code-block:: python
from sanic.response import json
@app.route("/json")
def post_json(request):
return json({ "received": True, "message": request.json })
- `args` (dict) - Query string variables. A query string is the section of a
URL that resembles ``?key1=value1&key2=value2``.
If that URL were to be parsed, the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value. Property is providing the default parsing
strategy. If you would like to change it look to the section below (`Changing the default parsing rules of the queryset`).
.. code-block:: python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
- `query_args` (list) - On many cases you would need to access the url arguments in
a less packed form. `query_args` is the list of `(key, value)` tuples.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`). For the same previous URL queryset `?key1=value1&key2=value2`,
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`. And in case of the multiple params
with the same key like `?key1=value1&key2=value2&key1=value3` the `query_args` list would look like
`[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
The difference between Request.args and Request.query_args for the queryset `?key1=value1&key2=value2&key1=value3`
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/test_request_args")
async def test_request_args(request):
return json({
"parsed": True,
"url": request.url,
"query_string": request.query_string,
"args": request.args,
"query_args": request.query_args,
})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
Output
.. code-block:: json
{
"parsed":true,
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
.. code-block:: python
from sanic.response import json
@app.route("/files")
def post_json(request):
test_file = request.files.get('test')
file_parameters = {
'body': test_file.body,
'name': test_file.name,
'type': test_file.type,
}
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
- `form` (dict) - Posted form variables.
.. code-block:: python
from sanic.response import json
@app.route("/form")
def post_json(request):
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
- `body` (bytes) - Posted raw body. This property allows retrieval of the
request's raw data, regardless of content type.
.. code-block:: python
from sanic.response import text
@app.route("/users", methods=["POST",])
def create_user(request):
return text("You are trying to create a user with the following POST: %s" % request.body)
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
- `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.
- `socket` (tuple) - (IP, port) of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
.. code-block:: python
from sanic.response import json
from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root(request):
if request.app.config['DEBUG']:
return json({'status': 'debug'})
else:
return json({'status': 'production'})
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
- `scheme`: The URL scheme associated with the request: 'http|https|ws|wss' or arbitrary value given by the headers.
- `host`: The host associated with the request(which in the `Host` header): `localhost:8080`
- `server_name`: The hostname of the server, without port number. the value is seeked in this order: `config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
- `server_port`: Like `server_name`. Seeked in this order: `x-forwarded-port` header, :func:`Request.host`, actual port used by the transport layer socket.
- `path`: The path of the request: `/posts/1/`
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
- `uri_template`: Template for matching route handler: `/posts/<id>/`
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
- `url_for`: Just like `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.
Changing the default parsing rules of the queryset
--------------------------------------------------
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
- `keep_blank_values` (bool): `False` - 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): `False` - flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
with the new values.
For the queryset `/?test1=value1&test2=&test3=value3`:
.. code-block:: python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
The output will be:
.. code-block:: JSON
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
Accessing values using `get` and `getlist`
------------------------------------------
The `request.args` returns a subclass of `dict` called `RequestParameters`.
The key difference when using this object is the distinction between the `get` and `getlist` methods.
- `get(key, default=None)` operates as normal, except that when the value of
the given key is a list, *only the first item is returned*.
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
.. code-block:: python
from sanic.request import RequestParameters
args = RequestParameters()
args['titles'] = ['Post 1', 'Post 2']
args.get('titles') # => 'Post 1'
args.getlist('titles') # => ['Post 1', 'Post 2']
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
def get_handler(request):
return json({
"p1": request.args.getlist("p1")
})
Accessing the handler name with the request.endpoint attribute
--------------------------------------------------------------
The `request.endpoint` attribute holds the handler's name. For instance, the below
route will return "hello".
.. code-block:: python
from sanic.response import text
from sanic import Sanic
app = Sanic(__name__)
@app.get("/")
def hello(request):
return text(request.endpoint)
Or, with a blueprint it will be include both, separated by a period. For example, the below route would return foo.bar:
.. code-block:: python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
app = Sanic(__name__)
blueprint = Blueprint('foo')
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
app.blueprint(blueprint)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,139 +0,0 @@
Response
========
Use functions in `sanic.response` module to create responses.
Plain Text
----------
.. code-block:: python
from sanic import response
@app.route('/text')
def handle_request(request):
return response.text('Hello world!')
HTML
----
.. code-block:: python
from sanic import response
@app.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
JSON
----
.. code-block:: python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json({'message': 'Hello world!'})
File
----
.. code-block:: python
from sanic import response
@app.route('/file')
async def handle_request(request):
return await response.file('/srv/www/whatever.png')
Streaming
---------
.. code-block:: python
from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
See `Streaming <streaming.html>`_ for more information.
File Streaming
--------------
For large files, a combination of File and Streaming above
.. code-block:: python
from sanic import response
@app.route('/big_file.png')
async def handle_request(request):
return await response.file_stream('/srv/www/whatever.png')
Redirect
--------
.. code-block:: python
from sanic import response
@app.route('/redirect')
def handle_request(request):
return response.redirect('/json')
Raw
---
Response without encoding the body
.. code-block:: python
from sanic import response
@app.route('/raw')
def handle_request(request):
return response.raw(b'raw data')
Empty
--------------
For responding with an empty message as defined by `RFC 2616 <https://tools.ietf.org/search/rfc2616#section-7.2.1>`_
.. code-block:: python
from sanic import response
@app.route('/empty')
async def handle_request(request):
return response.empty()
Modify headers or status
------------------------
To modify headers or status code, pass the `headers` or `status` argument to those functions:
.. code-block:: python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)

View File

@ -1,433 +0,0 @@
Routing
-------
Routing allows the user to specify handler functions for different URL endpoints.
A basic route looks like the following, where `app` is an instance of the
`Sanic` class:
.. code-block:: python
from sanic.response import json
@app.route("/")
async def test(request):
return json({ "hello": "world" })
When the url `http://server.url/` is accessed (the base url of the server), the
final `/` is matched by the router to the handler function, `test`, which then
returns a JSON object.
Sanic handler functions must be defined using the `async def` syntax, as they
are asynchronous functions.
Request parameters
==================
Sanic comes with a basic router that supports request parameters.
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
.. code-block:: python
from sanic.response import text
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text('Tag - {}'.format(tag))
To specify a type for the parameter, add a `:type` after the parameter name,
inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
Supported types
~~~~~~~~~~~~~~~
* `string`
* "Bob"
* "Python 3"
* `int`
* 10
* 20
* 30
* -10
* (No floats work here)
* `number`
* 1
* 1.5
* 10
* -10
* `alpha`
* "Bob"
* "Python"
* (If it contains a symbol or a non alphanumeric character it will fail)
* `path`
* "hello"
* "hello.text"
* "hello world"
* `uuid`
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
* `regex expression`
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
.. code-block:: python
from sanic.response import text
@app.route('/string/<string_arg:string>')
async def string_handler(request, string_arg):
return text('String - {}'.format(string_arg))
@app.route('/int/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/alpha/<alpha_arg:alpha>')
async def number_handler(request, alpha_arg):
return text('Alpha - {}'.format(alpha_arg))
@app.route('/path/<path_arg:path>')
async def number_handler(request, path_arg):
return text('Path - {}'.format(path_arg))
@app.route('/uuid/<uuid_arg:uuid>')
async def number_handler(request, uuid_arg):
return text('Uuid - {}'.format(uuid_arg))
@app.route('/person/<name:[A-z]+>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
.. warning::
`str` is not a valid type tag. If you want `str` recognition then you must use `string`
HTTP request types
==================
By default, a route defined on a URL will be available for only GET requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`,
which allows the handler function to work with any of the HTTP methods in the list.
.. code-block:: python
from sanic.response import text
@app.route('/post', methods=['POST'])
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request - {}'.format(request.args))
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is also a route with no host, it will be the default.
.. code-block:: python
@app.route('/get', methods=['GET'], host='example.com')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
# if the host header doesn't match example.com, this route will be used
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request in default - {}'.format(request.args))
There are also shorthand method decorators:
.. code-block:: python
from sanic.response import text
@app.post('/post')
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.get('/get')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
The `add_route` method
======================
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
.. code-block:: python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
URL building with `url_for`
===========================
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
.. code-block:: python
from sanic.response import redirect
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return text('Post - {}'.format(post_id))
Other things to keep in mind when using `url_for`:
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/5?arg_one=one&arg_two=two
- Multivalue argument can be passed to `url_for`. For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments at once
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised.
WebSocket routes
================
Routes for the WebSocket protocol can be defined with the `@app.websocket`
decorator:
.. code-block:: python
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
Alternatively, the `app.add_websocket_route` method can be used instead of the
decorator:
.. code-block:: python
async def feed(request, ws):
pass
app.add_websocket_route(my_websocket_handler, '/feed')
Handlers to a WebSocket route are invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
WebSocket support requires the `websockets <https://github.com/aaugustin/websockets>`_
package by Aymeric Augustin.
About `strict_slashes`
======================
You can make `routes` strict to trailing slash or not, it's configurable.
.. code-block:: python
# provide default strict_slashes value for all routes
app = Sanic('test_route_strict_slash', strict_slashes=True)
# you can also overwrite strict_slashes value for specific route
@app.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
# It also works for blueprints
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
@bp.get('/bp/get', strict_slashes=False)
def handler(request):
return text('OK')
app.blueprint(bp)
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
falls under the `strict_slashes` behavior.
| Route/
| ├──Blueprint/
| ├──Application/
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
found in the above order will be applied to the route in question.
.. code-block:: python
from sanic import Sanic, Blueprint
from sanic.response import text
app = Sanic("sample_strict_slashes", strict_slashes=True)
@app.get("/r1")
def r1(request):
return text("strict_slashes is applicable from App level")
@app.get("/r2", strict_slashes=False)
def r2(request):
return text("strict_slashes is not applicable due to False value set in route level")
bp = Blueprint("bp", strict_slashes=False)
@bp.get("/r3", strict_slashes=True)
def r3(request):
return text("strict_slashes applicable from blueprint route level")
bp1 = Blueprint("bp1", strict_slashes=True)
@bp.get("/r4")
def r3(request):
return text("strict_slashes applicable from blueprint level")
User defined route name
=======================
A custom route name can be used by passing a `name` argument while registering the route which will
override the default route name generated using the `handler.__name__` attribute.
.. code-block:: python
app = Sanic('test_named_route')
@app.get('/get', name='get_handler')
def handler(request):
return text('OK')
# then you need use `app.url_for('get_handler')`
# instead of # `app.url_for('handler')`
# It also works for blueprints
bp = Blueprint('test_named_bp')
@bp.get('/bp/get', name='get_handler')
def handler(request):
return text('OK')
app.blueprint(bp)
# then you need use `app.url_for('test_named_bp.get_handler')`
# instead of `app.url_for('test_named_bp.handler')`
# different names can be used for same url with different methods
@app.get('/test', name='route_test')
def handler(request):
return text('OK')
@app.post('/test', name='route_post')
def handler2(request):
return text('OK POST')
@app.put('/test', name='route_put')
def handler3(request):
return text('OK PUT')
# below url are the same, you can use any of them
# '/test'
app.url_for('route_test')
# app.url_for('route_post')
# app.url_for('route_put')
# for same handler name with different methods
# you need specify the name (it's url_for issue)
@app.get('/get')
def handler(request):
return text('OK')
@app.post('/post', name='post_handler')
def handler(request):
return text('OK')
# then
# app.url_for('handler') == '/get'
# app.url_for('post_handler') == '/post'
Build URL for static files
==========================
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored.
.. code-block:: python
app = Sanic('test_static')
app.static('/static', './static')
app.static('/uploads', './uploads', name='uploads')
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
bp = Blueprint('bp', url_prefix='bp')
bp.static('/static', './static')
bp.static('/uploads', './uploads', name='uploads')
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
# then build the url
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
app.url_for('static', name='best_png') == '/the_best.png'
# blueprint url building
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'

View File

@ -1,66 +0,0 @@
Sockets
=======
Sanic can use the python
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
non IPv4 sockets.
IPv6 example:
.. code:: python
from sanic import Sanic
from sanic.response import json
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 7777))
app = Sanic("ipv6_example")
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(sock=sock)
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
UNIX socket example:
.. code:: python
import signal
import sys
import socket
import os
from sanic import Sanic
from sanic.response import json
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_socket)
app = Sanic("unix_socket_example")
@app.route("/")
async def test(request):
return json({"hello": "world"})
def signal_handler(sig, frame):
print('Exiting')
os.unlink(server_socket)
sys.exit(0)
if __name__ == "__main__":
app.run(sock=sock)
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``

View File

@ -1,20 +0,0 @@
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
app.run(host="0.0.0.0", port=8443, ssl=context)
You can also pass in the locations of a certificate and key as a dictionary:
.. code:: python
ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"}
app.run(host="0.0.0.0", port=8443, ssl=ssl)

View File

@ -1,92 +0,0 @@
Static Files
============
Static files and directories, such as an image file, are served by Sanic when
registered with the `app.static()` method. The method takes an endpoint URL and a
filename. The file specified will then be accessible via the given endpoint.
.. code-block:: python
from sanic import Sanic
from sanic.blueprints import Blueprint
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
# use url_for to build the url, name defaults to 'static' and can be ignored
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
# you can use url_for to build the static file url
# you can ignore name and filename parameters if you don't define it
app.url_for('static', name='best_png') == '/the_best.png'
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
# you need define the name for other static files
app.static('/another.png', '/home/ubuntu/another.png', name='another')
app.url_for('static', name='another') == '/another.png'
app.url_for('static', name='another', filename='any') == '/another.png'
# also, you can use static for blueprint
bp = Blueprint('bp', url_prefix='/bp')
bp.static('/static', './static')
# specify a different content_type for your files
# such as adding 'charset'
app.static('/', '/public/index.html', content_type="text/html; charset=utf-8")
# servers the file directly
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
> **Note:** Sanic does not provide directory index when you serve a static directory.
Virtual Host
------------
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
app.static('/static', './static')
app.static('/example_static', './example_static', host='www.example.com')
Streaming Large File
--------------------
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
Here is an example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)

View File

@ -1,147 +0,0 @@
Streaming
=========
Request Streaming
-----------------
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
.. code-block:: python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.blueprints import Blueprint
from sanic.response import stream, text
bp = Blueprint('blueprint_request_stream')
app = Sanic(__name__)
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
@app.post('/stream', stream=True)
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
await response.write(body)
return stream(streaming)
@bp.put('/bp_stream', stream=True)
async def bp_put_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
# You can also use `bp.add_route()` with stream argument
async def bp_post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
async def post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
app.blueprint(bp)
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
Response Streaming
------------------
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
.. code-block:: python
from sanic import Sanic
from sanic.response import stream
app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
await response.write('foo,')
await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
.. code-block:: python
@app.route("/")
async def index(request):
async def stream_from_db(response):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
await response.write(record[0])
return stream(stream_from_db)
If a client supports HTTP/1.1, Sanic will use `chunked transfer encoding <https://en.wikipedia.org/wiki/Chunked_transfer_encoding>`_; you can explicitly enable or disable it using `chunked` option of the `stream` function.
File Streaming
--------------
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
.. code-block:: python
from aiofiles import os as async_os
from sanic.response import file_stream
@app.route("/")
async def index(request):
file_path = "/srv/www/whatever.png"
file_stat = await async_os.stat(file_path)
headers = {"Content-Length": str(file_stat.st_size)}
return await file_stream(
file_path,
headers=headers,
chunked=False,
)

View File

@ -1,171 +0,0 @@
Testing
=======
Sanic endpoints can be tested locally using the `test_client` object, which
depends on an additional package: `httpx <https://www.encode.io/httpx/>`_
library, which implements an API that mirrors the `requests` library.
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
for you to run against your application. A simple example (using pytest) is like follows:
.. code-block:: python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_index_returns_200():
request, response = app.test_client.get('/')
assert response.status == 200
def test_index_put_not_allowed():
request, response = app.test_client.put('/')
assert response.status == 405
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `httpx`.
The `test_client` methods accept the following arguments and keyword arguments:
- `uri` *(default `'/'`)* A string representing the URI to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `server_kwargs` *(default `{}`)* a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
For example, to supply data to a GET request, you would do the following:
.. code-block:: python
def test_get_request_includes_data():
params = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.get('/', params=params)
assert request.args.get('key1') == 'value1'
And to supply data to a JSON POST request:
.. code-block:: python
def test_post_json_request_includes_data():
data = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.post('/', data=json.dumps(data))
assert request.json.get('key1') == 'value1'
More information about
the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an
instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still
executed.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Using a random port
-------------------
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
.. code-block:: python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
pytest-sanic
------------
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ is a pytest plugin, it helps you to test your code asynchronously.
Just write tests like,
.. code-block:: python
async def test_sanic_db_find_by_id(app):
"""
Let's assume that, in db we have,
{
"id": "123",
"name": "Kobe Bryant",
"team": "Lakers",
}
"""
doc = await app.db["players"].find_by_id("123")
assert doc.name == "Kobe Bryant"
assert doc.team == "Lakers"
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ also provides some useful fixtures, like loop, unused_port,
test_server, test_client.
.. code-block:: python
@pytest.yield_fixture
def app():
app = Sanic("test_sanic_app")
@app.route("/test_get", methods=['GET'])
async def test_get(request):
return response.json({"GET": True})
@app.route("/test_post", methods=['POST'])
async def test_post(request):
return response.json({"POST": True})
yield app
@pytest.fixture
def test_cli(loop, app, test_client):
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
#########
# Tests #
#########
async def test_fixture_test_client_get(test_cli):
"""
GET request
"""
resp = await test_cli.get('/test_get')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"GET": True}
async def test_fixture_test_client_post(test_cli):
"""
POST request
"""
resp = await test_cli.post('/test_post')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"POST": True}

View File

@ -1,54 +0,0 @@
Versioning
==========
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
Per route
---------
You can pass a version number to the routes directly.
.. code-block:: python
from sanic import response
@app.route('/text', version=1)
def handle_request(request):
return response.text('Hello world! Version 1')
@app.route('/text', version=2)
def handle_request(request):
return response.text('Hello world! Version 2')
app.run(port=80)
Then with curl:
.. code-block:: bash
curl localhost/v1/text
curl localhost/v2/text
Global blueprint version
------------------------
You can also pass a version number to the blueprint, which will apply to all routes.
.. code-block:: python
from sanic import response
from sanic.blueprints import Blueprint
bp = Blueprint('test', version=1)
@bp.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
Then with curl:
.. code-block:: bash
curl localhost/v1/html

View File

@ -1,59 +0,0 @@
WebSocket
=========
Sanic provides an easy to use abstraction on top of `websockets`.
Sanic Supports websocket versions 7 and 8.
To setup a WebSocket:
.. code:: python
from sanic import Sanic
from sanic.response import json
from sanic.websocket import WebSocketProtocol
app = Sanic("websocket_example")
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, protocol=WebSocketProtocol)
Alternatively, the ``app.add_websocket_route`` method can be used instead of the
decorator:
.. code:: python
async def feed(request, ws):
pass
app.add_websocket_route(feed, '/feed')
Handlers for a WebSocket route is invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has ``send``
and ``recv`` methods to send and receive data respectively.
You could setup your own WebSocket configuration through ``app.config``, like
.. code:: python
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
app.config.WEBSOCKET_PING_INTERVAL = 20
app.config.WEBSOCKET_PING_TIMEOUT = 20
These settings will have no impact if running in ASGI mode.
Find more in ``Configuration`` section.

View File

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

View File

@ -11,7 +11,18 @@ from inspect import isawaitable
from socket import socket from socket import socket
from ssl import Purpose, SSLContext, create_default_context from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc from traceback import format_exc
from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union from typing import (
Any,
Callable,
Deque,
Dict,
Iterable,
List,
Optional,
Set,
Type,
Union,
)
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
from sanic_routing.exceptions import FinalizationError # type: ignore from sanic_routing.exceptions import FinalizationError # type: ignore
@ -53,6 +64,10 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic(BaseSanic): class Sanic(BaseSanic):
"""
The main application instance
"""
_app_registry: Dict[str, "Sanic"] = {} _app_registry: Dict[str, "Sanic"] = {}
test_mode = False test_mode = False
@ -62,7 +77,7 @@ class Sanic(BaseSanic):
router: Router = None, router: Router = None,
error_handler: ErrorHandler = None, error_handler: ErrorHandler = None,
load_env: bool = True, load_env: bool = True,
request_class: Request = None, request_class: Type[Request] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
@ -85,8 +100,8 @@ class Sanic(BaseSanic):
self.request_class = request_class self.request_class = request_class
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env) self.config = Config(load_env=load_env)
self.request_middleware: Iterable[MiddlewareType] = deque() self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Iterable[MiddlewareType] = deque() self.response_middleware: Deque[MiddlewareType] = deque()
self.blueprints: Dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self._blueprint_order: List[Blueprint] = [] self._blueprint_order: List[Blueprint] = []
self.configure_logging = configure_logging self.configure_logging = configure_logging
@ -98,8 +113,10 @@ class Sanic(BaseSanic):
self.is_running = False self.is_running = False
self.websocket_enabled = False self.websocket_enabled = False
self.websocket_tasks: Set[Future] = set() self.websocket_tasks: Set[Future] = set()
self.named_request_middleware: Dict[str, MiddlewareType] = {} self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, MiddlewareType] = {} self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
# self.named_request_middleware: Dict[str, MiddlewareType] = {}
# self.named_response_middleware: Dict[str, MiddlewareType] = {}
self._test_manager = None self._test_manager = None
self._test_client = None self._test_client = None
self._asgi_client = None self._asgi_client = None
@ -116,9 +133,12 @@ class Sanic(BaseSanic):
@property @property
def loop(self): def loop(self):
"""Synonymous with asyncio.get_event_loop(). """
Synonymous with asyncio.get_event_loop().
Only supported when using the `app.run` method. .. note::
Only supported when using the `app.run` method.
""" """
if not self.is_running and self.asgi is False: if not self.is_running and self.asgi is False:
raise SanicException( raise SanicException(
@ -131,12 +151,16 @@ class Sanic(BaseSanic):
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def add_task(self, task): def add_task(self, task) -> None:
"""Schedule a task to run later, after the loop has started. """
Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call also return a future, and the actual ensure_future call
is delayed until before server start. is delayed until before server start.
`See user guide
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
:param task: future, couroutine or awaitable :param task: future, couroutine or awaitable
""" """
try: try:
@ -147,7 +171,7 @@ class Sanic(BaseSanic):
partial(self._loop_add_task, task) partial(self._loop_add_task, task)
) )
def register_listener(self, listener, event): def register_listener(self, listener: Callable, event: str) -> Any:
""" """
Register the listener for a given event. Register the listener for a given event.
@ -165,7 +189,7 @@ class Sanic(BaseSanic):
self.listeners[_event].append(listener) self.listeners[_event].append(listener)
return listener return listener
def register_middleware(self, middleware, attach_to="request"): def register_middleware(self, middleware, attach_to: str = "request"):
""" """
Register an application level middleware that will be attached Register an application level middleware that will be attached
to all the API URLs registered under this application. to all the API URLs registered under this application.
@ -190,8 +214,23 @@ class Sanic(BaseSanic):
return middleware return middleware
def register_named_middleware( def register_named_middleware(
self, middleware, route_names, attach_to="request" self,
middleware,
route_names: Iterable[str],
attach_to: str = "request",
): ):
"""
Method for attaching middleware to specific routes. This is mainly an
internal tool for use by Blueprints to attach middleware to only its
specfic routes. But, it could be used in a more generalized fashion.
:param middleware: the middleware to execute
:param route_names: a list of the names of the endpoints
:type route_names: Iterable[str]
:param attach_to: whether to attach to request or response,
defaults to "request"
:type attach_to: str, optional
"""
if attach_to == "request": if attach_to == "request":
for _rn in route_names: for _rn in route_names:
if _rn not in self.named_request_middleware: if _rn not in self.named_request_middleware:
@ -204,6 +243,7 @@ class Sanic(BaseSanic):
self.named_response_middleware[_rn] = deque() self.named_response_middleware[_rn] = deque()
if middleware not in self.named_response_middleware[_rn]: if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware) self.named_response_middleware[_rn].appendleft(middleware)
return middleware
def _apply_exception_handler(self, handler: FutureException): def _apply_exception_handler(self, handler: FutureException):
"""Decorate a function to be registered as a handler for exceptions """Decorate a function to be registered as a handler for exceptions
@ -218,12 +258,12 @@ class Sanic(BaseSanic):
self.error_handler.add(e, handler.handler) self.error_handler.add(e, handler.handler)
else: else:
self.error_handler.add(exception, handler.handler) self.error_handler.add(exception, handler.handler)
return handler return handler.handler
def _apply_listener(self, listener: FutureListener): def _apply_listener(self, listener: FutureListener):
return self.register_listener(listener.listener, listener.event) return self.register_listener(listener.listener, listener.event)
def _apply_route(self, route: FutureRoute) -> Route: def _apply_route(self, route: FutureRoute) -> List[Route]:
params = route._asdict() params = route._asdict()
websocket = params.pop("websocket", False) websocket = params.pop("websocket", False)
subprotocols = params.pop("subprotocols", None) subprotocols = params.pop("subprotocols", None)
@ -238,7 +278,15 @@ class Sanic(BaseSanic):
websocket_handler.__name__ = route.handler.__name__ # type: ignore websocket_handler.__name__ = route.handler.__name__ # type: ignore
websocket_handler.is_websocket = True # type: ignore websocket_handler.is_websocket = True # type: ignore
params["handler"] = websocket_handler params["handler"] = websocket_handler
return self.router.add(**params)
routes = self.router.add(**params)
if isinstance(routes, Route):
routes = [routes]
for r in routes:
r.ctx.websocket = websocket
r.ctx.static = params.get("static", False)
return routes
def _apply_static(self, static: FutureStatic) -> Route: def _apply_static(self, static: FutureStatic) -> Route:
return self._register_static(static) return self._register_static(static)
@ -308,8 +356,11 @@ class Sanic(BaseSanic):
Keyword arguments that are not request parameters will be included in Keyword arguments that are not request parameters will be included in
the output URL's query string. the output URL's query string.
`See user guide
<https://sanicframework.org/guide/basics/routing.html#generating-a-url>`__
:param view_name: string referencing the view name :param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request :param kwargs: keys and values that are used to build request
parameters and query string arguments. parameters and query string arguments.
:return: the built URL :return: the built URL
@ -447,7 +498,18 @@ class Sanic(BaseSanic):
# Request Handling # Request Handling
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
async def handle_exception(self, request, exception): async def handle_exception(
self, request: Request, exception: BaseException
):
"""
A handler that catches specific exceptions and outputs a response.
:param request: The current request object
:type request: :class:`SanicASGITestClient`
:param exception: The exception that was raised
:type exception: BaseException
:raises ServerError: response 500
"""
# -------------------------------------------- # # -------------------------------------------- #
# Request Middleware # Request Middleware
# -------------------------------------------- # # -------------------------------------------- #
@ -480,11 +542,13 @@ class Sanic(BaseSanic):
response = await request.respond(response) response = await request.respond(response)
except BaseException: except BaseException:
# Skip response middleware # Skip response middleware
request.stream.respond(response) if request.stream:
request.stream.respond(response)
await response.send(end_stream=True) await response.send(end_stream=True)
raise raise
else: else:
response = request.stream.response if request.stream:
response = request.stream.response
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
await response.send(end_stream=True) await response.send(end_stream=True)
else: else:
@ -492,7 +556,7 @@ class Sanic(BaseSanic):
f"Invalid response type {response!r} (need HTTPResponse)" f"Invalid response type {response!r} (need HTTPResponse)"
) )
async def handle_request(self, request): async def handle_request(self, request: Request):
"""Take a request from the HTTP Server and return a response object """Take a request from the HTTP Server and return a response object
to be sent back The HTTP Server only expects a response object, so to be sent back The HTTP Server only expects a response object, so
exception handling must be done here exception handling must be done here
@ -521,7 +585,11 @@ class Sanic(BaseSanic):
request.name = name request.name = name
request._match_info = kwargs request._match_info = kwargs
if request.stream.request_body and not ignore_body: if (
request.stream
and request.stream.request_body
and not ignore_body
):
if self.router.is_stream_handler(request): if self.router.is_stream_handler(request):
# Streaming handler: lift the size limit # Streaming handler: lift the size limit
request.stream.request_max_size = float("inf") request.stream.request_max_size = float("inf")
@ -559,7 +627,8 @@ class Sanic(BaseSanic):
if response: if response:
response = await request.respond(response) response = await request.respond(response)
else: else:
response = request.stream.response if request.stream:
response = request.stream.response
# Make sure that response is finished / run StreamingHTTP callback # Make sure that response is finished / run StreamingHTTP callback
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
@ -567,7 +636,7 @@ class Sanic(BaseSanic):
else: else:
try: try:
# Fastest method for checking if the property exists # Fastest method for checking if the property exists
handler.is_websocket handler.is_websocket # type: ignore
except AttributeError: except AttributeError:
raise ServerError( raise ServerError(
f"Invalid response type {response!r} " f"Invalid response type {response!r} "
@ -631,6 +700,13 @@ class Sanic(BaseSanic):
@property @property
def asgi_client(self): # noqa def asgi_client(self): # noqa
"""
A testing client that uses ASGI to reach into the application to
execute hanlers.
:return: testing client
:rtype: :class:`SanicASGITestClient`
"""
if self._asgi_client: if self._asgi_client:
return self._asgi_client return self._asgi_client
elif self._test_manager: elif self._test_manager:
@ -661,7 +737,8 @@ class Sanic(BaseSanic):
unix: Optional[str] = None, unix: Optional[str] = None,
loop: None = None, loop: None = None,
) -> None: ) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term """
Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing. signal. On termination, drain connections before closing.
:param host: Address to host on :param host: Address to host on
@ -753,7 +830,9 @@ class Sanic(BaseSanic):
logger.info("Server Stopped") logger.info("Server Stopped")
def stop(self): def stop(self):
"""This kills the Sanic""" """
This kills the Sanic
"""
if not self.is_stopping: if not self.is_stopping:
self.is_stopping = True self.is_stopping = True
get_event_loop().stop() get_event_loop().stop()
@ -770,8 +849,8 @@ class Sanic(BaseSanic):
backlog: int = 100, backlog: int = 100,
access_log: Optional[bool] = None, access_log: Optional[bool] = None,
unix: Optional[str] = None, unix: Optional[str] = None,
return_asyncio_server=False, return_asyncio_server: bool = False,
asyncio_server_kwargs=None, asyncio_server_kwargs: Dict[str, Any] = None,
) -> Optional[AsyncioServer]: ) -> Optional[AsyncioServer]:
""" """
Asynchronous version of :func:`run`. Asynchronous version of :func:`run`.
@ -1019,25 +1098,29 @@ class Sanic(BaseSanic):
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
"""To be ASGI compliant, our instance must be a callable that accepts """
To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest/""" details: https://asgi.readthedocs.io/en/latest
# raise Exception("call") """
self.asgi = True self.asgi = True
self._asgi_app = await ASGIApp.create(self, scope, receive, send) self._asgi_app = await ASGIApp.create(self, scope, receive, send)
asgi_app = self._asgi_app asgi_app = self._asgi_app
await asgi_app() await asgi_app()
# _asgi_single_callable = True # We conform to ASGI 3.0 single-callable _asgi_single_callable = True # We conform to ASGI 3.0 single-callable
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Configuration # Configuration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def update_config(self, config: Union[bytes, str, dict, Any]): def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config. """
Update app.config. Full implementation can be found in the user guide.
Please refer to config.py::Config.update_config for documentation.""" `See user guide
<https://sanicframework.org/guide/deployment/configuration.html#basics>`__
"""
self.config.update_config(config) self.config.update_config(config)
@ -1047,7 +1130,9 @@ class Sanic(BaseSanic):
@classmethod @classmethod
def register_app(cls, app: "Sanic") -> None: def register_app(cls, app: "Sanic") -> None:
"""Register a Sanic instance""" """
Register a Sanic instance
"""
if not isinstance(app, cls): if not isinstance(app, cls):
raise SanicException("Registered app must be an instance of Sanic") raise SanicException("Registered app must be an instance of Sanic")
@ -1059,7 +1144,9 @@ class Sanic(BaseSanic):
@classmethod @classmethod
def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic": def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic":
"""Retrieve an instantiated Sanic instance""" """
Retrieve an instantiated Sanic instance
"""
try: try:
return cls._app_registry[name] return cls._app_registry[name]
except KeyError: except KeyError:

View File

@ -1,108 +1,19 @@
import asyncio
import warnings import warnings
from inspect import isawaitable from inspect import isawaitable
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union from typing import Optional
from urllib.parse import quote from urllib.parse import quote
import sanic.app # noqa import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import InvalidUsage, ServerError from sanic.exceptions import ServerError
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request from sanic.request import Request
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.websocket import WebSocketConnection from sanic.websocket import WebSocketConnection
ASGIScope = MutableMapping[str, Any]
ASGIMessage = MutableMapping[str, Any]
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()
def resume_writing(self) -> None:
self._not_paused.set()
async def complete(self) -> None:
self._not_paused.set()
await self.transport.send(
{"type": "http.response.body", "body": b"", "more_body": False}
)
@property
def is_complete(self) -> bool:
return self._complete.is_set()
async def push_data(self, data: bytes) -> None:
if not self.is_complete:
await self.transport.send(
{"type": "http.response.body", "body": data, "more_body": True}
)
async def drain(self) -> None:
await self._not_paused.wait()
class MockTransport:
_protocol: Optional[MockProtocol]
def __init__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
self.scope = scope
self._receive = receive
self._send = send
self._protocol = None
self.loop = None
def get_protocol(self) -> MockProtocol:
if not self._protocol:
self._protocol = MockProtocol(self, self.loop)
return self._protocol
def get_extra_info(self, info: str) -> Union[str, bool, None]:
if info == "peername":
return self.scope.get("client")
elif info == "sslcontext":
return self.scope.get("scheme") in ["https", "wss"]
return None
def get_websocket_connection(self) -> WebSocketConnection:
try:
return self._websocket_connection
except AttributeError:
raise InvalidUsage("Improper websocket connection.")
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection
def add_task(self) -> None:
raise NotImplementedError
async def send(self, data) -> None:
# TODO:
# - Validation on data and that it is formatted properly and is valid
await self._send(data)
async def receive(self) -> ASGIMessage:
return await self._receive()
class Lifespan: class Lifespan:
def __init__(self, asgi_app: "ASGIApp") -> None: def __init__(self, asgi_app: "ASGIApp") -> None:
self.asgi_app = asgi_app self.asgi_app = asgi_app

View File

@ -1,4 +1,5 @@
from collections.abc import MutableSequence from collections.abc import MutableSequence
from typing import List
class BlueprintGroup(MutableSequence): class BlueprintGroup(MutableSequence):
@ -9,6 +10,32 @@ class BlueprintGroup(MutableSequence):
some of the existing implementation, this class provides a custom some of the existing implementation, this class provides a custom
iterator implementation that will let you use the object of this iterator implementation that will let you use the object of this
class as a list/tuple inside the existing implementation. class as a list/tuple inside the existing implementation.
.. code-block:: python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
""" """
__slots__ = ("_blueprints", "_url_prefix") __slots__ = ("_blueprints", "_url_prefix")
@ -23,23 +50,27 @@ class BlueprintGroup(MutableSequence):
self._url_prefix = url_prefix self._url_prefix = url_prefix
@property @property
def url_prefix(self): def url_prefix(self) -> str:
""" """
Retrieve the URL prefix being used for the Current Blueprint Group Retrieve the URL prefix being used for the Current Blueprint Group
:return: string with url prefix :return: string with url prefix
""" """
return self._url_prefix return self._url_prefix
@property @property
def blueprints(self): def blueprints(self) -> List:
""" """
Retrieve a list of all the available blueprints under this group. Retrieve a list of all the available blueprints under this group.
:return: List of Blueprint instance :return: List of Blueprint instance
""" """
return self._blueprints return self._blueprints
def __iter__(self): def __iter__(self):
"""Tun the class Blueprint Group into an Iterable item""" """
Tun the class Blueprint Group into an Iterable item
"""
return iter(self._blueprints) return iter(self._blueprints)
def __getitem__(self, item): def __getitem__(self, item):
@ -85,6 +116,7 @@ class BlueprintGroup(MutableSequence):
def __len__(self) -> int: def __len__(self) -> int:
""" """
Get the Length of the blueprint group object. Get the Length of the blueprint group object.
:return: Length of Blueprint group object :return: Length of Blueprint group object
""" """
return len(self._blueprints) return len(self._blueprints)

View File

@ -1,41 +1,51 @@
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Optional
from sanic_routing.route import Route # type: ignore
from sanic.base import BaseSanic from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.handlers import ListenerType, MiddlewareType, RouteHandler
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
class Blueprint(BaseSanic): class Blueprint(BaseSanic):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
It is the main tool for grouping functionality and similar endpoints.
`See user guide
<https://sanicframework.org/guide/best-practices/blueprints.html>`__
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
"""
def __init__( def __init__(
self, self,
name, name: str,
url_prefix=None, url_prefix: Optional[str] = None,
host=None, host: Optional[str] = None,
version=None, version: Optional[int] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
): ):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
"""
self.name = name self.name = name
self.url_prefix = url_prefix self.url_prefix = url_prefix
self.host = host self.host = host
self.routes = [] self.routes: List[Route] = []
self.websocket_routes = [] self.websocket_routes: List[Route] = []
self.exceptions = [] self.exceptions: List[RouteHandler] = []
self.listeners = defaultdict(list) self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares = [] self.middlewares: List[MiddlewareType] = []
self.statics = [] self.statics: List[RouteHandler] = []
self.version = version self.version = version
self.strict_slashes = strict_slashes self.strict_slashes = strict_slashes
@ -100,6 +110,9 @@ class Blueprint(BaseSanic):
url_prefix = options.get("url_prefix", self.url_prefix) url_prefix = options.get("url_prefix", self.url_prefix)
routes = [] routes = []
middleware = []
exception_handlers = []
listeners = defaultdict(list)
# Routes # Routes
for future in self._future_routes: for future in self._future_routes:
@ -152,12 +165,22 @@ class Blueprint(BaseSanic):
# Middleware # Middleware
if route_names: if route_names:
for future in self._future_middleware: for future in self._future_middleware:
app._apply_middleware(future, route_names) middleware.append(app._apply_middleware(future, route_names))
# Exceptions # Exceptions
for future in self._future_exceptions: for future in self._future_exceptions:
app._apply_exception_handler(future) exception_handlers.append(app._apply_exception_handler(future))
# Event listeners # Event listeners
for listener in self._future_listeners: for listener in self._future_listeners:
app._apply_listener(listener) listeners[listener.event].append(app._apply_listener(listener))
self.routes = [route for route in routes if isinstance(route, Route)]
# Deprecate these in 21.6
self.websocket_routes = [
route for route in self.routes if route.ctx.websocket
]
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)

View File

@ -11,7 +11,24 @@ OS_IS_WINDOWS = os.name == "nt"
class Header(CIMultiDict): class Header(CIMultiDict):
def get_all(self, key): """
Container used for both request and response headers. It is a subclass of
`CIMultiDict
<https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy>`_.
It allows for multiple values for a single key in keeping with the HTTP
spec. Also, all keys are *case in-sensitive*.
Please checkout `the MultiDict documentation
<https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_
for more details about how to use the object. In general, it should work
very similar to a regular dictionary.
"""
def get_all(self, key: str):
"""
Convenience method mapped to ``getall()``.
"""
return self.getall(key, default=[]) return self.getall(key, default=[])

View File

@ -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,8 +113,8 @@ 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
as plain string. treated 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,9 @@ 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)):

View File

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

View File

@ -1,3 +1,18 @@
"""
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 +41,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 +73,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) -> HTTPResponse:
"""
Outputs the exception as a :class:`HTTPResponse`.
: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 +87,28 @@ class BaseRenderer:
) )
return output() return output()
def minimal(self): # noqa def minimal(self) -> HTTPResponse: # 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) -> HTTPResponse: # 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; }
@ -107,7 +146,7 @@ class HTMLRenderer(BaseRenderer):
"{body}" "{body}"
) )
def full(self): def full(self) -> HTTPResponse:
return html( return html(
self.OUTPUT_HTML.format( self.OUTPUT_HTML.format(
title=self.title, title=self.title,
@ -118,7 +157,7 @@ class HTMLRenderer(BaseRenderer):
status=self.status, status=self.status,
) )
def minimal(self): def minimal(self) -> HTTPResponse:
return html( return html(
self.OUTPUT_HTML.format( self.OUTPUT_HTML.format(
title=self.title, title=self.title,
@ -172,10 +211,14 @@ 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 = " "
def full(self): def full(self) -> HTTPResponse:
return text( return text(
self.OUTPUT_TEXT.format( self.OUTPUT_TEXT.format(
title=self.title, title=self.title,
@ -186,7 +229,7 @@ class TextRenderer(BaseRenderer):
status=self.status, status=self.status,
) )
def minimal(self): def minimal(self) -> HTTPResponse:
return text( return text(
self.OUTPUT_TEXT.format( self.OUTPUT_TEXT.format(
title=self.title, title=self.title,
@ -231,11 +274,15 @@ class TextRenderer(BaseRenderer):
class JSONRenderer(BaseRenderer): class JSONRenderer(BaseRenderer):
def full(self): """
Render an exception as JSON.
"""
def full(self) -> HTTPResponse:
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)
def minimal(self): def minimal(self) -> HTTPResponse:
output = self._generate_output(full=False) output = self._generate_output(full=False)
return json(output, status=self.status, dumps=dumps) return json(output, status=self.status, dumps=dumps)
@ -280,7 +327,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("&", "&amp;").replace("<", "&lt;") return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
@ -303,7 +352,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

View File

@ -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,18 +227,19 @@ 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.
STATUS_CODES from sanic.helpers for the given status code.
:param status_code: The HTTP status code to return. :param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in :param message: The HTTP response body. Defaults to the messages in
STATUS_CODES from sanic.helpers for the given status code.
""" """
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)

View File

@ -26,6 +26,7 @@ MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[ ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] [Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
] ]
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
class ErrorHandler: class ErrorHandler:

View File

@ -6,6 +6,9 @@ from urllib.parse import unquote
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
# TODO:
# - the Options object should be a typed object to allow for less casting
# across the application (in request.py for example)
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
HeaderBytesIterable = Iterable[Tuple[bytes, bytes]] HeaderBytesIterable = Iterable[Tuple[bytes, bytes]]
Options = Dict[str, Union[int, str]] # key=value fields in various headers Options = Dict[str, Union[int, str]] # key=value fields in various headers

View File

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

View File

@ -1,3 +1,12 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
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 +24,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 +46,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 +91,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 +110,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 +165,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,12 +256,14 @@ 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
if not data and getattr(res, "body", None): if not data and getattr(res, "body", None):
data, end_stream = res.body, True data, end_stream = res.body, True # type: ignore
size = len(data) size = len(data)
headers = res.headers headers = res.headers
@ -257,7 +301,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 +327,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 +354,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 +375,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 +396,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 +414,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 +435,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 +498,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 +515,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")

View File

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

View File

@ -1,6 +1,6 @@
from enum import Enum, auto from enum import Enum, auto
from functools import partial from functools import partial
from typing import List from typing import Any, Callable, Coroutine, List, Optional, Union
from sanic.models.futures import FutureListener from sanic.models.futures import FutureListener
@ -22,10 +22,29 @@ class ListenerMixin:
def _apply_listener(self, listener: FutureListener): def _apply_listener(self, listener: FutureListener):
raise NotImplementedError # noqa raise NotImplementedError # noqa
def listener(self, listener_or_event, event_or_none=None, apply=True): def listener(
"""Create a listener from a decorated function. self,
listener_or_event: Union[
Callable[..., Coroutine[Any, Any, None]], str
],
event_or_none: Optional[str] = None,
apply: bool = True,
):
"""
Create a listener from a decorated function.
:param event: Event to listen to. To be used as a deocrator:
.. code-block:: python
@bp.listener("before_server_start")
async def before_server_start(app, loop):
...
`See user guide
<https://sanicframework.org/guide/basics/listeners.html#listeners>`_
:param event: event to listen to
""" """
def register_listener(listener, event): def register_listener(listener, event):

View File

@ -19,6 +19,9 @@ class MiddlewareMixin:
Can either be called as *@app.middleware* or Can either be called as *@app.middleware* or
*@app.middleware('request')* *@app.middleware('request')*
`See user guide
<https://sanicframework.org/guide/basics/middleware.html>`_
:param: middleware_or_request: Optional parameter to use for :param: middleware_or_request: Optional parameter to use for
identifying which type of middleware is being registered. identifying which type of middleware is being registered.
""" """

View File

@ -5,7 +5,7 @@ from os import path
from pathlib import PurePath from pathlib import PurePath
from re import sub from re import sub
from time import gmtime, strftime from time import gmtime, strftime
from typing import Set, Union from typing import Iterable, List, Optional, Set, Union
from urllib.parse import unquote from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
@ -30,7 +30,7 @@ class RouteMixin:
self._future_routes: Set[FutureRoute] = set() self._future_routes: Set[FutureRoute] = set()
self._future_statics: Set[FutureStatic] = set() self._future_statics: Set[FutureStatic] = set()
self.name = "" self.name = ""
self.strict_slashes = False self.strict_slashes: Optional[bool] = False
def _apply_route(self, route: FutureRoute) -> Route: def _apply_route(self, route: FutureRoute) -> Route:
raise NotImplementedError # noqa raise NotImplementedError # noqa
@ -40,33 +40,33 @@ class RouteMixin:
def route( def route(
self, self,
uri, uri: str,
methods=None, methods: Optional[Iterable[str]] = None,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
stream=False, stream: bool = False,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
ignore_body=False, ignore_body: bool = False,
apply=True, apply: bool = True,
subprotocols=None, subprotocols: Optional[List[str]] = None,
websocket=False, websocket: bool = False,
unquote=False, unquote: bool = False,
static=False, static: bool = False,
): ):
"""Create a blueprint route from a decorated function. """
Decorate a function to be registered as a route
:param uri: endpoint at which the route will be accessible. :param uri: path of the URL
:param methods: list of acceptable HTTP methods. :param methods: list or tuple of methods allowed
:param host: IP Address of FQDN for the sanic server to use. :param host: the host, if required
:param strict_slashes: Enforce the API urls are requested with a :param strict_slashes: whether to apply strict slashes to the route
training */* :param stream: whether to allow the request to stream its body
:param stream: If the route should provide a streaming support :param version: route specific versioning
:param version: Blueprint Version :param name: user defined route name for url_for
:param name: Unique name to identify the Route :param ignore_body: whether the handler should ignore request
body (eg. GET requests)
:return a decorated method that when invoked will return an object :return: tuple of routes, decorated function
of type :class:`FutureRoute`
""" """
# Fix case where the user did not prefix the URL with a / # Fix case where the user did not prefix the URL with a /
@ -161,13 +161,13 @@ class RouteMixin:
def add_route( def add_route(
self, self,
handler, handler,
uri, uri: str,
methods=frozenset({"GET"}), methods: Iterable[str] = frozenset({"GET"}),
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
stream=False, stream: bool = False,
): ):
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
@ -220,12 +220,12 @@ class RouteMixin:
# Shorthand method decorators # Shorthand method decorators
def get( def get(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
ignore_body=True, ignore_body: bool = True,
): ):
""" """
Add an API URL under the **GET** *HTTP* method Add an API URL under the **GET** *HTTP* method
@ -250,12 +250,12 @@ class RouteMixin:
def post( def post(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
stream=False, stream: bool = False,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
): ):
""" """
Add an API URL under the **POST** *HTTP* method Add an API URL under the **POST** *HTTP* method
@ -280,12 +280,12 @@ class RouteMixin:
def put( def put(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
stream=False, stream: bool = False,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
): ):
""" """
Add an API URL under the **PUT** *HTTP* method Add an API URL under the **PUT** *HTTP* method
@ -310,13 +310,32 @@ class RouteMixin:
def head( def head(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
ignore_body=True, ignore_body: bool = True,
): ):
"""
Add an API URL under the **HEAD** *HTTP* method
:param uri: URL to be tagged to **HEAD** method of *HTTP*
:type uri: str
:param host: Host IP or FQDN for the service to use
:type host: Optional[str], optional
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:type strict_slashes: Optional[bool], optional
:param version: API Version
:type version: Optional[str], optional
:param name: Unique name that can be used to identify the Route
:type name: Optional[str], optional
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:return: Object decorated with :func:`route` method
"""
return self.route( return self.route(
uri, uri,
methods=frozenset({"HEAD"}), methods=frozenset({"HEAD"}),
@ -329,22 +348,30 @@ class RouteMixin:
def options( def options(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
ignore_body=True, ignore_body: bool = True,
): ):
""" """
Add an API URL under the **OPTIONS** *HTTP* method Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP* :param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:type uri: str
:param host: Host IP or FQDN for the service to use :param host: Host IP or FQDN for the service to use
:type host: Optional[str], optional
:param strict_slashes: Instruct :class:`Sanic` to check if the request :param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */* URLs need to terminate with a */*
:type strict_slashes: Optional[bool], optional
:param version: API Version :param version: API Version
:type version: Optional[str], optional
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:type name: Optional[str], optional
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -359,22 +386,32 @@ class RouteMixin:
def patch( def patch(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
stream=False, stream=False,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
): ):
""" """
Add an API URL under the **PATCH** *HTTP* method Add an API URL under the **PATCH** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP* :param uri: URL to be tagged to **PATCH** method of *HTTP*
:type uri: str
:param host: Host IP or FQDN for the service to use :param host: Host IP or FQDN for the service to use
:type host: Optional[str], optional
:param strict_slashes: Instruct :class:`Sanic` to check if the request :param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */* URLs need to terminate with a */*
:type strict_slashes: Optional[bool], optional
:param stream: whether to allow the request to stream its body
:type stream: Optional[bool], optional
:param version: API Version :param version: API Version
:type version: Optional[str], optional
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:type name: Optional[str], optional
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -389,12 +426,12 @@ class RouteMixin:
def delete( def delete(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
ignore_body=True, ignore_body: bool = True,
): ):
""" """
Add an API URL under the **DELETE** *HTTP* method Add an API URL under the **DELETE** *HTTP* method
@ -419,22 +456,25 @@ class RouteMixin:
def websocket( def websocket(
self, self,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
version=None, subprotocols: Optional[List[str]] = None,
name=None, version: Optional[int] = None,
subprotocols=None, name: Optional[str] = None,
apply: bool = True, apply: bool = True,
): ):
"""Create a blueprint websocket route from a decorated function. """
Decorate a function to be registered as a websocket route
:param uri: endpoint at which the route will be accessible. :param uri: path of the URL
:param host: IP Address of FQDN for the sanic server to use. :param host: Host IP or FQDN details
:param strict_slashes: Enforce the API urls are requested with a :param strict_slashes: If the API endpoint needs to terminate
training */* with a "/" or not
:param version: Blueprint Version :param subprotocols: optional list of str with supported subprotocols
:param name: Unique name to identify the Websocket Route :param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: tuple of routes, decorated function
""" """
return self.route( return self.route(
uri=uri, uri=uri,
@ -451,12 +491,12 @@ class RouteMixin:
def add_websocket_route( def add_websocket_route(
self, self,
handler, handler,
uri, uri: str,
host=None, host: Optional[str] = None,
strict_slashes=None, strict_slashes: Optional[bool] = None,
subprotocols=None, subprotocols=None,
version=None, version: Optional[int] = None,
name=None, name: Optional[str] = None,
): ):
""" """
A helper method to register a function as a websocket route. A helper method to register a function as a websocket route.

95
sanic/models/asgi.py Normal file
View File

@ -0,0 +1,95 @@
import asyncio
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
from sanic.exceptions import InvalidUsage
from sanic.websocket import WebSocketConnection
ASGIScope = MutableMapping[str, Any]
ASGIMessage = MutableMapping[str, Any]
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()
def resume_writing(self) -> None:
self._not_paused.set()
async def complete(self) -> None:
self._not_paused.set()
await self.transport.send(
{"type": "http.response.body", "body": b"", "more_body": False}
)
@property
def is_complete(self) -> bool:
return self._complete.is_set()
async def push_data(self, data: bytes) -> None:
if not self.is_complete:
await self.transport.send(
{"type": "http.response.body", "body": data, "more_body": True}
)
async def drain(self) -> None:
await self._not_paused.wait()
class MockTransport:
_protocol: Optional[MockProtocol]
def __init__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
self.scope = scope
self._receive = receive
self._send = send
self._protocol = None
self.loop = None
def get_protocol(self) -> MockProtocol:
if not self._protocol:
self._protocol = MockProtocol(self, self.loop)
return self._protocol
def get_extra_info(self, info: str) -> Union[str, bool, None]:
if info == "peername":
return self.scope.get("client")
elif info == "sslcontext":
return self.scope.get("scheme") in ["https", "wss"]
return None
def get_websocket_connection(self) -> WebSocketConnection:
try:
return self._websocket_connection
except AttributeError:
raise InvalidUsage("Improper websocket connection.")
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection
def add_task(self) -> None:
raise NotImplementedError
async def send(self, data) -> None:
# TODO:
# - Validation on data and that it is formatted properly and is valid
await self._send(data)
async def receive(self) -> ASGIMessage:
return await self._receive()

View File

@ -0,0 +1,44 @@
import sys
from typing import Any, AnyStr, TypeVar, Union
if sys.version_info < (3, 8):
from asyncio import BaseTransport
# from sanic.models.asgi import MockTransport
MockTransport = TypeVar("MockTransport")
TransportProtocol = Union[MockTransport, BaseTransport]
Range = Any
HTMLProtocol = Any
else:
# Protocol is a 3.8+ feature
from typing import Protocol
class TransportProtocol(Protocol):
def get_protocol(self):
...
def get_extra_info(self, info: str) -> Union[str, bool, None]:
...
class HTMLProtocol(Protocol):
def __html__(self) -> AnyStr:
...
def _repr_html_(self) -> AnyStr:
...
class Range(Protocol):
def start(self) -> int:
...
def end(self) -> int:
...
def size(self) -> int:
...
def total(self) -> int:
...

View File

@ -1,22 +1,44 @@
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
DefaultDict,
Dict,
List,
NamedTuple,
Optional,
Tuple,
Union,
)
if TYPE_CHECKING:
from sanic.server import ConnInfo
from sanic.app import Sanic
from sanic.http import Http
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 (
Options,
parse_content_header, parse_content_header,
parse_forwarded, parse_forwarded,
parse_host, parse_host,
parse_xforwarded, parse_xforwarded,
) )
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse
@ -33,21 +55,28 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
class RequestParameters(dict): class RequestParameters(dict):
"""Hosts a dict with lists as values where get returns the first """
Hosts a dict with lists as values where get returns the first
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
def get(self, name, default=None): 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"""
return super().get(name, [default])[0] return super().get(name, [default])[0]
def getlist(self, name, default=None): def getlist(
"""Return the entire list""" self, name: str, default: Optional[Any] = None
) -> Optional[Any]:
"""
Return the entire list
"""
return super().get(name, default) return super().get(name, default)
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__",
@ -81,11 +110,19 @@ 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: TransportProtocol,
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)
self._id = None self._id: Optional[Union[uuid.UUID, str, int]] = None
self.app = app self.app = app
self.headers = headers self.headers = headers
@ -95,21 +132,25 @@ 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: Optional[Options] = 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
self.uri_template = None ] = defaultdict(RequestParameters)
self.parsed_not_grouped_args: DefaultDict[
Tuple[bool, bool, str, str], List[Tuple[str, str]]
] = defaultdict(list)
self.uri_template: Optional[str] = None
self.request_middleware_started = False self.request_middleware_started = False
self._cookies = None self._cookies: Optional[Dict[str, str]] = None
self._match_info = {} self._match_info: Dict[str, Any] = {}
self.stream = None self.stream: Optional[Http] = None
self.endpoint = None self.endpoint: Optional[str] = None
def __repr__(self): def __repr__(self):
class_name = self.__class__.__name__ class_name = self.__class__.__name__
@ -120,17 +161,22 @@ class Request:
return uuid.uuid4() return uuid.uuid4()
async def respond( async def respond(
self, response=None, *, status=200, headers=None, content_type=None self,
response: Optional[BaseHTTPResponse] = None,
*,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
): ):
# This logic of determining which response to use is subject to change # This logic of determining which response to use is subject to change
if response is None: if response is None:
response = self.stream.response or HTTPResponse( response = (self.stream and self.stream.response) or HTTPResponse(
status=status, status=status,
headers=headers, headers=headers,
content_type=content_type, content_type=content_type,
) )
# Connect the response # Connect the response
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response) response = self.stream.respond(response)
# Run response middleware # Run response middleware
try: try:
@ -158,11 +204,35 @@ class Request:
self.body = b"".join([data async for data in self.stream]) self.body = b"".join([data async for data in self.stream])
@property @property
def id(self): def id(self) -> Optional[Union[uuid.UUID, str, int]]:
"""
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``.
.. code-block:: python
from sanic import Request, Sanic
from itertools import count
class IntRequest(Request):
counter = count()
def generate_id(self):
return next(self.counter)
app = Sanic("MyApp", request_class=IntRequest)
"""
if not self._id: if not self._id:
self._id = self.headers.get( self._id = self.headers.get(
self.app.config.REQUEST_ID_HEADER, self.app.config.REQUEST_ID_HEADER,
self.__class__.generate_id(self), self.__class__.generate_id(self), # type: ignore
) )
# Try casting to a UUID or an integer # Try casting to a UUID or an integer
@ -171,11 +241,11 @@ class Request:
self._id = uuid.UUID(self._id) self._id = uuid.UUID(self._id)
except ValueError: except ValueError:
try: try:
self._id = int(self._id) self._id = int(self._id) # type: ignore
except ValueError: except ValueError:
... ...
return self._id return self._id # type: ignore
@property @property
def json(self): def json(self):
@ -350,13 +420,21 @@ class Request:
] ]
query_args = property(get_query_args) query_args = property(get_query_args)
"""
Convenience property to access :meth:`Request.get_query_args` with
default values.
"""
@property @property
def cookies(self): def cookies(self) -> Dict[str, str]:
"""
:return: Incoming cookies on the request
:rtype: 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()
@ -366,27 +444,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._match_info return self._match_info
# 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
@ -396,13 +482,16 @@ class Request:
@property @property
def path(self) -> str: def path(self) -> str:
"""Path of the local HTTP request.""" """
:return: path of the local HTTP request
:rtype: str
"""
return self._parsed_url.path.decode("utf-8") return self._parsed_url.path.decode("utf-8")
# Proxy properties (using SERVER_NAME/forwarded/request/transport info) # Proxy properties (using SERVER_NAME/forwarded/request/transport info)
@property @property
def forwarded(self): def forwarded(self) -> Options:
""" """
Active proxy information obtained from request headers, as specified in Active proxy information obtained from request headers, as specified in
Sanic configuration. Sanic configuration.
@ -413,6 +502,9 @@ class Request:
- path is url-unencoded - path is url-unencoded
Additional values may be available from new style Forwarded headers. Additional values may be available from new style Forwarded headers.
:return: forwarded address info
:rtype: Dict[str, str]
""" """
if self.parsed_forwarded is None: if self.parsed_forwarded is None:
self.parsed_forwarded = ( self.parsed_forwarded = (
@ -428,10 +520,14 @@ class Request:
Client IP address, if available. Client IP address, if available.
1. proxied remote address `self.forwarded['for']` 1. proxied remote address `self.forwarded['for']`
2. local remote address `self.ip` 2. local remote address `self.ip`
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
:rtype: str
""" """
if not hasattr(self, "_remote_addr"): if not hasattr(self, "_remote_addr"):
self._remote_addr = self.forwarded.get("for", "") # or self.ip self._remote_addr = str(
self.forwarded.get("for", "")
) # or self.ip
return self._remote_addr return self._remote_addr
@property @property
@ -441,12 +537,14 @@ class Request:
1. `config.SERVER_NAME` if in full URL format 1. `config.SERVER_NAME` if in full URL format
2. proxied proto/scheme 2. proxied proto/scheme
3. local connection protocol 3. local connection protocol
:return: http|https|ws|wss or arbitrary value given by the headers. :return: http|https|ws|wss or arbitrary value given by the headers.
:rtype: str
""" """
if "//" in self.app.config.get("SERVER_NAME", ""): if "//" in self.app.config.get("SERVER_NAME", ""):
return self.app.config.SERVER_NAME.split("//")[0] return self.app.config.SERVER_NAME.split("//")[0]
if "proto" in self.forwarded: if "proto" in self.forwarded:
return self.forwarded["proto"] return str(self.forwarded["proto"])
if ( if (
self.app.websocket_enabled self.app.websocket_enabled
@ -470,48 +568,67 @@ class Request:
3. request host header 3. request host header
hostname and port may be separated by hostname and port may be separated by
`sanic.headers.parse_host(request.host)`. `sanic.headers.parse_host(request.host)`.
:return: the first matching host found, or empty string :return: the first matching host found, or empty string
:rtype: str
""" """
server_name = self.app.config.get("SERVER_NAME") server_name = self.app.config.get("SERVER_NAME")
if server_name: if server_name:
return server_name.split("//", 1)[-1].split("/", 1)[0] return server_name.split("//", 1)[-1].split("/", 1)[0]
return self.forwarded.get("host") or self.headers.get("host", "") return str(self.forwarded.get("host") or self.headers.get("host", ""))
@property @property
def server_name(self) -> str: def server_name(self) -> str:
"""The hostname the client connected to, by `request.host`.""" """
:return: hostname the client connected to, by ``request.host``
:rtype: str
"""
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``.
:return: port number
:rtype: int
""" """
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 int(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.""" """
return self.forwarded.get("path") or self.path :return: full path of current URL; uses proxied or local path
:rtype: str
"""
return str(self.forwarded.get("path") or self.path)
@property @property
def query_string(self): def query_string(self) -> str:
"""
:return: representation of the requested query
:rtype: str
"""
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:
"""
:return: the URL
:rtype: str
"""
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
@ -544,11 +661,24 @@ class Request:
) )
File = namedtuple("File", ["type", "body", "name"]) 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.
: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

View File

@ -1,12 +1,26 @@
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 pathlib import PurePath
from typing import (
Any,
AnyStr,
Callable,
Coroutine,
Dict,
Iterator,
Optional,
Tuple,
Union,
)
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
from sanic.models.protocol_types import HTMLProtocol, Range
try: try:
@ -20,25 +34,56 @@ except ImportError:
class BaseHTTPResponse: class BaseHTTPResponse:
def __init__(self): """
self.asgi = False The base class for all HTTP Responses
"""
def _encode_body(self, data): def __init__(self):
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Http = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
def _encode_body(self, data: Optional[AnyStr]):
if data is None: if data is None:
return b"" return b""
return data.encode() if hasattr(data, "encode") else data return (
data.encode() if hasattr(data, "encode") else data # type: ignore
)
@property @property
def cookies(self): def cookies(self) -> CookieJar:
"""
The response cookies. Cookies should be set and written as follows:
.. code-block:: python
response.cookies["test"] = "It worked!"
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True
`See user guide
<https://sanicframework.org/guide/basics/cookies.html>`_
:return: the cookie jar
:rtype: CookieJar
"""
if self._cookies is None: if self._cookies is None:
self._cookies = CookieJar(self.headers) self._cookies = CookieJar(self.headers)
return self._cookies return self._cookies
@property @property
def processed_headers(self): def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
"""Obtain a list of header tuples encoded in bytes for sending. """
Obtain a list of header tuples encoded in bytes for sending.
Add and remove headers based on status and content_type. Add and remove headers based on status and content_type.
:return: response headers
:rtype: Tuple[Tuple[bytes, bytes], ...]
""" """
# TODO: Make a blacklist set of header names and then filter with that # TODO: Make a blacklist set of header names and then filter with that
if self.status in (304, 412): # Not Modified, Precondition Failed if self.status in (304, 412): # Not Modified, Precondition Failed
@ -51,22 +96,66 @@ class BaseHTTPResponse:
for name, value in self.headers.items() for name, value in self.headers.items()
) )
async def send(self, data=None, end_stream=None): async def send(
"""Send any pending response headers and the given data as body. self,
data: Optional[Union[AnyStr]] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
Send any pending response headers and the given data as body.
:param data: str or bytes to be written :param data: str or bytes to be written
:end_stream: whether to close the stream after this block :param end_stream: whether to close the stream after this block
""" """
if data is None and end_stream is None: if data is None and end_stream is None:
end_stream = True end_stream = True
if end_stream and not data and self.stream.send is None: if end_stream and not data and self.stream.send is None:
return return
data = data.encode() if hasattr(data, "encode") else data or b"" data = (
data.encode() # type: ignore
if hasattr(data, "encode")
else data or b""
)
await self.stream.send(data, end_stream=end_stream) await self.stream.send(data, end_stream=end_stream)
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
class StreamingHTTPResponse(BaseHTTPResponse): class StreamingHTTPResponse(BaseHTTPResponse):
"""Old style streaming response. Use `request.respond()` instead of this in """
new code to avoid the callback.""" Old style streaming response where you pass a streaming function:
.. code-block:: python
async def sample_streaming_fn(response):
await response.write("foo")
await asyncio.sleep(1)
await response.write("bar")
await asyncio.sleep(1)
@app.post("/")
async def test(request):
return stream(sample_streaming_fn)
.. warning::
**Deprecated** and set for removal in v21.6. You can now achieve the
same functionality without a callback.
.. code-block:: python
@app.post("/")
async def test(request):
response = await request.respond()
await response.send("foo", False)
await asyncio.sleep(1)
await response.send("bar", False)
await asyncio.sleep(1)
await response.send("", True)
return response
"""
__slots__ = ( __slots__ = (
"streaming_fn", "streaming_fn",
@ -78,10 +167,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
def __init__( def __init__(
self, self,
streaming_fn, streaming_fn: StreamingFunction,
status=200, status: int = 200,
headers=None, headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type="text/plain; charset=utf-8", content_type: str = "text/plain; charset=utf-8",
chunked="deprecated", chunked="deprecated",
): ):
if chunked != "deprecated": if chunked != "deprecated":
@ -113,25 +202,40 @@ class StreamingHTTPResponse(BaseHTTPResponse):
class HTTPResponse(BaseHTTPResponse): class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
:param body: the body content to be returned
:type body: Optional[bytes]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional;
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
"""
__slots__ = ("body", "status", "content_type", "headers", "_cookies") __slots__ = ("body", "status", "content_type", "headers", "_cookies")
def __init__( def __init__(
self, self,
body=None, body: Optional[AnyStr] = None,
status=200, status: int = 200,
headers=None, headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type=None, content_type: Optional[str] = None,
): ):
super().__init__() super().__init__()
self.content_type = content_type self.content_type: Optional[str] = content_type
self.body = self._encode_body(body) self.body = self._encode_body(body)
self.status = status self.status = status
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self._cookies = None self._cookies = None
def empty(status=204, headers=None): def empty(
status=204, headers: Optional[Dict[str, str]] = None
) -> HTTPResponse:
""" """
Returns an empty response to the client. Returns an empty response to the client.
@ -142,13 +246,13 @@ def empty(status=204, headers=None):
def json( def json(
body, body: Any,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type="application/json", content_type: str = "application/json",
dumps=json_dumps, dumps: Callable[..., str] = json_dumps,
**kwargs, **kwargs,
): ) -> HTTPResponse:
""" """
Returns response object with body in json format. Returns response object with body in json format.
@ -166,8 +270,11 @@ def json(
def text( def text(
body, status=200, headers=None, content_type="text/plain; charset=utf-8" body: str,
): status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> HTTPResponse:
""" """
Returns response object with body in text format. Returns response object with body in text format.
@ -187,8 +294,11 @@ def text(
def raw( def raw(
body, status=200, headers=None, content_type="application/octet-stream" body: Optional[AnyStr],
): status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "application/octet-stream",
) -> HTTPResponse:
""" """
Returns response object without encoding the body. Returns response object without encoding the body.
@ -205,7 +315,11 @@ def raw(
) )
def html(body, status=200, headers=None): def html(
body: Union[str, bytes, HTMLProtocol],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
) -> HTTPResponse:
""" """
Returns response object with body in html format. Returns response object with body in html format.
@ -213,11 +327,13 @@ def html(body, status=200, headers=None):
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
""" """
if hasattr(body, "__html__"): if not isinstance(body, (str, bytes)):
body = body.__html__() if hasattr(body, "__html__"):
elif hasattr(body, "_repr_html_"): body = body.__html__()
body = body._repr_html_() elif hasattr(body, "_repr_html_"):
return HTTPResponse( body = body._repr_html_()
return HTTPResponse( # type: ignore
body, body,
status=status, status=status,
headers=headers, headers=headers,
@ -226,13 +342,13 @@ def html(body, status=200, headers=None):
async def file( async def file(
location, location: Union[str, PurePath],
status=200, status: int = 200,
mime_type=None, mime_type: Optional[str] = None,
headers=None, headers: Optional[Dict[str, str]] = None,
filename=None, filename: Optional[str] = None,
_range=None, _range: Optional[Range] = None,
): ) -> HTTPResponse:
"""Return a response object with file data. """Return a response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -269,15 +385,15 @@ async def file(
async def file_stream( async def file_stream(
location, location: Union[str, PurePath],
status=200, status: int = 200,
chunk_size=4096, chunk_size: int = 4096,
mime_type=None, mime_type: Optional[str] = None,
headers=None, headers: Optional[Dict[str, str]] = None,
filename=None, filename: Optional[str] = None,
chunked="deprecated", chunked="deprecated",
_range=None, _range: Optional[Range] = None,
): ) -> StreamingHTTPResponse:
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -336,10 +452,10 @@ async def file_stream(
def stream( def stream(
streaming_fn, streaming_fn: StreamingFunction,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type="text/plain; charset=utf-8", content_type: str = "text/plain; charset=utf-8",
chunked="deprecated", chunked="deprecated",
): ):
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts an coroutine `streaming_fn` which can be used to
@ -376,15 +492,19 @@ def stream(
def redirect( def redirect(
to, headers=None, status=302, content_type="text/html; charset=utf-8" to: str,
): headers: Optional[Dict[str, str]] = None,
"""Abort execution and cause a 302 redirect (by default). status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to :param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request :param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302 :param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response :param content_type: the content type (string) of the response
:returns: the redirecting Response
""" """
headers = headers or {} headers = headers or {}

View File

@ -1,5 +1,5 @@
from functools import lru_cache from functools import lru_cache
from typing import Iterable, List, Optional, Union from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from sanic_routing import BaseRouter # type: ignore from sanic_routing import BaseRouter # type: ignore
from sanic_routing.exceptions import NoMethod # type: ignore from sanic_routing.exceptions import NoMethod # type: ignore
@ -10,6 +10,7 @@ from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import MethodNotSupported, NotFound from sanic.exceptions import MethodNotSupported, NotFound
from sanic.handlers import RouteHandler
from sanic.request import Request from sanic.request import Request
@ -30,7 +31,9 @@ class Router(BaseRouter):
# However, overall application performance is significantly improved # However, overall application performance is significantly improved
# with the lru_cache on this method. # with the lru_cache on this method.
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, path, method, host): def _get(
self, path, method, host
) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool]:
try: try:
route, handler, params = self.resolve( route, handler, params = self.resolve(
path=path, path=path,
@ -74,7 +77,7 @@ class Router(BaseRouter):
self, self,
uri: str, uri: str,
methods: Iterable[str], methods: Iterable[str],
handler, handler: RouteHandler,
host: Optional[Union[str, Iterable[str]]] = None, host: Optional[Union[str, Iterable[str]]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
stream: bool = False, stream: bool = False,

View File

@ -1,3 +1,23 @@
from __future__ import annotations
from ssl import SSLContext
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Optional,
Type,
Union,
)
from sanic.handlers import ListenerType
if TYPE_CHECKING:
from sanic.app import Sanic
import asyncio import asyncio
import multiprocessing import multiprocessing
import os import os
@ -6,19 +26,20 @@ import socket
import stat import stat
from asyncio import CancelledError from asyncio import CancelledError
from asyncio.transports import Transport
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from ipaddress import ip_address 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.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.http import Http, Stage
from sanic.log import logger from sanic.log import logger
from sanic.models.protocol_types import TransportProtocol
from sanic.request import Request from sanic.request import Request
@ -50,8 +71,8 @@ class ConnInfo:
"ssl", "ssl",
) )
def __init__(self, transport, unix=None): def __init__(self, transport: TransportProtocol, unix=None):
self.ssl = bool(transport.get_extra_info("sslcontext")) self.ssl: bool = 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
self.peername = None self.peername = None
@ -117,8 +138,8 @@ class HttpProtocol(asyncio.Protocol):
self, self,
*, *,
loop, loop,
app, app: Sanic,
signal=Signal(), signal=None,
connections=None, connections=None,
state=None, state=None,
unix=None, unix=None,
@ -126,12 +147,12 @@ class HttpProtocol(asyncio.Protocol):
): ):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
self.loop = loop self.loop = loop
self.app = app self.app: Sanic = app
self.url = None self.url = None
self.transport = None self.transport: Optional[Transport] = None
self.conn_info = None self.conn_info: Optional[ConnInfo] = None
self.request = None self.request: Optional[Request] = None
self.signal = signal self.signal = signal or Signal()
self.access_log = self.app.config.ACCESS_LOG self.access_log = self.app.config.ACCESS_LOG
self.connections = connections if connections is not None else set() self.connections = connections if connections is not None else set()
self.request_handler = self.app.handle_request self.request_handler = self.app.handle_request
@ -159,7 +180,8 @@ class HttpProtocol(asyncio.Protocol):
self.check_timeouts() self.check_timeouts()
async def connection_task(self): async def connection_task(self):
"""Run a HTTP connection. """
Run a HTTP connection.
Timeouts and some additional error handling occur here, while most of Timeouts and some additional error handling occur here, while most of
everything else happens in class Http or in code called from there. everything else happens in class Http or in code called from there.
@ -186,13 +208,17 @@ class HttpProtocol(asyncio.Protocol):
logger.exception("Closing failed") logger.exception("Closing failed")
async def receive_more(self): async def receive_more(self):
"""Wait until more data is received into self._buffer.""" """
Wait until more data is received into the Server protocol's buffer
"""
self.transport.resume_reading() self.transport.resume_reading()
self._data_received.clear() self._data_received.clear()
await self._data_received.wait() await self._data_received.wait()
def check_timeouts(self): def check_timeouts(self):
"""Runs itself periodically to enforce any expired timeouts.""" """
Runs itself periodically to enforce any expired timeouts.
"""
try: try:
if not self._task: if not self._task:
return return
@ -223,15 +249,18 @@ class HttpProtocol(asyncio.Protocol):
logger.exception("protocol.check_timeouts") logger.exception("protocol.check_timeouts")
async def send(self, data): async def send(self, data):
"""Writes data with backpressure control.""" """
Writes data with backpressure control.
"""
await self._can_write.wait() await self._can_write.wait()
if self.transport.is_closing(): if self.transport.is_closing():
raise CancelledError raise CancelledError
self.transport.write(data) self.transport.write(data)
self._time = current_time() self._time = current_time()
def close_if_idle(self): def close_if_idle(self) -> bool:
"""Close the connection if a request is not being sent or received """
Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open :return: boolean - True if closed, false if staying open
""" """
@ -280,14 +309,17 @@ class HttpProtocol(asyncio.Protocol):
def resume_writing(self): def resume_writing(self):
self._can_write.set() self._can_write.set()
def data_received(self, data): def data_received(self, data: bytes):
try: try:
self._time = current_time() self._time = current_time()
if not data: if not data:
return self.close() return self.close()
self.recv_buffer += data self.recv_buffer += data
if len(self.recv_buffer) > self.app.config.REQUEST_BUFFER_SIZE: if (
len(self.recv_buffer) > self.app.config.REQUEST_BUFFER_SIZE
and self.transport
):
self.transport.pause_reading() self.transport.pause_reading()
if self._data_received: if self._data_received:
@ -296,16 +328,18 @@ class HttpProtocol(asyncio.Protocol):
logger.exception("protocol.data_received") logger.exception("protocol.data_received")
def trigger_events(events, loop): def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
"""Trigger event callbacks (functions or async) """
Trigger event callbacks (functions or async)
:param events: one or more sync or async functions to execute :param events: one or more sync or async functions to execute
:param loop: event loop :param loop: event loop
""" """
for event in events: if events:
result = event(loop) for event in events:
if isawaitable(result): result = event(loop)
loop.run_until_complete(result) if isawaitable(result):
loop.run_until_complete(result)
class AsyncioServer: class AsyncioServer:
@ -329,9 +363,9 @@ class AsyncioServer:
loop, loop,
serve_coro, serve_coro,
connections, connections,
after_start, after_start: Optional[Iterable[ListenerType]],
before_stop, before_stop: Optional[Iterable[ListenerType]],
after_stop, after_stop: Optional[Iterable[ListenerType]],
): ):
# Note, Sanic already called "before_server_start" events # Note, Sanic already called "before_server_start" events
# before this helper was even created. So we don't need it here. # before this helper was even created. So we don't need it here.
@ -344,18 +378,24 @@ class AsyncioServer:
self.connections = connections self.connections = connections
def after_start(self): def after_start(self):
"""Trigger "after_server_start" events""" """
Trigger "after_server_start" events
"""
trigger_events(self._after_start, self.loop) trigger_events(self._after_start, self.loop)
def before_stop(self): def before_stop(self):
"""Trigger "before_server_stop" events""" """
Trigger "before_server_stop" events
"""
trigger_events(self._before_stop, self.loop) trigger_events(self._before_stop, self.loop)
def after_stop(self): def after_stop(self):
"""Trigger "after_server_stop" events""" """
Trigger "after_server_stop" events
"""
trigger_events(self._after_stop, self.loop) trigger_events(self._after_stop, self.loop)
def is_serving(self): def is_serving(self) -> bool:
if self.server: if self.server:
return self.server.is_serving() return self.server.is_serving()
return False return False
@ -392,7 +432,9 @@ class AsyncioServer:
) )
def __await__(self): def __await__(self):
"""Starts the asyncio server, returns AsyncServerCoro""" """
Starts the asyncio server, returns AsyncServerCoro
"""
task = asyncio.ensure_future(self.serve_coro) task = asyncio.ensure_future(self.serve_coro)
while not task.done(): while not task.done():
yield yield
@ -404,20 +446,20 @@ def serve(
host, host,
port, port,
app, app,
before_start=None, before_start: Optional[Iterable[ListenerType]] = None,
after_start=None, after_start: Optional[Iterable[ListenerType]] = None,
before_stop=None, before_stop: Optional[Iterable[ListenerType]] = None,
after_stop=None, after_stop: Optional[Iterable[ListenerType]] = None,
ssl=None, ssl: Optional[SSLContext] = None,
sock=None, sock: Optional[socket.socket] = None,
unix=None, unix: Optional[str] = None,
reuse_port=False, reuse_port: bool = False,
loop=None, loop=None,
protocol=HttpProtocol, protocol: Type[asyncio.Protocol] = HttpProtocol,
backlog=100, backlog: int = 100,
register_sys_signals=True, register_sys_signals: bool = True,
run_multiple=False, run_multiple: bool = False,
run_async=False, run_async: bool = False,
connections=None, connections=None,
signal=Signal(), signal=Signal(),
state=None, state=None,
@ -542,7 +584,7 @@ def serve(
# instead of letting connection hangs forever. # instead of letting connection hangs forever.
# Let's roughly calcucate time. # Let's roughly calcucate time.
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
start_shutdown = 0 start_shutdown: float = 0
while connections and (start_shutdown < graceful): while connections and (start_shutdown < graceful):
loop.run_until_complete(asyncio.sleep(0.1)) loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1 start_shutdown = start_shutdown + 0.1
@ -566,7 +608,7 @@ def serve(
def _build_protocol_kwargs( def _build_protocol_kwargs(
protocol: Type[HttpProtocol], config: Config protocol: Type[asyncio.Protocol], config: Config
) -> Dict[str, Union[int, float]]: ) -> Dict[str, Union[int, float]]:
if hasattr(protocol, "websocket_handshake"): if hasattr(protocol, "websocket_handshake"):
return { return {
@ -642,7 +684,7 @@ def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
return sock return sock
def remove_unix_socket(path: str) -> None: def remove_unix_socket(path: Optional[str]) -> None:
"""Remove dead unix socket during server exit.""" """Remove dead unix socket during server exit."""
if not path: if not path:
return return

View File

@ -19,8 +19,6 @@ class HTTPMethodView:
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
return text('I am put method') return text('I am put method')
etc.
If someone tries to use a non-implemented method, there will be a If someone tries to use a non-implemented method, there will be a
405 response. 405 response.
@ -33,8 +31,9 @@ class HTTPMethodView:
return text('I am get method with %s' % my_param_here) return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use To add the view into the routing you could use
1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView.as_view()) 1) ``app.add_route(DummyView.as_view(), '/')``, OR
2) ``app.route('/')(DummyView.as_view())``
To add any decorator you could set it into decorators variable To add any decorator you could set it into decorators variable
""" """
@ -78,12 +77,13 @@ class CompositionView:
for every HTTP method you want to support. for every HTTP method you want to support.
For example: For example:
.. code-block:: python
view = CompositionView() view = CompositionView()
view.add(['GET'], lambda request: text('I am get method')) view.add(['GET'], lambda request: text('I am get method'))
view.add(['POST', 'PUT'], lambda request: text('I am post/put method')) view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
etc.
If someone tries to use a non-implemented method, there will be a If someone tries to use a non-implemented method, there will be a
405 response. 405 response.
""" """