Merge branch 'master' into unauthorized-exception
This commit is contained in:
commit
173c62acb6
28
README.rst
28
README.rst
|
@ -11,34 +11,6 @@ Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contribu
|
||||||
|
|
||||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
||||||
|
|
||||||
Benchmarks
|
|
||||||
----------
|
|
||||||
|
|
||||||
All tests were run on an AWS medium instance running ubuntu, using 1
|
|
||||||
process. Each script delivered a small JSON response and was tested with
|
|
||||||
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
|
|
||||||
not speed up requests.
|
|
||||||
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Server | Implementation | Requests/sec | Avg Latency |
|
|
||||||
+===========+=======================+================+===============+
|
|
||||||
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
|
||||||
+-----------+-----------------------+----------------+---------------+
|
|
||||||
|
|
||||||
Hello World Example
|
Hello World Example
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
225
docs/Makefile
225
docs/Makefile
|
@ -1,20 +1,225 @@
|
||||||
# Minimal makefile for Sphinx documentation
|
# Makefile for Sphinx documentation
|
||||||
#
|
#
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
# You can set these variables from the command line.
|
||||||
SPHINXOPTS =
|
SPHINXOPTS =
|
||||||
SPHINXBUILD = sphinx-build
|
SPHINXBUILD = sphinx-build
|
||||||
SPHINXPROJ = Sanic
|
PAPER =
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
BUILDDIR = _build
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " applehelp to make an Apple Help Book"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " epub3 to make an epub3"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " xml to make Docutils-native XML files"
|
||||||
|
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||||
|
@echo " dummy to check syntax errors of document sources"
|
||||||
|
|
||||||
.PHONY: help Makefile
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
.PHONY: html
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
html:
|
||||||
%: Makefile
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
.PHONY: dirhtml
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
.PHONY: singlehtml
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
.PHONY: pickle
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
.PHONY: json
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
.PHONY: htmlhelp
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
.PHONY: qthelp
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiographite.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiographite.qhc"
|
||||||
|
|
||||||
|
.PHONY: applehelp
|
||||||
|
applehelp:
|
||||||
|
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||||
|
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||||
|
"~/Library/Documentation/Help or install it in your application" \
|
||||||
|
"bundle."
|
||||||
|
|
||||||
|
.PHONY: devhelp
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/aiographite"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiographite"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
.PHONY: epub
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
.PHONY: epub3
|
||||||
|
epub3:
|
||||||
|
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||||
|
|
||||||
|
.PHONY: latex
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: latexpdf
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: latexpdfja
|
||||||
|
latexpdfja:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: text
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
.PHONY: man
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
.PHONY: texinfo
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: info
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
.PHONY: gettext
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
.PHONY: changes
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
.PHONY: linkcheck
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
.PHONY: doctest
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage:
|
||||||
|
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||||
|
@echo "Testing of coverage in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/coverage/python.txt."
|
||||||
|
|
||||||
|
.PHONY: xml
|
||||||
|
xml:
|
||||||
|
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||||
|
|
||||||
|
.PHONY: pseudoxml
|
||||||
|
pseudoxml:
|
||||||
|
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||||
|
|
||||||
|
.PHONY: dummy
|
||||||
|
dummy:
|
||||||
|
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. Dummy builder generates no files."
|
||||||
|
|
12
docs/conf.py
12
docs/conf.py
|
@ -13,6 +13,9 @@ import sys
|
||||||
# Add support for Markdown documentation using Recommonmark
|
# Add support for Markdown documentation using Recommonmark
|
||||||
from recommonmark.parser import CommonMarkParser
|
from recommonmark.parser import CommonMarkParser
|
||||||
|
|
||||||
|
# Add support for auto-doc
|
||||||
|
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())
|
||||||
|
@ -140,3 +143,12 @@ epub_exclude_files = ['search.html']
|
||||||
# -- Custom Settings -------------------------------------------------------
|
# -- Custom Settings -------------------------------------------------------
|
||||||
|
|
||||||
suppress_warnings = ['image.nonlocal_uri']
|
suppress_warnings = ['image.nonlocal_uri']
|
||||||
|
|
||||||
|
|
||||||
|
# app setup hook
|
||||||
|
def setup(app):
|
||||||
|
app.add_config_value('recommonmark_config', {
|
||||||
|
'enable_eval_rst': True,
|
||||||
|
'enable_auto_doc_ref': True,
|
||||||
|
}, True)
|
||||||
|
app.add_transform(AutoStructify)
|
||||||
|
|
|
@ -16,6 +16,7 @@ Guides
|
||||||
sanic/blueprints
|
sanic/blueprints
|
||||||
sanic/config
|
sanic/config
|
||||||
sanic/cookies
|
sanic/cookies
|
||||||
|
sanic/decorators
|
||||||
sanic/streaming
|
sanic/streaming
|
||||||
sanic/class_based_views
|
sanic/class_based_views
|
||||||
sanic/custom_protocol
|
sanic/custom_protocol
|
||||||
|
@ -25,6 +26,7 @@ Guides
|
||||||
sanic/deploying
|
sanic/deploying
|
||||||
sanic/extensions
|
sanic/extensions
|
||||||
sanic/contributing
|
sanic/contributing
|
||||||
|
sanic/api_reference
|
||||||
|
|
||||||
|
|
||||||
Module Documentation
|
Module Documentation
|
||||||
|
@ -33,4 +35,5 @@ Module Documentation
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
265
docs/make.bat
265
docs/make.bat
|
@ -1,19 +1,64 @@
|
||||||
@ECHO OFF
|
@ECHO OFF
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
if "%SPHINXBUILD%" == "" (
|
||||||
set SPHINXBUILD=sphinx-build
|
set SPHINXBUILD=sphinx-build
|
||||||
)
|
)
|
||||||
set SOURCEDIR=.
|
|
||||||
set BUILDDIR=_build
|
set BUILDDIR=_build
|
||||||
set SPHINXPROJ=Sanic
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
if "%1" == "" goto help
|
if "%1" == "" goto help
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. epub3 to make an epub3
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. xml to make Docutils-native XML files
|
||||||
|
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
echo. coverage to run coverage check of the documentation if enabled
|
||||||
|
echo. dummy to check syntax errors of document sources
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REM Check if sphinx-build is available and fallback to Python version if any
|
||||||
|
%SPHINXBUILD% 1>NUL 2>NUL
|
||||||
|
if errorlevel 9009 goto sphinx_python
|
||||||
|
goto sphinx_ok
|
||||||
|
|
||||||
|
:sphinx_python
|
||||||
|
|
||||||
|
set SPHINXBUILD=python -m sphinx.__init__
|
||||||
|
%SPHINXBUILD% 2> nul
|
||||||
if errorlevel 9009 (
|
if errorlevel 9009 (
|
||||||
echo.
|
echo.
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
@ -26,11 +71,211 @@ if errorlevel 9009 (
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
:sphinx_ok
|
||||||
goto end
|
|
||||||
|
|
||||||
:help
|
|
||||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiographite.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiographite.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub3" (
|
||||||
|
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdf" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdfja" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf-ja
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "coverage" (
|
||||||
|
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of coverage in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/coverage/python.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "xml" (
|
||||||
|
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pseudoxml" (
|
||||||
|
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dummy" (
|
||||||
|
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. Dummy builder generates no files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
:end
|
:end
|
||||||
popd
|
|
||||||
|
|
150
docs/sanic/api_reference.rst
Normal file
150
docs/sanic/api_reference.rst
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
sanic.app module
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.app
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.blueprints module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.blueprints
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.config module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.config
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.constants module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.constants
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.cookies module
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.cookies
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.exceptions module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.exceptions
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.handlers module
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.handlers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.log module
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.log
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.request module
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.request
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.response module
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.response
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.router module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.router
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.server module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.server
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.static module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.static
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.testing module
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.testing
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.views module
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.views
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.websocket module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.websocket
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.worker module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.worker
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: sanic
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
|
@ -169,7 +169,7 @@ app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
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:
|
takes the format `<blueprint_name>.<handler_name>`. For example:
|
||||||
|
|
||||||
```
|
```python
|
||||||
@blueprint_v1.route('/')
|
@blueprint_v1.route('/')
|
||||||
async def root(request):
|
async def root(request):
|
||||||
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||||
|
|
|
@ -31,10 +31,10 @@ There are several ways how to load configuration.
|
||||||
|
|
||||||
### From environment variables.
|
### 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. You can pass the `load_vars` boolean to the Sanic constructor to override that:
|
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. You can pass the `load_env` boolean to the Sanic constructor to override that:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app = Sanic(load_vars=False)
|
app = Sanic(load_env=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
### From an Object
|
### From an Object
|
||||||
|
|
|
@ -1,75 +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.
|
|
||||||
|
|
||||||
## Reading cookies
|
|
||||||
|
|
||||||
A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```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.
|
|
87
docs/sanic/cookies.rst
Normal file
87
docs/sanic/cookies.rst
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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/
|
|
@ -57,6 +57,12 @@ for Gunicorn `worker-class` argument:
|
||||||
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
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.
|
||||||
|
|
||||||
## Asynchronous support
|
## Asynchronous support
|
||||||
This is suitable if you *need* to share the sanic process with other applications, in particular 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
|
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||||
|
|
|
@ -23,3 +23,4 @@ A list of Sanic extensions created by the community.
|
||||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||||
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
||||||
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
||||||
|
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
||||||
|
|
|
@ -214,4 +214,3 @@ and `recv` methods to send and receive data respectively.
|
||||||
|
|
||||||
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
||||||
package by Aymeric Augustin.
|
package by Aymeric Augustin.
|
||||||
|
|
||||||
|
|
|
@ -57,3 +57,71 @@ def test_post_json_request_includes_data():
|
||||||
More information about
|
More information about
|
||||||
the available arguments to aiohttp can be found
|
the available arguments to aiohttp can be found
|
||||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||||
|
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
```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}
|
||||||
|
```
|
||||||
|
|
50
docs/sanic/versioning.md
Normal file
50
docs/sanic/versioning.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/text', verion=1)
|
||||||
|
def handle_request(request):
|
||||||
|
return response.text('Hello world! Version 1')
|
||||||
|
|
||||||
|
@app.route('/text', verion=2)
|
||||||
|
def handle_request(request):
|
||||||
|
return response.text('Hello world! Version 2')
|
||||||
|
|
||||||
|
app.run(port=80)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then with curl:
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
```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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl localhost/v1/html
|
||||||
|
```
|
47
sanic/app.py
47
sanic/app.py
|
@ -113,7 +113,7 @@ class Sanic:
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||||
strict_slashes=False, stream=False):
|
strict_slashes=False, stream=False, version=None):
|
||||||
"""Decorate a function to be registered as a route
|
"""Decorate a function to be registered as a route
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
|
@ -136,42 +136,49 @@ class Sanic:
|
||||||
if stream:
|
if stream:
|
||||||
handler.is_stream = stream
|
handler.is_stream = stream
|
||||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||||
host=host, strict_slashes=strict_slashes)
|
host=host, strict_slashes=strict_slashes,
|
||||||
|
version=version)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Shorthand method decorators
|
# Shorthand method decorators
|
||||||
def get(self, uri, host=None, strict_slashes=False):
|
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def post(self, uri, host=None, strict_slashes=False, stream=False):
|
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def put(self, uri, host=None, strict_slashes=False, stream=False):
|
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def head(self, uri, host=None, strict_slashes=False):
|
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def options(self, uri, host=None, strict_slashes=False):
|
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def patch(self, uri, host=None, strict_slashes=False, stream=False):
|
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def delete(self, uri, host=None, strict_slashes=False):
|
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||||
strict_slashes=False):
|
strict_slashes=False, version=None):
|
||||||
"""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
|
||||||
routes.
|
routes.
|
||||||
|
@ -204,7 +211,8 @@ class Sanic:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.route(uri=uri, methods=methods, host=host,
|
self.route(uri=uri, methods=methods, host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)(handler)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
|
@ -701,7 +709,8 @@ class Sanic:
|
||||||
'backlog': backlog,
|
'backlog': backlog,
|
||||||
'has_log': has_log,
|
'has_log': has_log,
|
||||||
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
||||||
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE
|
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
||||||
|
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
|
@ -4,8 +4,8 @@ from sanic.constants import HTTP_METHODS
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
FutureRoute = namedtuple('Route',
|
FutureRoute = namedtuple('Route',
|
||||||
['handler', 'uri', 'methods',
|
['handler', 'uri', 'methods', 'host',
|
||||||
'host', 'strict_slashes', 'stream'])
|
'strict_slashes', 'stream', 'version'])
|
||||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||||
|
@ -14,7 +14,7 @@ FutureStatic = namedtuple('Route',
|
||||||
|
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
def __init__(self, name, url_prefix=None, host=None):
|
def __init__(self, name, url_prefix=None, host=None, version=None):
|
||||||
"""Create a new blueprint
|
"""Create a new blueprint
|
||||||
|
|
||||||
:param name: unique name of the blueprint
|
:param name: unique name of the blueprint
|
||||||
|
@ -30,6 +30,7 @@ class Blueprint:
|
||||||
self.listeners = defaultdict(list)
|
self.listeners = defaultdict(list)
|
||||||
self.middlewares = []
|
self.middlewares = []
|
||||||
self.statics = []
|
self.statics = []
|
||||||
|
self.version = version
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""Register the blueprint to the sanic app."""
|
"""Register the blueprint to the sanic app."""
|
||||||
|
@ -43,12 +44,16 @@ class Blueprint:
|
||||||
future.handler.__blueprintname__ = self.name
|
future.handler.__blueprintname__ = self.name
|
||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
|
|
||||||
|
version = future.version or self.version
|
||||||
|
|
||||||
app.route(
|
app.route(
|
||||||
uri=uri[1:] if uri.startswith('//') else uri,
|
uri=uri[1:] if uri.startswith('//') else uri,
|
||||||
methods=future.methods,
|
methods=future.methods,
|
||||||
host=future.host or self.host,
|
host=future.host or self.host,
|
||||||
strict_slashes=future.strict_slashes,
|
strict_slashes=future.strict_slashes,
|
||||||
stream=future.stream
|
stream=future.stream,
|
||||||
|
version=version
|
||||||
)(future.handler)
|
)(future.handler)
|
||||||
|
|
||||||
for future in self.websocket_routes:
|
for future in self.websocket_routes:
|
||||||
|
@ -89,7 +94,7 @@ class Blueprint:
|
||||||
app.listener(event)(listener)
|
app.listener(event)(listener)
|
||||||
|
|
||||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||||
strict_slashes=False, stream=False):
|
strict_slashes=False, stream=False, version=None):
|
||||||
"""Create a blueprint route from a decorated function.
|
"""Create a blueprint route from a decorated function.
|
||||||
|
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
|
@ -97,13 +102,13 @@ class Blueprint:
|
||||||
"""
|
"""
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
route = FutureRoute(
|
route = FutureRoute(
|
||||||
handler, uri, methods, host, strict_slashes, stream)
|
handler, uri, methods, host, strict_slashes, stream, version)
|
||||||
self.routes.append(route)
|
self.routes.append(route)
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||||
strict_slashes=False):
|
strict_slashes=False, version=None):
|
||||||
"""Create a blueprint route from a function.
|
"""Create a blueprint route from a function.
|
||||||
|
|
||||||
:param handler: function for handling uri requests. Accepts function,
|
:param handler: function for handling uri requests. Accepts function,
|
||||||
|
@ -125,21 +130,22 @@ class Blueprint:
|
||||||
methods = handler.handlers.keys()
|
methods = handler.handlers.keys()
|
||||||
|
|
||||||
self.route(uri=uri, methods=methods, host=host,
|
self.route(uri=uri, methods=methods, host=host,
|
||||||
strict_slashes=strict_slashes)(handler)
|
strict_slashes=strict_slashes, version=version)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def websocket(self, uri, host=None, strict_slashes=False):
|
def websocket(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
"""Create a blueprint websocket route from a decorated function.
|
"""Create a blueprint websocket route from a decorated function.
|
||||||
|
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
"""
|
"""
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
route = FutureRoute(handler, uri, [], host, strict_slashes, False)
|
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||||
|
False, version)
|
||||||
self.websocket_routes.append(route)
|
self.websocket_routes.append(route)
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_websocket_route(self, handler, uri, host=None):
|
def add_websocket_route(self, handler, uri, host=None, version=None):
|
||||||
"""Create a blueprint websocket route from a function.
|
"""Create a blueprint websocket route from a function.
|
||||||
|
|
||||||
:param handler: function for handling uri requests. Accepts function,
|
:param handler: function for handling uri requests. Accepts function,
|
||||||
|
@ -147,7 +153,7 @@ class Blueprint:
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
self.websocket(uri=uri, host=host)(handler)
|
self.websocket(uri=uri, host=host, version=version)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def listener(self, event):
|
def listener(self, event):
|
||||||
|
@ -193,30 +199,36 @@ class Blueprint:
|
||||||
self.statics.append(static)
|
self.statics.append(static)
|
||||||
|
|
||||||
# Shorthand method decorators
|
# Shorthand method decorators
|
||||||
def get(self, uri, host=None, strict_slashes=False):
|
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=["GET"], host=host,
|
return self.route(uri, methods=["GET"], host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def post(self, uri, host=None, strict_slashes=False, stream=False):
|
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=["POST"], host=host,
|
return self.route(uri, methods=["POST"], host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def put(self, uri, host=None, strict_slashes=False, stream=False):
|
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=["PUT"], host=host,
|
return self.route(uri, methods=["PUT"], host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def head(self, uri, host=None, strict_slashes=False):
|
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=["HEAD"], host=host,
|
return self.route(uri, methods=["HEAD"], host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def options(self, uri, host=None, strict_slashes=False):
|
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
||||||
def patch(self, uri, host=None, strict_slashes=False, stream=False):
|
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||||
|
version=None):
|
||||||
return self.route(uri, methods=["PATCH"], host=host,
|
return self.route(uri, methods=["PATCH"], host=host,
|
||||||
strict_slashes=strict_slashes, stream=stream)
|
strict_slashes=strict_slashes, stream=stream,
|
||||||
|
version=version)
|
||||||
|
|
||||||
def delete(self, uri, host=None, strict_slashes=False):
|
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||||
return self.route(uri, methods=["DELETE"], host=host,
|
return self.route(uri, methods=["DELETE"], host=host,
|
||||||
strict_slashes=strict_slashes)
|
strict_slashes=strict_slashes, version=version)
|
||||||
|
|
|
@ -12,11 +12,12 @@ _address_dict = {
|
||||||
'Windows': ('localhost', 514),
|
'Windows': ('localhost', 514),
|
||||||
'Darwin': '/var/run/syslog',
|
'Darwin': '/var/run/syslog',
|
||||||
'Linux': '/dev/log',
|
'Linux': '/dev/log',
|
||||||
'FreeBSD': '/dev/log'
|
'FreeBSD': '/var/run/log'
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
'filters': {
|
'filters': {
|
||||||
'accessFilter': {
|
'accessFilter': {
|
||||||
'()': DefaultFilter,
|
'()': DefaultFilter,
|
||||||
|
@ -127,6 +128,7 @@ class Config(dict):
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||||
self.WEBSOCKET_MAX_QUEUE = 32
|
self.WEBSOCKET_MAX_QUEUE = 32
|
||||||
|
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||||
|
|
||||||
if load_env:
|
if load_env:
|
||||||
self.load_environment_vars()
|
self.load_environment_vars()
|
||||||
|
@ -195,10 +197,16 @@ class Config(dict):
|
||||||
|
|
||||||
def load_environment_vars(self):
|
def load_environment_vars(self):
|
||||||
"""
|
"""
|
||||||
Looks for any SANIC_ prefixed environment variables and applies
|
Looks for any ``SANIC_`` prefixed environment variables and applies
|
||||||
them to the configuration if present.
|
them to the configuration if present.
|
||||||
"""
|
"""
|
||||||
for k, v in os.environ.items():
|
for k, v in os.environ.items():
|
||||||
if k.startswith(SANIC_PREFIX):
|
if k.startswith(SANIC_PREFIX):
|
||||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||||
|
try:
|
||||||
|
self[config_key] = int(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
self[config_key] = float(v)
|
||||||
|
except ValueError:
|
||||||
self[config_key] = v
|
self[config_key] = v
|
||||||
|
|
|
@ -194,6 +194,11 @@ class ContentRangeError(SanicException):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(403)
|
||||||
|
class Forbidden(SanicException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidRangeType(ContentRangeError):
|
class InvalidRangeType(ContentRangeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -205,8 +210,8 @@ class Unauthorized(SanicException):
|
||||||
|
|
||||||
:param scheme: Name of the authentication scheme to be used.
|
:param scheme: Name of the authentication scheme to be used.
|
||||||
:param challenge: A dict containing values to add to the WWW-Authenticate
|
:param challenge: A dict containing values to add to the WWW-Authenticate
|
||||||
header that is generated. This is especially useful when dealing with the
|
header that is generated. This is especially useful when dealing with
|
||||||
Digest scheme. (optional)
|
the Digest scheme. (optional)
|
||||||
|
|
||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
|
@ -227,7 +232,6 @@ class Unauthorized(SanicException):
|
||||||
# With a Bearer auth-scheme, realm is optional:
|
# With a Bearer auth-scheme, realm is optional:
|
||||||
challenge = {"realm": "Restricted Area"}
|
challenge = {"realm": "Restricted Area"}
|
||||||
raise Unauthorized("Auth required.", "Bearer", challenge)
|
raise Unauthorized("Auth required.", "Bearer", challenge)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -249,6 +253,7 @@ def abort(status_code, message=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.
|
||||||
|
|
||||||
: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
|
:param message: The HTTP response body. Defaults to the messages
|
||||||
in response.py for the given status code.
|
in response.py for the given status code.
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Request(dict):
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
'_ip', '_parsed_url', 'uri_template', 'stream'
|
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method, transport):
|
def __init__(self, url_bytes, headers, version, method, transport):
|
||||||
|
@ -86,10 +86,14 @@ class Request(dict):
|
||||||
|
|
||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
|
prefixes = ('Bearer', 'Token')
|
||||||
auth_header = self.headers.get('Authorization')
|
auth_header = self.headers.get('Authorization')
|
||||||
if auth_header is not None and 'Token ' in auth_header:
|
|
||||||
return auth_header.partition('Token ')[-1]
|
if auth_header is not None:
|
||||||
else:
|
for prefix in prefixes:
|
||||||
|
if prefix in auth_header:
|
||||||
|
return auth_header.partition(prefix)[-1].strip()
|
||||||
|
|
||||||
return auth_header
|
return auth_header
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -138,7 +142,7 @@ class Request(dict):
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
cookie = self.headers.get('Cookie')
|
||||||
if cookie is not None:
|
if cookie is not None:
|
||||||
cookies = SimpleCookie()
|
cookies = SimpleCookie()
|
||||||
cookies.load(cookie)
|
cookies.load(cookie)
|
||||||
|
@ -155,6 +159,25 @@ class Request(dict):
|
||||||
(None, None))
|
(None, None))
|
||||||
return self._ip
|
return self._ip
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remote_addr(self):
|
||||||
|
"""Attempt to return the original client ip based on X-Forwarded-For.
|
||||||
|
|
||||||
|
:return: original client ip.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_remote_addr'):
|
||||||
|
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
|
||||||
|
remote_addrs = [
|
||||||
|
addr for addr in [
|
||||||
|
addr.strip() for addr in forwarded_for
|
||||||
|
] if addr
|
||||||
|
]
|
||||||
|
if len(remote_addrs) > 0:
|
||||||
|
self._remote_addr = remote_addrs[0]
|
||||||
|
else:
|
||||||
|
self._remote_addr = ''
|
||||||
|
return self._remote_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheme(self):
|
def scheme(self):
|
||||||
if self.app.websocket_enabled \
|
if self.app.websocket_enabled \
|
||||||
|
@ -234,15 +257,15 @@ def parse_multipart_form(body, boundary):
|
||||||
break
|
break
|
||||||
|
|
||||||
colon_index = form_line.index(':')
|
colon_index = form_line.index(':')
|
||||||
form_header_field = form_line[0:colon_index]
|
form_header_field = form_line[0:colon_index].lower()
|
||||||
form_header_value, form_parameters = parse_header(
|
form_header_value, form_parameters = parse_header(
|
||||||
form_line[colon_index + 2:])
|
form_line[colon_index + 2:])
|
||||||
|
|
||||||
if form_header_field == 'Content-Disposition':
|
if form_header_field == 'content-disposition':
|
||||||
if 'filename' in form_parameters:
|
if 'filename' in form_parameters:
|
||||||
file_name = form_parameters['filename']
|
file_name = form_parameters['filename']
|
||||||
field_name = form_parameters.get('name')
|
field_name = form_parameters.get('name')
|
||||||
elif form_header_field == 'Content-Type':
|
elif form_header_field == 'content-type':
|
||||||
file_type = form_header_value
|
file_type = form_header_value
|
||||||
|
|
||||||
post_data = form_part[line_index:-4]
|
post_data = form_part[line_index:-4]
|
||||||
|
|
|
@ -237,6 +237,7 @@ def json(body, status=200, headers=None,
|
||||||
content_type="application/json", **kwargs):
|
content_type="application/json", **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns response object with body in json format.
|
Returns response object with body in json format.
|
||||||
|
|
||||||
:param body: Response data to be serialized.
|
:param body: Response data to be serialized.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
@ -250,6 +251,7 @@ def text(body, status=200, headers=None,
|
||||||
content_type="text/plain; charset=utf-8"):
|
content_type="text/plain; charset=utf-8"):
|
||||||
"""
|
"""
|
||||||
Returns response object with body in text format.
|
Returns response object with body in text format.
|
||||||
|
|
||||||
:param body: Response data to be encoded.
|
:param body: Response data to be encoded.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
@ -264,6 +266,7 @@ def raw(body, status=200, headers=None,
|
||||||
content_type="application/octet-stream"):
|
content_type="application/octet-stream"):
|
||||||
"""
|
"""
|
||||||
Returns response object without encoding the body.
|
Returns response object without encoding the body.
|
||||||
|
|
||||||
:param body: Response data.
|
:param body: Response data.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
@ -276,6 +279,7 @@ def raw(body, status=200, headers=None,
|
||||||
def html(body, status=200, headers=None):
|
def html(body, status=200, headers=None):
|
||||||
"""
|
"""
|
||||||
Returns response object with body in html format.
|
Returns response object with body in html format.
|
||||||
|
|
||||||
:param body: Response data to be encoded.
|
:param body: Response data to be encoded.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
|
|
@ -98,8 +98,25 @@ class Router:
|
||||||
|
|
||||||
return name, _type, pattern
|
return name, _type, pattern
|
||||||
|
|
||||||
def add(self, uri, methods, handler, host=None, strict_slashes=False):
|
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||||
|
version=None):
|
||||||
|
"""Add a handler to the route list
|
||||||
|
|
||||||
|
:param uri: path to match
|
||||||
|
:param methods: sequence of accepted method names. If none are
|
||||||
|
provided, any method is allowed
|
||||||
|
:param handler: request handler function.
|
||||||
|
When executed, it should provide a response object.
|
||||||
|
:param strict_slashes: strict to trailing slash
|
||||||
|
:param version: current version of the route or blueprint. See
|
||||||
|
docs for further details.
|
||||||
|
:return: Nothing
|
||||||
|
"""
|
||||||
|
if version is not None:
|
||||||
|
if uri.startswith('/'):
|
||||||
|
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
|
||||||
|
else:
|
||||||
|
uri = "/".join(["/v{}".format(str(version)), uri])
|
||||||
# add regular version
|
# add regular version
|
||||||
self._add(uri, methods, handler, host)
|
self._add(uri, methods, handler, host)
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
signal=Signal(), connections=set(), request_timeout=60,
|
signal=Signal(), connections=set(), request_timeout=60,
|
||||||
request_max_size=None, request_class=None, has_log=True,
|
request_max_size=None, request_class=None, has_log=True,
|
||||||
keep_alive=True, is_request_stream=False, router=None,
|
keep_alive=True, is_request_stream=False, router=None,
|
||||||
**kwargs):
|
state=None, debug=False, **kwargs):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.request = None
|
self.request = None
|
||||||
|
@ -99,12 +99,18 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._request_handler_task = None
|
self._request_handler_task = None
|
||||||
self._request_stream_task = None
|
self._request_stream_task = None
|
||||||
self._keep_alive = keep_alive
|
self._keep_alive = keep_alive
|
||||||
|
self._header_fragment = b''
|
||||||
|
self.state = state if state else {}
|
||||||
|
if 'requests_count' not in self.state:
|
||||||
|
self.state['requests_count'] = 0
|
||||||
|
self._debug = debug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keep_alive(self):
|
def keep_alive(self):
|
||||||
return (self._keep_alive
|
return (
|
||||||
and not self.signal.stopped
|
self._keep_alive and
|
||||||
and self.parser.should_keep_alive())
|
not self.signal.stopped and
|
||||||
|
self.parser.should_keep_alive())
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Connection
|
# Connection
|
||||||
|
@ -154,22 +160,39 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.headers = []
|
self.headers = []
|
||||||
self.parser = HttpRequestParser(self)
|
self.parser = HttpRequestParser(self)
|
||||||
|
|
||||||
|
# requests count
|
||||||
|
self.state['requests_count'] = self.state['requests_count'] + 1
|
||||||
|
|
||||||
# Parse request chunk or close connection
|
# Parse request chunk or close connection
|
||||||
try:
|
try:
|
||||||
self.parser.feed_data(data)
|
self.parser.feed_data(data)
|
||||||
except HttpParserError:
|
except HttpParserError:
|
||||||
exception = InvalidUsage('Bad Request')
|
message = 'Bad Request'
|
||||||
|
if self._debug:
|
||||||
|
message += '\n' + traceback.format_exc()
|
||||||
|
exception = InvalidUsage(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
def on_url(self, url):
|
def on_url(self, url):
|
||||||
|
if not self.url:
|
||||||
self.url = url
|
self.url = url
|
||||||
|
else:
|
||||||
|
self.url += url
|
||||||
|
|
||||||
def on_header(self, name, value):
|
def on_header(self, name, value):
|
||||||
if name == b'Content-Length' and int(value) > self.request_max_size:
|
self._header_fragment += name
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
if self._header_fragment == b'Content-Length' \
|
||||||
|
and int(value) > self.request_max_size:
|
||||||
exception = PayloadTooLarge('Payload Too Large')
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
self.headers.append((name.decode().casefold(), value.decode()))
|
self.headers.append(
|
||||||
|
(self._header_fragment.decode().casefold(),
|
||||||
|
value.decode()))
|
||||||
|
|
||||||
|
self._header_fragment = b''
|
||||||
|
|
||||||
def on_headers_complete(self):
|
def on_headers_complete(self):
|
||||||
self.request = self.request_class(
|
self.request = self.request_class(
|
||||||
|
@ -357,6 +380,14 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Force close the connection.
|
||||||
|
"""
|
||||||
|
if self.transport is not None:
|
||||||
|
self.transport.close()
|
||||||
|
self.transport = None
|
||||||
|
|
||||||
|
|
||||||
def update_current_time(loop):
|
def update_current_time(loop):
|
||||||
"""Cache the current time, since it is needed at the end of every
|
"""Cache the current time, since it is needed at the end of every
|
||||||
|
@ -389,7 +420,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
register_sys_signals=True, run_async=False, connections=None,
|
register_sys_signals=True, run_async=False, connections=None,
|
||||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
||||||
is_request_stream=False, router=None, websocket_max_size=None,
|
is_request_stream=False, router=None, websocket_max_size=None,
|
||||||
websocket_max_queue=None):
|
websocket_max_queue=None, state=None,
|
||||||
|
graceful_shutdown_timeout=15.0):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
|
@ -427,8 +459,6 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
if debug:
|
if debug:
|
||||||
loop.set_debug(debug)
|
loop.set_debug(debug)
|
||||||
|
|
||||||
trigger_events(before_start, loop)
|
|
||||||
|
|
||||||
connections = connections if connections is not None else set()
|
connections = connections if connections is not None else set()
|
||||||
server = partial(
|
server = partial(
|
||||||
protocol,
|
protocol,
|
||||||
|
@ -445,7 +475,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
is_request_stream=is_request_stream,
|
is_request_stream=is_request_stream,
|
||||||
router=router,
|
router=router,
|
||||||
websocket_max_size=websocket_max_size,
|
websocket_max_size=websocket_max_size,
|
||||||
websocket_max_queue=websocket_max_queue
|
websocket_max_queue=websocket_max_queue,
|
||||||
|
state=state,
|
||||||
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
|
@ -457,6 +489,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
sock=sock,
|
sock=sock,
|
||||||
backlog=backlog
|
backlog=backlog
|
||||||
)
|
)
|
||||||
|
|
||||||
# Instead of pulling time at the end of every request,
|
# Instead of pulling time at the end of every request,
|
||||||
# pull it once per minute
|
# pull it once per minute
|
||||||
loop.call_soon(partial(update_current_time, loop))
|
loop.call_soon(partial(update_current_time, loop))
|
||||||
|
@ -464,6 +497,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
if run_async:
|
if run_async:
|
||||||
return server_coroutine
|
return server_coroutine
|
||||||
|
|
||||||
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
except:
|
except:
|
||||||
|
@ -499,8 +534,26 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
for connection in connections:
|
for connection in connections:
|
||||||
connection.close_if_idle()
|
connection.close_if_idle()
|
||||||
|
|
||||||
while connections:
|
# Gracefully shutdown timeout.
|
||||||
|
# We should provide graceful_shutdown_timeout,
|
||||||
|
# instead of letting connection hangs forever.
|
||||||
|
# Let's roughly calcucate time.
|
||||||
|
start_shutdown = 0
|
||||||
|
while connections and (start_shutdown < graceful_shutdown_timeout):
|
||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
|
start_shutdown = start_shutdown + 0.1
|
||||||
|
|
||||||
|
# Force close non-idle connection after waiting for
|
||||||
|
# graceful_shutdown_timeout
|
||||||
|
coros = []
|
||||||
|
for conn in connections:
|
||||||
|
if hasattr(conn, "websocket") and conn.websocket:
|
||||||
|
coros.append(conn.websocket.close_connection(force=True))
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
_shutdown = asyncio.gather(*coros, loop=loop)
|
||||||
|
loop.run_until_complete(_shutdown)
|
||||||
|
|
||||||
trigger_events(after_stop, loop)
|
trigger_events(after_stop, loop)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import sys
|
||||||
import signal
|
import signal
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
@ -29,7 +30,7 @@ class GunicornWorker(base.Worker):
|
||||||
self.ssl_context = self._create_ssl_context(cfg)
|
self.ssl_context = self._create_ssl_context(cfg)
|
||||||
else:
|
else:
|
||||||
self.ssl_context = None
|
self.ssl_context = None
|
||||||
self.servers = []
|
self.servers = {}
|
||||||
self.connections = set()
|
self.connections = set()
|
||||||
self.exit_code = 0
|
self.exit_code = 0
|
||||||
self.signal = Signal()
|
self.signal = Signal()
|
||||||
|
@ -69,9 +70,15 @@ class GunicornWorker(base.Worker):
|
||||||
trigger_events(self._server_settings.get('before_stop', []),
|
trigger_events(self._server_settings.get('before_stop', []),
|
||||||
self.loop)
|
self.loop)
|
||||||
self.loop.run_until_complete(self.close())
|
self.loop.run_until_complete(self.close())
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
finally:
|
finally:
|
||||||
|
try:
|
||||||
trigger_events(self._server_settings.get('after_stop', []),
|
trigger_events(self._server_settings.get('after_stop', []),
|
||||||
self.loop)
|
self.loop)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
self.loop.close()
|
self.loop.close()
|
||||||
|
|
||||||
sys.exit(self.exit_code)
|
sys.exit(self.exit_code)
|
||||||
|
@ -91,16 +98,37 @@ class GunicornWorker(base.Worker):
|
||||||
for conn in self.connections:
|
for conn in self.connections:
|
||||||
conn.close_if_idle()
|
conn.close_if_idle()
|
||||||
|
|
||||||
while self.connections:
|
# gracefully shutdown timeout
|
||||||
|
start_shutdown = 0
|
||||||
|
graceful_shutdown_timeout = self.cfg.graceful_timeout
|
||||||
|
while self.connections and \
|
||||||
|
(start_shutdown < graceful_shutdown_timeout):
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
start_shutdown = start_shutdown + 0.1
|
||||||
|
|
||||||
|
# Force close non-idle connection after waiting for
|
||||||
|
# graceful_shutdown_timeout
|
||||||
|
coros = []
|
||||||
|
for conn in self.connections:
|
||||||
|
if hasattr(conn, "websocket") and conn.websocket:
|
||||||
|
coros.append(conn.websocket.close_connection(force=True))
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||||
|
await _shutdown
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
for sock in self.sockets:
|
for sock in self.sockets:
|
||||||
self.servers.append(await serve(
|
state = dict(requests_count=0)
|
||||||
|
self._server_settings["host"] = None
|
||||||
|
self._server_settings["port"] = None
|
||||||
|
server = await serve(
|
||||||
sock=sock,
|
sock=sock,
|
||||||
connections=self.connections,
|
connections=self.connections,
|
||||||
|
state=state,
|
||||||
**self._server_settings
|
**self._server_settings
|
||||||
))
|
)
|
||||||
|
self.servers[server] = state
|
||||||
|
|
||||||
async def _check_alive(self):
|
async def _check_alive(self):
|
||||||
# If our parent changed then we shut down.
|
# If our parent changed then we shut down.
|
||||||
|
@ -109,7 +137,15 @@ class GunicornWorker(base.Worker):
|
||||||
while self.alive:
|
while self.alive:
|
||||||
self.notify()
|
self.notify()
|
||||||
|
|
||||||
if pid == os.getpid() and self.ppid != os.getppid():
|
req_count = sum(
|
||||||
|
self.servers[srv]["requests_count"] for srv in self.servers
|
||||||
|
)
|
||||||
|
if self.max_requests and req_count > self.max_requests:
|
||||||
|
self.alive = False
|
||||||
|
self.log.info(
|
||||||
|
"Max requests exceeded, shutting down: %s", self
|
||||||
|
)
|
||||||
|
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.log.info("Parent changed, shutting down: %s", self)
|
self.log.info("Parent changed, shutting down: %s", self)
|
||||||
else:
|
else:
|
||||||
|
@ -166,3 +202,4 @@ class GunicornWorker(base.Worker):
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.exit_code = 1
|
self.exit_code = 1
|
||||||
self.cfg.worker_abort(self)
|
self.cfg.worker_abort(self)
|
||||||
|
sys.exit(1)
|
||||||
|
|
|
@ -1,16 +1,42 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||||
|
from sanic.constants import HTTP_METHODS
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||||
|
def test_versioned_routes_get(method):
|
||||||
|
app = Sanic('test_shorhand_routes_get')
|
||||||
|
bp = Blueprint('test_text')
|
||||||
|
|
||||||
|
method = method.lower()
|
||||||
|
|
||||||
|
func = getattr(bp, method)
|
||||||
|
if callable(func):
|
||||||
|
@func('/{}'.format(method), version=1)
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
else:
|
||||||
|
print(func)
|
||||||
|
raise
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
client_method = getattr(app.test_client, method)
|
||||||
|
|
||||||
|
request, response = client_method('/v1/{}'.format(method))
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
def test_bp():
|
def test_bp():
|
||||||
app = Sanic('test_text')
|
app = Sanic('test_text')
|
||||||
bp = Blueprint('test_text')
|
bp = Blueprint('test_text')
|
||||||
|
|
|
@ -4,7 +4,7 @@ from bs4 import BeautifulSoup
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
||||||
from sanic.exceptions import abort
|
from sanic.exceptions import Forbidden, abort
|
||||||
|
|
||||||
|
|
||||||
class SanicExceptionTestException(Exception):
|
class SanicExceptionTestException(Exception):
|
||||||
|
@ -27,6 +27,10 @@ def exception_app():
|
||||||
def handler_404(request):
|
def handler_404(request):
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
@app.route('/403')
|
||||||
|
def handler_403(request):
|
||||||
|
raise Forbidden("Forbidden")
|
||||||
|
|
||||||
@app.route('/401/basic')
|
@app.route('/401/basic')
|
||||||
def handler_401_basic(request):
|
def handler_401_basic(request):
|
||||||
raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"})
|
raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"})
|
||||||
|
@ -113,6 +117,12 @@ def test_not_found_exception(exception_app):
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_forbidden_exception(exception_app):
|
||||||
|
"""Test the built-in Forbidden exception"""
|
||||||
|
request, response = exception_app.test_client.get('/403')
|
||||||
|
assert response.status == 403
|
||||||
|
|
||||||
|
|
||||||
def test_unauthorized_exception(exception_app):
|
def test_unauthorized_exception(exception_app):
|
||||||
"""Test the built-in Unauthorized exception"""
|
"""Test the built-in Unauthorized exception"""
|
||||||
request, response = exception_app.test_client.get('/401/basic')
|
request, response = exception_app.test_client.get('/401/basic')
|
||||||
|
|
|
@ -175,7 +175,7 @@ def test_token():
|
||||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
headers = {
|
headers = {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'Authorization': 'Bearer Token {}'.format(token)
|
'Authorization': 'Bearer {}'.format(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
request, response = app.test_client.get('/', headers=headers)
|
request, response = app.test_client.get('/', headers=headers)
|
||||||
|
@ -211,6 +211,32 @@ def test_content_type():
|
||||||
assert response.text == 'application/json'
|
assert response.text == 'application/json'
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_addr():
|
||||||
|
app = Sanic('test_content_type')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text(request.remote_addr)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'X-Forwarded-For': '127.0.0.1, 127.0.1.2'
|
||||||
|
}
|
||||||
|
request, response = app.test_client.get('/', headers=headers)
|
||||||
|
assert request.remote_addr == '127.0.0.1'
|
||||||
|
assert response.text == '127.0.0.1'
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/')
|
||||||
|
assert request.remote_addr == ''
|
||||||
|
assert response.text == ''
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'X-Forwarded-For': '127.0.0.1, , ,,127.0.1.2'
|
||||||
|
}
|
||||||
|
request, response = app.test_client.get('/', headers=headers)
|
||||||
|
assert request.remote_addr == '127.0.0.1'
|
||||||
|
assert response.text == '127.0.0.1'
|
||||||
|
|
||||||
|
|
||||||
def test_match_info():
|
def test_match_info():
|
||||||
app = Sanic('test_match_info')
|
app = Sanic('test_match_info')
|
||||||
|
|
||||||
|
@ -260,19 +286,26 @@ def test_post_form_urlencoded():
|
||||||
assert request.form.get('test') == 'OK'
|
assert request.form.get('test') == 'OK'
|
||||||
|
|
||||||
|
|
||||||
def test_post_form_multipart_form_data():
|
@pytest.mark.parametrize(
|
||||||
|
'payload', [
|
||||||
|
'------sanic\r\n' \
|
||||||
|
'Content-Disposition: form-data; name="test"\r\n' \
|
||||||
|
'\r\n' \
|
||||||
|
'OK\r\n' \
|
||||||
|
'------sanic--\r\n',
|
||||||
|
'------sanic\r\n' \
|
||||||
|
'content-disposition: form-data; name="test"\r\n' \
|
||||||
|
'\r\n' \
|
||||||
|
'OK\r\n' \
|
||||||
|
'------sanic--\r\n',
|
||||||
|
])
|
||||||
|
def test_post_form_multipart_form_data(payload):
|
||||||
app = Sanic('test_post_form_multipart_form_data')
|
app = Sanic('test_post_form_multipart_form_data')
|
||||||
|
|
||||||
@app.route('/', methods=['POST'])
|
@app.route('/', methods=['POST'])
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
|
|
||||||
payload = '------sanic\r\n' \
|
|
||||||
'Content-Disposition: form-data; name="test"\r\n' \
|
|
||||||
'\r\n' \
|
|
||||||
'OK\r\n' \
|
|
||||||
'------sanic--\r\n'
|
|
||||||
|
|
||||||
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
|
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
|
||||||
|
|
||||||
request, response = app.test_client.post(data=payload, headers=headers)
|
request, response = app.test_client.post(data=payload, headers=headers)
|
||||||
|
|
|
@ -4,12 +4,33 @@ import pytest
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.router import RouteExists, RouteDoesNotExist
|
from sanic.router import RouteExists, RouteDoesNotExist
|
||||||
|
from sanic.constants import HTTP_METHODS
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# UTF-8
|
# UTF-8
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||||
|
def test_versioned_routes_get(method):
|
||||||
|
app = Sanic('test_shorhand_routes_get')
|
||||||
|
|
||||||
|
method = method.lower()
|
||||||
|
|
||||||
|
func = getattr(app, method)
|
||||||
|
if callable(func):
|
||||||
|
@func('/{}'.format(method), version=1)
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
else:
|
||||||
|
print(func)
|
||||||
|
raise
|
||||||
|
|
||||||
|
client_method = getattr(app.test_client, method)
|
||||||
|
|
||||||
|
request, response = client_method('/v1/{}'.format(method))
|
||||||
|
assert response.status== 200
|
||||||
|
|
||||||
def test_shorthand_routes_get():
|
def test_shorthand_routes_get():
|
||||||
app = Sanic('test_shorhand_routes_get')
|
app = Sanic('test_shorhand_routes_get')
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@ import json
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from unittest import mock
|
||||||
|
from sanic.worker import GunicornWorker
|
||||||
|
from sanic.app import Sanic
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,3 +24,112 @@ def test_gunicorn_worker(gunicorn_worker):
|
||||||
with urllib.request.urlopen('http://localhost:1337/') as f:
|
with urllib.request.urlopen('http://localhost:1337/') as f:
|
||||||
res = json.loads(f.read(100).decode())
|
res = json.loads(f.read(100).decode())
|
||||||
assert res['test']
|
assert res['test']
|
||||||
|
|
||||||
|
|
||||||
|
class GunicornTestWorker(GunicornWorker):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.app = mock.Mock()
|
||||||
|
self.app.callable = Sanic("test_gunicorn_worker")
|
||||||
|
self.servers = {}
|
||||||
|
self.exit_code = 0
|
||||||
|
self.cfg = mock.Mock()
|
||||||
|
self.notify = mock.Mock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def worker():
|
||||||
|
return GunicornTestWorker()
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_init_process(worker):
|
||||||
|
with mock.patch('sanic.worker.asyncio') as mock_asyncio:
|
||||||
|
try:
|
||||||
|
worker.init_process()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert mock_asyncio.get_event_loop.return_value.close.called
|
||||||
|
assert mock_asyncio.new_event_loop.called
|
||||||
|
assert mock_asyncio.set_event_loop.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_init_signals(worker):
|
||||||
|
worker.loop = mock.Mock()
|
||||||
|
worker.init_signals()
|
||||||
|
assert worker.loop.add_signal_handler.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_abort(worker):
|
||||||
|
with mock.patch('sanic.worker.sys') as mock_sys:
|
||||||
|
worker.handle_abort(object(), object())
|
||||||
|
assert not worker.alive
|
||||||
|
assert worker.exit_code == 1
|
||||||
|
mock_sys.exit.assert_called_with(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_quit(worker):
|
||||||
|
worker.handle_quit(object(), object())
|
||||||
|
assert not worker.alive
|
||||||
|
assert worker.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_max_requests_exceeded(worker):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
worker.ppid = 1
|
||||||
|
worker.alive = True
|
||||||
|
sock = mock.Mock()
|
||||||
|
sock.cfg_addr = ('localhost', 8080)
|
||||||
|
worker.sockets = [sock]
|
||||||
|
worker.wsgi = mock.Mock()
|
||||||
|
worker.connections = set()
|
||||||
|
worker.log = mock.Mock()
|
||||||
|
worker.loop = loop
|
||||||
|
worker.servers = {
|
||||||
|
"server1": {"requests_count": 14},
|
||||||
|
"server2": {"requests_count": 15},
|
||||||
|
}
|
||||||
|
worker.max_requests = 10
|
||||||
|
worker._run = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
|
||||||
|
|
||||||
|
# exceeding request count
|
||||||
|
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
|
||||||
|
loop.run_until_complete(_runner)
|
||||||
|
|
||||||
|
assert worker.alive == False
|
||||||
|
worker.notify.assert_called_with()
|
||||||
|
worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s",
|
||||||
|
worker)
|
||||||
|
|
||||||
|
def test_worker_close(worker):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.sleep = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
|
||||||
|
worker.ppid = 1
|
||||||
|
worker.pid = 2
|
||||||
|
worker.cfg.graceful_timeout = 1.0
|
||||||
|
worker.signal = mock.Mock()
|
||||||
|
worker.signal.stopped = False
|
||||||
|
worker.wsgi = mock.Mock()
|
||||||
|
conn = mock.Mock()
|
||||||
|
conn.websocket = mock.Mock()
|
||||||
|
conn.websocket.close_connection = mock.Mock(
|
||||||
|
wraps=asyncio.coroutine(lambda *a, **kw: None)
|
||||||
|
)
|
||||||
|
worker.connections = set([conn])
|
||||||
|
worker.log = mock.Mock()
|
||||||
|
worker.loop = loop
|
||||||
|
server = mock.Mock()
|
||||||
|
server.close = mock.Mock(wraps=lambda *a, **kw: None)
|
||||||
|
server.wait_closed = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
|
||||||
|
worker.servers = {
|
||||||
|
server: {"requests_count": 14},
|
||||||
|
}
|
||||||
|
worker.max_requests = 10
|
||||||
|
|
||||||
|
# close worker
|
||||||
|
_close = asyncio.ensure_future(worker.close(), loop=loop)
|
||||||
|
loop.run_until_complete(_close)
|
||||||
|
|
||||||
|
assert worker.signal.stopped == True
|
||||||
|
conn.websocket.close_connection.assert_called_with(force=True)
|
||||||
|
assert len(worker.servers) == 0
|
||||||
|
|
Loading…
Reference in New Issue
Block a user